done column: 'verify N' header button to backfill ✓ → ✓✓
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m5s
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:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "netfelix-audio-fix",
|
"name": "netfelix-audio-fix",
|
||||||
"version": "2026.04.14.23",
|
"version": "2026.04.14.24",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev:server": "NODE_ENV=development bun --hot server/index.tsx",
|
"dev:server": "NODE_ENV=development bun --hot server/index.tsx",
|
||||||
"dev:client": "vite",
|
"dev:client": "vite",
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import type { Job, MediaItem, MediaStream } from "../types";
|
|||||||
* match the plan, so the check always passed immediately. Jellyfin is the
|
* match the plan, so the check always passed immediately. Jellyfin is the
|
||||||
* independent observer that matters.
|
* independent observer that matters.
|
||||||
*/
|
*/
|
||||||
async function handOffToJellyfin(itemId: number): Promise<void> {
|
export async function handOffToJellyfin(itemId: number): Promise<void> {
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
const row = db.prepare("SELECT jellyfin_id FROM media_items WHERE id = ?").get(itemId) as
|
const row = db.prepare("SELECT jellyfin_id FROM media_items WHERE id = ?").get(itemId) as
|
||||||
| { jellyfin_id: string }
|
| { jellyfin_id: string }
|
||||||
@@ -354,6 +354,40 @@ app.post("/clear-completed", (c) => {
|
|||||||
return c.json({ ok: true, cleared: result.changes });
|
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 ─────────────────────────────────────────────────────────
|
// ─── Stop running job ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
app.post("/stop", (c) => {
|
app.post("/stop", (c) => {
|
||||||
|
|||||||
@@ -20,12 +20,24 @@ export function DoneColumn({ items, onMutate }: DoneColumnProps) {
|
|||||||
onMutate();
|
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 (
|
return (
|
||||||
<ColumnShell
|
<ColumnShell title="Done" count={items.length} actions={actions.length > 0 ? actions : undefined}>
|
||||||
title="Done"
|
|
||||||
count={items.length}
|
|
||||||
actions={items.length > 0 ? [{ label: "Clear", onClick: clear }] : undefined}
|
|
||||||
>
|
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
const verified = item.status === "done" && item.verified === 1;
|
const verified = item.status === "done" && item.verified === 1;
|
||||||
const mark = verified ? "✓✓" : item.status === "done" ? "✓" : "✗";
|
const mark = verified ? "✓✓" : item.status === "done" ? "✓" : "✗";
|
||||||
|
|||||||
Reference in New Issue
Block a user