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; 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; enforce_override_timeout?: boolean; is_overridden?: boolean; override_system_id?: string | null; override_timeout_at?: string | null; } 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; } 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; } 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; 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[]; }