done column: 'verify N' header button to backfill ✓ → ✓✓
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m5s

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) <noreply@anthropic.com>
This commit is contained in:
2026-04-14 20:59:28 +02:00
parent 51d56a4082
commit 0e53640b94
3 changed files with 53 additions and 7 deletions

View File

@@ -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",

View File

@@ -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<void> {
export async function handOffToJellyfin(itemId: number): Promise<void> {
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) => {

View File

@@ -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 (
<ColumnShell
title="Done"
count={items.length}
actions={items.length > 0 ? [{ label: "Clear", onClick: clear }] : undefined}
>
<ColumnShell title="Done" count={items.length} actions={actions.length > 0 ? actions : undefined}>
{items.map((item) => {
const verified = item.status === "done" && item.verified === 1;
const mark = verified ? "✓✓" : item.status === "done" ? "✓" : "✗";