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 # --------------------------------------------------------------------------- # Trips # --------------------------------------------------------------------------- class TripCreate(BaseModel): name: str location: str maps_link: Optional[str] = None start_date: str # YYYY-MM-DD end_date: str # YYYY-MM-DD available_tags: List[str] = [] # tag labels configured for this trip overlap_tags: List[str] = [] # subset of available_tags that allow time overlap visibility: str = "public" # "public" | "private" invited_discord_ids: List[str] = [] # discord user IDs allowed on private trips class TripEventCreate(BaseModel): title: str date: str # YYYY-MM-DD, must fall within parent trip range start_time: Optional[str] = None # HH:MM (24h) end_time: Optional[str] = None # HH:MM (24h) location: Optional[str] = None # inherits trip location if None maps_link: Optional[str] = None place_id: Optional[str] = None # Google Place ID notes: Optional[str] = None tags: List[str] = [] # tag labels applied to this event class TripEventUpdate(BaseModel): title: Optional[str] = None date: Optional[str] = None start_time: Optional[str] = None end_time: Optional[str] = None location: Optional[str] = None maps_link: Optional[str] = None place_id: Optional[str] = None notes: Optional[str] = None tags: Optional[List[str]] = None class AttendeeAction(BaseModel): discord_user_id: str discord_username: Optional[str] = None