generated from coulomb/repo-seed
Add per-workstream task counts to state summary and dashboard
API: - WorkstreamWithTaskCounts schema extends WorkstreamRead with tasks_total/todo/in_progress/blocked/done fields - /state/summary now includes these counts in open_workstreams via a single extra GROUP BY query (workstream_id, status) Dashboard: - Replace domain workstream-count bar with a horizontal stacked progress bar per workstream (done/in-progress/blocked/todo) - Workstreams with no tasks show "no tasks yet" annotation - Workstreams with tasks show "X/N done" label after the bar - Sorted by domain then title so domains group naturally Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -23,7 +23,7 @@ from api.schemas.state import (
|
||||
)
|
||||
from api.schemas.task import TaskRead
|
||||
from api.schemas.topic import TopicWithWorkstreams
|
||||
from api.schemas.workstream import WorkstreamRead
|
||||
from api.schemas.workstream import WorkstreamRead, WorkstreamWithTaskCounts
|
||||
|
||||
router = APIRouter(prefix="/state", tags=["state"])
|
||||
|
||||
@@ -63,6 +63,13 @@ async def get_summary(session: AsyncSession = Depends(get_session)) -> StateSumm
|
||||
)
|
||||
open_ws = list(open_ws_rows.scalars().all())
|
||||
|
||||
# Task counts per workstream (used to enrich open_workstreams)
|
||||
task_per_ws: dict = {}
|
||||
for ws_id, tstat, cnt in await session.execute(
|
||||
select(Task.workstream_id, Task.status, func.count()).group_by(Task.workstream_id, Task.status)
|
||||
):
|
||||
task_per_ws.setdefault(ws_id, {})[tstat] = cnt
|
||||
|
||||
# Totals — one GROUP BY per table
|
||||
topic_counts = {r[0]: r[1] for r in await session.execute(
|
||||
select(Topic.status, func.count()).group_by(Topic.status)
|
||||
@@ -115,7 +122,17 @@ async def get_summary(session: AsyncSession = Depends(get_session)) -> StateSumm
|
||||
blocking_decisions=[DecisionRead.model_validate(d) for d in blocking],
|
||||
blocked_tasks=[TaskRead.model_validate(t) for t in blocked],
|
||||
recent_progress=[ProgressEventRead.model_validate(e) for e in recent],
|
||||
open_workstreams=[WorkstreamRead.model_validate(w) for w in open_ws],
|
||||
open_workstreams=[
|
||||
WorkstreamWithTaskCounts(
|
||||
**WorkstreamRead.model_validate(w).model_dump(),
|
||||
tasks_total=sum(task_per_ws.get(w.id, {}).values()),
|
||||
tasks_todo=task_per_ws.get(w.id, {}).get(TaskStatus.todo, 0),
|
||||
tasks_in_progress=task_per_ws.get(w.id, {}).get(TaskStatus.in_progress, 0),
|
||||
tasks_blocked=task_per_ws.get(w.id, {}).get(TaskStatus.blocked, 0),
|
||||
tasks_done=task_per_ws.get(w.id, {}).get(TaskStatus.done, 0),
|
||||
)
|
||||
for w in open_ws
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from api.schemas.decision import DecisionRead
|
||||
from api.schemas.progress_event import ProgressEventRead
|
||||
from api.schemas.task import TaskRead
|
||||
from api.schemas.topic import TopicWithWorkstreams
|
||||
from api.schemas.workstream import WorkstreamRead
|
||||
from api.schemas.workstream import WorkstreamWithTaskCounts
|
||||
|
||||
|
||||
class TopicTotals(BaseModel):
|
||||
@@ -55,4 +55,4 @@ class StateSummary(BaseModel):
|
||||
blocking_decisions: list[DecisionRead]
|
||||
blocked_tasks: list[TaskRead]
|
||||
recent_progress: list[ProgressEventRead]
|
||||
open_workstreams: list[WorkstreamRead]
|
||||
open_workstreams: list[WorkstreamWithTaskCounts]
|
||||
|
||||
@@ -36,3 +36,11 @@ class WorkstreamRead(BaseModel):
|
||||
due_date: date | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class WorkstreamWithTaskCounts(WorkstreamRead):
|
||||
tasks_total: int = 0
|
||||
tasks_todo: int = 0
|
||||
tasks_in_progress: int = 0
|
||||
tasks_blocked: int = 0
|
||||
tasks_done: int = 0
|
||||
|
||||
Reference in New Issue
Block a user