"use client"; import { KeyRound, LogOut, Play, RefreshCw, ShieldAlert } from "lucide-react"; import Link from "next/link"; import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react"; import { SENSOR_TYPES, TIERS } from "@websensor/core/client"; import { useMounted } from "@/components/theme"; import { Chip, Empty, HealthPill, Panel, Skeleton, Sparkline, Stat, Table, Td } from "@/components/ui"; import { clientApiBase } from "@/lib/api"; import { FactoryPanel } from "./factory-panel"; import { fmtBytes, fmtInt, fmtMs, fmtPct, relTime, untilTime, utcDateTime, utcTime } from "@/lib/format"; const KEY = "ws_admin"; // --------------------------------------------------------------------------------------- // Types for GET /api/v1/admin/ops (kept local: internal shape, not part of the public client) // --------------------------------------------------------------------------------------- interface Ops { db: { latency_ms: number }; queue: { due: number; overdue_10m: number; p0: number; p0_due: number; oldest_due: string | null }; engine: { inflight?: number; concurrency?: number; due?: number; busyHosts?: { host: string; inflight: number }[]; circuitOpen?: { host: string; failures: number; until: string }[]; at?: string; version?: string; [k: string]: unknown } | null; outcomes_1h: { outcome: string; n: number }[]; top_failing_domains: { host: string; failures: number; sensors: number; last_error: string | null; last_at: string }[]; slowest_sensors: { id: string; name: string; source_id: string; url: string; connector: string; avg_latency_ms: number; total_runs: number }[]; throughput_24h: { t: string; checks: number; not_modified: number; errors: number; events: number; avg_ms: number | null }[]; storage: { db_size: string; db_bytes: string | number; snapshots: number; snapshots_with_raw: number; raw_bytes_uncompressed: string | number; changes: number; events: number; runs: number; blobs: { files: number; bytes: number } | null }; llm_24h: { model: string; calls: number; input_tokens: string | number; output_tokens: string | number; failures: number }[]; sensors_by_status: { status: string; health: string; n: number }[]; recent_errors: { started_at: string; sensor_id: string; source_id: string; connector: string; http_status: number | null; outcome: string; error: string | null }[]; cache: { entries: number; keys: string[] }; live: { clients: number; published: number }; connectors: { key: string; name: string; version?: string; sensorTypes?: string[]; description?: string }[]; generated_at: string; } class AdminError extends Error { status: number; body: Record; constructor(status: number, body: Record) { super(typeof body.error === "string" ? body.error : `HTTP ${status}`); this.status = status; this.body = body; } } function readToken(): string { if (typeof window === "undefined") return ""; return window.sessionStorage.getItem(KEY) ?? ""; } async function adminFetch(path: string, init: RequestInit = {}, token = readToken()): Promise { const headers: Record = { accept: "application/json", "x-websensor-admin": token, ...((init.headers as Record) ?? {}) }; if (init.body !== undefined && !headers["content-type"]) headers["content-type"] = "application/json"; const res = await fetch(`${clientApiBase()}${path}`, { ...init, headers }); const body = (await res.json().catch(() => ({}))) as Record; if (!res.ok) throw new AdminError(res.status, body); return body as T; } /** A heartbeat older than two minutes means the scheduler stopped publishing. */ function heartbeatStale(at: string | undefined): boolean { return Boolean(at) && Date.now() - new Date(at as string).getTime() > 120_000; } function gateMessage(e: unknown): string { if (e instanceof AdminError) { if (e.status === 401) return "invalid token"; if (e.status === 404) return "admin API disabled (WS_ADMIN_TOKEN not set)"; return `${e.message}${typeof e.body.detail === "string" ? ` — ${e.body.detail}` : ""}`; } return (e as Error).message; } // --------------------------------------------------------------------------------------- export function Ops() { const mounted = useMounted(); /** null = use the token stored in sessionStorage; "" = signed out; otherwise the token entered in this session. */ const [override, setOverride] = useState(null); const token: string | null = !mounted ? null : override ?? readToken(); const [gate, setGate] = useState(null); const [data, setData] = useState(null); const [err, setErr] = useState(null); const [refreshing, setRefreshing] = useState(false); const [input, setInput] = useState(""); const [tick, setTick] = useState(0); // Poll /admin/ops every 15 s while a token is present (and on manual refresh via `tick`). useEffect(() => { if (!token) return; const t = token; let cancelled = false; const pull = (): void => { adminFetch("/api/v1/admin/ops", {}, t) .then((d) => { if (cancelled) return; setData(d); setErr(null); setGate(null); }) .catch((e: unknown) => { if (cancelled) return; const msg = gateMessage(e); if (e instanceof AdminError && (e.status === 401 || e.status === 404)) { setGate(msg); setData(null); } else setErr(msg); }) .finally(() => { if (!cancelled) setRefreshing(false); }); }; pull(); const iv = setInterval(pull, 15_000); return () => { cancelled = true; clearInterval(iv); }; }, [token, tick]); const refresh = (): void => { setRefreshing(true); setTick((x) => x + 1); }; const factoryFetcher = useCallback((path: string, init: RequestInit = {}): Promise => adminFetch(path, init, token ?? ""), [token]); const submitToken = async (): Promise => { const t = input.trim(); if (!t) return; try { const d = await adminFetch("/api/v1/admin/ops", {}, t); window.sessionStorage.setItem(KEY, t); setData(d); setGate(null); setErr(null); setOverride(t); setInput(""); } catch (e) { window.sessionStorage.removeItem(KEY); setGate(gateMessage(e)); } }; const signOut = (): void => { window.sessionStorage.removeItem(KEY); setOverride(""); setData(null); setGate(null); }; if (token === null) return ; if (!token || (gate && !data)) { return (
{ e.preventDefault(); void submitToken(); }} >

