feat(15-01): add site alert rules/events migration, models, schemas, and config
- Create Alembic migration 035 with site_alert_rules and site_alert_events tables, RLS policies, and GRANT - Add SiteAlertRule/SiteAlertEvent ORM models with enums for rule_type, severity, state - Add Pydantic schemas for rule/event CRUD and signal history points - Add SIGNAL_DEGRADATION_THRESHOLD_DB, ALERT_EVALUATION_INTERVAL_SECONDS, TREND_DETECTION_INTERVAL_SECONDS to Settings Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
168
backend/app/schemas/site_alert.py
Normal file
168
backend/app/schemas/site_alert.py
Normal file
@@ -0,0 +1,168 @@
|
||||
"""Pydantic schemas for site alert rules, events, and signal history."""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, field_validator
|
||||
|
||||
|
||||
class SiteAlertRuleCreate(BaseModel):
|
||||
"""Schema for creating a new site alert rule."""
|
||||
|
||||
name: str
|
||||
rule_type: str
|
||||
threshold_value: float
|
||||
threshold_unit: str
|
||||
sector_id: Optional[uuid.UUID] = None
|
||||
description: Optional[str] = None
|
||||
enabled: bool = True
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def validate_name(cls, v: str) -> str:
|
||||
v = v.strip()
|
||||
if len(v) < 1 or len(v) > 255:
|
||||
raise ValueError("Rule name must be 1-255 characters")
|
||||
return v
|
||||
|
||||
@field_validator("rule_type")
|
||||
@classmethod
|
||||
def validate_rule_type(cls, v: str) -> str:
|
||||
allowed = {
|
||||
"device_offline_percent",
|
||||
"device_offline_count",
|
||||
"sector_signal_avg",
|
||||
"sector_client_drop",
|
||||
"signal_degradation",
|
||||
}
|
||||
if v not in allowed:
|
||||
raise ValueError(f"rule_type must be one of: {', '.join(sorted(allowed))}")
|
||||
return v
|
||||
|
||||
@field_validator("threshold_unit")
|
||||
@classmethod
|
||||
def validate_threshold_unit(cls, v: str) -> str:
|
||||
allowed = {"percent", "count", "dBm"}
|
||||
if v not in allowed:
|
||||
raise ValueError(f"threshold_unit must be one of: {', '.join(sorted(allowed))}")
|
||||
return v
|
||||
|
||||
|
||||
class SiteAlertRuleUpdate(BaseModel):
|
||||
"""Schema for updating an existing site alert rule. All fields optional."""
|
||||
|
||||
name: Optional[str] = None
|
||||
rule_type: Optional[str] = None
|
||||
threshold_value: Optional[float] = None
|
||||
threshold_unit: Optional[str] = None
|
||||
sector_id: Optional[uuid.UUID] = None
|
||||
description: Optional[str] = None
|
||||
enabled: Optional[bool] = None
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def validate_name(cls, v: Optional[str]) -> Optional[str]:
|
||||
if v is None:
|
||||
return v
|
||||
v = v.strip()
|
||||
if len(v) < 1 or len(v) > 255:
|
||||
raise ValueError("Rule name must be 1-255 characters")
|
||||
return v
|
||||
|
||||
@field_validator("rule_type")
|
||||
@classmethod
|
||||
def validate_rule_type(cls, v: Optional[str]) -> Optional[str]:
|
||||
if v is None:
|
||||
return v
|
||||
allowed = {
|
||||
"device_offline_percent",
|
||||
"device_offline_count",
|
||||
"sector_signal_avg",
|
||||
"sector_client_drop",
|
||||
"signal_degradation",
|
||||
}
|
||||
if v not in allowed:
|
||||
raise ValueError(f"rule_type must be one of: {', '.join(sorted(allowed))}")
|
||||
return v
|
||||
|
||||
@field_validator("threshold_unit")
|
||||
@classmethod
|
||||
def validate_threshold_unit(cls, v: Optional[str]) -> Optional[str]:
|
||||
if v is None:
|
||||
return v
|
||||
allowed = {"percent", "count", "dBm"}
|
||||
if v not in allowed:
|
||||
raise ValueError(f"threshold_unit must be one of: {', '.join(sorted(allowed))}")
|
||||
return v
|
||||
|
||||
|
||||
class SiteAlertRuleResponse(BaseModel):
|
||||
"""Site alert rule response schema."""
|
||||
|
||||
id: uuid.UUID
|
||||
tenant_id: uuid.UUID
|
||||
site_id: uuid.UUID
|
||||
sector_id: Optional[uuid.UUID] = None
|
||||
rule_type: str
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
threshold_value: float
|
||||
threshold_unit: str
|
||||
enabled: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class SiteAlertRuleListResponse(BaseModel):
|
||||
"""List of site alert rules with total count."""
|
||||
|
||||
items: list[SiteAlertRuleResponse]
|
||||
total: int
|
||||
|
||||
|
||||
class SiteAlertEventResponse(BaseModel):
|
||||
"""Site alert event response schema."""
|
||||
|
||||
id: uuid.UUID
|
||||
tenant_id: uuid.UUID
|
||||
site_id: uuid.UUID
|
||||
sector_id: Optional[uuid.UUID] = None
|
||||
rule_id: Optional[uuid.UUID] = None
|
||||
device_id: Optional[uuid.UUID] = None
|
||||
link_id: Optional[uuid.UUID] = None
|
||||
severity: str
|
||||
message: str
|
||||
state: str
|
||||
consecutive_hits: int
|
||||
triggered_at: datetime
|
||||
resolved_at: Optional[datetime] = None
|
||||
resolved_by: Optional[uuid.UUID] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class SiteAlertEventListResponse(BaseModel):
|
||||
"""List of site alert events with total count."""
|
||||
|
||||
items: list[SiteAlertEventResponse]
|
||||
total: int
|
||||
|
||||
|
||||
class SignalHistoryPoint(BaseModel):
|
||||
"""A single time-bucketed signal history data point."""
|
||||
|
||||
timestamp: datetime
|
||||
signal_avg: int
|
||||
signal_min: int
|
||||
signal_max: int
|
||||
|
||||
|
||||
class SignalHistoryResponse(BaseModel):
|
||||
"""Signal history response with time-bucketed data points."""
|
||||
|
||||
items: list[SignalHistoryPoint]
|
||||
mac_address: str
|
||||
range: str
|
||||
Reference in New Issue
Block a user