feat: Added paginated GET /admin/pipeline/technique-pages endpoint with…

- "backend/routers/pipeline.py"
- "backend/schemas.py"

GSD-Task: S06/T01
This commit is contained in:
jlightner 2026-04-03 01:55:35 +00:00
parent 304f3bc069
commit bd8a928c95
2 changed files with 131 additions and 2 deletions

View file

@ -22,9 +22,9 @@ from sqlalchemy.ext.asyncio import AsyncSession
from config import get_settings from config import get_settings
from database import get_session from database import get_session
from models import PipelineEvent, PipelineRun, PipelineRunStatus, SourceVideo, Creator, KeyMoment, TranscriptSegment, ProcessingStatus from models import PipelineEvent, PipelineRun, PipelineRunStatus, SourceVideo, Creator, KeyMoment, TranscriptSegment, ProcessingStatus, TechniquePage, TechniquePageVideo, TechniquePageVersion
from redis_client import get_redis from redis_client import get_redis
from schemas import DebugModeResponse, DebugModeUpdate, TokenStageSummary, TokenSummaryResponse from schemas import DebugModeResponse, DebugModeUpdate, TokenStageSummary, TokenSummaryResponse, AdminTechniquePageItem, AdminTechniquePageListResponse
logger = logging.getLogger("chrysopedia.pipeline") logger = logging.getLogger("chrysopedia.pipeline")
@ -188,6 +188,108 @@ async def list_pipeline_videos(
} }
# ── Admin: Technique Pages ───────────────────────────────────────────────────
@router.get(
"/admin/pipeline/technique-pages",
response_model=AdminTechniquePageListResponse,
)
async def list_admin_technique_pages(
multi_source_only: bool = False,
creator: Annotated[str | None, Query(description="Filter by creator slug")] = None,
sort: Annotated[str, Query(description="Sort: recent, alpha, creator")] = "recent",
offset: Annotated[int, Query(ge=0)] = 0,
limit: Annotated[int, Query(ge=1, le=200)] = 50,
db: AsyncSession = Depends(get_session),
):
"""List technique pages with source video counts, version counts, and creator info.
Supports filtering by multi-source pages only and by creator slug.
"""
# Correlated subquery: source video count per page
video_count_sq = (
select(func.count())
.select_from(TechniquePageVideo)
.where(TechniquePageVideo.technique_page_id == TechniquePage.id)
.correlate(TechniquePage)
.scalar_subquery()
.label("source_video_count")
)
# Correlated subquery: version count per page
version_count_sq = (
select(func.count())
.select_from(TechniquePageVersion)
.where(TechniquePageVersion.technique_page_id == TechniquePage.id)
.correlate(TechniquePage)
.scalar_subquery()
.label("version_count")
)
stmt = (
select(
TechniquePage.id,
TechniquePage.title,
TechniquePage.slug,
TechniquePage.topic_category,
TechniquePage.body_sections_format,
TechniquePage.created_at,
TechniquePage.updated_at,
Creator.name.label("creator_name"),
Creator.slug.label("creator_slug"),
video_count_sq,
version_count_sq,
)
.join(Creator, TechniquePage.creator_id == Creator.id)
)
# Filters
if multi_source_only:
stmt = stmt.where(video_count_sq > 1)
if creator:
stmt = stmt.where(Creator.slug == creator)
# Count total before pagination
count_stmt = select(func.count()).select_from(stmt.subquery())
total = (await db.execute(count_stmt)).scalar() or 0
# Sort
if sort == "alpha":
stmt = stmt.order_by(TechniquePage.title.asc())
elif sort == "creator":
stmt = stmt.order_by(Creator.name.asc(), TechniquePage.title.asc())
else: # "recent" default
stmt = stmt.order_by(TechniquePage.updated_at.desc())
stmt = stmt.offset(offset).limit(limit)
result = await db.execute(stmt)
rows = result.all()
items = [
AdminTechniquePageItem(
id=r.id,
title=r.title,
slug=r.slug,
creator_name=r.creator_name,
creator_slug=r.creator_slug,
topic_category=r.topic_category,
body_sections_format=r.body_sections_format,
source_video_count=r.source_video_count or 0,
version_count=r.version_count or 0,
created_at=r.created_at,
updated_at=r.updated_at,
)
for r in rows
]
return AdminTechniquePageListResponse(
items=items,
total=total,
offset=offset,
limit=limit,
)
# ── Admin: Retrigger ───────────────────────────────────────────────────────── # ── Admin: Retrigger ─────────────────────────────────────────────────────────
@router.post("/admin/pipeline/trigger/{video_id}") @router.post("/admin/pipeline/trigger/{video_id}")

View file

@ -432,3 +432,30 @@ class TokenSummaryResponse(BaseModel):
video_id: str video_id: str
stages: list[TokenStageSummary] = Field(default_factory=list) stages: list[TokenStageSummary] = Field(default_factory=list)
grand_total_tokens: int grand_total_tokens: int
# ── Admin: Technique Pages ───────────────────────────────────────────────────
class AdminTechniquePageItem(BaseModel):
"""Technique page with aggregated source/version counts for admin view."""
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
title: str
slug: str
creator_name: str
creator_slug: str
topic_category: str
body_sections_format: str
source_video_count: int = 0
version_count: int = 0
created_at: datetime
updated_at: datetime
class AdminTechniquePageListResponse(BaseModel):
"""Paginated list of technique pages for admin view."""
items: list[AdminTechniquePageItem] = Field(default_factory=list)
total: int = 0
offset: int = 0
limit: int = 50