File size: 4,196 Bytes
d646dd7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
"""Projects API (原 tasks) — 按项目聚合 episode,列出状态分布。

任务概念已移除:episode 归属项目(Session.project_id → Project.name),
这里按项目名分组统计,供导航树和项目页面使用。
"""

from fastapi import APIRouter, Depends, Query
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession

from app.database import get_session
from app.models import Episode, Session as SessionModel, Project
from app.routes.ingestion import _ep_to_out

router = APIRouter(prefix="/api/v1/projects", tags=["projects"])


@router.get("/summary")
async def project_summary(
    status: str | None = Query(default=None, description="Filter by episode status"),
    search: str | None = Query(default=None, description="Search project name"),
    limit: int = Query(default=50, ge=1, le=200),
    offset: int = Query(default=0, ge=0),
    db: AsyncSession = Depends(get_session),
):
    """按项目聚合 episode 状态分布(原 /api/v1/tasks)。

    返回字段保留 ``task_name``(= 项目名)以兼容旧导航树调用。
    """
    # 聚合:project → status → counts
    cols = [
        Project.name.label("project_name"),
        Episode.status,
        func.count(Episode.id).label("cnt"),
        func.avg(Episode.frame_count).label("avg_frames"),
        func.max(Episode.received_at).label("last_upload"),
    ]
    base = (
        select(*cols)
        .select_from(Episode)
        .join(SessionModel, Episode.session_id == SessionModel.id)
        .join(Project, SessionModel.project_id == Project.id)
        .where(Episode.deleted_at.is_(None))
        .group_by(Project.name, Episode.status)
    )
    if status:
        base = base.where(Episode.status == status)

    rows = (await db.execute(base)).all()

    # 无项目归属的 episode → Uncategorized
    orphan_base = (
        select(
            func.count(Episode.id).label("cnt"),
            Episode.status,
        )
        .select_from(Episode)
        .outerjoin(SessionModel, Episode.session_id == SessionModel.id)
        .where(Episode.deleted_at.is_(None), SessionModel.id.is_(None))
        .group_by(Episode.status)
    )
    if status:
        orphan_base = orphan_base.where(Episode.status == status)
    orphan_rows = (await db.execute(orphan_base)).all()

    projects_map: dict[str, dict] = {}
    stats: dict[str, dict] = {}
    for pname, ep_status, cnt, avg_fr, last_up in rows:
        pname = pname or "unknown"
        if pname not in projects_map:
            projects_map[pname] = {"task_name": pname, "project_name": pname,
                                   "total_episodes": 0, "status_counts": {}}
            stats[pname] = {"avg_frames": 0, "last_upload_at": None, "failed_count": 0}
        projects_map[pname]["total_episodes"] += cnt
        projects_map[pname]["status_counts"][ep_status] = cnt
        if avg_fr:
            stats[pname]["avg_frames"] = round(float(avg_fr), 1)
        if last_up:
            stats[pname]["last_upload_at"] = last_up.isoformat()
        if ep_status == "failed":
            stats[pname]["failed_count"] += cnt

    for _cnt, ep_status in orphan_rows:
        pname = "Uncategorized"
        if pname not in projects_map:
            projects_map[pname] = {"task_name": pname, "project_name": pname,
                                   "total_episodes": 0, "status_counts": {}}
            stats[pname] = {"avg_frames": 0, "last_upload_at": None, "failed_count": 0}
        projects_map[pname]["total_episodes"] += _cnt
        projects_map[pname]["status_counts"][ep_status] = _cnt

    projects = list(projects_map.values())
    if search:
        low = search.lower()
        projects = [p for p in projects if low in p["task_name"].lower()]
    projects.sort(key=lambda p: p["total_episodes"], reverse=True)

    for p in projects:
        st = stats.get(p["task_name"], {})
        p["avg_frames"] = st.get("avg_frames", 0)
        p["last_upload_at"] = st.get("last_upload_at")
        p["cleaning_failed"] = st.get("failed_count", 0)

    total = len(projects)
    return {"tasks": projects[offset:offset + limit], "total": total,
            "limit": limit, "offset": offset}