"""admin 设备存活监控(只读):列设备心跳/在线掉线 + 顶部统计卡片。 数据源 device_liveness 表(心跳 last_heartbeat_at + liveness_state + kill_alert_pending, 见 app/models/device.py)。在线/掉线、掉线时长由 repo 按 HEARTBEAT_TIMEOUT_MINUTES 阈值派生。 纯读:无写、无审计。任意登录管理员可看(同大盘/设备管理,无角色门)。 """ from __future__ import annotations from typing import Annotated from fastapi import APIRouter, Depends, Query from app.admin.deps import AdminDb, get_current_admin from app.admin.repositories import queries from app.admin.schemas.common import CursorPage from app.admin.schemas.device import DeviceLivenessItem, DeviceLivenessStats router = APIRouter( prefix="/admin/api/device-liveness", tags=["admin-device-liveness"], dependencies=[Depends(get_current_admin)], ) @router.get("/stats", response_model=DeviceLivenessStats, summary="设备存活统计(顶部卡片)") def device_stats(db: AdminDb) -> DeviceLivenessStats: return DeviceLivenessStats(**queries.device_liveness_stats(db)) @router.get( "", response_model=CursorPage[DeviceLivenessItem], summary="设备存活列表(在线情况/设备id/归属用户 筛选 + 排序 + 分页,默认掉线置顶)", ) def list_devices( db: AdminDb, status: Annotated[str | None, Query(pattern="^(online|offline|never)$")] = None, device_id: Annotated[str | None, Query(max_length=128)] = None, phone: Annotated[str | None, Query(max_length=20)] = None, user_id: Annotated[int | None, Query()] = None, sort_by: Annotated[ str, Query(pattern="^(status|last_heartbeat_at|created_at)$") ] = "status", sort_order: Annotated[str, Query(pattern="^(asc|desc)$")] = "desc", limit: Annotated[int, Query(ge=1, le=100)] = 20, cursor: Annotated[int | None, Query()] = None, ) -> CursorPage[DeviceLivenessItem]: items, next_cursor, total = queries.list_device_liveness( db, status=status, device_id=device_id, phone=phone, user_id=user_id, sort_by=sort_by, sort_order=sort_order, limit=limit, cursor=cursor, ) return CursorPage( items=[DeviceLivenessItem.model_validate(d) for d in items], next_cursor=next_cursor, total=total, )