From 0e53640b943dbe559950c0078e417bf7c4bbcde9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20F=C3=B6rtsch?= Date: Tue, 14 Apr 2026 20:59:28 +0200 Subject: [PATCH] =?UTF-8?q?done=20column:=20'verify=20N'=20header=20button?= =?UTF-8?q?=20to=20backfill=20=E2=9C=93=20=E2=86=92=20=E2=9C=93=E2=9C=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit new POST /api/execute/verify-unverified that picks every plan with status=done + verified=0 and runs handOffToJellyfin sequentially in the background. each handoff fires the existing plan_update sse so the done column promotes cards as jellyfin's verdict lands. exported handOffToJellyfin so the route can reuse the same flow as a fresh job. done column header shows a 'Verify N' action whenever there are unverified done plans, alongside the existing 'Clear'. one click and the user can backfill ✓✓ across every legacy done item without re-transcoding. Co-Authored-By: Claude Opus 4.6 (1M context) --- package.json | 2 +- server/api/execute.ts | 36 +++++++++++++++++++++++++++- src/features/pipeline/DoneColumn.tsx | 22 +++++++++++++---- 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 4efc5b3..e35cd63 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "netfelix-audio-fix", - "version": "2026.04.14.23", + "version": "2026.04.14.24", "scripts": { "dev:server": "NODE_ENV=development bun --hot server/index.tsx", "dev:client": "vite", diff --git a/server/api/execute.ts b/server/api/execute.ts index cdcb8c9..22c055f 100644 --- a/server/api/execute.ts +++ b/server/api/execute.ts @@ -35,7 +35,7 @@ import type { Job, MediaItem, MediaStream } from "../types"; * match the plan, so the check always passed immediately. Jellyfin is the * independent observer that matters. */ -async function handOffToJellyfin(itemId: number): Promise { +export async function handOffToJellyfin(itemId: number): Promise { const db = getDb(); const row = db.prepare("SELECT jellyfin_id FROM media_items WHERE id = ?").get(itemId) as | { jellyfin_id: string } @@ -354,6 +354,40 @@ app.post("/clear-completed", (c) => { return c.json({ ok: true, cleared: result.changes }); }); +// ─── Verify all unverified done plans ──────────────────────────────────────── +// Backfill: kicks off the post-job jellyfin handoff for every plan that's +// status=done + verified=0. Sequential with a small inter-call delay to +// avoid hammering jellyfin's metadata refresher (each one waits up to 15s +// for DateLastRefreshed to advance). Returns immediately with the count; +// each individual handoff emits a plan_update SSE so the UI promotes ✓ → ✓✓ +// (or flips back to Review on disagreement) as it lands. +app.post("/verify-unverified", (c) => { + const db = getDb(); + const rows = db + .prepare(` + SELECT mi.id as item_id FROM review_plans rp + JOIN media_items mi ON mi.id = rp.item_id + WHERE rp.status = 'done' AND rp.verified = 0 + ORDER BY rp.reviewed_at DESC NULLS LAST + `) + .all() as { item_id: number }[]; + + if (rows.length === 0) return c.json({ ok: true, count: 0 }); + + (async () => { + for (const row of rows) { + try { + await handOffToJellyfin(row.item_id); + } catch (err) { + warn(`verify-unverified: handoff for item ${row.item_id} threw: ${String(err)}`); + } + } + log(`verify-unverified: processed ${rows.length} unverified done plan(s)`); + })(); + + return c.json({ ok: true, count: rows.length }); +}); + // ─── Stop running job ───────────────────────────────────────────────────────── app.post("/stop", (c) => { diff --git a/src/features/pipeline/DoneColumn.tsx b/src/features/pipeline/DoneColumn.tsx index 2ecd2b5..ff38dd8 100644 --- a/src/features/pipeline/DoneColumn.tsx +++ b/src/features/pipeline/DoneColumn.tsx @@ -20,12 +20,24 @@ export function DoneColumn({ items, onMutate }: DoneColumnProps) { onMutate(); }; + const verifyUnverified = async () => { + await api.post("/api/execute/verify-unverified"); + // Server processes sequentially in the background; each plan_update + // SSE will trigger a pipeline reload as items get verified. + }; + + const unverifiedCount = items.filter((i) => i.status === "done" && i.verified !== 1).length; + + const actions = []; + if (unverifiedCount > 0) { + actions.push({ label: `Verify ${unverifiedCount}`, onClick: verifyUnverified }); + } + if (items.length > 0) { + actions.push({ label: "Clear", onClick: clear }); + } + return ( - 0 ? [{ label: "Clear", onClick: clear }] : undefined} - > + 0 ? actions : undefined}> {items.map((item) => { const verified = item.status === "done" && item.verified === 1; const mark = verified ? "✓✓" : item.status === "done" ? "✓" : "✗";