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; 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; /** 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 /** 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; vocabulary?: string[]; vocabulary_pending?: VocabularyPendingTerm[]; vocabulary_bootstrapped?: boolean; ten_codes?: Record; // {"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; 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; 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; corr_path: Record; corr_consensus: Record; llm_decisions: number; est_cost_usd: number; ai_failures?: Record; gemini_usage?: Record; } 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; 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[]; }