// screens-monitor.jsx — Worker A: Dashboard + Logs (the data-viz centerpiece). // Built on Phase-2 primitives (window.*) + tokens from base.css only. // Per-screen CSS lives in screens-monitor.css (.dash- / .logs- prefixes). // Screens receive props: { liveStatus, statusError, statusStale, // refreshStatus, onAction, navigate, pushToast } (() => { const { useState, useEffect, useRef, useMemo, useCallback } = React; const { Icon, api, useLive, fmtUptime, fmtCount, fmtPct, fmtMs, timeAgo, PageHeader, Card, StatCard, Button, IconButton, Badge, Tabs, Switch, Input, CopyButton, SecretField, KeyValue, Sparkline, AreaChart, Skeleton, Spinner, EmptyState, useToast, Modal, } = window; const cx = (...p) => p.filter(Boolean).join(" "); const ZERO60 = Array.from({ length: 60 }, () => 0); const num = (v) => (v == null || isNaN(Number(v)) ? 0 : Number(v)); /* ════════════════════════════════════════════════════════════════ DASHBOARD ════════════════════════════════════════════════════════════════ */ function Dashboard({ liveStatus, statusStale, onAction, navigate, pushToast }) { const [chartTab, setChartTab] = useState("requests"); // On-demand validation (GET /ui/api/validate) — same contract as old code. const [validation, setValidation] = useState(null); // {ok, ran_at, model, upstream_model, steps, duration_total} const [validating, setValidating] = useState(false); const runValidation = useCallback(() => { setValidating(true); // Per-upstream round-trips can take ~15-20s (agy is a subprocess), so allow // well beyond the 10s default; surface failures instead of silently blanking. api.get("/ui/api/validate", { timeoutMs: 60000 }) .then((res) => setValidation(res || null)) .catch((e) => { if (pushToast) pushToast("Validation failed: " + (e.message || e), "error"); }) .finally(() => setValidating(false)); }, []); useEffect(() => { runValidation(); }, [runValidation]); // Manual "Apply to Claude Code" — start/stop scripts no longer auto-sync settings. const toast = useToast(); const ccNotify = useCallback((msg, tone) => { if (toast) { tone === "error" ? toast.error(msg) : tone === "success" ? toast.success(msg) : toast.info(msg); } else if (pushToast) pushToast(msg, tone || "info"); }, [toast, pushToast]); const [ccApplied, setCcApplied] = useState(null); // null = unknown const [ccBusy, setCcBusy] = useState(false); const [ccConfirm, setCcConfirm] = useState(false); const refreshCc = useCallback(() => { api.get("/ui/api/settings/apply").then((r) => setCcApplied(!!r.applied)).catch(() => {}); }, []); useEffect(() => { refreshCc(); }, [refreshCc]); const applyCc = useCallback(async () => { setCcBusy(true); try { const r = await api.post("/ui/api/settings/apply"); setCcApplied(true); setCcConfirm(false); ccNotify((r && r.message) || "Applied proxy settings to Claude Code", "success"); } catch (e) { ccNotify("Apply failed: " + (e.message || e), "error"); } finally { setCcBusy(false); } }, [ccNotify]); const loading = !liveStatus; const running = !!(liveStatus && liveStatus.proxy_running); const dashboard = (liveStatus && liveStatus.dashboard) || {}; const traffic = dashboard.traffic || {}; const sparks = dashboard.sparks || {}; const update = (liveStatus && liveStatus.update) || {}; // Endpoint URLs (replicates old derivation, prefers server-provided fields). const rootUrl = (liveStatus && (liveStatus.display_url || liveStatus.public_url || liveStatus.local_url)) || "http://127.0.0.1:4000"; const anthropicUrl = (liveStatus && liveStatus.anthropic_url) || rootUrl + "/anthropic"; const openaiUrl = (liveStatus && liveStatus.openai_url) || rootUrl + "/openai/v1"; const isPublic = !!(liveStatus && liveStatus.public_url); // Sparklines. const reqSpark = sparks.requests || ZERO60; const latSpark = sparks.latency || ZERO60; const tokSpark = sparks.tokens || ZERO60; const errSpark = sparks.errors || ZERO60; // StatCard values. const reqPerMin = num(dashboard.requests_per_min); const reqDelta = dashboard.requests_delta_pct; const avgLatency = num(dashboard.avg_latency_ms); const tokPerMin = num(dashboard.tokens_per_min); const inTok = num(dashboard.input_tokens); const outTok = num(dashboard.output_tokens); const errRate = num(dashboard.error_rate); const errCount = num(dashboard.error_count); // Chart series — map the active spark to labeled points (−60s … now). const chartConf = { requests: { data: reqSpark, tone: "accent", fmt: (v) => fmtCount(v), unit: "requests" }, latency: { data: latSpark, tone: "info", fmt: (v) => fmtMs(v), unit: "avg latency" }, tokens: { data: tokSpark, tone: "success", fmt: (v) => fmtCount(v), unit: "tokens" }, errors: { data: errSpark, tone: "danger", fmt: (v) => fmtCount(v), unit: "errors" }, }[chartTab]; const chartData = useMemo(() => { const arr = chartConf.data || ZERO60; const n = arr.length; const points = arr.map((v, i) => { const secsAgo = n - 1 - i; return { value: num(v), label: secsAgo === 0 ? "now" : "−" + secsAgo + "s" }; }); // All-zero window → render the chart's empty state instead of a flat line. const hasSignal = points.some((p) => p.value > 0); return hasSignal ? points : []; }, [chartConf, chartTab]); // Status breakdown bars. const breakdown = [ { key: "2xx", label: "2xx success", tone: "success", count: num(traffic.status_2xx), pct: num(traffic.status_2xx_pct) }, { key: "4xx", label: "4xx client", tone: "warning", count: num(traffic.status_4xx), pct: num(traffic.status_4xx_pct) }, { key: "5xx", label: "5xx server", tone: "danger", count: num(traffic.status_5xx), pct: num(traffic.status_5xx_pct) }, { key: "streamed", label: "Streamed (SSE)", tone: "info", count: num(traffic.streamed), pct: num(traffic.streamed_pct) }, ]; const breakdownTotal = num(traffic.total) || (num(traffic.status_2xx) + num(traffic.status_4xx) + num(traffic.status_5xx)); // System card. const codexAuth = (liveStatus && liveStatus.codex_auth) || {}; const updateAvailable = !!update.update_available; const updateRunning = !!update.running; const currentVersion = update.current_version || (liveStatus && liveStatus.version) || "—"; const latestVersion = update.latest_version || ""; const autoUpdate = !!update.auto_update; const headerActions = ( <> {running ? ( <> ) : ( )} ); return (
Dashboard {loading ? ( ) : ( {running ? "Running" : "Stopped"} )} {running && liveStatus && liveStatus.uptime_seconds != null && ( up {fmtUptime(liveStatus.uptime_seconds)} )} } description="Live overview of proxy health, traffic and the Claude Code → Codex round-trip." actions={headerActions} /> {/* Stat cards */}
0 ? fmtCount(reqPerMin) + " req sampled" : "no traffic yet"} spark={latSpark} sparkTone="info" loading={loading} /> 0 ? fmtCount(errCount) + " err" : "clean")} deltaTone={errCount > 0 ? "danger" : "success"} sub={errCount > 0 ? fmtCount(errCount) + " in window" : "no errors"} spark={errSpark} sparkTone="danger" loading={loading} />
{/* Traffic chart */} } > {loading ? ( ) : ( )} {/* Endpoints + System */}
Anthropic {loading ? : {anthropicUrl}}
OpenAI {loading ? : {openaiUrl}}
Local API key {loading ? ( ) : ( )}
{isPublic ? "Public HTTPS" : "Local-only · 127.0.0.1"} {isPublic ? "The local app stays private; clients use the public URL." : "Bound to loopback for local clients."}
{loading ? (
{[0, 1, 2, 3, 4].map((i) => )}
) : ( <> {liveStatus.upstream || "codex"} chatgpt subscription ), }, { label: "Codex auth", value: ( <> {codexAuth.exists ? ( Found · {codexAuth.mode || "unknown"} ) : ( Missing )} {codexAuth.path || "~/.codex/auth.json"} ), }, { label: "Version", value: ( <> {String(currentVersion).replace(/^v/, "v")} {updateRunning ? ( Updating ) : updateAvailable ? ( v{String(latestVersion).replace(/^v/, "")} available ) : ( Up to date )} ), }, ]} />
onAction("toggle-auto-update")} label="Auto-update" />
)}
{/* Claude Code integration (manual settings apply) */} {ccApplied == null ? "—" : ccApplied ? "Applied" : "Not applied"} } >
Writes ANTHROPIC_BASE_URL, the auth token and model defaults into{" "} ~/.claude/settings.json. This session keeps working; restart Claude Code to pick up the change.
{ if (!ccBusy) setCcConfirm(false); }} title="Apply settings to Claude Code?" description="This rewrites ~/.claude/settings.json so Claude Code routes through this proxy. New Claude Code sessions use it after a restart; the proxy server itself is unaffected." footer={
} /> {/* Validation + Status breakdown */}
Run again } flush > {!validation && validating ? (
Running validation…
) : !validation || !(validation.steps && validation.steps.length) ? ( Run validation} /> ) : ( <> {validation.steps.map((s, i) => ( ))}
Check Result Duration
{s.name} {s.skipped ? "skipped" : `${num(s.status) || (s.ok ? 200 : 0)} ${s.ok ? "OK" : "FAIL"}`} {fmtMs(num(s.duration_ms))}
{validation.ok ? "All checks passed" : "Some checks failed"} · ran {validation.ran_at || "—"} · {validation.upstream_model || validation.model || "—"}
)}
{loading ? (
{[0, 1, 2, 3].map((i) => )}
) : breakdownTotal === 0 ? ( ) : (
{breakdown.map((b) => (
{b.label} {b.count} · {fmtPct(b.pct)}
0 ? 2 : 0, Math.min(100, b.pct)) + "%" }} />
))}
)}
); } /* ════════════════════════════════════════════════════════════════ LOGS ════════════════════════════════════════════════════════════════ */ const SOURCE_TABS = [ { id: "requests", label: "Requests", icon: "logs" }, { id: "stdout", label: "stdout", icon: "terminal" }, { id: "stderr", label: "stderr", icon: "terminal" }, { id: "trace", label: "trace", icon: "activity" }, ]; const LEVELS = [ { id: "info", label: "info", tone: "info" }, { id: "warn", label: "warn", tone: "warning" }, { id: "error", label: "error", tone: "danger" }, ]; // Normalize a row level to one of the three filter buckets. const levelBucket = (lvl) => { const l = String(lvl || "").toLowerCase(); if (l === "error" || l === "err" || l === "fatal") return "error"; if (l === "warn" || l === "warning") return "warn"; return "info"; // info, debug, trace, "" → info bucket }; const statusTone = (status) => { const s = num(status); if (s === 0) return "subtle"; if (s >= 500) return "danger"; if (s >= 400) return "warning"; if (s >= 300) return "info"; return "success"; }; function Logs() { const { data, stale, reconnecting, lastUpdated } = useLive("/ui/api/logs", { interval: 1200 }); const [source, setSource] = useState("requests"); const [levelOn, setLevelOn] = useState({ info: true, warn: true, error: true }); const [query, setQuery] = useState(""); const [autoScroll, setAutoScroll] = useState(true); const [paused, setPaused] = useState(false); // Frozen snapshot while paused; "cleared" hides everything up to a watermark. const [frozen, setFrozen] = useState(null); const [clearedAt, setClearedAt] = useState(0); // ignore rows with .at <= this const scrollRef = useRef(null); const pinnedRef = useRef(true); // user is pinned to the bottom const live = data || {}; const snapshot = paused && frozen ? frozen : live; const allRows = Array.isArray(snapshot.rows) ? snapshot.rows : []; // Drop rows cleared locally (watermark on row .at timestamp). const visibleRows = useMemo( () => (clearedAt > 0 ? allRows.filter((r) => num(r.at) > clearedAt) : allRows), [allRows, clearedAt] ); // Live level counts (over the request rows, post-clear). const counts = useMemo(() => { const c = { info: 0, warn: 0, error: 0 }; visibleRows.forEach((r) => { c[levelBucket(r.lvl)]++; }); return c; }, [visibleRows]); // Wired search + level filtering for the structured (requests) view. const q = query.trim().toLowerCase(); const filteredRows = useMemo(() => { return visibleRows.filter((r) => { if (!levelOn[levelBucket(r.lvl)]) return false; if (!q) return true; const hay = [r.path, r.note, r.meth, r.model, r.upstream, r.status != null ? String(r.status) : ""] .filter(Boolean).join(" ").toLowerCase(); return hay.includes(q); }); }, [visibleRows, levelOn, q]); // Raw text views (stdout/stderr/trace), greppable by the same search box. const rawText = useMemo(() => { if (source === "requests") return ""; const txt = snapshot[source] || ""; if (!q) return txt; const matched = txt.split("\n").filter((line) => line.toLowerCase().includes(q)); return matched.join("\n"); }, [source, snapshot, q]); const rawLineCount = useMemo(() => { if (source === "requests") return 0; const txt = (paused && frozen ? frozen : live)[source] || ""; return txt ? txt.split("\n").filter((l) => l.length > 0).length : 0; }, [source, live, frozen, paused]); // Smart pin: track whether the user is near the bottom; re-pin within 40px. const onScroll = useCallback(() => { const el = scrollRef.current; if (!el) return; const dist = el.scrollHeight - el.scrollTop - el.clientHeight; pinnedRef.current = dist < 40; // Disable auto-scroll when the user scrolls up; re-enable when re-pinned. setAutoScroll((cur) => (pinnedRef.current ? true : cur && false)); }, []); // Auto-scroll to bottom on new content when pinned + enabled. useEffect(() => { const el = scrollRef.current; if (!el || !autoScroll || !pinnedRef.current) return; el.scrollTop = el.scrollHeight; }, [filteredRows, rawText, autoScroll, source]); const togglePause = () => { setPaused((p) => { const next = !p; if (next) setFrozen(live); // freeze the current snapshot else setFrozen(null); return next; }); }; const onToggleAutoScroll = (val) => { setAutoScroll(val); if (val) { pinnedRef.current = true; const el = scrollRef.current; if (el) el.scrollTop = el.scrollHeight; } }; const clearView = () => { // Local-only clear: watermark the latest row time + remember raw lengths. const latest = allRows.reduce((mx, r) => Math.max(mx, num(r.at)), 0); setClearedAt(latest || Date.now()); }; // Copy current view as text. const copyText = source === "requests" ? filteredRows.map((r) => { const st = r.status ? " " + r.status : ""; const du = r.dur ? " " + r.dur + "ms" : ""; const meth = r.meth ? r.meth.padEnd(5) : " "; return (r.ts || "") + " " + levelBucket(r.lvl).toUpperCase().padEnd(5) + " " + meth + (r.path || "") + st + du + (r.note ? " " + r.note : ""); }).join("\n") : rawText; const totalLines = source === "requests" ? visibleRows.length : rawLineCount; const shownLines = source === "requests" ? filteredRows.length : (rawText ? rawText.split("\n").filter((l) => l.length).length : 0); const liveState = paused ? "paused" : reconnecting ? "reconnecting" : "live"; const isRaw = source !== "requests"; const emptyStructured = filteredRows.length === 0; const emptyRaw = !rawText; return (
Logs {paused ? "Paused" : reconnecting ? "Reconnecting" : "Live"} } description="Request stream, process stdout/stderr and structured trace — updated every 1.2s." /> {/* Toolbar */}
setQuery(e.target.value)} aria-label="Filter log lines" /> {query && ( setQuery("")} /> )}
{LEVELS.map((lv) => ( ))}
copyText} label="Copy" size="sm" />
{/* Log body */}
{isRaw ? ( emptyRaw ? ( ) : (
{rawText}
) ) : emptyStructured ? ( ) : (
{filteredRows.map((r, i) => { const bucket = levelBucket(r.lvl); return (
{r.ts || ""} {bucket} {r.meth && {r.meth}} {r.path && {r.path}} {r.status != null && r.status !== 0 && ( {r.status} )} {r.dur != null && r.dur !== 0 && {r.dur}ms} {r.model && {r.model}} {r.upstream && →{r.upstream}} {(num(r.input_tokens) > 0 || num(r.output_tokens) > 0) && ( {num(r.input_tokens)}↓ {num(r.output_tokens)}↑ )} {r.stream && SSE} {r.note && {r.note}}
); })}
)}
{/* Footer */}
{totalLines} {totalLines === 1 ? "line" : "lines"} {shownLines !== totalLines && · showing {shownLines}}
); } window.Dashboard = Dashboard; window.Logs = Logs; })();