openclaw/ui/src/ui/views/overview.ts
2026-01-02 12:06:05 -06:00

174 lines
6.2 KiB
TypeScript

import { html } from "lit";
import type { GatewayHelloOk } from "../gateway";
import { formatAgo, formatDurationMs } from "../format";
import { formatNextRun } from "../presenter";
import type { UiSettings } from "../storage";
export type OverviewProps = {
connected: boolean;
hello: GatewayHelloOk | null;
settings: UiSettings;
password: string;
lastError: string | null;
presenceCount: number;
sessionsCount: number | null;
cronEnabled: boolean | null;
cronNext: number | null;
lastProvidersRefresh: number | null;
onSettingsChange: (next: UiSettings) => void;
onPasswordChange: (next: string) => void;
onSessionKeyChange: (next: string) => void;
onRefresh: () => void;
};
export function renderOverview(props: OverviewProps) {
const snapshot = props.hello?.snapshot as
| { uptimeMs?: number; policy?: { tickIntervalMs?: number } }
| undefined;
const uptime = snapshot?.uptimeMs ? formatDurationMs(snapshot.uptimeMs) : "n/a";
const tick = snapshot?.policy?.tickIntervalMs
? `${snapshot.policy.tickIntervalMs}ms`
: "n/a";
return html`
<section class="grid grid-cols-2">
<div class="card">
<div class="card-title">Gateway Access</div>
<div class="card-sub">Where the dashboard connects and how it authenticates.</div>
<div class="form-grid" style="margin-top: 16px;">
<label class="field">
<span>WebSocket URL</span>
<input
.value=${props.settings.gatewayUrl}
@input=${(e: Event) => {
const v = (e.target as HTMLInputElement).value;
props.onSettingsChange({ ...props.settings, gatewayUrl: v });
}}
placeholder="ws://100.x.y.z:18789"
/>
</label>
<label class="field">
<span>Gateway Token</span>
<input
.value=${props.settings.token}
@input=${(e: Event) => {
const v = (e.target as HTMLInputElement).value;
props.onSettingsChange({ ...props.settings, token: v });
}}
placeholder="CLAWDIS_GATEWAY_TOKEN"
/>
</label>
<label class="field">
<span>Password (not stored)</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"
/>
</label>
<label class="field">
<span>Default Session Key</span>
<input
.value=${props.settings.sessionKey}
@input=${(e: Event) => {
const v = (e.target as HTMLInputElement).value;
props.onSessionKeyChange(v);
}}
/>
</label>
</div>
<div class="row" style="margin-top: 14px;">
<button class="btn" @click=${() => props.onRefresh()}>Refresh</button>
<span class="muted">Reconnect to apply changes.</span>
</div>
</div>
<div class="card">
<div class="card-title">Snapshot</div>
<div class="card-sub">Latest gateway handshake information.</div>
<div class="stat-grid" style="margin-top: 16px;">
<div class="stat">
<div class="stat-label">Status</div>
<div class="stat-value ${props.connected ? "ok" : "warn"}">
${props.connected ? "Connected" : "Disconnected"}
</div>
</div>
<div class="stat">
<div class="stat-label">Uptime</div>
<div class="stat-value">${uptime}</div>
</div>
<div class="stat">
<div class="stat-label">Tick Interval</div>
<div class="stat-value">${tick}</div>
</div>
<div class="stat">
<div class="stat-label">Last Providers Refresh</div>
<div class="stat-value">
${props.lastProvidersRefresh
? formatAgo(props.lastProvidersRefresh)
: "n/a"}
</div>
</div>
</div>
${props.lastError
? html`<div class="callout danger" style="margin-top: 14px;">
${props.lastError}
</div>`
: html`<div class="callout" style="margin-top: 14px;">
Use Connections to link WhatsApp, Telegram, Discord, Signal, or iMessage.
</div>`}
</div>
</section>
<section class="grid grid-cols-3" style="margin-top: 18px;">
<div class="card stat-card">
<div class="stat-label">Instances</div>
<div class="stat-value">${props.presenceCount}</div>
<div class="muted">Presence beacons in the last 5 minutes.</div>
</div>
<div class="card stat-card">
<div class="stat-label">Sessions</div>
<div class="stat-value">${props.sessionsCount ?? "n/a"}</div>
<div class="muted">Recent session keys tracked by the gateway.</div>
</div>
<div class="card stat-card">
<div class="stat-label">Cron</div>
<div class="stat-value">
${props.cronEnabled == null
? "n/a"
: props.cronEnabled
? "Enabled"
: "Disabled"}
</div>
<div class="muted">Next wake ${formatNextRun(props.cronNext)}</div>
</div>
</section>
<section class="card" style="margin-top: 18px;">
<div class="card-title">Notes</div>
<div class="card-sub">Quick reminders for remote control setups.</div>
<div class="note-grid" style="margin-top: 14px;">
<div>
<div class="note-title">Tailscale serve</div>
<div class="muted">
Prefer serve mode to keep the gateway on loopback with tailnet auth.
</div>
</div>
<div>
<div class="note-title">Session hygiene</div>
<div class="muted">Use /new or sessions.patch to reset context.</div>
</div>
<div>
<div class="note-title">Cron reminders</div>
<div class="muted">Use isolated sessions for recurring runs.</div>
</div>
</div>
</section>
`;
}