From 027cf74d4758c6b196457696bb9eda9bb52f6e64 Mon Sep 17 00:00:00 2001 From: dev Date: Sat, 27 Jun 2026 19:27:28 +0200 Subject: [PATCH] ui(downloads): Unverified review rows get the Quarantine-style card design MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Quarantine sub-view was reworked into rich cards (artwork, source line, row-click to expand an inline detail panel, consistent action cluster) but the Unverified sub-view was left on the generic download-row layout that opened a modal on click. Bring it to parity: - dedicated _verifUnverifiedRowHtml / _verifUnverifiedRows renderer, used via a sibling branch in _adlRender (mirrors the quarantine sub-view branch). - row click toggles an inline details panel (why flagged, download source, quality, file, downloaded-at), open-state keyed by stable id so it survives the 2 s poll re-render — same pattern as verifQuarInspect. - reuses the existing verif-quar-* / verif-actions / adl-row CSS (no new styles) and the existing play/compare/audit/approve/delete handlers. - NO grouping: each unverified import is its own track (grouping only makes sense for the quarantine alternates), per design intent. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01LWJk7EuM7YktQeNyqQwTZY --- webui/static/pages-extra.js | 90 +++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/webui/static/pages-extra.js b/webui/static/pages-extra.js index 560ac261..06392b36 100644 --- a/webui/static/pages-extra.js +++ b/webui/static/pages-extra.js @@ -2760,6 +2760,84 @@ async function verifDelete(hid, btn) { } catch (e) { showToast && showToast('Delete failed', 'error'); } if (btn) btn.disabled = false; } + +// ---- Unverified review rows (Quarantine-style cards) ---- +// The Unverified sub-view used to piggyback on the generic download row and +// open a modal on click. These give it the same card design the Quarantine +// sub-view got — row click expands an inline detail panel in place — minus the +// grouping (each unverified import is its own track, nothing to group). +let _verifUnvData = []; +const _verifUnvOpenDetails = new Set(); + +function _verifUnvKey(dl) { + return verifHistoryId(dl) || dl.task_id || ''; +} + +function _verifUnvDetailRows(dl) { + const reason = dl.verification_status === 'force_imported' + ? 'Accepted as the best available candidate after the retry budget was exhausted (version-mismatch fallback). A library AcoustID scan reports these as informational.' + : 'AcoustID could not hard-confirm this file (ambiguous / cross-script metadata / no fingerprint match). Imported, but not verified.'; + const source = dl.batch_source || dl.batch_name || ''; + return [ + `
Why flagged: ${_adlEsc(reason)}
`, + source ? `
Download source: ${_adlEsc(source)}
` : '', + dl.quality ? `
Quality: ${_adlEsc(dl.quality)}
` : '', + dl.file_path ? `
File: ${_adlEsc(dl.file_path)}
` : '', + dl.created_at ? `
Downloaded: ${_adlEsc(dl.created_at)}
` : '', + ].filter(Boolean).join(''); +} + +function _verifUnverifiedRowHtml(dl, idx) { + const hid = verifHistoryId(dl); + const title = _adlEsc(dl.title || 'Unknown Track'); + const meta = [_adlEsc(dl.artist || ''), _adlEsc(dl.album || '')].filter(Boolean).join(' · '); + const source = _adlEsc(dl.batch_source || dl.batch_name || ''); + const timeAgo = _verifTimeAgo(dl.created_at); + const artHtml = dl.artwork + ? `` + : '
'; + const detailsOpen = _verifUnvOpenDetails.has(_verifUnvKey(dl)); + const actions = hid ? ` + + + + + ` : ''; + return `
+ ${artHtml} +
+
${title}
+ ${meta ? `
${meta}
` : ''} + ${source ? `
${source}
` : ''} +
${_verifUnvDetailRows(dl)}
+
+
+ ${_verifReasonBadge(dl)} + ${dl.quality ? `${_adlEsc(dl.quality)}` : ''} + ${timeAgo ? `${timeAgo}` : ''} + ${actions} +
+
`; +} + +function _verifUnverifiedRows(items) { + _verifUnvData = items; + if (!items.length) return ''; + return items.map((dl, idx) => _verifUnverifiedRowHtml(dl, idx)).join(''); +} + +function verifUnvInspect(idx) { + // Open-state lives in a Set keyed by the row's stable id (not the DOM) so it + // survives the 2 s polling re-render — same pattern as verifQuarInspect. + const dl = _verifUnvData[idx]; + if (!dl) return; + const key = _verifUnvKey(dl); + if (_verifUnvOpenDetails.has(key)) _verifUnvOpenDetails.delete(key); + else _verifUnvOpenDetails.add(key); + const el = document.getElementById(`verif-unv-details-${idx}`); + if (el) el.style.display = _verifUnvOpenDetails.has(key) ? '' : 'none'; +} + // ---- Quarantine sub-view inside the ⚠ filter ---- // The review queue covers BOTH kinds of suspect files: imported-but- // unconfirmed (unverified/force_imported) and not-imported-at-all @@ -3293,6 +3371,18 @@ function _adlRender() { return; } + // Unverified review sub-view: render the Quarantine-style cards (inline + // expandable details), not the generic download rows. + if (_adlFilter === 'unverified' && _verifSubView === 'unverified') { + const uhtml = _verifUnverifiedRows(filtered); + const uEmptyEl = document.getElementById('adl-empty'); + const uEmptyHtml = uEmptyEl ? uEmptyEl.outerHTML : ''; + list.innerHTML = uEmptyHtml + uhtml; + const uNewEmpty = document.getElementById('adl-empty'); + if (uNewEmpty) uNewEmpty.style.display = uhtml ? 'none' : ''; + return; + } + if (filtered.length === 0) { if (empty) empty.style.display = ''; // Clear any existing rows but keep the empty message