"use client"; // --------------------------------------------------------------------------- // Replay — re-run the intelligence pipeline over a past time range into a // sandbox, then compare runs. Backend: drb-c2-core/app/internal/replay.py. // Nothing here touches a live call or incident; every run spends real AI // credits, so the form estimates first and only then offers Start. // --------------------------------------------------------------------------- import { useCallback, useEffect, useState } from "react"; import { c2api } from "@/lib/c2api"; import { useSystems } from "@/lib/useSystems"; import type { ReplayEstimate, ReplayIncident, ReplayIncidents, ReplayMode, ReplayRun, ReplayCallRow, } from "@/lib/types"; const MODES: { key: ReplayMode; label: string; help: string }[] = [ { key: "transcripts", label: "Saved transcripts", help: "Reuse each call's transcript; re-run extraction + correlation." }, { key: "audio", label: "Re-transcribe audio", help: "Whisper the saved audio again, then extract + correlate. For ranges where AI was off." }, { key: "reuse", label: "Correlation only", help: "Reuse an earlier run's extraction; re-run correlation only. Isolates a correlator change." }, ]; // Clears that came from the radio traffic itself, vs the 90-minute idle timer. 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"; 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"; function toLocalInput(d: Date): string { const pad = (n: number) => String(n).padStart(2, "0"); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`; } function fmtTime(iso: string | null | undefined): string { if (!iso) return "—"; return new Date(iso).toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }); } function sum(rec: Record | undefined, keys: string[]): number { return keys.reduce((n, k) => n + (rec?.[k] ?? 0), 0); } // --------------------------------------------------------------------------- function NewRunForm({ runs, busy, onStarted }: { runs: ReplayRun[]; busy: boolean; onStarted: () => void }) { const { systems } = useSystems(); const [from, setFrom] = useState(() => toLocalInput(new Date(Date.now() - 6 * 3600_000))); const [to, setTo] = useState(() => toLocalInput(new Date())); const [mode, setMode] = useState("transcripts"); const [systemIds, setSystemIds] = useState([]); const [sourceRun, setSourceRun] = useState(""); const [label, setLabel] = useState(""); const [est, setEst] = useState(null); const [working, setWorking] = useState(false); const [error, setError] = useState(null); // Any change to what would be replayed invalidates the estimate. useEffect(() => { setEst(null); }, [from, to, mode, systemIds]); const sources = runs.filter((r) => r.status === "done" && r.mode !== "reuse"); const range = () => ({ date_from: new Date(from).toISOString(), date_to: new Date(to).toISOString() }); async function estimate() { setWorking(true); setError(null); try { setEst(await c2api.estimateReplay({ ...range(), mode, system_ids: systemIds })); } catch (e) { setError(String(e)); } finally { setWorking(false); } } async function start() { setWorking(true); setError(null); try { await c2api.startReplay({ ...range(), mode, system_ids: systemIds, label, source_run_id: mode === "reuse" ? sourceRun : null, }); setEst(null); setLabel(""); onStarted(); } catch (e) { setError(String(e)); } finally { setWorking(false); } } function toggleSystem(id: string) { setSystemIds((s) => (s.includes(id) ? s.filter((x) => x !== id) : [...s, id])); } const canStart = est && !est.truncated && est.calls > 0 && !busy && (mode !== "reuse" || sourceRun); return (
setFrom(e.target.value)} className={input} />
setTo(e.target.value)} className={input} />
setLabel(e.target.value)} placeholder="what changed?" className={`${input} w-56`} />
{MODES.map((m) => ( ))}
{mode === "reuse" && (

Set the range to match that run; calls outside it are skipped.

)} {systems.length > 1 && (
Systems (none = all): {systems.map((s) => ( ))}
)}
{busy && a run is in progress}
{est && (

{est.truncated ? ( More than {est.max_calls} calls — narrow the range. ) : ( <> {est.calls} calls ·{" "} {est.calls_with_transcript} with transcripts · {est.audio_minutes} audio min ·{" "} ~${est.est_cost_usd.toFixed(2)} est. {mode === "transcripts" && est.calls_with_transcript < est.calls / 2 && ( — most calls have no transcript; consider Re-transcribe audio. )} )}

)} {error &&

{error}

}
); } // --------------------------------------------------------------------------- function RunsTable({ runs, activeId, selected, onSelect, onChanged, }: { runs: ReplayRun[]; activeId: string | null; selected: string | null; onSelect: (id: string) => void; onChanged: () => void; }) { async function cancel(id: string) { await c2api.cancelReplay(id).catch(() => undefined); onChanged(); } async function remove(id: string) { if (!window.confirm("Delete this run and its sandbox incidents?")) return; await c2api.deleteReplay(id).catch(() => undefined); onChanged(); } if (!runs.length) return

