"""FastAPI router for Content Brief v2 — struggle-first pipeline.""" import os from fastapi import APIRouter, HTTPException, Query, Header, Depends from typing import Optional from .models import ( BriefListResponse, BriefDetailResponse, CreateBriefRequest, UpdateBriefRequest, SubmitBriefResponse, ApproveBriefResponse, RejectBriefResponse, BriefOutputResponse, StruggleScoreResponse, StyleReferenceListResponse, StyleReference, ) from .brief_service import ( create_brief, get_brief, list_briefs, update_brief, submit_brief, approve_brief, reject_brief, update_generation_status, calculate_struggle_score, BriefNotFoundError, BriefValidationError, ) from .service import list_posts # For style references from .notifications import on_brief_submitted, on_brief_approved from .generation_v2 import trigger_generation from .notifications import on_brief_submitted, on_brief_approved # Admin token auth — same as main router ADMIN_TOKEN = os.getenv("BLOG_ADMIN_TOKEN", "changeme-please-update") def verify_admin_token(x_admin_token: str = Header(None)) -> str: """Verify admin token from header.""" if x_admin_token != ADMIN_TOKEN: raise HTTPException(status_code=401, detail="Unauthorized - invalid or missing token") return x_admin_token router = APIRouter(tags=["content-briefs"]) @router.post("/", response_model=BriefDetailResponse) async def api_create_brief( request: CreateBriefRequest, token: str = Depends(verify_admin_token), ): """Create a new content brief.""" brief = create_brief(request) return {"brief": brief} @router.get("/", response_model=BriefListResponse) async def api_list_briefs( status: Optional[str] = Query(None, description="Filter by status"), created_by: Optional[str] = Query(None, description="Filter by creator"), page: int = Query(1, ge=1), per_page: int = Query(20, ge=1, le=100), token: str = Depends(verify_admin_token), ): """List content briefs with optional filtering.""" briefs, total = list_briefs(status=status, created_by=created_by, page=page, per_page=per_page) total_pages = (total + per_page - 1) // per_page return { "briefs": briefs, "total": total, "page": page, "per_page": per_page, "total_pages": total_pages, } @router.get("/{brief_id}", response_model=BriefDetailResponse) async def api_get_brief( brief_id: str, token: str = Depends(verify_admin_token), ): """Get a single content brief by ID.""" try: brief = get_brief(brief_id) return {"brief": brief} except BriefNotFoundError: raise HTTPException(status_code=404, detail=f"Brief '{brief_id}' not found") @router.patch("/{brief_id}", response_model=BriefDetailResponse) async def api_update_brief( brief_id: str, request: UpdateBriefRequest, token: str = Depends(verify_admin_token), ): """Update an existing brief. Only works in draft or rejected status.""" try: brief = update_brief(brief_id, request) return {"brief": brief} except BriefNotFoundError: raise HTTPException(status_code=404, detail=f"Brief '{brief_id}' not found") except BriefValidationError as e: raise HTTPException(status_code=400, detail=str(e)) @router.post("/{brief_id}/submit", response_model=SubmitBriefResponse) async def api_submit_brief( brief_id: str, token: str = Depends(verify_admin_token), ): """Submit a brief for approval. Validates voice checklist first.""" try: brief = submit_brief(brief_id) # Notify Matt via Telegram on_brief_submitted(brief.id, brief.title, brief.created_by) return { "id": brief.id, "status": brief.status, "message": "Brief submitted for approval. Awaiting review.", } except BriefNotFoundError: raise HTTPException(status_code=404, detail=f"Brief '{brief_id}' not found") except BriefValidationError as e: raise HTTPException(status_code=400, detail=str(e)) @router.post("/{brief_id}/approve", response_model=ApproveBriefResponse) async def api_approve_brief( brief_id: str, token: str = Depends(verify_admin_token), ): """Approve a brief and trigger generation. Matt-only.""" try: brief = approve_brief(brief_id, approved_by="matt") # Notify via Telegram on_brief_approved(brief.id, brief.title, brief.generation_job_id or "") # Trigger content generation trigger_generation(brief_id) return { "id": brief.id, "status": brief.status, "message": "Brief approved. Generation started.", "generation_job_id": brief.generation_job_id, } except BriefNotFoundError: raise HTTPException(status_code=404, detail=f"Brief '{brief_id}' not found") except BriefValidationError as e: raise HTTPException(status_code=400, detail=str(e)) @router.post("/{brief_id}/reject", response_model=RejectBriefResponse) async def api_reject_brief( brief_id: str, feedback: str = Query("", description="Rejection feedback"), token: str = Depends(verify_admin_token), ): """Reject a brief and return to draft with feedback.""" try: brief = reject_brief(brief_id, feedback=feedback) return { "id": brief.id, "status": brief.status, "message": f"Brief rejected. Feedback appended to reflection.", } except BriefNotFoundError: raise HTTPException(status_code=404, detail=f"Brief '{brief_id}' not found") except BriefValidationError as e: raise HTTPException(status_code=400, detail=str(e)) @router.get("/{brief_id}/output", response_model=BriefOutputResponse) async def api_get_brief_output( brief_id: str, token: str = Depends(verify_admin_token), ): """Get generated content output for a brief.""" try: brief = get_brief(brief_id) if brief.status != "completed": raise HTTPException(status_code=400, detail=f"Brief status is '{brief.status}', not 'completed'") return { "id": brief.id, "title": brief.title, "content_md": brief.content_output or "", "content_html": brief.content_html or "", "struggle_score": brief.struggle_score, "generated_at": brief.updated_at, } except BriefNotFoundError: raise HTTPException(status_code=404, detail=f"Brief '{brief_id}' not found") @router.post("/{brief_id}/score", response_model=StruggleScoreResponse) async def api_calculate_score( brief_id: str, token: str = Depends(verify_admin_token), ): """Calculate struggle score for a brief's generated content.""" try: brief = get_brief(brief_id) if not brief.content_output: raise HTTPException(status_code=400, detail="Brief has no generated content to score") result = calculate_struggle_score(brief.content_output) return { "score": result["score"], "max_score": 100.0, "checks": result["checks"], "interpretation": result["interpretation"], } except BriefNotFoundError: raise HTTPException(status_code=404, detail=f"Brief '{brief_id}' not found") @router.get("/style-references", response_model=StyleReferenceListResponse) async def api_list_style_references( limit: int = Query(20, ge=1, le=100), token: str = Depends(verify_admin_token), ): """List published posts that can be used as style references.""" posts, _ = list_posts(status="published", page=1, per_page=limit) styles = [ StyleReference( slug=p.slug, title=p.title, excerpt=p.excerpt or "", ) for p in posts ] return {"styles": styles}