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:
parent
d6776f9f57
commit
0eac0ea46e
2 changed files with 77 additions and 28 deletions
50
tests/test_video_enrichment_manager.py
Normal file
50
tests/test_video_enrichment_manager.py
Normal 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
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue