fix(frontend): finish the token migration — raw grey classes -> theme tokens

UI_REDESIGN chunk 4 was never finished: 39 app/component files still used
raw Tailwind greys, kept readable in light mode only by the !important
override sheet. In dark mode text-gray-600 is ~2.3:1 on surface, and the
light override maps it to #94a3b8 (~2.6:1 on white) — unreadable in both.

Mechanical mapping (886 classes): gray-100/200 -> ink, 300/400 -> ink-2,
500/600/700 -> ink-muted, bg 950/900/800 -> page/surface/raised,
border 800 -> line, 700/600 -> line-strong, plus hover/divide/placeholder.
Alpha suffixes kept (tokens are color-mix, so /60 works). bg-gray-700,
text-white and marketing pages left alone. text-[9px] -> text-[10px].

Frontend unverified: no Node on the authoring box (#207).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Logan Cusano
2026-10-25 21:29:13 -04:00
co-authored by Claude Opus 5.5
parent 3968b49b80
commit bf206334e3
39 changed files with 655 additions and 655 deletions
@@ -58,16 +58,16 @@ export function CameraProvidersTab() {
return (
<section className="space-y-3">
<p className="text-gray-400 text-xs leading-snug">
<p className="text-ink-2 text-xs leading-snug">
Public camera sources for the Live wall. Every provider is <b>off</b> until enabled here. Licence status comes from the
camera-sources catalog (2026-10-04); a red or amber licence means DRB does not hold a confirmed right to show that imagery.
Required attributions render on every camera tile.
</p>
{error && <div className="bg-red-950 border border-red-800 rounded-lg p-3 text-red-400 text-sm font-mono break-words">{error}</div>}
{!providers ? (
<p className="text-gray-500 text-sm font-mono">Loading…</p>
<p className="text-ink-muted text-sm font-mono">Loading…</p>
) : (
<div className="bg-gray-900 border border-gray-800 rounded-xl divide-y divide-gray-800">
<div className="bg-surface border border-line rounded-xl divide-y divide-line">
{providers.map((p) => {
const lic = LICENCE_STYLE[p.licence.status];
const lockReason = !p.implemented ? "No working adapter (stub)" : p.licence.status === "prohibited" ? "Source terms forbid this use" : null;
@@ -77,8 +77,8 @@ export function CameraProvidersTab() {
<div className="min-w-0 space-y-1">
<span className={`inline-block text-xs font-semibold px-2 py-0.5 rounded border ${lic.cls}`}>{lic.label}</span>
<p className="text-white text-sm font-semibold">{p.name}</p>
<p className="text-gray-400 text-xs">{p.licence.label}</p>
<p className="text-gray-500 text-xs">{p.region} · {p.formats}</p>
<p className="text-ink-2 text-xs">{p.licence.label}</p>
<p className="text-ink-muted text-xs">{p.region} · {p.formats}</p>
</div>
<div className="shrink-0 text-right space-y-1">
<button
@@ -93,25 +93,25 @@ export function CameraProvidersTab() {
>
<span className={`inline-block h-4 w-4 rounded-full bg-white shadow transition-transform ${p.enabled ? "translate-x-6" : "translate-x-1"}`} />
</button>
<p className={`text-[11px] font-mono ${p.serving ? "text-green-400" : "text-gray-500"}`}>
<p className={`text-[11px] font-mono ${p.serving ? "text-green-400" : "text-ink-muted"}`}>
{p.serving ? "serving" : lockReason ?? p.not_serving_reason ?? "off"}
</p>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-[11px] font-mono text-gray-400">
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-[11px] font-mono text-ink-2">
<span>cameras: {p.health ? p.health.count : "—"}</span>
<span>last fetch: {fmtEpoch(p.health?.fetched_at)}</span>
<span className={p.health?.error ? "text-red-400" : ""}>health: {p.health ? (p.health.error ? "error" : p.health.fetched_at ? "ok" : "not fetched yet") : "n/a"}</span>
<span className={p.key_missing ? "text-amber-400" : ""}>{p.key_setting ? `key: ${p.key_missing ? "missing" : "set"}` : "no key needed"}</span>
</div>
{p.health?.error && <p className="text-[11px] font-mono text-red-400 break-words">{p.health.error}</p>}
{p.licence.attribution && <p className="text-[11px] text-gray-400">Attribution shown with imagery: “{p.licence.attribution}”</p>}
{p.licence.attribution && <p className="text-[11px] text-ink-2">Attribution shown with imagery: “{p.licence.attribution}”</p>}
{p.acknowledgement && (
<p className="text-[11px] text-amber-300">
Licence warning acknowledged by {p.acknowledgement.by_email || p.acknowledgement.by_uid} on {new Date(p.acknowledgement.at).toLocaleString()}
</p>
)}
<details className="text-[11px] text-gray-500">
<details className="text-[11px] text-ink-muted">
<summary className="cursor-pointer select-none">Terms & adapter notes</summary>
<p className="mt-1">{p.licence.terms}</p>
<p className="mt-1">{p.notes}</p>
@@ -129,16 +129,16 @@ export function CameraProvidersTab() {
{confirming && (
<div className="fixed inset-0 z-50 bg-black/70 flex items-center justify-center p-4" role="alertdialog" aria-modal="true" aria-labelledby="cam-ack-title">
<div className="bg-gray-900 border border-red-700 rounded-xl max-w-lg w-full p-5 space-y-3">
<div className="bg-surface border border-red-700 rounded-xl max-w-lg w-full p-5 space-y-3">
<h2 id="cam-ack-title" className="text-white font-semibold">Enable {confirming.name}?</h2>
<p className="text-red-300 text-sm font-semibold">{LICENCE_STYLE[confirming.licence.status].label}</p>
<p className="text-gray-300 text-sm leading-snug">{confirming.licence.warning}</p>
<label className="flex items-start gap-2 text-sm text-gray-200">
<p className="text-ink-2 text-sm leading-snug">{confirming.licence.warning}</p>
<label className="flex items-start gap-2 text-sm text-ink">
<input type="checkbox" checked={ackChecked} onChange={(e) => setAckChecked(e.target.checked)} className="mt-1" />
<span>I confirm DRB holds the right to show this source&apos;s imagery. This confirmation is recorded with my account and the time in the audit log.</span>
</label>
<div className="flex justify-end gap-2">
<button type="button" onClick={() => setConfirming(null)} className="text-sm px-3 py-1.5 rounded border border-gray-700 text-gray-300">Cancel</button>
<button type="button" onClick={() => setConfirming(null)} className="text-sm px-3 py-1.5 rounded border border-line-strong text-ink-2">Cancel</button>
<button type="button" disabled={!ackChecked || busy === confirming.id} onClick={() => toggle(confirming, true, true)}
className="text-sm px-3 py-1.5 rounded bg-red-700 text-white disabled:opacity-40">Enable and record</button>
</div>
@@ -60,7 +60,7 @@ export function OutageProvidersTab() {
return (
<section className="space-y-3">
<p className="text-gray-400 text-xs leading-snug">
<p className="text-ink-2 text-xs leading-snug">
Power-outage sources for the map. Every provider is <b>off</b> until enabled here. Licence status comes from the
source-terms research on server-26#184 (2026-10-05); a red or amber licence means DRB does not hold a confirmed right
to show that data. Scraped utility outage maps are never shipped — those sources stay locked until a data-sharing
@@ -68,9 +68,9 @@ export function OutageProvidersTab() {
</p>
{error && <div className="bg-red-950 border border-red-800 rounded-lg p-3 text-red-400 text-sm font-mono break-words">{error}</div>}
{!providers ? (
<p className="text-gray-500 text-sm font-mono">Loading…</p>
<p className="text-ink-muted text-sm font-mono">Loading…</p>
) : (
<div className="bg-gray-900 border border-gray-800 rounded-xl divide-y divide-gray-800">
<div className="bg-surface border border-line rounded-xl divide-y divide-line">
{providers.map((p) => {
const lic = LICENCE_STYLE[p.licence.status];
const lockReason = !p.implemented ? "No working adapter (stub)" : p.licence.status === "prohibited" ? "Source terms forbid this use" : null;
@@ -80,8 +80,8 @@ export function OutageProvidersTab() {
<div className="min-w-0 space-y-1">
<span className={`inline-block text-xs font-semibold px-2 py-0.5 rounded border ${lic.cls}`}>{lic.label}</span>
<p className="text-white text-sm font-semibold">{p.name}</p>
<p className="text-gray-400 text-xs">{p.licence.label}</p>
<p className="text-gray-500 text-xs">{p.region} · {p.granularity}</p>
<p className="text-ink-2 text-xs">{p.licence.label}</p>
<p className="text-ink-muted text-xs">{p.region} · {p.granularity}</p>
</div>
<div className="shrink-0 text-right space-y-1">
<button
@@ -96,27 +96,27 @@ export function OutageProvidersTab() {
>
<span className={`inline-block h-4 w-4 rounded-full bg-white shadow transition-transform ${p.enabled ? "translate-x-6" : "translate-x-1"}`} />
</button>
<p className={`text-[11px] font-mono ${p.serving ? "text-green-400" : "text-gray-500"}`}>
<p className={`text-[11px] font-mono ${p.serving ? "text-green-400" : "text-ink-muted"}`}>
{p.serving ? "serving" : lockReason ?? p.not_serving_reason ?? "off"}
</p>
</div>
</div>
{p.implemented && (
<div className="grid grid-cols-2 md:grid-cols-3 gap-2 text-[11px] font-mono text-gray-400">
<div className="grid grid-cols-2 md:grid-cols-3 gap-2 text-[11px] font-mono text-ink-2">
<span>counties out: {p.health ? p.health.count : "—"}</span>
<span>last fetch: {fmtEpoch(p.health?.fetched_at)}</span>
<span className={p.health?.error ? "text-red-400" : ""}>health: {p.health ? (p.health.error ? "error" : p.health.fetched_at ? "ok" : "not fetched yet") : "n/a"}</span>
</div>
)}
{p.health?.error && <p className="text-[11px] font-mono text-red-400 break-words">{p.health.error}</p>}
{p.coverage_note && <p className="text-[11px] text-gray-400">{p.coverage_note}</p>}
{p.licence.attribution && <p className="text-[11px] text-gray-400">Attribution shown with the data: “{p.licence.attribution}”</p>}
{p.coverage_note && <p className="text-[11px] text-ink-2">{p.coverage_note}</p>}
{p.licence.attribution && <p className="text-[11px] text-ink-2">Attribution shown with the data: “{p.licence.attribution}”</p>}
{p.acknowledgement && (
<p className="text-[11px] text-amber-300">
Licence warning acknowledged by {p.acknowledgement.by_email || p.acknowledgement.by_uid} on {new Date(p.acknowledgement.at).toLocaleString()}
</p>
)}
<details className="text-[11px] text-gray-500">
<details className="text-[11px] text-ink-muted">
<summary className="cursor-pointer select-none">Terms & adapter notes</summary>
<p className="mt-1">{p.licence.terms}</p>
<p className="mt-1">{p.notes}</p>
@@ -134,17 +134,17 @@ export function OutageProvidersTab() {
{confirming && (
<div className="fixed inset-0 z-50 bg-black/70 flex items-center justify-center p-4" role="alertdialog" aria-modal="true" aria-labelledby="outage-ack-title">
<div className="bg-gray-900 border border-red-700 rounded-xl max-w-lg w-full p-5 space-y-3">
<div className="bg-surface border border-red-700 rounded-xl max-w-lg w-full p-5 space-y-3">
<h2 id="outage-ack-title" className="text-white font-semibold">Enable {confirming.name}?</h2>
<p className="text-red-300 text-sm font-semibold">{LICENCE_STYLE[confirming.licence.status].label}</p>
<p className="text-gray-300 text-sm leading-snug">{confirming.licence.warning}</p>
<p className="text-ink-2 text-sm leading-snug">{confirming.licence.warning}</p>
{error && <p className="text-red-400 text-xs font-mono break-words">{error}</p>}
<label className="flex items-start gap-2 text-sm text-gray-200">
<label className="flex items-start gap-2 text-sm text-ink">
<input type="checkbox" checked={ackChecked} onChange={(e) => setAckChecked(e.target.checked)} className="mt-1" />
<span>I confirm DRB holds the right to show this source&apos;s outage data. This confirmation is recorded with my account and the time in the audit log.</span>
</label>
<div className="flex justify-end gap-2">
<button type="button" onClick={() => setConfirming(null)} className="text-sm px-3 py-1.5 rounded border border-gray-700 text-gray-300">Cancel</button>
<button type="button" onClick={() => setConfirming(null)} className="text-sm px-3 py-1.5 rounded border border-line-strong text-ink-2">Cancel</button>
<button type="button" disabled={!ackChecked || busy === confirming.id} onClick={() => toggle(confirming, true, true)}
className="text-sm px-3 py-1.5 rounded bg-red-700 text-white disabled:opacity-40">Enable and record</button>
</div>
+39 -39
View File
@@ -24,9 +24,9 @@ const MODES: { key: ReplayMode; label: string; help: string }[] = [
const SIGNAL_RESOLVES = ["units_cleared", "llm_closure", "reassignment", "children_resolved"];
const input =
"bg-gray-800 border border-gray-700 rounded-lg px-3 py-1.5 text-white text-sm font-mono focus:outline-none focus:border-indigo-500";
"bg-raised border border-line-strong rounded-lg px-3 py-1.5 text-white text-sm font-mono focus:outline-none focus:border-indigo-500";
const btn =
"bg-gray-800 hover:bg-gray-700 disabled:opacity-50 border border-gray-700 text-white text-sm font-mono px-4 py-1.5 rounded-lg transition-colors";
"bg-raised hover:bg-gray-700 disabled:opacity-50 border border-line-strong text-white text-sm font-mono px-4 py-1.5 rounded-lg transition-colors";
function toLocalInput(d: Date): string {
const pad = (n: number) => String(n).padStart(2, "0");
@@ -88,18 +88,18 @@ function NewRunForm({ runs, busy, onStarted }: { runs: ReplayRun[]; busy: boolea
const canStart = est && !est.truncated && est.calls > 0 && !busy && (mode !== "reuse" || sourceRun);
return (
<div className="bg-gray-900 border border-gray-800 rounded-xl p-4 space-y-4">
<div className="bg-surface border border-line rounded-xl p-4 space-y-4">
<div className="flex flex-wrap items-end gap-4">
<div>
<label className="text-xs text-gray-400 block mb-1">From</label>
<label className="text-xs text-ink-2 block mb-1">From</label>
<input type="datetime-local" value={from} max={to} onChange={(e) => setFrom(e.target.value)} className={input} />
</div>
<div>
<label className="text-xs text-gray-400 block mb-1">To</label>
<label className="text-xs text-ink-2 block mb-1">To</label>
<input type="datetime-local" value={to} min={from} onChange={(e) => setTo(e.target.value)} className={input} />
</div>
<div>
<label className="text-xs text-gray-400 block mb-1">Label</label>
<label className="text-xs text-ink-2 block mb-1">Label</label>
<input value={label} onChange={(e) => setLabel(e.target.value)} placeholder="what changed?" className={`${input} w-56`} />
</div>
</div>
@@ -109,14 +109,14 @@ function NewRunForm({ runs, busy, onStarted }: { runs: ReplayRun[]; busy: boolea
<label key={m.key} className="flex items-start gap-2 text-sm font-mono cursor-pointer">
<input type="radio" checked={mode === m.key} onChange={() => setMode(m.key)} className="mt-1" />
<span className="text-white">{m.label}</span>
<span className="text-gray-500 text-xs mt-0.5">{m.help}</span>
<span className="text-ink-muted text-xs mt-0.5">{m.help}</span>
</label>
))}
</div>
{mode === "reuse" && (
<div>
<label className="text-xs text-gray-400 block mb-1">Reuse extraction from</label>
<label className="text-xs text-ink-2 block mb-1">Reuse extraction from</label>
<select value={sourceRun} onChange={(e) => setSourceRun(e.target.value)} className={input}>
<option value="">— pick a finished run —</option>
{sources.map((r) => (
@@ -125,15 +125,15 @@ function NewRunForm({ runs, busy, onStarted }: { runs: ReplayRun[]; busy: boolea
</option>
))}
</select>
<p className="text-xs text-gray-500 mt-1">Set the range to match that run; calls outside it are skipped.</p>
<p className="text-xs text-ink-muted mt-1">Set the range to match that run; calls outside it are skipped.</p>
</div>
)}
{systems.length > 1 && (
<div className="flex flex-wrap gap-3">
<span className="text-xs text-gray-400">Systems (none = all):</span>
<span className="text-xs text-ink-2">Systems (none = all):</span>
{systems.map((s) => (
<label key={s.system_id} className="flex items-center gap-1 text-xs font-mono text-gray-300 cursor-pointer">
<label key={s.system_id} className="flex items-center gap-1 text-xs font-mono text-ink-2 cursor-pointer">
<input type="checkbox" checked={systemIds.includes(s.system_id)} onChange={() => toggleSystem(s.system_id)} />
{s.name}
</label>
@@ -154,7 +154,7 @@ function NewRunForm({ runs, busy, onStarted }: { runs: ReplayRun[]; busy: boolea
</div>
{est && (
<p className="text-sm font-mono text-gray-300">
<p className="text-sm font-mono text-ink-2">
{est.truncated ? (
<span className="text-red-400">More than {est.max_calls} calls — narrow the range.</span>
) : (
@@ -193,15 +193,15 @@ function RunsTable({
onChanged();
}
if (!runs.length) return <p className="text-sm text-gray-500 font-mono">No runs yet.</p>;
if (!runs.length) return <p className="text-sm text-ink-muted font-mono">No runs yet.</p>;
const th = "text-left text-xs text-gray-500 font-normal px-2 py-1.5 whitespace-nowrap";
const th = "text-left text-xs text-ink-muted font-normal px-2 py-1.5 whitespace-nowrap";
const td = "px-2 py-1.5 whitespace-nowrap";
return (
<div className="overflow-x-auto">
<table className="w-full text-sm font-mono">
<thead>
<tr className="border-b border-gray-800">
<tr className="border-b border-line">
<th className={th}>Run</th>
<th className={th}>Range</th>
<th className={th}>Status</th>
@@ -222,13 +222,13 @@ function RunsTable({
<tr
key={r.run_id}
onClick={() => onSelect(r.run_id)}
className={`border-b border-gray-800/60 cursor-pointer ${selected === r.run_id ? "bg-gray-800/60" : "hover:bg-gray-900"}`}
className={`border-b border-line/60 cursor-pointer ${selected === r.run_id ? "bg-raised/60" : "hover:bg-raised"}`}
>
<td className={td}>
<div className="text-white">{r.label || r.run_id}</div>
<div className="text-xs text-gray-500">{r.mode} · {r.git_sha?.slice(0, 7)} · {fmtTime(r.created_at)}</div>
<div className="text-xs text-ink-muted">{r.mode} · {r.git_sha?.slice(0, 7)} · {fmtTime(r.created_at)}</div>
</td>
<td className={`${td} text-xs text-gray-400`}>{fmtTime(r.date_from)} → {fmtTime(r.date_to)}</td>
<td className={`${td} text-xs text-ink-2`}>{fmtTime(r.date_from)} → {fmtTime(r.date_to)}</td>
<td className={td}>
{running ? (
<span className="text-amber-400">{r.progress.done}/{r.progress.total}</span>
@@ -245,7 +245,7 @@ function RunsTable({
<>
<span className="text-green-400">{sum(m.resolved_via, SIGNAL_RESOLVES)}</span>
{" / "}
<span className="text-gray-400">{m.resolved_via.idle_timeout ?? 0}</span>
<span className="text-ink-2">{m.resolved_via.idle_timeout ?? 0}</span>
</>
) : "—"}
</td>
@@ -255,7 +255,7 @@ function RunsTable({
{running ? (
<button onClick={() => cancel(r.run_id)} className="text-xs text-amber-400 hover:text-amber-300">cancel</button>
) : (
<button onClick={() => remove(r.run_id)} className="text-xs text-gray-500 hover:text-red-400">delete</button>
<button onClick={() => remove(r.run_id)} className="text-xs text-ink-muted hover:text-red-400">delete</button>
)}
</td>
</tr>
@@ -278,21 +278,21 @@ function CallLine({ call }: { call: ReplayCallRow }) {
} catch { /* audio is a convenience */ }
}
return (
<div className="py-1.5 border-t border-gray-800/60 text-xs font-mono">
<div className="flex flex-wrap gap-2 text-gray-500">
<div className="py-1.5 border-t border-line/60 text-xs font-mono">
<div className="flex flex-wrap gap-2 text-ink-muted">
<span>{fmtTime(call.started_at)}</span>
<span>{call.talkgroup_name}</span>
{call.corr_path.map((p, i) => <span key={i} className="text-indigo-400">{p}</span>)}
{call.units?.length ? <span>units {call.units.join(", ")}</span> : null}
{call.cleared_units?.length ? <span className="text-green-400">cleared {call.cleared_units.join(", ")}</span> : null}
{call.skip_reason && <span className="text-gray-600">{call.skip_reason}</span>}
{call.skip_reason && <span className="text-ink-muted">{call.skip_reason}</span>}
{audio ? (
<audio src={audio} controls autoPlay className="h-6" />
) : (
<button onClick={play} className="text-gray-400 hover:text-white">▶ audio</button>
<button onClick={play} className="text-ink-2 hover:text-white">▶ audio</button>
)}
</div>
<div className="text-gray-200 mt-0.5">{call.transcript || <span className="text-gray-600">(no transcript)</span>}</div>
<div className="text-ink mt-0.5">{call.transcript || <span className="text-ink-muted">(no transcript)</span>}</div>
</div>
);
}
@@ -301,13 +301,13 @@ function IncidentCard({ inc }: { inc: ReplayIncident }) {
const [open, setOpen] = useState(false);
const signal = inc.resolved_via && SIGNAL_RESOLVES.includes(inc.resolved_via);
return (
<div className="bg-gray-900 border border-gray-800 rounded-lg px-3 py-2">
<div className="bg-surface border border-line rounded-lg px-3 py-2">
<button onClick={() => setOpen(!open)} className="w-full text-left flex flex-wrap items-center gap-x-3 gap-y-1 text-sm font-mono">
<span className="text-gray-500">{open ? "▾" : "▸"}</span>
<span className="text-ink-muted">{open ? "▾" : "▸"}</span>
<span className="text-white">{inc.title || inc.incident_id}</span>
<span className="text-gray-500 text-xs">{inc.calls.length} call{inc.calls.length !== 1 ? "s" : ""}</span>
<span className="text-gray-500 text-xs">{fmtTime(inc.started_at)} → {fmtTime(inc.resolved_at)}</span>
<span className={`text-xs ${signal ? "text-green-400" : "text-gray-500"}`}>{inc.resolved_via ?? inc.status}</span>
<span className="text-ink-muted text-xs">{inc.calls.length} call{inc.calls.length !== 1 ? "s" : ""}</span>
<span className="text-ink-muted text-xs">{fmtTime(inc.started_at)} → {fmtTime(inc.resolved_at)}</span>
<span className={`text-xs ${signal ? "text-green-400" : "text-ink-muted"}`}>{inc.resolved_via ?? inc.status}</span>
{inc.location_coords && <span className="text-xs text-indigo-400">📍 {inc.location}</span>}
</button>
{open && <div className="mt-2">{inc.calls.map((c) => <CallLine key={c.call_id} call={c} />)}</div>}
@@ -347,22 +347,22 @@ function RunDetail({ run }: { run: ReplayRun }) {
["With units cleared", m.incidents_with_units_cleared],
["With map pin", m.incidents_with_coords],
].map(([k, v]) => (
<div key={k as string} className="bg-gray-900 border border-gray-800 rounded-lg p-2">
<div className="text-gray-500">{k}</div>
<div key={k as string} className="bg-surface border border-line rounded-lg p-2">
<div className="text-ink-muted">{k}</div>
<div className="text-white text-base">{v}</div>
</div>
))}
</div>
)}
{m && (
<p className="text-xs font-mono text-gray-500">
<p className="text-xs font-mono text-ink-muted">
resolved: {Object.entries(m.resolved_via).map(([k, v]) => `${k} ${v}`).join(" · ")}
<br />
paths: {Object.entries(m.corr_path).map(([k, v]) => `${k} ${v}`).join(" · ")}
</p>
)}
{m?.gemini_usage && Object.keys(m.gemini_usage).length > 0 && (
<p className="text-xs font-mono text-gray-500">
<p className="text-xs font-mono text-ink-muted">
Gemini tokens:{" "}
{Object.entries(m.gemini_usage)
.map(([k, u]) => `${k} ${u.calls} calls, in ${u.in.toLocaleString()} / out ${u.out.toLocaleString()} / thinking ${u.thinking.toLocaleString()}`)
@@ -380,7 +380,7 @@ function RunDetail({ run }: { run: ReplayRun }) {
{run.errors.map((e, i) => <div key={i}>{e}</div>)}
</details>
)}
{run.status === "running" && <p className="text-sm text-gray-500 font-mono">Incidents appear when the run finishes.</p>}
{run.status === "running" && <p className="text-sm text-ink-muted font-mono">Incidents appear when the run finishes.</p>}
{error && <p className="text-red-400 text-sm font-mono">{error}</p>}
{data && (
<>
@@ -389,20 +389,20 @@ function RunDetail({ run }: { run: ReplayRun }) {
<button
key={f}
onClick={() => setFilter(f)}
className={`px-3 py-1 rounded-md ${filter === f ? "bg-gray-800 text-white" : "text-gray-500 hover:text-gray-300"}`}
className={`px-3 py-1 rounded-md ${filter === f ? "bg-raised text-white" : "text-ink-muted hover:text-ink"}`}
>
{f === "all" ? `All (${data.incidents.length})` : f === "multi" ? "Multi-call" : "Single-call"}
</button>
))}
<button
onClick={() => setShowOrphans(!showOrphans)}
className={`px-3 py-1 rounded-md ${showOrphans ? "bg-gray-800 text-white" : "text-gray-500 hover:text-gray-300"}`}
className={`px-3 py-1 rounded-md ${showOrphans ? "bg-raised text-white" : "text-ink-muted hover:text-ink"}`}
>
Orphans ({data.orphans.length})
</button>
</div>
{showOrphans ? (
<div className="bg-gray-900 border border-gray-800 rounded-lg px-3 py-2">
<div className="bg-surface border border-line rounded-lg px-3 py-2">
{data.orphans.map((c) => <CallLine key={c.call_id} call={c} />)}
</div>
) : (
@@ -443,7 +443,7 @@ export function ReplayTab() {
return (
<div className="space-y-5">
<p className="text-xs text-gray-500 font-mono">
<p className="text-xs text-ink-muted font-mono">
Re-run the pipeline over a past time range, in the calls&apos; original order, into a sandbox — live incidents are never
touched. Replay the same range after each change and compare the rows below. A run starts with no incidents
open, so its first ~90 minutes split more than live did — compare runs over the same range, not a run against live.