feat(devices): add DeviceHealthSettings model for per-device alert thresholds

Two-tier (warning/critical) threshold config per device — restarts/week,
RSSI floor, free-heap floor, CPU temp ceiling, plus an offline timeout
used for both a client-computed health status icon and the Health tab
chart's gap detection. Purely advisory for now: no server-side
email/push alerting infra exists yet, so email_on_threshold and
push_on_crash_boot are placeholders for that future work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-21 18:27:35 +03:00
co-authored by Claude Sonnet 5
parent cc138e3d82
commit 0bec4c4648
+31
View File
@@ -98,6 +98,35 @@ class DeviceStatistics(BaseModel):
maintainancePeriod: int = 0 maintainancePeriod: int = 0
class HealthThresholdTier(BaseModel):
"""One severity tier's worth of metric thresholds. Two tiers (warning,
critical) make up DeviceHealthSettings — see there for why."""
restarts_per_week_threshold: int
rssi_floor_dbm: int
free_heap_floor_kb: int
cpu_temp_ceiling_c: int
class DeviceHealthSettings(BaseModel):
"""Per-device alert thresholds shown/edited on the Health tab's Settings
sub-tab. Purely advisory today — thresholds drive a client-computed
ok/warning/critical status icon, not any server-side alerting yet (no
email/push infra exists). Two severity tiers per metric (warning fires
first, critical is the harder breach) so the header status icon can show
orange vs. red rather than a single degraded/healthy binary.
offline_timeout_sec also feeds the Health tab's chart gap detection
(LineChart's gapThresholdMs) so both share one source of truth."""
warning: HealthThresholdTier = HealthThresholdTier(
restarts_per_week_threshold=6, rssi_floor_dbm=-80, free_heap_floor_kb=30, cpu_temp_ceiling_c=65,
)
critical: HealthThresholdTier = HealthThresholdTier(
restarts_per_week_threshold=12, rssi_floor_dbm=-90, free_heap_floor_kb=15, cpu_temp_ceiling_c=80,
)
offline_timeout_sec: int = 90
email_on_threshold: bool = False
push_on_crash_boot: bool = False
class MelodyMainItem(BaseModel): class MelodyMainItem(BaseModel):
"""Mirrors the Melody schema used in the melodies collection.""" """Mirrors the Melody schema used in the melodies collection."""
information: dict = {} information: dict = {}
@@ -118,6 +147,7 @@ class DeviceCreate(BaseModel):
device_attributes: DeviceAttributes = DeviceAttributes() device_attributes: DeviceAttributes = DeviceAttributes()
device_subscription: DeviceSubInformation = DeviceSubInformation() device_subscription: DeviceSubInformation = DeviceSubInformation()
device_stats: DeviceStatistics = DeviceStatistics() device_stats: DeviceStatistics = DeviceStatistics()
device_health_settings: DeviceHealthSettings = DeviceHealthSettings()
events_on: bool = False events_on: bool = False
device_location_coordinates: Any = None # GeoPoint dict {lat, lng} or legacy str device_location_coordinates: Any = None # GeoPoint dict {lat, lng} or legacy str
device_melodies_all: List[MelodyMainItem] = [] device_melodies_all: List[MelodyMainItem] = []
@@ -143,6 +173,7 @@ class DeviceUpdate(BaseModel):
device_attributes: Optional[Dict[str, Any]] = None device_attributes: Optional[Dict[str, Any]] = None
device_subscription: Optional[Dict[str, Any]] = None device_subscription: Optional[Dict[str, Any]] = None
device_stats: Optional[Dict[str, Any]] = None device_stats: Optional[Dict[str, Any]] = None
device_health_settings: Optional[Dict[str, Any]] = None
events_on: Optional[bool] = None events_on: Optional[bool] = None
device_location_coordinates: Optional[Any] = None # dict {lat, lng} or legacy str device_location_coordinates: Optional[Any] = None # dict {lat, lng} or legacy str
device_melodies_all: Optional[List[MelodyMainItem]] = None device_melodies_all: Optional[List[MelodyMainItem]] = None