// 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 = (
<>
Validate
{running ? (
<>
onAction("restart")}>Restart
onAction("stop")}>Stop
>
) : (
onAction("start")}>Start endpoints
)}
>
);
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("update")}
>
{updateRunning ? "Updating…" : "Update now"}
onAction("toggle-auto-update")}
label="Auto-update"
/>
navigate("updates")}>
Manage
>
)}
{/* 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.
setCcConfirm(true)}>
Apply to Claude Code
{ 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={
setCcConfirm(false)} disabled={ccBusy}>Cancel
Apply
}
/>
{/* Validation + Status breakdown */}
Run again
}
flush
>
{!validation && validating ? (
Running validation…
) : !validation || !(validation.steps && validation.steps.length) ? (
Run validation}
/>
) : (
<>
Check
Result
Duration
{validation.steps.map((s, i) => (
{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 */}
{LEVELS.map((lv) => (
setLevelOn((s) => ({ ...s, [lv.id]: !s[lv.id] }))}
>
{lv.label}
{counts[lv.id]}
))}
{paused ? "Resume" : "Pause"}
copyText} label="Copy" size="sm" />
Clear
{/* 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} }
{liveState}
{stale && lastUpdated &&
· updated {timeAgo(lastUpdated)} }
);
}
window.Dashboard = Dashboard;
window.Logs = Logs;
})();