"""admin 设备存活监控 schemas(只读)。 数据源 device_liveness 表(见 app/models/device.py)。online / display_state / offline_seconds 为后端按 HEARTBEAT_TIMEOUT_MINUTES 阈值派生的瞬态字段(非 DB 列), 在 repo 里算好挂到行上,供 from_attributes 读出。 """ from __future__ import annotations from datetime import datetime from pydantic import BaseModel, ConfigDict class DeviceLivenessItem(BaseModel): model_config = ConfigDict(from_attributes=True) id: int user_id: int # join user 取(瞬态;理论上 user 恒存在,留空仅防脏数据) phone: str | None = None nickname: str | None = None device_id: str device_model: str | None = None # 由 device_id 解析(device_<机型>_);非 DB 列 platform: str app_version: str | None = None registration_id: str | None = None # 非空 = 拿到极光 token、可推送 ever_protected: bool # 是否开过无障碍(=该设备对功能有意义) first_protected_at: datetime | None = None # 首次开无障碍时刻(老设备为 null) last_heartbeat_at: datetime | None = None last_report_protection_on: bool # 心跳里上报的无障碍开关(恒 true,仅观测) liveness_state: str # unknown / alive / silent(当前未用) / notified notified_at: datetime | None = None kill_alert_pending: bool # 掉线召回待客户端 ack(与 state 解耦) created_at: datetime updated_at: datetime # ===== 后端派生(非 DB 列)===== online: bool = False # ever_protected 且距上次心跳 ≤ 阈值 # 显示态:online=在线 / offline=已掉线 / never=从未启用(没开过无障碍) display_state: str = "never" # 掉线时长(秒):仅 offline 时有值;在线 / 从未启用为 None offline_seconds: int | None = None class DeviceLivenessStats(BaseModel): """顶部卡片:总设备数 + 在线 / 已掉线 / 未启用(按心跳阈值派生的 operator 口径, 不暴露内部状态机 unknown/alive/silent/notified)。""" total: int online: int offline: int never: int