From 2d54e00483221bf061851baf750490e1171306d6 Mon Sep 17 00:00:00 2001 From: Justin Holmes Date: Mon, 1 Jun 2026 17:16:55 +0000 Subject: [PATCH] delivery-kid: finalize webhook updates draft state (status, final_cid) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Coconut webhook handler was only updating the *job* state — not the draft state — when a finalize-side job completed. Net effect: - HLS got transcoded and pinned to IPFS ✓ - job.hlsCid was set ✓ - But draft.json never got `status: finalized` or `final_cid` written - Wiki diagnostics panel stayed stuck on "finalizing" forever - Blue Railroad Imports bot never saw a finalized draft, so no Release page got created Verified end-to-end against terrapin re-finalize (PR #94 deploy): Coconut V2 ran cleanly, HLS pinned at QmVUq..., but the wiki page showed nothing past "transcoding-submitted" and no Release appeared. Two fixes: ## 1. content.py finalize path adds draftId to job_state The preview-path job_state already had draftId so the webhook handler could map back to a draft. The finalize-path job_state was missing it — so even with the new _update_draft_finalize below, the webhook wouldn't know which draft to update. ## 2. coconut.py adds _update_draft_finalize + wires it in Mirror of _update_draft_preview for the finalize side. On job.completed: state.status = "finalized", state.final_cid = hlsCid, finalize_log appended, wiki snapshot fired. On job.failed: state.status = "finalize_failed", error logged. Webhook handler's tail now branches on isPreview to call the right updater rather than only handling preview jobs. This is a pre-existing gap exposed by PR #94 — pre-V2, the finalize fast path was reusing preview_cid and the slow Coconut-finalize path was rarely hit, so the missing draft-state update went unnoticed. --- .../pinning-service/app/routes/coconut.py | 72 ++++++++++++++++++- .../pinning-service/app/routes/content.py | 6 ++ 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/delivery-kid/pinning-service/app/routes/coconut.py b/delivery-kid/pinning-service/app/routes/coconut.py index 33ce54c..46c9533 100644 --- a/delivery-kid/pinning-service/app/routes/coconut.py +++ b/delivery-kid/pinning-service/app/routes/coconut.py @@ -67,6 +67,67 @@ def _append_preview_log(staging_dir: Path, draft_id: str, message: str, logger.error("[%s] Failed to append preview log: %s", draft_id[:8], e) +def _update_draft_finalize(staging_dir: Path, job: dict) -> None: + """Update a content draft's finalize state after Coconut webhook. + + Mirrors ``_update_draft_preview`` but for the finalize-side job: + sets ``state.status = 'finalized'`` + ``state.final_cid``, appends to + ``finalize_log`` so the wiki diagnostics panel can render the + completion, and fires the wiki-snapshot hook on the terminal state. + + Without this, finalize jobs would pin HLS to IPFS but never write + anything back to the draft — the wiki page would stay stuck on + 'finalizing' forever and the Blue Railroad Imports bot would have no + 'finalized' state to react to, so no Release page would ever appear. + """ + draft_id = job["draftId"] + draft_json = staging_dir / "drafts" / draft_id / "draft.json" + if not draft_json.exists(): + logger.warning("[%s] Finalize draft not found: %s", job["id"], draft_id) + return + try: + data = json.loads(draft_json.read_text()) + log = data.get("finalize_log") or [] + now = datetime.now(timezone.utc).isoformat() + if job["status"] == "complete" and job.get("hlsCid"): + cid = job["hlsCid"] + data["status"] = "finalized" + data["final_cid"] = cid + data["finalized_at"] = now + log.append({ + "ts": now, + "stage": "complete", + "message": f"Finalize complete · HLS pinned ({cid[:12]}…)", + "progress": 100, + }) + logger.info("[%s] Draft %s finalized: hls=%s", + job["id"], draft_id[:8], cid) + else: + data["status"] = "finalize_failed" + err = job.get("error") or "Unknown error" + log.append({ + "ts": now, + "stage": "error", + "message": f"Finalize transcode failed: {err}", + "error": err, + }) + logger.warning("[%s] Draft %s finalize failed: %s", + job["id"], draft_id[:8], err) + # Match the cap used by _append_finalize_log in routes/content.py. + data["finalize_log"] = log[-200:] + draft_json.write_text(json.dumps(data, indent=2, default=str)) + + # Snapshot the terminal finalize state to the wiki sub-page so it + # survives a delivery-kid storage rebuild. + try: + import asyncio as _asyncio + _asyncio.create_task(snapshot_diagnostics_for_dict_async(draft_id, data)) + except RuntimeError: + logger.debug("[%s] No running loop; skipping diagnostics snapshot", job["id"]) + except Exception as e: + logger.error("[%s] Failed to update draft finalize state: %s", job["id"], e) + + def _update_draft_preview(staging_dir: Path, job: dict) -> None: """Update a content draft's preview state after Coconut webhook.""" draft_id = job["draftId"] @@ -283,9 +344,14 @@ async def webhook_coconut(request: Request, settings: Settings = Depends(get_set save_job(staging_dir, job_id, job) - # If this is a preview job, update the draft state - if job.get("isPreview") and job.get("draftId"): - _update_draft_preview(staging_dir, job) + # Mirror the job result back into the draft so the wiki page can + # render it. Two-headed: preview-side updates preview_status / + # preview_cid; finalize-side updates status / final_cid. + if job.get("draftId"): + if job.get("isPreview"): + _update_draft_preview(staging_dir, job) + else: + _update_draft_finalize(staging_dir, job) return {"received": True} except Exception as e: diff --git a/delivery-kid/pinning-service/app/routes/content.py b/delivery-kid/pinning-service/app/routes/content.py index 3f867c7..7b91416 100644 --- a/delivery-kid/pinning-service/app/routes/content.py +++ b/delivery-kid/pinning-service/app/routes/content.py @@ -686,6 +686,12 @@ async def send_event(event: str, data: dict): "id": job_id, "coconutJobId": coconut_job_id, "status": "processing", + # draftId lets the Coconut webhook handler map back to + # the right draft when the job completes — otherwise the + # webhook pins HLS but never updates state.final_cid / + # state.status, leaving the wiki page stuck on + # "finalizing" forever (preview path already does this). + "draftId": draft_id, "keepOriginal": request.preserve_original, "title": request.title, "fileType": request.file_type,