from pydantic import BaseModel, Field from typing import Optional, List, Dict, Any from datetime import datetime # --------------------------------------------------------------------------- # Nodes # --------------------------------------------------------------------------- class NodeRecord(BaseModel): node_id: str name: str lat: float = 0.0 lon: float = 0.0 status: str = "offline" # online / offline / recording / unconfigured configured: bool = False last_seen: Optional[datetime] = None assigned_system_id: Optional[str] = None class CommandPayload(BaseModel): action: str # discord_join / discord_leave / op25_restart guild_id: Optional[str] = None channel_id: Optional[str] = None # --------------------------------------------------------------------------- # Systems # --------------------------------------------------------------------------- class SystemRecord(BaseModel): system_id: str name: str type: str # P25 / DMR / NBFM config: Dict[str, Any] = {} # OP25-compatible config blob ten_codes: Dict[str, str] = {} # {"10-10": "Commercial Alarm", ...} class SystemCreate(BaseModel): name: str type: str config: Dict[str, Any] = {} ten_codes: Dict[str, str] = {} # --------------------------------------------------------------------------- # Calls # --------------------------------------------------------------------------- class CallRecord(BaseModel): call_id: str node_id: str system_id: Optional[str] = None talkgroup_id: Optional[int] = None talkgroup_name: Optional[str] = None freq: Optional[float] = None srcaddr: Optional[str] = None started_at: datetime ended_at: Optional[datetime] = None audio_url: Optional[str] = None transcript: Optional[str] = None # populated later by STT incident_ids: List[str] = [] # one per scene detected in the recording location: Optional[Dict[str, float]] = None # {lat, lng} tags: List[str] = [] status: str = "active" # active / ended # --------------------------------------------------------------------------- # Incidents # --------------------------------------------------------------------------- class IncidentRecord(BaseModel): incident_id: str title: Optional[str] = None type: Optional[str] = None # fire / police / ems / etc. status: str = "active" # active / resolved location: Optional[Dict[str, float]] = None call_ids: List[str] = [] started_at: datetime updated_at: datetime summary: Optional[str] = None tags: List[str] = [] class IncidentCreate(BaseModel): title: str type: str = "other" status: str = "active" location: Optional[Dict[str, float]] = None call_ids: List[str] = [] summary: Optional[str] = None tags: List[str] = [] class IncidentUpdate(BaseModel): title: Optional[str] = None type: Optional[str] = None status: Optional[str] = None location: Optional[Dict[str, float]] = None summary: Optional[str] = None tags: Optional[List[str]] = None # --------------------------------------------------------------------------- # Alerts # --------------------------------------------------------------------------- class AlertRule(BaseModel): rule_id: Optional[str] = None name: str keywords: List[str] = [] talkgroup_ids: List[int] = [] enabled: bool = True discord_webhook: Optional[str] = None # POST here when rule fires class AlertRuleUpdate(BaseModel): name: Optional[str] = None keywords: Optional[List[str]] = None talkgroup_ids: Optional[List[int]] = None enabled: Optional[bool] = None discord_webhook: Optional[str] = None class AlertEvent(BaseModel): alert_id: Optional[str] = None rule_id: str rule_name: str call_id: str node_id: str talkgroup_id: Optional[int] = None talkgroup_name: Optional[str] = None matched_keywords: List[str] = [] transcript_snippet: Optional[str] = None triggered_at: Optional[datetime] = None acknowledged: bool = False