"""STEP 05 — Publish Jobs + Rollback + Diff.""" import json from fastapi import APIRouter, Depends, HTTPException from app.auth import CurrentUser from app.config import settings from app.contracts import RollbackRequest from app.db import ( get_publish_job, list_publish_jobs, update_publish_job, update_candidate_entity, get_conn, ) from app.project_context import ProjectContext, get_project_context router = APIRouter() @router.get("/publish-jobs") async def _list( limit: int = 50, context: ProjectContext = Depends(get_project_context), _user: CurrentUser = None, ): return await list_publish_jobs(context.tenant_id, context.project_id, limit) @router.get("/publish-jobs/{job_id}") async def _get(job_id: int, _user: CurrentUser = None): job = await get_publish_job(job_id) if not job: raise HTTPException(404, "Publish job not found") return job @router.post("/publish-jobs") async def _create( body: dict, user: CurrentUser, context: ProjectContext = Depends(get_project_context), ): """Create a publish job for approved candidates.""" s = settings.db_schema candidate_ids = body.get("candidate_ids", []) diff = { "entities_added": len(candidate_ids), "entities_updated": 0, "relations_added": 0, "field_changes": {}, } async with get_conn() as conn: async with conn.cursor() as cur: await cur.execute( f"""INSERT INTO {s}.publish_jobs (tenant_id, project_id, candidate_ids, status, actor, diff_summary_jsonb) VALUES (%s, %s, %s, 'pending', %s, %s) RETURNING *""", ( context.tenant_id, context.project_id, json.dumps(candidate_ids), user["username"], json.dumps(diff), ), ) job = await cur.fetchone() await conn.commit() # Mark candidates as published for cid in candidate_ids: await update_candidate_entity(cid, {"status": "published"}) # Mark job as completed await update_publish_job(job["id"], {"status": "completed"}) return {**job, "status": "completed"} @router.get("/publish-jobs/{job_id}/diff") async def _diff(job_id: int, _user: CurrentUser = None): """Return the diff summary before/after a publish.""" job = await get_publish_job(job_id) if not job: raise HTTPException(404, "Publish job not found") diff = job.get("diff_summary_jsonb") if isinstance(diff, str): diff = json.loads(diff) return {"job_id": job_id, "diff": diff} @router.post("/publish-jobs/{job_id}/rollback") async def _rollback(job_id: int, body: RollbackRequest | None = None, user: CurrentUser = None): """Rollback a publish — revert candidate statuses.""" job = await get_publish_job(job_id) if not job: raise HTTPException(404, "Publish job not found") if job["status"] != "completed": raise HTTPException(400, "Can only rollback completed jobs") cids = job.get("candidate_ids") if isinstance(cids, str): cids = json.loads(cids) # Revert candidates back to approved for cid in (cids or []): await update_candidate_entity(cid, {"status": "approved"}) await update_publish_job(job_id, { "status": "rolled_back", "rollback_target_release_id": job_id, }) return { "rolled_back": True, "job_id": job_id, "reason": body.reason if body else None, "reverted_candidates": len(cids or []), }