diff --git a/delivery-kid/pinning-service/app/routes/coconut.py b/delivery-kid/pinning-service/app/routes/coconut.py index 46c9533..a9bb2e3 100644 --- a/delivery-kid/pinning-service/app/routes/coconut.py +++ b/delivery-kid/pinning-service/app/routes/coconut.py @@ -94,6 +94,23 @@ def _update_draft_finalize(staging_dir: Path, job: dict) -> None: data["status"] = "finalized" data["final_cid"] = cid data["finalized_at"] = now + # Narrate each webhook-side step we actually did so the wiki + # diagnostics panel doesn't have a 2-minute gap between + # "transcoding-submitted" and "complete". The webhook handler + # itself only writes to draft.json once, so the multiple + # entries below are persisted in one save (we don't have a + # streaming mechanism back to the user's browser; the panel + # picks them all up on its next poll). + log.append({ + "ts": now, + "stage": "webhook", + "message": "Coconut callback received — HLS ready", + }) + log.append({ + "ts": now, + "stage": "pin", + "message": f"HLS pinned to IPFS as {cid}", + }) log.append({ "ts": now, "stage": "complete", @@ -124,6 +141,28 @@ def _update_draft_finalize(staging_dir: Path, job: dict) -> None: _asyncio.create_task(snapshot_diagnostics_for_dict_async(draft_id, data)) except RuntimeError: logger.debug("[%s] No running loop; skipping diagnostics snapshot", job["id"]) + + # Update the ReleaseDraft:{id} wiki page YAML so the Blue Railroad + # Imports bot's find_cid_from_history picks up the new final_cid + # on its next cron run and creates the Release:{cid} page. + # Pre-V2 this edit was done by the browser-side JS when the SSE + # delivered a 'complete' event; the slow Coconut path closes the + # SSE long before completion, so we write it here instead. + if data.get("status") == "finalized" and data.get("final_cid"): + try: + import asyncio as _asyncio + from ..services.pickipedia_client import ( + write_finalized_to_releasedraft_async, + ) + _asyncio.create_task(write_finalized_to_releasedraft_async( + draft_id, data["final_cid"], data["finalized_at"], + )) + except RuntimeError: + logger.debug("[%s] No running loop; skipping ReleaseDraft YAML update", + job["id"]) + except Exception as e: + logger.warning("[%s] Failed to schedule ReleaseDraft YAML update: %s", + job["id"], e) except Exception as e: logger.error("[%s] Failed to update draft finalize state: %s", job["id"], e) diff --git a/delivery-kid/pinning-service/app/services/pickipedia_client.py b/delivery-kid/pinning-service/app/services/pickipedia_client.py index 4a0eb43..cb9f921 100644 --- a/delivery-kid/pinning-service/app/services/pickipedia_client.py +++ b/delivery-kid/pinning-service/app/services/pickipedia_client.py @@ -159,3 +159,89 @@ def snapshot_diagnostics_for_dict_async(draft_id: str, draft_data: dict): "preview_log": draft_data.get("preview_log") or [], } return asyncio.to_thread(snapshot_diagnostics, draft_id, payload) + + +def write_finalized_to_releasedraft( + draft_id: str, + final_cid: str, + finalized_at: str, +) -> bool: + """Edit the ``ReleaseDraft:{draft_id}`` wiki page YAML to mark it finalized. + + Pre-V2-Coconut-migration, the browser-side JS handled this edit when + the finalize SSE delivered a ``complete`` event. For the slow Coconut + path that event never fires (the SSE closes after submission and the + user's browser doesn't see the eventual completion), so we write from + the webhook side instead. + + The edit: + - Loads the current YAML + - Sets ``status: finalized``, ``final_cid: ``, ``finalized_at: `` + - Saves with summary ``Finalized: pinned to IPFS as `` + + The summary line is what the Blue Railroad Imports bot's + ``find_cid_from_history`` greps for to trigger Release page creation + on its next cron run. + + Returns True if the edit succeeded, False if creds are missing or the + write failed. Never raises. + """ + site = _get_site() + if site is None: + return False + + try: + import yaml as _yaml + except ImportError: + logger.error("pickipedia_client: PyYAML not installed; cannot edit ReleaseDraft YAML") + return False + + title = f"ReleaseDraft:{draft_id}" + try: + page = site.pages[title] + if not page.exists: + logger.warning("pickipedia_client: %s does not exist; cannot mark finalized", title) + return False + + existing_text = page.text() + try: + data = _yaml.safe_load(existing_text) or {} + except _yaml.YAMLError as e: + logger.error("pickipedia_client: failed to parse %s YAML: %s", title, e) + return False + + if not isinstance(data, dict): + logger.error("pickipedia_client: %s YAML is not a mapping; refusing to edit", title) + return False + + data["status"] = "finalized" + data["final_cid"] = final_cid + data["finalized_at"] = finalized_at + + new_text = _yaml.safe_dump(data, default_flow_style=False, sort_keys=False) + summary = f"Finalized: pinned to IPFS as {final_cid}" + + if new_text.strip() == existing_text.strip(): + return True + + page.save(new_text, summary=summary) + logger.info("pickipedia_client: marked %s finalized (final_cid=%s)", title, final_cid) + return True + except Exception as e: + logger.error("pickipedia_client: failed to mark %s finalized: %s", title, e) + return False + + +async def write_finalized_to_releasedraft_async( + draft_id: str, + final_cid: str, + finalized_at: str, +): + """Async wrapper for ``write_finalized_to_releasedraft``. + + Runs the sync mwclient calls in a thread so the FastAPI event loop + isn't blocked while the wiki round-trip happens. + """ + return await asyncio.to_thread( + write_finalized_to_releasedraft, draft_id, final_cid, finalized_at, + ) diff --git a/delivery-kid/pinning-service/requirements.txt b/delivery-kid/pinning-service/requirements.txt index 45cbed0..33844f3 100644 --- a/delivery-kid/pinning-service/requirements.txt +++ b/delivery-kid/pinning-service/requirements.txt @@ -24,3 +24,7 @@ libtorrent>=2.0.0 # MediaWiki client — used by pickipedia_client to snapshot draft # diagnostics to ReleaseDraft:{id}/diagnostics on terminal state. mwclient>=0.10.1 + +# YAML parser — used by pickipedia_client to read/update the +# ReleaseDraft:{id} wiki page YAML when finalize completes. +PyYAML>=6.0