From f8afe566423da39dff9750a33454bba172b81757 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Tue, 2 Jun 2026 19:58:14 -0700 Subject: [PATCH] Enrichment priority: pinning sweeps the whole group; rail shows live phase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aligns the 'process this group first' behaviour with intent: - Pinning a group now also re-queues that group's previously-failed (not_found -> pending) items, so the worker processes ALL unmatched in the group (pending + missing), not just never-tried ones. Safe from loops: each is attempted once, still-unmatched return to not_found, and the pending-only worker hook won't re-pick them. Toast reports how many were re-queued. - The left rail now shows each worker's current group while running ('Running · albums'), so you can see what every service is on at a glance. Frontend only; reuses the tested /priority + /retry endpoints. --- webui/static/enrichment-manager.js | 58 +++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 9 deletions(-) diff --git a/webui/static/enrichment-manager.js b/webui/static/enrichment-manager.js index 9eadc351..82d9a8de 100644 --- a/webui/static/enrichment-manager.js +++ b/webui/static/enrichment-manager.js @@ -264,6 +264,16 @@ function _emOverallPct(status) { return total ? Math.round((matched / total) * 100) : 0; } +// Rail sub-line: while running, show the group it's on ("Running · albums"); +// otherwise the status + overall coverage %. +function _emRailSubText(status) { + const info = _emStatusInfo(status); + const phase = _emCurrentPhase(status); + if (info.cls === 'running' && phase) return `${info.label} · ${_emEntityLabel(phase, true).toLowerCase()}`; + const pct = _emOverallPct(status); + return `${info.label}${pct == null ? '' : ` · ${pct}%`}`; +} + function renderEnrichmentRail() { const rail = document.getElementById('em-rail'); if (!rail) return; @@ -279,7 +289,7 @@ function renderEnrichmentRail() { ${_emIconHtml(w.id)} ${_emEscape(w.name)} - ${info.label}${pct == null ? '' : ` · ${pct}%`} + ${_emRailSubText(status)} ${cov} @@ -304,7 +314,7 @@ function _emUpdateRailRow(id) { const dot = row.querySelector('.em-dot'); if (dot) { dot.className = `em-dot em-dot--${info.cls}`; dot.title = info.label; } const sub = row.querySelector('.em-worker-sub'); - if (sub) sub.textContent = `${info.label}${pct == null ? '' : ` · ${pct}%`}`; + if (sub) sub.textContent = _emRailSubText(status); const cov = row.querySelector('.em-rail-cov-fill'); if (cov && pct != null) cov.style.width = `${pct}%`; } @@ -465,6 +475,7 @@ function _emRenderChain() { async function setEnrichmentPriority(entity) { const id = enrichmentManagerState.selected; + const name = _emWorkerById[id].name; try { const res = await fetch(`/api/enrichment/${id}/priority`, { method: 'POST', @@ -472,15 +483,44 @@ async function setEnrichmentPriority(entity) { body: JSON.stringify({ entity: entity || 'none' }), }); const data = await res.json().catch(() => ({})); - if (res.ok && data.success) { - enrichmentManagerState.priority = data.priority || ''; - showToast(enrichmentManagerState.priority - ? `${_emWorkerById[id].name} will process ${_emEntityLabel(enrichmentManagerState.priority, true).toLowerCase()} first` - : `${_emWorkerById[id].name} back to automatic order`, 'success'); - _emRenderChain(); - } else { + if (!res.ok || !data.success) { showToast(data.error || 'Could not set processing order', 'error'); + return; } + enrichmentManagerState.priority = data.priority || ''; + + if (!enrichmentManagerState.priority) { + showToast(`${name} back to automatic order`, 'success'); + _emRenderChain(); + return; + } + + // Pin means "process the whole group". Re-queue this group's + // previously-failed items (not_found -> pending) so the worker sweeps + // ALL unmatched, not just never-tried ones. Safe: each is attempted + // once; still-unmatched become not_found again and the pending-only + // pin won't re-pick them, so no loop. + let reset = 0; + try { + const rr = await fetch(`/api/enrichment/${id}/retry`, { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ entity_type: entity, scope: 'failed' }), + }); + const rd = await rr.json().catch(() => ({})); + if (rr.ok) reset = rd.reset || 0; + } catch (_e) { /* priority still set; failed sweep is best-effort */ } + + const label = _emEntityLabel(entity, true).toLowerCase(); + showToast(reset + ? `${name} will process all ${label} first · re-queued ${reset.toLocaleString()} previously-failed` + : `${name} will process ${label} first`, 'success'); + + // Counts changed (failed -> pending) — refresh stats, chain and list. + await Promise.all([_emLoadBreakdown(id), _emLoadUnmatched()]); + _emRenderStats(); + _emRenderChain(); + _emRenderUnmatchedControls(); + _emRenderUnmatchedList(); } catch (_e) { showToast('Error setting processing order', 'error'); }