Files
server-26/drb-frontend/lib/types.ts
T
Logan CusanoandClaude Opus 5.5 aff3f16d32 Admin Replay: re-run the pipeline over past calls in a sandbox (#170)
Correlation has only ever been measured through live AI windows: days of
wall time per change, and the 09-20→22 window was invalidated outright by
unfunded AI accounts (#169). Recordings are kept regardless of AI, so the
traffic to measure against already exists.

- internal/replay.py: runs a time range of real calls through the live
  pipeline code in original order, clock pinned per call, into
  replay_runs/{run_id}/calls|incidents. Modes: audio (re-transcribe),
  transcripts (re-extract), reuse (correlation only from a prior run's
  scenes). Simulates the idle-resolve and orphan-recorrelation sweeps on
  virtual time. No alerts, summaries, vocab, AI-health alerts or pending
  terms. One run at a time, <=5000 calls, <=7 days.
- firestore.py: ContextVar sandbox redirect for calls/incidents.
- clock.py: ContextVar-pinnable now(), used on the correlation path.
- feature_flags.py: ContextVar flag override so replay runs with live AI off.
- upload.py: scene loop extracted to _extract_and_correlate, shared by the
  live pipeline and replay so replay measures the code that runs live.
- resolved_via on every incident resolve, so a real clear can be told
  from the idle timeout — live and in replay.
- routers/replay.py + /admin Replay tab: estimate, start, compare runs,
  drill into incidents with audio.

Reviewed by drb-correlation-review; its leak and fidelity findings are
fixed and covered by tests. c2-core: 456 pass. Frontend typecheck not run
(no Node on the authoring box).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 15:23:57 -04:00

399 lines
11 KiB
TypeScript

export type NodeStatus = "online" | "offline" | "recording" | "unconfigured";
export type ApprovalStatus = "pending" | "approved" | "rejected";
export type UserRole = "admin" | "operator" | "viewer";
export interface UserRecord {
uid: string;
email: string | null;
display_name: string | null;
role: UserRole;
owned_node_ids: string[];
disabled: boolean;
creation_time: string | null;
last_sign_in: string | null;
discord_linked: boolean;
discord_username: string | null;
discord_user_id: string | null;
// only present on GET /admin/users/{uid}
sessions?: UserSession[];
// only present on POST /admin/users response
invite_link?: string | null;
}
export interface UserSession {
session_id: string;
uid: string;
email: string;
timestamp: string;
ip: string | null;
user_agent: string | null;
}
export interface AuditEntry {
log_id: string;
action: string;
actor_uid: string;
actor_email: string;
target_uid: string | null;
target_email: string | null;
details: Record<string, unknown>;
timestamp: string;
}
export interface NodeRecord {
node_id: string;
name: string;
lat: number;
lon: number;
status: NodeStatus;
configured: boolean;
last_seen: string | null;
assigned_system_id: string | null;
approval_status: ApprovalStatus | null;
hardware_preset?: string;
ppm_override?: number | null;
node_type?: string;
secondary_sdr_mode?: string;
sdr_count?: number;
enforce_override_timeout?: boolean;
is_overridden?: boolean;
override_system_id?: string | null;
override_timeout_at?: string | null;
}
export interface AircraftTrack {
icao: string;
org_id?: string;
node_id: string;
callsign: string | null;
lat: number | null;
lon: number | null;
altitude_ft: number | null;
ground_speed_kt: number | null;
track_deg: number | null;
last_seen: string;
}
export interface VesselTrack {
mmsi: string;
org_id?: string;
node_id: string;
name: string | null;
lat: number | null;
lon: number | null;
speed_kt: number | null;
heading_deg: number | null;
last_seen: string;
}
export interface VocabularyPendingTerm {
term: string;
source: "induction" | "correction";
added_at: string;
source_call_ids?: string[];
}
export interface SystemRecord {
system_id: string;
name: string;
type: string; // P25 | DMR | NBFM
config: Record<string, unknown>;
vocabulary?: string[];
vocabulary_pending?: VocabularyPendingTerm[];
vocabulary_bootstrapped?: boolean;
ten_codes?: Record<string, string>; // {"10-10": "Commercial Alarm", ...}
preferred_token_id?: string | null;
}
export interface TranscriptSegment {
start: number;
end: number;
text: string;
}
export interface CallRecord {
call_id: string;
node_id: string;
system_id: string | null;
talkgroup_id: number | null;
talkgroup_name: string | null;
freq: number | null;
started_at: string;
ended_at: string | null;
/** Private gs:// object location. Present on the Firestore doc; not playable. */
audio_gcs_uri?: string | null;
/**
* Short-lived playback link. Minted per read by the API — only populated on
* calls fetched via c2api, never on docs read straight from Firestore.
* On pre-fix docs this holds a legacy (unplayable) gs:// URI instead.
*/
audio_url: string | null;
transcript: string | null;
transcript_corrected: string | null;
segments: TranscriptSegment[] | null;
/** New: one entry per scene detected in the recording. */
incident_ids: string[];
/** Legacy field — present on calls recorded before the multi-scene migration. */
incident_id?: string | null;
location: string | null;
/** Per-call geocode, written by intelligence.py. Powers the incident-path polyline on the map. */
location_coords?: { lat: number; lng: number } | null;
/** Unit callsigns mentioned in this specific transmission (e.g. "15-9", "E-41"). */
units?: string[];
vehicles?: string[];
/** Units this call reported as clearing/back in service. */
cleared_units?: string[];
/** Set when dedup.py identifies this as a second node's recording of the same transmission — the canonical copy has this null. */
duplicate_of?: string | null;
/** Radio source address, when the system exposes it. */
srcaddr?: string | null;
tags: string[];
status: "active" | "ended";
/** Four-level ladder: routine | minor | moderate | major. Legacy docs may still carry "unknown". */
severity?: string | null;
// Correlation debug — written by the correlator, present after a call is linked
corr_path?: string | null;
corr_score?: number | null;
corr_distance_km?: number | null;
corr_incident_idle_min?: number | null;
corr_shared_units?: number | null;
corr_candidates?: number | null;
/** Human-verified reference transcript for the STT eval harness (server-26#163) — never read by anything downstream. */
eval_transcript?: string | null;
eval_transcript_by?: string | null;
eval_transcript_at?: string | null;
}
export interface IncidentRecord {
incident_id: string;
title: string | null;
type: string | null;
status: "active" | "resolved";
location: string | null;
location_coords: { lat: number; lng: number } | null;
call_ids: string[];
system_ids: string[];
talkgroup_ids: string[];
/** Omitted on incident docs written before these fields existed. */
units?: string[];
vehicles?: string[];
/** Units currently believed on scene — maintained by incident_correlator.py `_attach`. */
units_active?: string[];
/** Units that reported clearing/back in service on this incident. */
units_cleared?: string[];
/** Free-text location mentions accumulated across the incident's calls, beyond the primary `location`. */
location_mentions?: string[];
/** ISO timestamp of the last thin/status-only call attached (doesn't refresh `updated_at`). */
last_thin_at?: string | null;
severity: string | null;
started_at: string;
updated_at: string;
summary: string | null;
tags: string[];
}
export interface AlertRule {
rule_id: string;
name: string;
keywords: string[];
talkgroup_ids: number[];
enabled: boolean;
discord_webhook: string | null;
created_at?: string;
}
export interface TripEvent {
event_id: string;
trip_id: string;
title: string;
date: string;
start_time: string | null;
end_time: string | null;
location: string;
location_inherited: boolean;
maps_link: string | null;
place_id: string | null;
notes: string | null;
tags: string[];
attendees: Record<string, string>;
created_at: string;
}
export interface PlaceResult {
name: string;
address: string;
place_id: string;
lat: number;
lng: number;
maps_link: string;
rating?: number;
}
export interface TripRecord {
trip_id: string;
name: string;
location: string;
maps_link: string | null;
start_date: string;
end_date: string;
attendees: Record<string, string>;
available_tags: string[];
overlap_tags: string[];
visibility: "public" | "private";
invited_discord_ids: string[];
created_at: string;
events?: TripEvent[];
}
export interface AlertEvent {
alert_id: string;
rule_id: string;
rule_name: string;
call_id: string;
node_id: string;
talkgroup_id: number | null;
talkgroup_name: string | null;
matched_keywords: string[];
transcript_snippet: string | null;
triggered_at: string;
acknowledged: boolean;
}
/**
* Ground truth about the area a system or talkgroup covers, handed to the
* transcript corrector so it can recognise local street, town and business
* names that Whisper mangles (server-26#36).
*
* Set on the system, and optionally narrowed per talkgroup inside
* `config.talkgroups[]` — the talkgroup's entries rank ABOVE the system's, so
* a multi-county system can be specific per channel without its wider list
* burying the detail.
*/
export interface LocalKnowledgeEntry {
term: string;
meaning?: string | null;
}
export interface AreaContext {
// Set by an operator. Every field nullable on purpose: which SCOPE gets
// filled is the declaration of how homogeneous the system is. A one-town
// system is described once here and inherited by every talkgroup; a
// statewide one is left empty here and described per talkgroup.
municipality?: string;
county?: string;
state?: string;
local_knowledge?: LocalKnowledgeEntry[];
// Written by the backend, read-only here. Absent means the place is unset or
// too wide to discriminate, and location verification skips entirely
// (server-26#37) — never send these back.
center?: { lat: number; lng: number };
radius_km?: number;
resolved_from?: string;
resolved_at?: string;
}
/** A term the verifier or the induction loop proposed for one talkgroup. */
export interface PendingLocalTerm {
term: string;
meaning?: string | null;
source: string;
added_at: string;
source_call_ids?: string[];
}
export interface TalkgroupPending {
talkgroup_id: number;
talkgroup_name?: string;
pending: PendingLocalTerm[];
}
// ---------------------------------------------------------------------------
// Replay (admin) — drb-c2-core/app/internal/replay.py
// ---------------------------------------------------------------------------
export type ReplayMode = "audio" | "transcripts" | "reuse";
export interface ReplayEstimate {
calls: number;
calls_with_transcript: number;
calls_with_audio: number;
audio_minutes: number;
est_cost_usd: number;
truncated: boolean;
max_calls: number;
}
export interface ReplayMetrics {
calls: number;
calls_linked: number;
calls_orphaned: number;
incidents: number;
single_call_incidents: number;
single_call_pct: number | null;
median_calls_per_incident: number | null;
max_calls_in_incident: number | null;
incidents_with_units_cleared: number;
incidents_with_coords: number;
resolved_via: Record<string, number>;
corr_path: Record<string, number>;
corr_consensus: Record<string, number>;
llm_decisions: number;
est_cost_usd: number;
}
export interface ReplayRun {
run_id: string;
label: string;
mode: ReplayMode;
source_run_id: string | null;
date_from: string;
date_to: string;
system_ids: string[];
git_sha: string;
created_by: string;
created_at: string;
finished_at?: string;
status: "running" | "done" | "cancelled" | "failed" | "interrupted";
estimate: Omit<ReplayEstimate, "truncated" | "max_calls">;
progress: { total: number; done: number; errors: number; skipped?: number };
metrics: ReplayMetrics | null;
errors: string[];
}
export interface ReplayCallRow {
call_id: string;
started_at: string;
talkgroup_name: string | null;
transcript: string | null;
units: string[] | null;
cleared_units: string[] | null;
location: string | null;
skip_reason: string | null;
corr_path: string[];
incident_ids: string[];
}
export interface ReplayIncident {
incident_id: string;
title: string | null;
type: string | null;
severity: string | null;
status: string;
resolved_via: string | null;
started_at: string;
updated_at: string | null;
resolved_at: string | null;
location: string | null;
location_coords: { lat: number; lng: number } | null;
units: string[] | null;
units_active: string[] | null;
units_cleared: string[] | null;
talkgroup_ids: number[] | null;
calls: ReplayCallRow[];
}
export interface ReplayIncidents {
incidents: ReplayIncident[];
orphans: ReplayCallRow[];
}