No runs yet.

; const th = "text-left text-xs text-gray-500 font-normal px-2 py-1.5 whitespace-nowrap"; const td = "px-2 py-1.5 whitespace-nowrap"; return (
{runs.map((r) => { const m = r.metrics; const running = r.run_id === activeId; return ( 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"}`} > ); })}
Run Range Status Inc 1-call Orphans Real clears / timeouts LLM Cost
{r.label || r.run_id}
{r.mode} · {r.git_sha?.slice(0, 7)} · {fmtTime(r.created_at)}
{fmtTime(r.date_from)} → {fmtTime(r.date_to)} {running ? ( {r.progress.done}/{r.progress.total} ) : ( {r.status} )} {r.progress.errors > 0 && · {r.progress.errors} err} {m?.incidents ?? "—"} {m?.single_call_pct != null ? `${m.single_call_pct}%` : "—"} {m ? `${m.calls_orphaned}/${m.calls}` : "—"} {m ? ( <> {sum(m.resolved_via, SIGNAL_RESOLVES)} {" / "} {m.resolved_via.idle_timeout ?? 0} ) : "—"} {m?.llm_decisions ?? "—"} {m ? `$${m.est_cost_usd.toFixed(2)}` : `~$${r.estimate.est_cost_usd.toFixed(2)}`} e.stopPropagation()}> {running ? ( ) : ( )}
); } // --------------------------------------------------------------------------- function CallLine({ call }: { call: ReplayCallRow }) { const [audio, setAudio] = useState(null); async function play() { try { const c = await c2api.getCall(call.call_id); setAudio(c.audio_url); } catch { /* audio is a convenience */ } } return (
{fmtTime(call.started_at)} {call.talkgroup_name} {call.corr_path.map((p, i) => {p})} {call.units?.length ? units {call.units.join(", ")} : null} {call.cleared_units?.length ? cleared {call.cleared_units.join(", ")} : null} {call.skip_reason && {call.skip_reason}} {audio ? (
{call.transcript || (no transcript)}
); } function IncidentCard({ inc }: { inc: ReplayIncident }) { const [open, setOpen] = useState(false); const signal = inc.resolved_via && SIGNAL_RESOLVES.includes(inc.resolved_via); return (
{open &&
{inc.calls.map((c) => )}
}
); } function RunDetail({ run }: { run: ReplayRun }) { const [data, setData] = useState(null); const [error, setError] = useState(null); const [filter, setFilter] = useState<"all" | "multi" | "single">("all"); const [showOrphans, setShowOrphans] = useState(false); useEffect(() => { setData(null); setError(null); if (run.status === "running") return; c2api.getReplayIncidents(run.run_id).then(setData).catch((e) => setError(String(e))); }, [run.run_id, run.status]); const m = run.metrics; const shown = (data?.incidents ?? []).filter((i) => filter === "all" ? true : filter === "multi" ? i.calls.length > 1 : i.calls.length === 1, ); return (
{m && (
{[ ["Linked calls", `${m.calls_linked}/${m.calls}`], ["Median calls / incident", m.median_calls_per_incident ?? "—"], ["With units cleared", m.incidents_with_units_cleared], ["With map pin", m.incidents_with_coords], ].map(([k, v]) => (
{k}
{v}
))}
)} {m && (

resolved: {Object.entries(m.resolved_via).map(([k, v]) => `${k} ${v}`).join(" · ")}
paths: {Object.entries(m.corr_path).map(([k, v]) => `${k} ${v}`).join(" · ")}

)} {run.errors?.length > 0 && (
{run.errors.length} error(s) {run.errors.map((e, i) =>
{e}
)}
)} {run.status === "running" &&

Incidents appear when the run finishes.

} {error &&

{error}

} {data && ( <>
{(["all", "multi", "single"] as const).map((f) => ( ))}
{showOrphans ? (
{data.orphans.map((c) => )}
) : (
{shown.map((i) => )}
)} )}
); } // --------------------------------------------------------------------------- export function ReplayTab() { const [runs, setRuns] = useState([]); const [activeId, setActiveId] = useState(null); const [selected, setSelected] = useState(null); const [error, setError] = useState(null); const load = useCallback(async () => { try { const res = await c2api.listReplays(); setRuns(res.runs); setActiveId(res.active_run_id); setError(null); } catch (e) { setError(String(e)); } }, []); useEffect(() => { load(); }, [load]); // Poll only while something is running. useEffect(() => { if (!activeId) return; const t = setInterval(load, 4000); return () => clearInterval(t); }, [activeId, load]); const run = runs.find((r) => r.run_id === selected) ?? null; return (

Re-run the pipeline over a past time range, in the calls' 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.

{error &&

{error}

} {run && }
); }