fix: resolve control UI overview merge conflict
This commit is contained in:
parent
2d10d72524
commit
ed6a06f6ce
@ -1,12 +1,29 @@
|
||||
import { html } from "lit";
|
||||
import { ConnectErrorDetailCodes } from "../../../../src/gateway/protocol/connect-error-details.js";
|
||||
import { html, nothing } from "lit";
|
||||
import { t, i18n, SUPPORTED_LOCALES, type Locale } from "../../i18n/index.ts";
|
||||
import type { EventLogEntry } from "../app-events.ts";
|
||||
import { buildExternalLinkRel, EXTERNAL_LINK_TARGET } from "../external-link.ts";
|
||||
import { formatRelativeTimestamp, formatDurationHuman } from "../format.ts";
|
||||
import type { GatewayHelloOk } from "../gateway.ts";
|
||||
import { formatNextRun } from "../presenter.ts";
|
||||
import { icons } from "../icons.ts";
|
||||
import type { UiSettings } from "../storage.ts";
|
||||
import { shouldShowPairingHint } from "./overview-hints.ts";
|
||||
import type {
|
||||
AttentionItem,
|
||||
CronJob,
|
||||
CronStatus,
|
||||
SessionsListResult,
|
||||
SessionsUsageResult,
|
||||
SkillStatusReport,
|
||||
} from "../types.ts";
|
||||
import { renderOverviewAttention } from "./overview-attention.ts";
|
||||
import { renderOverviewCards } from "./overview-cards.ts";
|
||||
import { renderOverviewEventLog } from "./overview-event-log.ts";
|
||||
import {
|
||||
shouldShowAuthHint,
|
||||
shouldShowAuthRequiredHint,
|
||||
shouldShowInsecureContextHint,
|
||||
shouldShowPairingHint,
|
||||
} from "./overview-hints.ts";
|
||||
import { renderOverviewLogTail } from "./overview-log-tail.ts";
|
||||
|
||||
export type OverviewProps = {
|
||||
connected: boolean;
|
||||
@ -20,21 +37,35 @@ export type OverviewProps = {
|
||||
cronEnabled: boolean | null;
|
||||
cronNext: number | null;
|
||||
lastChannelsRefresh: number | null;
|
||||
// New dashboard data
|
||||
usageResult: SessionsUsageResult | null;
|
||||
sessionsResult: SessionsListResult | null;
|
||||
skillsReport: SkillStatusReport | null;
|
||||
cronJobs: CronJob[];
|
||||
cronStatus: CronStatus | null;
|
||||
attentionItems: AttentionItem[];
|
||||
eventLog: EventLogEntry[];
|
||||
overviewLogLines: string[];
|
||||
showGatewayToken: boolean;
|
||||
showGatewayPassword: boolean;
|
||||
onSettingsChange: (next: UiSettings) => void;
|
||||
onPasswordChange: (next: string) => void;
|
||||
onSessionKeyChange: (next: string) => void;
|
||||
onToggleGatewayTokenVisibility: () => void;
|
||||
onToggleGatewayPasswordVisibility: () => void;
|
||||
onConnect: () => void;
|
||||
onRefresh: () => void;
|
||||
onOpenCortexPreview: () => void;
|
||||
onOpenCortexConflicts: () => void;
|
||||
onOpenCortexSync: () => void;
|
||||
onNavigate: (tab: string) => void;
|
||||
onRefreshLogs: () => void;
|
||||
};
|
||||
|
||||
export function renderOverview(props: OverviewProps) {
|
||||
const snapshot = props.hello?.snapshot as
|
||||
| {
|
||||
uptimeMs?: number;
|
||||
policy?: { tickIntervalMs?: number };
|
||||
authMode?: "none" | "token" | "password" | "trusted-proxy";
|
||||
cortex?: {
|
||||
enabled?: boolean;
|
||||
@ -48,8 +79,9 @@ export function renderOverview(props: OverviewProps) {
|
||||
}
|
||||
| undefined;
|
||||
const uptime = snapshot?.uptimeMs ? formatDurationHuman(snapshot.uptimeMs) : t("common.na");
|
||||
const tick = snapshot?.policy?.tickIntervalMs
|
||||
? `${snapshot.policy.tickIntervalMs}ms`
|
||||
const tickIntervalMs = props.hello?.policy?.tickIntervalMs;
|
||||
const tick = tickIntervalMs
|
||||
? `${(tickIntervalMs / 1000).toFixed(tickIntervalMs % 1000 === 0 ? 0 : 1)}s`
|
||||
: t("common.na");
|
||||
const authMode = snapshot?.authMode;
|
||||
const isTrustedProxy = authMode === "trusted-proxy";
|
||||
@ -95,38 +127,12 @@ export function renderOverview(props: OverviewProps) {
|
||||
if (props.connected || !props.lastError) {
|
||||
return null;
|
||||
}
|
||||
const lower = props.lastError.toLowerCase();
|
||||
const authRequiredCodes = new Set<string>([
|
||||
ConnectErrorDetailCodes.AUTH_REQUIRED,
|
||||
ConnectErrorDetailCodes.AUTH_TOKEN_MISSING,
|
||||
ConnectErrorDetailCodes.AUTH_PASSWORD_MISSING,
|
||||
ConnectErrorDetailCodes.AUTH_TOKEN_NOT_CONFIGURED,
|
||||
ConnectErrorDetailCodes.AUTH_PASSWORD_NOT_CONFIGURED,
|
||||
]);
|
||||
const authFailureCodes = new Set<string>([
|
||||
...authRequiredCodes,
|
||||
ConnectErrorDetailCodes.AUTH_UNAUTHORIZED,
|
||||
ConnectErrorDetailCodes.AUTH_TOKEN_MISMATCH,
|
||||
ConnectErrorDetailCodes.AUTH_PASSWORD_MISMATCH,
|
||||
ConnectErrorDetailCodes.AUTH_DEVICE_TOKEN_MISMATCH,
|
||||
ConnectErrorDetailCodes.AUTH_RATE_LIMITED,
|
||||
ConnectErrorDetailCodes.AUTH_TAILSCALE_IDENTITY_MISSING,
|
||||
ConnectErrorDetailCodes.AUTH_TAILSCALE_PROXY_MISSING,
|
||||
ConnectErrorDetailCodes.AUTH_TAILSCALE_WHOIS_FAILED,
|
||||
ConnectErrorDetailCodes.AUTH_TAILSCALE_IDENTITY_MISMATCH,
|
||||
]);
|
||||
const authFailed = props.lastErrorCode
|
||||
? authFailureCodes.has(props.lastErrorCode)
|
||||
: lower.includes("unauthorized") || lower.includes("connect failed");
|
||||
if (!authFailed) {
|
||||
if (!shouldShowAuthHint(props.connected, props.lastError, props.lastErrorCode)) {
|
||||
return null;
|
||||
}
|
||||
const hasToken = Boolean(props.settings.token.trim());
|
||||
const hasPassword = Boolean(props.password.trim());
|
||||
const isAuthRequired = props.lastErrorCode
|
||||
? authRequiredCodes.has(props.lastErrorCode)
|
||||
: !hasToken && !hasPassword;
|
||||
if (isAuthRequired) {
|
||||
if (shouldShowAuthRequiredHint(hasToken, hasPassword, props.lastErrorCode)) {
|
||||
return html`
|
||||
<div class="muted" style="margin-top: 8px">
|
||||
${t("overview.auth.required")}
|
||||
@ -172,15 +178,7 @@ export function renderOverview(props: OverviewProps) {
|
||||
if (isSecureContext) {
|
||||
return null;
|
||||
}
|
||||
const lower = props.lastError.toLowerCase();
|
||||
const insecureContextCode =
|
||||
props.lastErrorCode === ConnectErrorDetailCodes.CONTROL_UI_DEVICE_IDENTITY_REQUIRED ||
|
||||
props.lastErrorCode === ConnectErrorDetailCodes.DEVICE_IDENTITY_REQUIRED;
|
||||
if (
|
||||
!insecureContextCode &&
|
||||
!lower.includes("secure context") &&
|
||||
!lower.includes("device identity required")
|
||||
) {
|
||||
if (!shouldShowInsecureContextHint(props.connected, props.lastError, props.lastErrorCode)) {
|
||||
return null;
|
||||
}
|
||||
return html`
|
||||
@ -215,18 +213,22 @@ export function renderOverview(props: OverviewProps) {
|
||||
const currentLocale = i18n.getLocale();
|
||||
|
||||
return html`
|
||||
<section class="grid grid-cols-2">
|
||||
<section class="grid">
|
||||
<div class="card">
|
||||
<div class="card-title">${t("overview.access.title")}</div>
|
||||
<div class="card-sub">${t("overview.access.subtitle")}</div>
|
||||
<div class="form-grid" style="margin-top: 16px;">
|
||||
<label class="field">
|
||||
<div class="ov-access-grid" style="margin-top: 16px;">
|
||||
<label class="field ov-access-grid__full">
|
||||
<span>${t("overview.access.wsUrl")}</span>
|
||||
<input
|
||||
.value=${props.settings.gatewayUrl}
|
||||
@input=${(e: Event) => {
|
||||
const v = (e.target as HTMLInputElement).value;
|
||||
props.onSettingsChange({ ...props.settings, gatewayUrl: v });
|
||||
props.onSettingsChange({
|
||||
...props.settings,
|
||||
gatewayUrl: v,
|
||||
token: v.trim() === props.settings.gatewayUrl.trim() ? props.settings.token : "",
|
||||
});
|
||||
}}
|
||||
placeholder="ws://100.x.y.z:18789"
|
||||
/>
|
||||
@ -237,26 +239,57 @@ export function renderOverview(props: OverviewProps) {
|
||||
: html`
|
||||
<label class="field">
|
||||
<span>${t("overview.access.token")}</span>
|
||||
<input
|
||||
.value=${props.settings.token}
|
||||
@input=${(e: Event) => {
|
||||
const v = (e.target as HTMLInputElement).value;
|
||||
props.onSettingsChange({ ...props.settings, token: v });
|
||||
}}
|
||||
placeholder="OPENCLAW_GATEWAY_TOKEN"
|
||||
/>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<input
|
||||
type=${props.showGatewayToken ? "text" : "password"}
|
||||
autocomplete="off"
|
||||
style="flex: 1;"
|
||||
.value=${props.settings.token}
|
||||
@input=${(e: Event) => {
|
||||
const v = (e.target as HTMLInputElement).value;
|
||||
props.onSettingsChange({ ...props.settings, token: v });
|
||||
}}
|
||||
placeholder="OPENCLAW_GATEWAY_TOKEN"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn--icon ${props.showGatewayToken ? "active" : ""}"
|
||||
style="width: 36px; height: 36px;"
|
||||
title=${props.showGatewayToken ? "Hide token" : "Show token"}
|
||||
aria-label="Toggle token visibility"
|
||||
aria-pressed=${props.showGatewayToken}
|
||||
@click=${props.onToggleGatewayTokenVisibility}
|
||||
>
|
||||
${props.showGatewayToken ? icons.eye : icons.eyeOff}
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>${t("overview.access.password")}</span>
|
||||
<input
|
||||
type="password"
|
||||
.value=${props.password}
|
||||
@input=${(e: Event) => {
|
||||
const v = (e.target as HTMLInputElement).value;
|
||||
props.onPasswordChange(v);
|
||||
}}
|
||||
placeholder="system or shared password"
|
||||
/>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<input
|
||||
type=${props.showGatewayPassword ? "text" : "password"}
|
||||
autocomplete="off"
|
||||
style="flex: 1;"
|
||||
.value=${props.password}
|
||||
@input=${(e: Event) => {
|
||||
const v = (e.target as HTMLInputElement).value;
|
||||
props.onPasswordChange(v);
|
||||
}}
|
||||
placeholder="system or shared password"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn--icon ${props.showGatewayPassword ? "active" : ""}"
|
||||
style="width: 36px; height: 36px;"
|
||||
title=${props.showGatewayPassword ? "Hide password" : "Show password"}
|
||||
aria-label="Toggle password visibility"
|
||||
aria-pressed=${props.showGatewayPassword}
|
||||
@click=${props.onToggleGatewayPasswordVisibility}
|
||||
>
|
||||
${props.showGatewayPassword ? icons.eye : icons.eyeOff}
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
`
|
||||
}
|
||||
@ -294,6 +327,30 @@ export function renderOverview(props: OverviewProps) {
|
||||
isTrustedProxy ? t("overview.access.trustedProxy") : t("overview.access.connectHint")
|
||||
}</span>
|
||||
</div>
|
||||
${
|
||||
!props.connected
|
||||
? html`
|
||||
<div class="login-gate__help" style="margin-top: 16px;">
|
||||
<div class="login-gate__help-title">${t("overview.connection.title")}</div>
|
||||
<ol class="login-gate__steps">
|
||||
<li>${t("overview.connection.step1")}<code>openclaw gateway run</code></li>
|
||||
<li>${t("overview.connection.step2")}<code>openclaw dashboard --no-open</code></li>
|
||||
<li>${t("overview.connection.step3")}</li>
|
||||
<li>${t("overview.connection.step4")}<code>openclaw doctor --generate-gateway-token</code></li>
|
||||
</ol>
|
||||
<div class="login-gate__docs">
|
||||
${t("overview.connection.docsHint")}
|
||||
<a
|
||||
class="session-link"
|
||||
href="https://docs.openclaw.ai/web/dashboard"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${t("overview.connection.docsLink")}</a>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
@ -360,45 +417,32 @@ export function renderOverview(props: OverviewProps) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="grid grid-cols-3" style="margin-top: 18px;">
|
||||
<div class="card stat-card">
|
||||
<div class="stat-label">${t("overview.stats.instances")}</div>
|
||||
<div class="stat-value">${props.presenceCount}</div>
|
||||
<div class="muted">${t("overview.stats.instancesHint")}</div>
|
||||
</div>
|
||||
<div class="card stat-card">
|
||||
<div class="stat-label">${t("overview.stats.sessions")}</div>
|
||||
<div class="stat-value">${props.sessionsCount ?? t("common.na")}</div>
|
||||
<div class="muted">${t("overview.stats.sessionsHint")}</div>
|
||||
</div>
|
||||
<div class="card stat-card">
|
||||
<div class="stat-label">${t("overview.stats.cron")}</div>
|
||||
<div class="stat-value">
|
||||
${props.cronEnabled == null ? t("common.na") : props.cronEnabled ? t("common.enabled") : t("common.disabled")}
|
||||
</div>
|
||||
<div class="muted">${t("overview.stats.cronNext", { time: formatNextRun(props.cronNext) })}</div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="ov-section-divider"></div>
|
||||
|
||||
${renderOverviewCards({
|
||||
usageResult: props.usageResult,
|
||||
sessionsResult: props.sessionsResult,
|
||||
skillsReport: props.skillsReport,
|
||||
cronJobs: props.cronJobs,
|
||||
cronStatus: props.cronStatus,
|
||||
presenceCount: props.presenceCount,
|
||||
onNavigate: props.onNavigate,
|
||||
})}
|
||||
|
||||
${renderOverviewAttention({ items: props.attentionItems })}
|
||||
|
||||
<div class="ov-section-divider"></div>
|
||||
|
||||
<div class="ov-bottom-grid" style="margin-top: 18px;">
|
||||
${renderOverviewEventLog({
|
||||
events: props.eventLog,
|
||||
})}
|
||||
|
||||
${renderOverviewLogTail({
|
||||
lines: props.overviewLogLines,
|
||||
onRefreshLogs: props.onRefreshLogs,
|
||||
})}
|
||||
</div>
|
||||
|
||||
<section class="card" style="margin-top: 18px;">
|
||||
<div class="card-title">${t("overview.notes.title")}</div>
|
||||
<div class="card-sub">${t("overview.notes.subtitle")}</div>
|
||||
<div class="note-grid" style="margin-top: 14px;">
|
||||
<div>
|
||||
<div class="note-title">${t("overview.notes.tailscaleTitle")}</div>
|
||||
<div class="muted">
|
||||
${t("overview.notes.tailscaleText")}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="note-title">${t("overview.notes.sessionTitle")}</div>
|
||||
<div class="muted">${t("overview.notes.sessionText")}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="note-title">${t("overview.notes.cronTitle")}</div>
|
||||
<div class="muted">${t("overview.notes.cronText")}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user