fix(analysis): emit Cancelled event + assert lock-release after cancel (0.87.13)

Two 0.87.4 follow-ups:

1. Cron tick: existing shutdown-on-cancel test now also asserts the
   advisory lock is re-acquirable after cancel-during-tick. Without
   this, a refactor moving `pg_advisory_unlock` under the cancel arm
   would ship green and break multi-replica safety.

2. Analysis worker: cancel-mid-dispatch left the dashboard banner stuck
   on the cancelled page until a later page overwrote it. Add a
   `Cancelled` AnalysisEvent variant, publish on the cancel-release
   branch, wire the analysis and admin overview dashboards to clear
   on it. Per-page chip rolls back from `analyzing` to `queued`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-23 08:01:53 +02:00
parent 95b98eebf1
commit 651fb27522
9 changed files with 88 additions and 5 deletions

View File

@@ -857,7 +857,12 @@ export type AnalysisEvent =
chapter_id: string | null;
}
| {
kind: 'started' | 'completed' | 'failed';
// `cancelled` fires when the worker drops an in-flight dispatch
// on daemon shutdown / settings-toggle-off. The lease was
// released, not failed — the page returns to pending. The
// dashboard banner clears on this so it doesn't stick on the
// cancelled page until another `started` overwrites it.
kind: 'started' | 'completed' | 'failed' | 'cancelled';
page_id: string;
manga_id: string;
manga_title: string;

View File

@@ -121,6 +121,16 @@
function applyAnalysisEvent(ev: AnalysisEvent) {
if (ev.kind === 'enqueued') return;
// Cancelled (daemon shutdown / settings toggle-off mid-dispatch)
// is a non-terminal release — the lease went back to pending.
// Clear the banner instead of frame-flipping to a "done"/"failed"
// misrepresentation.
if (ev.kind === 'cancelled') {
if (nowAnalyzing && nowAnalyzing.page === ev.page_number) {
nowAnalyzing = null;
}
return;
}
const phase =
ev.kind === 'started' ? 'analyzing' : ev.kind === 'completed' ? 'done' : 'failed';
nowAnalyzing = {

View File

@@ -247,6 +247,21 @@
scheduleClear(ev.page_id);
}
pushTicker(`✗ Failed page ${ev.page_number}`);
} else if (ev.kind === 'cancelled') {
// Daemon shutdown / analysis toggled off mid-dispatch. The
// lease was released, not failed — the page is still pending
// server-side. We just need to clear the "Analyzing pN" banner
// and roll the per-page live status back to queued so the
// chip doesn't stay spinner-frozen forever.
const cur = liveStatus[ev.page_id];
if (cur === 'analyzing') {
liveStatus = { ...liveStatus, [ev.page_id]: 'queued' };
}
if (nowAnalyzing?.page_id === ev.page_id) {
nowAnalyzing = null;
if (clearTimer) clearTimeout(clearTimer);
}
pushTicker(`↺ Cancelled page ${ev.page_number}`);
}
}