"""Чтение журнала действий по задачам (ТЗ 3.20).
Первый пагинированный список в API: конверт `{items, total, page, per_page}`.
Потолок `per_page` — 100: журнал растёт быстро, отдавать его целиком нельзя.
"""
from fastapi import APIRouter, Query
from sqlalchemy import func, select
from app.dependencies import DbDep, UserIdDep
from app.models import TaskEvent
from app.schemas import TaskEventOut, TaskEventPage
router = APIRouter(prefix="/api/task-events", tags=["task-events"])
MAX_PER_PAGE = 100
@router.get("", response_model=TaskEventPage)
async def list_task_events(
db: DbDep,
user_id: UserIdDep,
kind: str | None = Query(None, description="Тип события (created, completed, …)"),
actor_kind: str | None = Query(None, pattern="^(user|agent)$"),
via: str | None = Query(None, pattern="^(ui|api|mcp)$"),
project_id: int | None = Query(None),
task_id: int | None = Query(None),
page: int = Query(1, ge=1),
per_page: int = Query(50, ge=1, le=MAX_PER_PAGE),
) -> TaskEventPage:
"""Журнал пользователя, свежие записи первыми."""
filters = [TaskEvent.user_id == user_id]
if kind:
filters.append(TaskEvent.kind == kind)
if actor_kind:
filters.append(TaskEvent.actor_kind == actor_kind)
if via:
filters.append(TaskEvent.via == via)
if project_id:
filters.append(TaskEvent.project_id == project_id)
if task_id:
filters.append(TaskEvent.task_id == task_id)
total = db.scalar(select(func.count()).select_from(TaskEvent).where(*filters)) or 0
rows = db.scalars(
select(TaskEvent)
.where(*filters)
# id вторым ключом: у записей одной секунды иначе нет устойчивого порядка
.order_by(TaskEvent.created_at.desc(), TaskEvent.id.desc())
.limit(per_page)
.offset((page - 1) * per_page)
).all()
return TaskEventPage(
items=[TaskEventOut.model_validate(row) for row in rows],
total=total,
page=page,
per_page=per_page,
)