"""Dashboard overview endpoints.""" import os from datetime import datetime, timezone from typing import Any from fastapi import APIRouter, Depends from sqlalchemy import func, or_ from sqlalchemy.orm import Session from database import get_db from models import Annotation, Frame, ProcessingTask, Project, Template, User from routers.auth import get_current_user 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), current_user: User = Depends(get_current_user), ) -> dict[str, Any]: """Return live dashboard data derived from persisted backend records.""" owned_project_ids_query = db.query(Project.id).filter(Project.owner_user_id == current_user.id) project_count = db.query(func.count(Project.id)).filter(Project.owner_user_id == current_user.id).scalar() or 0 frame_count = db.query(func.count(Frame.id)).filter(Frame.project_id.in_(owned_project_ids_query)).scalar() or 0 annotation_count = ( db.query(func.count(Annotation.id)) .filter(Annotation.project_id.in_(owned_project_ids_query)) .scalar() or 0 ) template_count = ( db.query(func.count(Template.id)) .filter(or_(Template.owner_user_id == current_user.id, Template.owner_user_id.is_(None))) .scalar() or 0 ) active_task_count = ( db.query(func.count(ProcessingTask.id)) .outerjoin(Project, Project.id == ProcessingTask.project_id) .filter((ProcessingTask.project_id.is_(None)) | (Project.owner_user_id == current_user.id)) .filter(ProcessingTask.status.in_(ACTIVE_TASK_STATUSES)) .scalar() or 0 ) projects = ( db.query(Project) .filter(Project.owner_user_id == current_user.id) .order_by(Project.updated_at.desc()) .all() ) recent_tasks = ( db.query(ProcessingTask) .outerjoin(Project, Project.id == ProcessingTask.project_id) .filter((ProcessingTask.project_id.is_(None)) | (Project.owner_user_id == current_user.id)) .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) .filter(Annotation.project_id.in_(owned_project_ids_query)) .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) .filter(or_(Template.owner_user_id == current_user.id, Template.owner_user_id.is_(None))) .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], }