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" ? "✓" : "✗";