push verified=1 to the UI via a plan_update SSE event
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m5s

the ✓✓ write was landing in the db but never reaching the browser.
job_update fires once at job completion (card renders ✓, verified=0),
then handOffToJellyfin takes ~15s to refresh jellyfin + re-analyze +
UPDATE review_plans SET verified=1. no further sse, so the pipeline
page never re-polled and the card stayed at ✓ until the user
navigated away and back.

new plan_update event emitted at the end of handOffToJellyfin. the
pipeline page listens and triggers the same 1s-coalesced reload as
job_update, so the done column promotes ✓ → ✓✓ within a second of
jellyfin's verdict landing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-14 20:51:47 +02:00
parent 3be22a5742
commit 51d56a4082
3 changed files with 23 additions and 1 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "netfelix-audio-fix", "name": "netfelix-audio-fix",
"version": "2026.04.14.22", "version": "2026.04.14.23",
"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",

View File

@@ -88,6 +88,10 @@ async function handOffToJellyfin(itemId: number): Promise<void> {
const result = await upsertJellyfinItem(db, fresh, rescanCfg, { source: "webhook" }); const result = await upsertJellyfinItem(db, fresh, rescanCfg, { source: "webhook" });
log(`Post-job verify for item ${itemId}: is_noop=${result.isNoop}`); log(`Post-job verify for item ${itemId}: is_noop=${result.isNoop}`);
// Nudge connected clients so the Done column re-polls and promotes
// the card from ✓ to ✓✓ (or flips it back to Review if jellyfin
// disagreed).
emitPlanUpdate(itemId);
} catch (err) { } catch (err) {
warn(`Post-job verification for item ${itemId} failed: ${String(err)}`); warn(`Post-job verification for item ${itemId} failed: ${String(err)}`);
} }
@@ -169,6 +173,18 @@ function emitJobProgress(jobId: number, seconds: number, total: number): void {
for (const l of jobListeners) l(line); for (const l of jobListeners) l(line);
} }
/**
* Emit when a review_plan mutates asynchronously (after the job already
* finished). Right now the only producer is handOffToJellyfin — the
* verified=1 write that lands ~15s after a job completes. Without this
* the UI would keep showing ✓ indefinitely until the user navigates
* away and back, since we'd never fire job_update again for that item.
*/
function emitPlanUpdate(itemId: number): void {
const line = `event: plan_update\ndata: ${JSON.stringify({ itemId })}\n\n`;
for (const l of jobListeners) l(line);
}
/** Parse "Duration: HH:MM:SS.MS" from ffmpeg startup output. */ /** Parse "Duration: HH:MM:SS.MS" from ffmpeg startup output. */
function parseFFmpegDuration(line: string): number | null { function parseFFmpegDuration(line: string): number | null {
const match = line.match(/Duration:\s*(\d+):(\d+):(\d+)\.(\d+)/); const match = line.match(/Duration:\s*(\d+):(\d+):(\d+)\.(\d+)/);

View File

@@ -64,6 +64,12 @@ export function PipelinePage() {
} }
scheduleReload(); scheduleReload();
}); });
// plan_update lands ~15s after a job finishes — the post-job jellyfin
// verification writes verified=1 (or flips the plan back to pending).
// Without refreshing here the Done column would never promote ✓ to ✓✓.
es.addEventListener("plan_update", () => {
scheduleReload();
});
es.addEventListener("job_progress", (e) => { es.addEventListener("job_progress", (e) => {
setProgress(JSON.parse((e as MessageEvent).data)); setProgress(JSON.parse((e as MessageEvent).data));
}); });