diff --git a/backend/routers/pipeline.py b/backend/routers/pipeline.py index 7b720b2..5299739 100644 --- a/backend/routers/pipeline.py +++ b/backend/routers/pipeline.py @@ -22,9 +22,9 @@ from sqlalchemy.ext.asyncio import AsyncSession from config import get_settings 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 schemas import DebugModeResponse, DebugModeUpdate, TokenStageSummary, TokenSummaryResponse +from schemas import DebugModeResponse, DebugModeUpdate, TokenStageSummary, TokenSummaryResponse, AdminTechniquePageItem, AdminTechniquePageListResponse 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 ───────────────────────────────────────────────────────── @router.post("/admin/pipeline/trigger/{video_id}") diff --git a/backend/schemas.py b/backend/schemas.py index d38d657..1418d6e 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -432,3 +432,30 @@ class TokenSummaryResponse(BaseModel): video_id: str stages: list[TokenStageSummary] = Field(default_factory=list) 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