From 0eac0ea46ee29188059411556b04b796b0798a12 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Sat, 20 Jun 2026 12:50:18 -0700 Subject: [PATCH] video Manage Workers modal: decouple coverage click from global priority; Retry-all covers all kinds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- tests/test_video_enrichment_manager.py | 50 +++++++++++++++++ .../static/video/video-enrichment-manager.js | 55 +++++++++---------- 2 files changed, 77 insertions(+), 28 deletions(-) create mode 100644 tests/test_video_enrichment_manager.py diff --git a/tests/test_video_enrichment_manager.py b/tests/test_video_enrichment_manager.py new file mode 100644 index 00000000..948ca150 --- /dev/null +++ b/tests/test_video_enrichment_manager.py @@ -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 diff --git a/webui/static/video/video-enrichment-manager.js b/webui/static/video/video-enrichment-manager.js index 9e5a4883..f64e60ea 100644 --- a/webui/static/video/video-enrichment-manager.js +++ b/webui/static/video/video-enrichment-manager.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;