Checkin now carries disk_free_bytes/disk_total_bytes. Stored on the node doc (not overwritten by older nodes that don't send them). Node page shows a Disk row; node cards warn amber under 15% free, red under 5% or 2 GB. A full disk stopped node-002 recording for 17 days while it showed Online. Frontend unverified locally (no Node, #207); CI build typechecks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
730 lines
22 KiB
TypeScript
730 lines
22 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;
|
|
/** The org whose data this user can read; null = none (sees nothing). */
|
|
org_id?: string | null;
|
|
org_role?: "owner" | "member" | 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;
|
|
}
|
|
|
|
/** One conventional channel as the node reports it (node-26#24). */
|
|
export interface RadioChannel {
|
|
name: string;
|
|
freq: number | null; // Hz
|
|
nac: number | null;
|
|
encrypted: boolean; // the channel's most recent call was encrypted
|
|
encrypted_now: boolean;
|
|
enc_calls: number;
|
|
clear_calls: number;
|
|
algid: number | null;
|
|
keyid: number | null;
|
|
voice: boolean;
|
|
}
|
|
|
|
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; // legacy; priority[0] on current nodes
|
|
/** Ordered decoders for the SDRs beyond OP25's, run top-down (node-26#9). */
|
|
secondary_sdr_priority?: string[];
|
|
/** What the node's last checkin reported actually running. */
|
|
secondary_sdr_running?: string[] | null;
|
|
/** node-26#11: service (op25/adsb/ais) -> dongle serial; absent = automatic. */
|
|
sdr_pins?: Record<string, string>;
|
|
/** Dongles the node detected, from its checkin; null/absent = not reported. */
|
|
sdr_devices?: SdrDevice[] | null;
|
|
/** The dongle OP25 is actually using, per the node. */
|
|
op25_sdr_serial?: string | null;
|
|
sdr_count?: number | null; // null = never reported
|
|
/** Host disk the node records to (node-26#41); null/absent = not reported. */
|
|
disk_free_bytes?: number | null;
|
|
disk_total_bytes?: number | null;
|
|
/** node-26#24: conventional channels from the last checkin; [] on a trunked system. */
|
|
radio_channels?: RadioChannel[] | null;
|
|
enforce_override_timeout?: boolean;
|
|
is_overridden?: boolean;
|
|
override_system_id?: string | null;
|
|
override_timeout_at?: string | null;
|
|
/** node-26#15 host updater: last "Update Node" request, and what the node did with it. */
|
|
update_requested_at?: string | null;
|
|
update_applied_at?: string | null;
|
|
update_result?: "ok" | "failed" | null;
|
|
/** Last HTTPS check-in from the host updater — alive even when MQTT is not. */
|
|
updater_last_check_at?: string | null;
|
|
updater_sha?: string | null;
|
|
}
|
|
|
|
export interface SdrDevice {
|
|
index: number;
|
|
serial: string | null;
|
|
name: string;
|
|
duplicate_serial: boolean;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/** One point of an aircraft's flight path — aircraft/{icao}/positions. */
|
|
export interface AircraftTrailPoint {
|
|
lat: number;
|
|
lon: number;
|
|
altitude_ft: number | null;
|
|
t: 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;
|
|
}
|
|
|
|
// 511NY traffic layer (server-26#183) — GET /traffic/511, cached server-side,
|
|
// not Firestore. *_local times are New York local, no offset: display only.
|
|
/**
|
|
* One public camera from any enabled provider (GET /cameras, live ops wall).
|
|
* `id` is provider-prefixed ("ny511:NYSDOT-1"). `heading_deg` is APPROXIMATE —
|
|
* most sources publish the road's direction of travel, not the camera heading.
|
|
*/
|
|
export interface Camera {
|
|
id: string;
|
|
provider: string;
|
|
lat: number;
|
|
lon: number;
|
|
name: string;
|
|
roadway: string;
|
|
direction: string;
|
|
heading_deg: number | null;
|
|
image_url: string | null;
|
|
video_url: string | null;
|
|
}
|
|
/** Kept for older call sites; every camera is now a provider camera. */
|
|
export type Ny511Camera = Camera;
|
|
|
|
/** What must travel with a provider's imagery: credit, link, pass-through disclaimer. */
|
|
export interface CameraCredit {
|
|
attribution: string | null;
|
|
attribution_url: string | null;
|
|
disclaimer: string | null;
|
|
}
|
|
|
|
export interface CameraProviderFeed extends CameraCredit {
|
|
id: string;
|
|
name: string;
|
|
fetched_at: number | null; // epoch s of the provider's last successful catalog fetch
|
|
error: string | null;
|
|
total_in_bbox: number;
|
|
truncated: boolean;
|
|
}
|
|
|
|
export interface CamerasResponse {
|
|
cameras: Camera[];
|
|
providers: CameraProviderFeed[];
|
|
serving_count: number; // providers enabled anywhere (0 → "none enabled" empty state)
|
|
}
|
|
|
|
export type LicenceStatus =
|
|
| "commercial_ok" | "attribution_required" | "agreement_required"
|
|
| "non_commercial" | "unverified" | "paid" | "prohibited";
|
|
|
|
/** Admin → Camera providers (GET /admin/camera-providers). */
|
|
export interface CameraProviderAdmin {
|
|
id: string;
|
|
name: string;
|
|
region: string;
|
|
formats: string;
|
|
implemented: boolean;
|
|
notes: string;
|
|
homepage: string | null;
|
|
licence: {
|
|
status: LicenceStatus;
|
|
label: string;
|
|
terms: string;
|
|
terms_url: string | null;
|
|
attribution: string | null;
|
|
attribution_url: string | null;
|
|
disclaimer: string | null;
|
|
issue: string | null;
|
|
gated: boolean;
|
|
warning: string | null;
|
|
};
|
|
enabled: boolean;
|
|
serving: boolean;
|
|
not_serving_reason: string | null;
|
|
acknowledgement: { by_uid: string; by_email: string; at: string; licence_status: string; warning: string } | null;
|
|
updated_at: string | null;
|
|
updated_by: { uid: string; email: string } | null;
|
|
key_setting: string | null;
|
|
key_missing: boolean;
|
|
health: { fetched_at: number | null; last_attempt_at: number | null; error: string | null; count: number } | null;
|
|
}
|
|
|
|
// Power outages (server-26#184) — GET /outages, cached server-side, not Firestore.
|
|
/**
|
|
* One county with active outages from an enabled provider. `id` is
|
|
* provider-prefixed ("odin:36067"). Counts are utility-reported meters, used
|
|
* as customers. `lat`/`lon` is the centre of the county's bounding box — a
|
|
* label point, not an outage location. Times are ISO-8601 UTC.
|
|
*/
|
|
export interface OutageArea {
|
|
id: string;
|
|
provider: string;
|
|
fips: string;
|
|
county: string | null;
|
|
state: string | null;
|
|
customers_out: number; // live events only
|
|
/** Meters on events the source still lists whose own ETR passed >24h ago (ODIN keeps months-old rows). */
|
|
stale_customers_out: number;
|
|
stale: boolean; // every event in the county is stale: dim or hide, never show as live
|
|
customers_served: number | null; // null: no county-level denominator from the source
|
|
etr: string | null; // latest estimated restoration across the county's outages
|
|
since: string | null; // earliest reported start
|
|
updated_at: string | null; // newest time a reporting utility here last sent data
|
|
lat: number | null;
|
|
lon: number | null;
|
|
bbox: [number, number, number, number] | null; // south, west, north, east
|
|
utilities: { name: string | null; eia_id: string | null; customers_out: number; stale_customers_out: number }[];
|
|
}
|
|
|
|
export interface OutageProviderFeed extends CameraCredit {
|
|
id: string;
|
|
name: string;
|
|
granularity: string;
|
|
coverage_note: string | null;
|
|
covers_bbox: boolean;
|
|
fetched_at: number | null; // epoch s of the provider's last successful fetch
|
|
error: string | null;
|
|
total_in_bbox: number;
|
|
truncated: boolean;
|
|
}
|
|
|
|
export interface OutagesResponse {
|
|
areas: OutageArea[];
|
|
providers: OutageProviderFeed[];
|
|
serving_count: number;
|
|
/** Why the list is what it is — an empty layer is never ambiguous. */
|
|
status: "ok" | "none_enabled" | "no_source" | "error";
|
|
message: string | null;
|
|
}
|
|
|
|
/** Admin → Outage providers (GET /admin/outage-providers). */
|
|
export interface OutageProviderAdmin {
|
|
id: string;
|
|
name: string;
|
|
region: string;
|
|
granularity: string;
|
|
implemented: boolean;
|
|
notes: string;
|
|
homepage: string | null;
|
|
licence: CameraProviderAdmin["licence"];
|
|
enabled: boolean;
|
|
serving: boolean;
|
|
not_serving_reason: string | null;
|
|
acknowledgement: CameraProviderAdmin["acknowledgement"];
|
|
updated_at: string | null;
|
|
updated_by: { uid: string; email: string } | null;
|
|
coverage_note: string | null;
|
|
health: CameraProviderAdmin["health"];
|
|
}
|
|
|
|
export interface Ny511Event {
|
|
id: string;
|
|
lat: number;
|
|
lon: number;
|
|
type: string;
|
|
subtype: string;
|
|
severity: string;
|
|
roadway: string;
|
|
direction: string;
|
|
county: string;
|
|
description: string;
|
|
start_local: string | null;
|
|
planned_end_local: string | null;
|
|
updated_local: string | null;
|
|
}
|
|
|
|
export interface Ny511FeedStatus {
|
|
fetched_at: number | null; // epoch seconds of the last successful 511NY fetch
|
|
error: string | null; // set when the latest refresh failed (data is then stale)
|
|
total_in_bbox: number;
|
|
truncated: boolean;
|
|
/** True when an admin has not enabled the 511NY provider (default OFF, DAA-gated). */
|
|
disabled?: boolean;
|
|
}
|
|
|
|
export interface Ny511Response {
|
|
cameras?: Camera[];
|
|
cameras_status?: Ny511FeedStatus;
|
|
events?: Ny511Event[];
|
|
events_status?: Ny511FeedStatus;
|
|
/** DAA: source credit + NYSDOT as-is disclaimer, present when the feed is served. */
|
|
credit?: CameraCredit;
|
|
}
|
|
|
|
// ── Weather context (server-26#237) — GET /weather/alerts, GET /weather/wind ──
|
|
|
|
/** GeoJSON (Multi)Polygon, [lon, lat] order. */
|
|
export type WeatherGeometry =
|
|
| { type: "Polygon"; coordinates: number[][][] }
|
|
| { type: "MultiPolygon"; coordinates: number[][][][] };
|
|
|
|
/** One active NWS alert as c2-core normalises it (app/internal/weather.py normalize_alert). */
|
|
export interface WeatherAlertBase {
|
|
id: string;
|
|
event: string;
|
|
severity: string | null;
|
|
urgency: string | null;
|
|
headline: string | null;
|
|
onset: string | null;
|
|
expires: string | null;
|
|
ends: string | null;
|
|
/** An NWS office ("NWS Upton NY"), never a person. */
|
|
sender_name: string | null;
|
|
area_desc: string | null;
|
|
zones: string[];
|
|
}
|
|
|
|
export interface WeatherAlert extends WeatherAlertBase {
|
|
geometry: WeatherGeometry;
|
|
/** "alert": NWS drew the polygon; "zones": assembled from the affected forecast/county zones. */
|
|
geometry_source: "alert" | "zones";
|
|
/** [south, west, north, east]. */
|
|
bbox: [number, number, number, number] | null;
|
|
/** >0: some zones could not be fetched, so the drawn area is smaller than the real one. */
|
|
zones_unresolved?: number;
|
|
}
|
|
|
|
export interface WeatherAlertsResponse {
|
|
alerts: WeatherAlert[];
|
|
/** Alerts with no resolvable shape — listed, never dropped, never bbox-filtered. */
|
|
zone_only: WeatherAlertBase[];
|
|
/** Epoch s of the server's last SUCCESSFUL NWS fetch; null = never. */
|
|
fetched_at: number | null;
|
|
/** Set when the latest refresh failed; `alerts` is then the last good data. */
|
|
error: string | null;
|
|
credit: string;
|
|
}
|
|
|
|
export interface WindObs {
|
|
station: string;
|
|
station_name: string;
|
|
lat: number;
|
|
lon: number;
|
|
obs_time: string | null;
|
|
/** Direction the wind blows FROM, degrees true; null when variable or calm. */
|
|
wind_dir_deg: number | null;
|
|
variable: boolean;
|
|
wind_kt: number;
|
|
gust_kt: number | null;
|
|
raw: string | null;
|
|
distance_km: number;
|
|
/** Report older than 2 h. */
|
|
stale: boolean;
|
|
}
|
|
|
|
export interface WindResponse {
|
|
available: boolean;
|
|
/** Why there is no wind, when `available` is false. */
|
|
reason: string | null;
|
|
wind: WindObs | null;
|
|
fetched_at: number | null;
|
|
error: string | null;
|
|
credit: 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;
|
|
}
|
|
|
|
/**
|
|
* Google Geocoding precision, persisted with the coords since server-26#236.
|
|
* ROOFTOP = a street address; RANGE_INTERPOLATED = estimated between two
|
|
* address points; GEOMETRIC_CENTER = the centre of a road, intersection or
|
|
* named place — can be hundreds of metres off. Absent on older docs.
|
|
*/
|
|
export type GeoLocationType = "ROOFTOP" | "RANGE_INTERPOLATED" | "GEOMETRIC_CENTER";
|
|
|
|
export interface GeoPoint {
|
|
lat: number;
|
|
lng: number;
|
|
location_type?: GeoLocationType | string | null;
|
|
}
|
|
|
|
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?: GeoPoint | 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";
|
|
/** Set by the idle sweep; a manual resolve (PUT /incidents) doesn't set it. */
|
|
resolved_at?: string | null;
|
|
location: string | null;
|
|
location_coords: GeoPoint | null;
|
|
/**
|
|
* The label the pin was geocoded from (server-26#23). Absent on incidents
|
|
* written before that fix — their pin cannot be tied to their label and is
|
|
* drawn ghosted (DEFERRED.md `_verified_pin` row, server-26#28).
|
|
*/
|
|
location_coords_source?: string | 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;
|
|
ai_failures?: Record<string, number>;
|
|
gemini_usage?: Record<string, { calls: number; in: number; out: number; thinking: 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[];
|
|
}
|