video Manage Workers modal: decouple coverage click from global priority; Retry-all covers all kinds

Two UX fixes from feedback:
- Clicking a coverage card on a worker was also setting the GLOBAL 'process first'
  priority (and silently re-queuing that kind's failed items) — so picking a worker's
  coverage reached across and re-prioritised every worker. Now a coverage card just
  switches the view; priority changes only via the top Movies/Shows/Auto tabs, and
  re-queuing is the explicit 'Retry all failed' button. (Removed the now-dead
  requeueFailed helper.)
- 'Retry all failed' now re-queues EVERY coverage kind the worker handles (movie+show,
  etc.), not just the tab you're viewing — matching the 'all' in the label.

3 wiring tests; node --check clean.
This commit is contained in:
BoulderBadgeDad 2026-06-20 12:50:18 -07:00
parent d6776f9f57
commit 0eac0ea46e
2 changed files with 77 additions and 28 deletions

View file

@ -0,0 +1,50 @@
"""Wiring guards for the video "Manage Workers" modal (video-enrichment-manager.js).
Pins two behaviours the user asked for:
- clicking a coverage card just SWITCHES THE VIEW it must not change the global
"process first" priority or silently re-queue failed items (those are the top
Movies/Shows/Auto tabs and the explicit "Retry all failed" button);
- "Retry all failed" re-queues EVERY coverage kind the worker handles, not just
the tab being viewed.
"""
from __future__ import annotations
import re
from pathlib import Path
_ROOT = Path(__file__).resolve().parent.parent
_JS = (_ROOT / "webui" / "static" / "video" / "video-enrichment-manager.js").read_text(encoding="utf-8")
def _func(name: str) -> str:
"""Return the body of `function name(` up to the next top-level `function `."""
i = _JS.index("function " + name + "(")
nxt = _JS.find("\n function ", i + 1)
return _JS[i:nxt if nxt != -1 else len(_JS)]
def test_switch_kind_only_switches_view():
body = _func("switchKind")
assert "state.kind = kind" in body
# must NOT reach across to the global priority or auto-retry
assert "setPriority(" not in body
assert "requeueFailed(" not in body
def test_priority_changes_only_from_top_tabs():
# setPriority is still wired — but only to the data-em-priority (top tabs) click.
assert "function setPriority(" in _JS
assert "data-em-priority')) setPriority(" in _JS
# the dead per-coverage requeue helper is gone
assert "function requeueFailed(" not in _JS
def test_retry_all_failed_covers_every_kind_of_the_worker():
body = _func("retryAllFailed")
# iterate the worker's kinds (movie+show / show / video …), retry each
assert "workerDef(state.selected)" in body
assert "w.kinds" in body
assert re.search(r"kinds\.map\(", body)
# the button is wired to the multi-kind handler, not the single-kind retry
assert "data-em-retry-all')) retryAllFailed()" in _JS

View file

@ -391,35 +391,14 @@
Promise.all([loadBreakdown(id), loadUnmatched()]).then(function () { renderPanel(); });
}
function switchKind(kind) {
// A coverage card just SWITCHES THE VIEW to that kind — it must NOT change
// the global "process first" priority (that's the top Movies/Shows/Auto
// tabs only) or silently re-queue failed items (the explicit "Retry all
// failed" button does that on demand). Keeping them separate so clicking a
// worker's coverage doesn't reach across and re-prioritise every worker.
state.kind = kind; state.page = 0;
renderCards();
// Like the music worker manager: clicking a coverage group also "pins" it
// and RE-QUEUES its previously-failed items (not_found/error -> pending) so
// the worker sweeps ALL unmatched, not just never-tried ones — otherwise
// failed items sit forever (in the retry cooldown) and the worker reports
// "all matched". (Episodes are a sync cascade, not a match queue.)
if (kind === 'movie' || kind === 'show') {
setPriority(kind);
requeueFailed(kind);
} else {
loadUnmatched().then(function () { renderControls(); renderList(); });
}
}
function requeueFailed(kind) {
fetch('/api/video/enrichment/' + state.selected + '/retry', {
method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify({ kind: kind, scope: 'failed' }),
}).then(function (r) { return r.ok ? r.json() : null; })
.then(function (d) {
var n = (d && d.reset) || 0;
if (n && typeof showToast === 'function') {
showToast('Re-queued ' + n + ' previously-failed ' +
(KIND_LABEL[kind] || kind).toLowerCase(), 'success');
}
return Promise.all([loadBreakdown(state.selected), loadUnmatched()]);
})
.then(function () { renderCards(); renderControls(); renderList(); })
.catch(function () { loadUnmatched().then(function () { renderControls(); renderList(); }); });
loadUnmatched().then(function () { renderControls(); renderList(); });
}
function togglePause() {
var s = state.statuses[state.selected] || {};
@ -438,6 +417,26 @@
return Promise.all([loadBreakdown(state.selected), loadUnmatched()]);
}).then(function () { renderCards(); renderList(); });
}
// "Retry all failed" applies to EVERY coverage kind this worker handles (not
// just the tab you're viewing) — the label says "all", so re-queue movie + show
// (or whatever kinds the worker covers) in one click.
function retryAllFailed() {
var w = workerDef(state.selected);
var kinds = (w && w.kinds && w.kinds.length) ? w.kinds : [state.kind];
Promise.all(kinds.map(function (k) {
return fetch('/api/video/enrichment/' + state.selected + '/retry', {
method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify({ kind: k, scope: 'failed' }),
}).then(function (r) { return r.ok ? r.json() : null; });
})).then(function (results) {
var total = results.reduce(function (a, r) { return a + ((r && r.reset) || 0); }, 0);
if (total && typeof showToast === 'function') {
showToast('Re-queued ' + total + ' failed item' + (total === 1 ? '' : 's') +
' across ' + kinds.length + ' categor' + (kinds.length === 1 ? 'y' : 'ies'), 'success');
}
return Promise.all([loadBreakdown(state.selected), loadUnmatched()]);
}).then(function () { renderCards(); renderControls(); renderList(); });
}
function setPriority(kind) {
state.priority = kind;
renderGlobalTabs();
@ -505,7 +504,7 @@
else if (t.hasAttribute('data-em-select')) selectWorker(t.getAttribute('data-em-select'));
else if (t.hasAttribute('data-em-pause')) togglePause();
else if (t.hasAttribute('data-em-kind')) switchKind(t.getAttribute('data-em-kind'));
else if (t.hasAttribute('data-em-retry-all')) retry('failed', null);
else if (t.hasAttribute('data-em-retry-all')) retryAllFailed();
else if (t.hasAttribute('data-em-retry-item')) retry('item', Number(t.getAttribute('data-em-retry-item')));
else if (t.hasAttribute('data-em-page')) {
state.page += (t.getAttribute('data-em-page') === 'next') ? 1 : -1;