"""Dashboard overview endpoints.""" import os from datetime import datetime, timezone from typing import Any from fastapi import APIRouter, Depends from sqlalchemy import func from sqlalchemy.orm import Session from database import get_db from models import Annotation, Frame, ProcessingTask, Project, Template router = APIRouter(prefix="/api/dashboard", tags=["Dashboard"]) ACTIVE_TASK_STATUSES = {"queued", "running"} MONITORED_TASK_STATUSES = {"queued", "running", "success", "failed", "cancelled"} def _system_load_percent() -> int: """Return a real host load estimate without adding a psutil dependency.""" try: load_1m = os.getloadavg()[0] cpu_count = os.cpu_count() or 1 return min(100, max(0, round((load_1m / cpu_count) * 100))) except (AttributeError, OSError): return 0 def _iso_or_none(value: datetime | None) -> str | None: if value is None: return None if value.tzinfo is None: value = value.replace(tzinfo=timezone.utc) return value.isoformat() def _task_payload(task: ProcessingTask) -> dict[str, Any]: result = task.result or {} return { "id": f"task-{task.id}", "task_id": task.id, "project_id": task.project_id or 0, "name": task.project.name if task.project else f"任务 {task.id}", "progress": task.progress, "status": task.message or task.status, "raw_status": task.status, "frame_count": result.get("frames_extracted", result.get("processed_frame_count", 0)), "error": task.error, "updated_at": _iso_or_none(task.updated_at), } @router.get("/overview", summary="Get dashboard overview") def get_dashboard_overview(db: Session = Depends(get_db)) -> dict[str, Any]: """Return live dashboard data derived from persisted backend records.""" project_count = db.query(func.count(Project.id)).scalar() or 0 frame_count = db.query(func.count(Frame.id)).scalar() or 0 annotation_count = db.query(func.count(Annotation.id)).scalar() or 0 template_count = db.query(func.count(Template.id)).scalar() or 0 active_task_count = ( db.query(func.count(ProcessingTask.id)) .filter(ProcessingTask.status.in_(ACTIVE_TASK_STATUSES)) .scalar() or 0 ) projects = db.query(Project).order_by(Project.updated_at.desc()).all() recent_tasks = ( db.query(ProcessingTask) .order_by(ProcessingTask.created_at.desc()) .limit(50) .all() ) tasks = [_task_payload(task) for task in recent_tasks if task.status in MONITORED_TASK_STATUSES] activities: list[dict[str, Any]] = [] for task in recent_tasks[:10]: project_name = task.project.name if task.project else f"项目 {task.project_id}" activities.append({ "id": f"task-{task.id}", "kind": "task", "time": _iso_or_none(task.updated_at), "message": task.message or f"任务状态: {task.status}", "project": project_name, }) for project in projects[:10]: activities.append({ "id": f"project-{project.id}", "kind": "project", "time": _iso_or_none(project.updated_at), "message": f"项目状态: {project.status}", "project": project.name, }) recent_annotations = ( db.query(Annotation) .order_by(Annotation.updated_at.desc()) .limit(10) .all() ) for annotation in recent_annotations: project_name = annotation.project.name if annotation.project else f"项目 {annotation.project_id}" activities.append({ "id": f"annotation-{annotation.id}", "kind": "annotation", "time": _iso_or_none(annotation.updated_at), "message": f"标注已更新 #{annotation.id}", "project": project_name, }) recent_templates = ( db.query(Template) .order_by(Template.created_at.desc()) .limit(10) .all() ) for template in recent_templates: activities.append({ "id": f"template-{template.id}", "kind": "template", "time": _iso_or_none(template.created_at), "message": f"模板可用: {template.name}", "project": "系统", }) activities.sort(key=lambda item: item["time"] or "", reverse=True) return { "summary": { "project_count": project_count, "parsing_task_count": active_task_count, "annotation_count": annotation_count, "frame_count": frame_count, "template_count": template_count, "system_load_percent": _system_load_percent(), }, "tasks": tasks, "activity": activities[:10], }