"""``/processing-log`` (admin) — filterable Operations log view. Path is kebab-case per CONTRACT.md §9. Filters are camelCase queries: - ``bookId`` (not ``book_id``) - ``stage`` - ``status`` — one of ``success | failed | in_flight`` - ``since`` — ISO timestamp lower bound on ``started_at`` - ``limit`` (default 100, max 500) Admin-only because the log surfaces error messages from every book the user has ingested — sensitive enough to keep behind ``require_admin``. """ from __future__ import annotations from fastapi import APIRouter, Depends, Query from src.api.deps import require_admin from src.api.dto.processing_log import ProcessingLogEntryDTO, ProcessingOutcomeEnum from src.lib.auth.models import User from src.lib.processing.models import StageRun from src.lib.processing.repo import query router = APIRouter(tags=["processing-log"]) _STATUS_TO_OUTCOME: dict[ProcessingOutcomeEnum, str] = { ProcessingOutcomeEnum.success: "success", ProcessingOutcomeEnum.failed: "failed", ProcessingOutcomeEnum.in_flight: "in-flight", } def _to_dto(run: StageRun) -> ProcessingLogEntryDTO: if run.success == 1: outcome = ProcessingOutcomeEnum.success elif run.success == 0: outcome = ProcessingOutcomeEnum.failed else: outcome = ProcessingOutcomeEnum.in_flight return ProcessingLogEntryDTO( id=run.id, book_id=run.book_id, stage=run.stage, started_at=run.started_at, finished_at=run.finished_at, outcome=outcome, pages_done=run.pages_done, engine=run.engine, provider=run.provider, backend=run.backend, where=run.where, error=run.error, duration_ms=run.duration_ms, ) @router.get("/processing-log", response_model=list[ProcessingLogEntryDTO]) def list_processing_log( _: User = Depends(require_admin), book_id: str | None = Query(default=None, alias="bookId"), stage: str | None = Query(default=None), status: ProcessingOutcomeEnum | None = Query(default=None), since: str | None = Query( default=None, description="ISO timestamp lower bound on started_at; applied client-side", ), limit: int = Query(default=100, ge=1, le=500), ) -> list[ProcessingLogEntryDTO]: """Filterable list — newest first. The repo's :func:`query` does book/stage/outcome filtering in SQL; the ``since`` filter applies in Python since the repo signature doesn't expose it (a Stage 3 router shouldn't grow the lib's signature for one filter; if the FE wants this pushed down later we revisit). """ outcome_str = _STATUS_TO_OUTCOME[status] if status is not None else None runs = query( book_id=book_id, stage=stage, outcome=outcome_str, limit=limit if since is None else max(limit * 2, 500), ) if since: runs = [r for r in runs if r.started_at >= since] return [_to_dto(r) for r in runs[:limit]]