DataFlowing / processor /app /routes /dashboard.py
stouchinglhh
collector: 同步采集工程 v1.2.1(全部 LFS 模式文件指针化,与 Space 现有文件合并)
d646dd7
Raw History Blame Contribute Delete
4.37 kB
"""Dashboard API — 文件系统扫描统计(无数据库)。"""
from __future__ import annotations
from datetime import datetime, timezone, timedelta
from fastapi import APIRouter, Query
from app.localstore import (
scan_sessions, list_deleted_episodes, read_episode_state, list_projects,
)
router = APIRouter(prefix="/api/v1/dashboard", tags=["dashboard"])
def _today_start() -> datetime:
return datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
def _alive_episodes() -> list[dict]:
deleted = {d["id"] for d in list_deleted_episodes()}
return [e for e in scan_sessions() if e["id"] not in deleted]
def _status_of(e: dict) -> str:
return e.get("status") or "unknown"
@router.get("/overview")
def dashboard_overview():
"""聚合统计 — 6 张统计卡。"""
today = _today_start()
eps = _alive_episodes()
def _in_today(e: dict) -> bool:
try:
return datetime.fromisoformat(e.get("created_at") or "") >= today
except Exception:
return False
total_all = len(eps)
total_today = sum(1 for e in eps if _in_today(e))
reviewing = [e for e in eps if _status_of(e) in ("completed", "to_review")]
approved = [e for e in eps if _status_of(e) in ("reviewed", "approved")]
failed = [e for e in eps if _status_of(e) == "failed"]
return {
"total": {"total": total_all, "today": total_today, "label": "All Episodes"},
"reviewing": {"total": len(reviewing), "today": sum(1 for e in reviewing if _in_today(e)), "label": "To Review"},
"approved": {"total": len(approved), "today": sum(1 for e in approved if _in_today(e)), "label": "Approved"},
# 首页展示当前仍失败的批次,不把同一批次历史重试产生的多个
# failed run 或异常详情重复累加。历史失败运行仍可在异常页追溯。
"failed": {"total": len(failed), "today": sum(1 for e in failed if _in_today(e)), "label": "Failed"},
"active_tasks": {"total": len(list_projects()), "today": 0, "label": "Active Tasks"},
"datasets": {"total": 0, "today": 0, "label": "Datasets"},
"updated_at": datetime.now(timezone.utc).isoformat(),
}
@router.get("/recent-episodes")
def recent_episodes(limit: int = Query(default=6, ge=1, le=20)):
eps = sorted(_alive_episodes(), key=lambda e: e.get("created_at") or "", reverse=True)[:limit]
episodes = []
for e in eps:
duration = 0.0
fps = e.get("fps") or 30
fc = e.get("frame_count") or 0
if fps and fc:
duration = round(fc / fps, 1)
state = read_episode_state(e["id"])
episodes.append({
"id": e["id"],
"task_name": e.get("project") or e.get("name") or e["id"],
"status": e.get("status"),
"received_at": e.get("created_at"),
"duration_sec": duration,
"frame_count": fc,
"cleaning_passed": (state.get("cleaning_report") or {}).get("passed"),
})
return {"episodes": episodes}
@router.get("/pipeline")
def pipeline_status():
eps = _alive_episodes()
stages = [
{"name": "Received", "count": sum(1 for e in eps if _status_of(e) == "received")},
{"name": "Processing", "count": sum(1 for e in eps if _status_of(e) == "processing")},
{"name": "Review", "count": sum(1 for e in eps if _status_of(e) in ("completed", "to_review"))},
{"name": "Approved", "count": sum(1 for e in eps if _status_of(e) in ("reviewed", "approved"))},
{"name": "Dataset", "count": 0},
]
return {"stages": stages}
@router.get("/trend")
def trend(days: int = Query(default=30, ge=7, le=90)):
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
day_map: dict[str, int] = {}
for e in _alive_episodes():
try:
dt = datetime.fromisoformat(e.get("created_at") or "")
except Exception:
continue
if dt >= cutoff:
key = str(dt.date())
day_map[key] = day_map.get(key, 0) + 1
labels, counts = [], []
for i in range(days - 1, -1, -1):
d = datetime.now(timezone.utc) - timedelta(days=i)
key = str(d.date())
labels.append(d.strftime("%m-%d"))
counts.append(day_map.get(key, 0))
return {"labels": labels, "counts": counts}