Enter the admin token (WS_ADMIN_TOKEN). It is kept in sessionStorage for this tab only and sent as X-WebSensor-Admin.

setInput(e.target.value)} placeholder="admin token" autoComplete="off" aria-label="Admin token" className="h-8 min-w-0 flex-1 rounded-md border border-line bg-panel px-2 font-mono text-[12.5px]" />
{gate &&

{gate}

}

Public health is at /health.

); } return (
{data ? `generated ${utcTime(data.generated_at)} UTC · db ${data.db.latency_ms} ms · ${data.live.clients} WS clients` : "loading…"} {err && · {err}}
{data ? : }

Source Factory · discovery → shadow → acceptance

); } // --------------------------------------------------------------------------------------- // Dashboard panels // --------------------------------------------------------------------------------------- function Dashboard({ d }: { d: Ops }) { const eng = d.engine; const stale = heartbeatStale(eng?.at); const tp = d.throughput_24h; const sum = (k: "checks" | "events" | "errors" | "not_modified"): number => tp.reduce((a, r) => a + Number(r[k] ?? 0), 0); const outcomes = d.outcomes_1h; const oTotal = outcomes.reduce((a, o) => a + o.n, 0); const byStatus = useMemo(() => { const m = new Map }>(); for (const r of d.sensors_by_status) { const cur = m.get(r.status) ?? { n: 0, health: {} }; cur.n += r.n; cur.health[r.health] = (cur.health[r.health] ?? 0) + r.n; m.set(r.status, cur); } return [...m.entries()].sort((a, b) => b[1].n - a[1].n); }, [d.sensors_by_status]); const raw = Number(d.storage.raw_bytes_uncompressed); const blobBytes = d.storage.blobs?.bytes ?? 0; return ( <>
0 ? "warn" : undefined} hint={`${fmtInt(d.queue.overdue_10m)} overdue > 10 min`} /> 0 ? "hot" : undefined} hint={d.queue.oldest_due ? `oldest ${untilTime(d.queue.oldest_due)}` : "—"} /> 0 ? "warn" : undefined} hint={`${eng?.busyHosts?.length ?? 0} busy hosts`} />
{eng ? (
`${h.host} ×${h.inflight}`).join(", ") : "none"} /> `${h.host} (${h.failures}, until ${utcTime(h.until, false)})`).join(", ") : "none"} tone={eng.circuitOpen?.length ? "text-warn" : ""} />
) : ( No heartbeat in Redis — the engine is not running or cannot reach Redis. )}
{outcomes.length ? (
    {outcomes.map((o) => (
  • {o.outcome}
    {fmtInt(o.n)}
  • ))}
) : ( No runs in the last hour. )}
{tp.length ? (
r.checks)} tone="info" /> r.events)} tone="signal" /> r.errors)} tone="hot" />
) : ( No sensor runs recorded in the last 24 h. )} {tp.length > 0 && (
{[...tp].reverse().map((r) => ( ))}
{r.t.slice(5, 16).replace("T", " ")} {fmtInt(r.checks)} {fmtInt(r.not_modified)} 0 ? "text-danger" : ""}>{fmtInt(r.errors)} {fmtInt(r.events)} {fmtMs(r.avg_ms)}
)}
{byStatus.length ? ( {byStatus.map(([status, v]) => ( ))}
{status} {fmtInt(v.n)}
{Object.entries(v.health).sort((a, b) => b[1] - a[1]).map(([h, n]) => ( {fmtInt(n)} ))}
) : ( )}
{d.llm_24h.length ? ( {d.llm_24h.map((m) => ( ))}
{m.model} {fmtInt(m.calls)} {fmtInt(Number(m.input_tokens))} {fmtInt(Number(m.output_tokens))} {fmtInt(m.failures)}
) : ( No LLM calls in 24 h (heuristics only). )}
{d.cache.keys.slice(0, 40).map((k) => {k})}
{d.top_failing_domains.length ? ( {d.top_failing_domains.map((f) => ( ))}
{f.host} {fmtInt(f.failures)} {fmtInt(f.sensors)} {f.last_error ?? "—"} {relTime(f.last_at)}
) : ( No failures in the last 6 h. )}
{d.slowest_sensors.length ? ( {d.slowest_sensors.map((s) => ( ))}
{s.name}
{s.url}
{s.source_id} {s.connector} 10_000 ? "text-warn" : ""}>{fmtMs(s.avg_latency_ms)} {fmtInt(s.total_runs)}
) : ( )}
{d.recent_errors.length ? ( {d.recent_errors.map((r, i) => ( ))}
{utcTime(r.started_at)} {r.sensor_id} {r.source_id} {r.connector} {r.http_status ?? "—"} {r.outcome} {r.error ?? "—"}
) : ( No errors in the last hour. )}
); } function Row({ k, v, tone = "" }: { k: string; v: ReactNode; tone?: string }) { return ( <>
{k}
{v}
); } function Spark({ label, values, tone }: { label: string; values: number[]; tone: "signal" | "hot" | "info" }) { const last = values[values.length - 1] ?? 0; const max = Math.max(...values, 0); return (
{label} last h {fmtInt(last)} · max {fmtInt(max)}
); } // --------------------------------------------------------------------------------------- // Tools // --------------------------------------------------------------------------------------- type Json = Record; function Result({ r }: { r: { ok: boolean; body: Json | null; error?: string } | null }) { if (!r) return null; return (
{r.error &&
{r.error}
} {r.body &&
{JSON.stringify(r.body, null, 2)}
}
); } function useAction() { const [busy, setBusy] = useState(false); const [res, setRes] = useState<{ ok: boolean; body: Json | null; error?: string } | null>(null); const run = async (fn: () => Promise): Promise => { setBusy(true); setRes(null); try { const body = await fn(); setRes({ ok: true, body }); return body; } catch (e) { setRes({ ok: false, body: e instanceof AdminError ? e.body : null, error: gateMessage(e) }); return null; } finally { setBusy(false); } }; return { busy, res, run }; } const input = "h-8 min-w-0 rounded-md border border-line bg-panel px-2 font-mono text-[12px]"; const btn = "inline-flex h-8 items-center gap-1 rounded-md border border-line bg-panel-2 px-2.5 text-[12px] hover:border-line-strong disabled:opacity-50"; function Tools({ connectors, onDone }: { connectors: Ops["connectors"]; onDone: () => void }) { return ( <>

Tools

); } function TestConnector({ connectors }: { connectors: Ops["connectors"] }) { const [f, setF] = useState({ url: "", sensor_id: "", connector: "http", type: "HTML", config: "{}" }); const { busy, res, run } = useAction(); const preview = res?.body?.normalized as Json | undefined; const meta = res?.body?.meta as Json | undefined; const submit = (): void => { let config: Json = {}; try { config = f.config.trim() ? (JSON.parse(f.config) as Json) : {}; } catch { void run(() => Promise.reject(new Error("config is not valid JSON"))); return; } void run(() => adminFetch("/api/v1/admin/sensors/test", { method: "POST", body: JSON.stringify({ ...(f.sensor_id.trim() ? { sensor_id: f.sensor_id.trim() } : { url: f.url.trim() }), connector: f.connector, type: f.type, config }) })); }; return (
{ e.preventDefault(); submit(); }} > setF({ ...f, url: e.target.value })} placeholder="https://example.com/feed.xml" aria-label="URL" className={input} /> setF({ ...f, sensor_id: e.target.value })} placeholder="… or an existing sensor id (overrides URL)" aria-label="Sensor id" className={input} />