feat(stats): optimize dashboard data loading with internal ui endpoints

This commit is contained in:
Alexander Kalinovsky
2026-04-01 19:05:57 +03:00
parent 19d659df6b
commit 9b9c7b5575
11 changed files with 552 additions and 140 deletions

120
app/api/ui_data.py Normal file
View File

@@ -0,0 +1,120 @@
from __future__ import annotations
from datetime import datetime
from fastapi import APIRouter, Depends, Path
from fastapi.responses import JSONResponse
from app.api.dto.tasks import ErrorDTO, TaskBoardDTO, TaskBoardItemDTO, TaskDetailsDTO
from app.api.tasks import get_task_or_error, get_task_repository
from app.storage import JsonFileTaskRepository, StoredTask
router = APIRouter(prefix="/ui_data", tags=["ui_data"])
def get_task_status(task: StoredTask) -> str:
if task.done_at is not None:
return "done"
if task.started_at is not None:
return "in_progress"
return "backlog"
def format_duration(task: StoredTask) -> str:
if task.started_at is None or task.done_at is None:
return ""
delta = task.done_at - task.started_at
total_seconds = int(delta.total_seconds())
hours, remainder = divmod(total_seconds, 3600)
minutes, seconds = divmod(remainder, 60)
return f"{hours}h {minutes}m {seconds}s"
def to_board_item_dto(task: StoredTask) -> TaskBoardItemDTO:
status = get_task_status(task)
display_date_label: str
display_date_value: datetime | None
if status == "backlog":
display_date_label = "Created"
display_date_value = task.created_at
elif status == "in_progress":
display_date_label = "Started"
display_date_value = task.started_at
else:
display_date_label = "Done"
display_date_value = task.done_at
return TaskBoardItemDTO(
id=task.id,
title=task.title,
status=status,
display_date_label=display_date_label,
display_date_value=display_date_value,
)
def to_task_details_dto(task: StoredTask) -> TaskDetailsDTO:
return TaskDetailsDTO(
id=task.id,
title=task.title,
status=get_task_status(task),
created_at=task.created_at,
started_at=task.started_at,
done_at=task.done_at,
cycle_time=format_duration(task),
)
@router.get(
"/stats/board",
response_model=TaskBoardDTO,
responses={
400: {"model": ErrorDTO},
},
include_in_schema=False,
)
def stats_board(
repo: JsonFileTaskRepository = Depends(get_task_repository),
) -> TaskBoardDTO:
tasks = repo.list_tasks()
backlog_tasks: list[TaskBoardItemDTO] = []
in_progress_tasks: list[TaskBoardItemDTO] = []
done_tasks: list[TaskBoardItemDTO] = []
for task in tasks:
item = to_board_item_dto(task)
if item.status == "backlog":
backlog_tasks.append(item)
elif item.status == "in_progress":
in_progress_tasks.append(item)
else:
done_tasks.append(item)
return TaskBoardDTO(
backlog_tasks=backlog_tasks,
in_progress_tasks=in_progress_tasks,
done_tasks=done_tasks,
)
@router.get(
"/tasks/{task_id}",
response_model=TaskDetailsDTO,
responses={
400: {"model": ErrorDTO},
404: {"model": ErrorDTO},
},
include_in_schema=False,
)
def task_details(
task_id: str = Path(...),
repo: JsonFileTaskRepository = Depends(get_task_repository),
) -> TaskDetailsDTO | JSONResponse:
task, error = get_task_or_error(repo=repo, task_id=task_id)
if error is not None:
return error
return to_task_details_dto(task)