From 0594673cadc6311b541d924d78469cee6f4ddc91 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Sun, 15 Mar 2026 13:00:44 -0700 Subject: [PATCH] Polish finding detail renderers with richer visuals - Cover art findings: show artwork image thumbnail preview - Duplicate findings: KEEP/REMOVE badges highlighting best quality copy - Incomplete album: completion progress bar with percentage - Fake lossless: spectral analysis bar comparing detected vs expected cutoff - Resolved findings: show descriptive action label (e.g. "Entry Removed") - Fix operator precedence bug in fake lossless nyquist calculation --- webui/static/script.js | 87 +++++++++++++++++++++++------- webui/static/style.css | 118 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+), 18 deletions(-) diff --git a/webui/static/script.js b/webui/static/script.js index 39353a17..e40bcb7a 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -50438,8 +50438,17 @@ async function loadRepairFindings() { container.innerHTML = items.map(f => { const icon = severityIcons[f.severity] || 'ℹ️'; const age = formatCacheAge(f.created_at); - const statusBadge = f.status !== 'pending' ? - `${f.status}` : ''; + const actionLabels = { + removed_db_entry: 'Entry Removed', deleted_file: 'File Deleted', + already_gone: 'Already Gone', fixed_track_number: 'Track # Fixed', + applied_cover_art: 'Art Applied', applied_metadata: 'Metadata Applied', + removed_duplicates: 'Duplicates Removed', + }; + let statusBadge = ''; + if (f.status !== 'pending') { + const actionText = actionLabels[f.user_action] || f.status; + statusBadge = `${actionText}`; + } const typeLabel = typeLabels[f.finding_type] || f.finding_type.replace(/_/g, ' '); const d = f.details || {}; const filePath = f.file_path || d.original_path || d.file_path || ''; @@ -50555,41 +50564,76 @@ function _renderFindingDetail(f) { if (f.file_path) rows.push(['File', f.file_path, 'path']); return _gridRows(rows); - case 'fake_lossless': + case 'fake_lossless': { + const cutoff = d.detected_cutoff_khz || 0; + const expectedMin = d.expected_min_khz || 0; + const nyquist = d.nyquist_khz || (d.sample_rate ? d.sample_rate / 2000 : 22.05); + let flHtml = ''; + if (cutoff && expectedMin) { + const cutoffPct = Math.min(100, Math.round((cutoff / nyquist) * 100)); + const expectedPct = Math.min(100, Math.round((expectedMin / nyquist) * 100)); + flHtml += `
+
Spectral Analysis
+
+
+
+
+
+ ${cutoff} kHz detected + ${expectedMin} kHz expected min +
+
`; + } if (d.format) rows.push(['Format', d.format.toUpperCase()]); - rows.push(['Spectral Cutoff', `${d.detected_cutoff_khz || '?'} kHz`]); - rows.push(['Expected Min', `${d.expected_min_khz || '?'} kHz`]); if (d.sample_rate) rows.push(['Sample Rate', `${d.sample_rate} Hz`]); - if (d.nyquist_khz) rows.push(['Nyquist', `${d.nyquist_khz} kHz`]); if (d.bit_depth) rows.push(['Bit Depth', `${d.bit_depth}-bit`]); if (d.bitrate) rows.push(['Bitrate', `${d.bitrate} kbps`]); if (d.file_size) rows.push(['File Size', _formatFileSize(d.file_size)]); if (f.file_path) rows.push(['File', f.file_path, 'path']); - return _gridRows(rows); + return flHtml + _gridRows(rows); + } case 'duplicate_tracks': if (!d.tracks || !d.tracks.length) return _gridRows([['Count', d.count || '?']]); - return `
${d.tracks.map((t, i) => ` -
- Copy ${i + 1}: ${_escFinding(t.title)} by ${_escFinding(t.artist)} - Album: ${_escFinding(t.album || 'Unknown')} - ${t.bitrate ? `Bitrate: ${t.bitrate} kbps${t.duration ? ` · Duration: ${Math.round(t.duration)}s` : ''}` : ''} + // Determine best copy (same logic as backend: highest bitrate, then duration) + const bestDup = d.tracks.reduce((best, t) => { + const bBr = best.bitrate || 0, tBr = t.bitrate || 0; + const bDur = best.duration || 0, tDur = t.duration || 0; + return (tBr > bBr || (tBr === bBr && tDur > bDur)) ? t : best; + }, d.tracks[0]); + return `
${d.tracks.map((t, i) => { + const isBest = t.id === bestDup.id; + return `
+ + ${isBest ? 'KEEP' : 'REMOVE'} + ${_escFinding(t.title)} by ${_escFinding(t.artist)} + + Album: ${_escFinding(t.album || 'Unknown')}${t.bitrate ? ` · ${t.bitrate} kbps` : ''}${t.duration ? ` · ${Math.round(t.duration)}s` : ''} ${t.file_path ? `${_escFinding(t.file_path)}` : ''} -
`).join('')}
`; +
`; + }).join('')}
`; case 'incomplete_album': if (d.artist) rows.push(['Artist', d.artist]); if (d.album_title) rows.push(['Album', d.album_title]); - rows.push(['Tracks Found', `${d.actual_tracks || '?'} of ${d.expected_tracks || '?'}`]); if (d.spotify_album_id) rows.push(['Spotify ID', d.spotify_album_id]); + let incHtml = _gridRows(rows); + const actual = d.actual_tracks || 0, expected = d.expected_tracks || 0; + if (expected > 0) { + const pct = Math.round((actual / expected) * 100); + incHtml += `
+
${actual} of ${expected} tracks (${pct}%)
+
+
`; + } if (d.missing_tracks && d.missing_tracks.length) { - return _gridRows(rows) + `
${d.missing_tracks.map(t => ` + incHtml += `
${d.missing_tracks.map(t => `
#${t.track_number || '?'} ${_escFinding(t.name || t.title || 'Unknown')} ${t.duration_ms ? `Duration: ${Math.round(t.duration_ms / 1000)}s` : ''}
`).join('')}
`; } - return _gridRows(rows); + return incHtml; case 'path_mismatch': if (d.from) rows.push(['Current Path', d.from, 'path']); @@ -50612,8 +50656,15 @@ function _renderFindingDetail(f) { if (d.artist) rows.push(['Artist', d.artist]); if (d.album_title) rows.push(['Album', d.album_title]); if (d.spotify_album_id) rows.push(['Spotify ID', d.spotify_album_id]); - if (d.found_artwork_url) rows.push(['Artwork URL', d.found_artwork_url, 'success']); - return _gridRows(rows); + let artHtml = _gridRows(rows); + if (d.found_artwork_url) { + artHtml += `
+ Album artwork + Found artwork +
`; + } + return artHtml; case 'track_number_mismatch': if (d.matched_title) rows.push(['Matched To', d.matched_title]); diff --git a/webui/static/style.css b/webui/static/style.css index 705826c9..20c4e23c 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -40580,6 +40580,124 @@ tr.tag-diff-same { color: rgba(255, 255, 255, 0.35); } +/* Duplicate keep/remove badges */ +.repair-detail-subitem.best { + border-color: rgba(34, 197, 94, 0.2); + background: rgba(34, 197, 94, 0.04); +} +.repair-detail-subitem.removable { + opacity: 0.65; +} +.repair-keep-badge { + display: inline-block; + font-size: 9px; + font-weight: 700; + padding: 1px 5px; + border-radius: 3px; + background: rgba(34, 197, 94, 0.15); + color: #22c55e; + margin-right: 4px; + letter-spacing: 0.04em; +} +.repair-remove-badge { + display: inline-block; + font-size: 9px; + font-weight: 700; + padding: 1px 5px; + border-radius: 3px; + background: rgba(239, 68, 68, 0.12); + color: #ef4444; + margin-right: 4px; + letter-spacing: 0.04em; +} + +/* Artwork preview (for missing cover art) */ +.repair-artwork-preview { + margin-top: 8px; + display: flex; + align-items: flex-end; + gap: 10px; +} +.repair-artwork-preview img { + width: 80px; + height: 80px; + border-radius: 6px; + object-fit: cover; + border: 1px solid rgba(255, 255, 255, 0.1); +} +.repair-artwork-label { + font-size: 10px; + color: rgba(255, 255, 255, 0.35); +} + +/* Completion bar (for incomplete albums) */ +.repair-completion-bar { + margin: 8px 0 4px; +} +.repair-completion-label { + font-size: 11px; + color: rgba(255, 255, 255, 0.5); + margin-bottom: 4px; +} +.repair-completion-track { + height: 6px; + background: rgba(255, 255, 255, 0.06); + border-radius: 3px; + overflow: hidden; +} +.repair-completion-fill { + height: 100%; + background: linear-gradient(90deg, #f59e0b, #f97316); + border-radius: 3px; + transition: width 0.3s ease; +} + +/* Spectrum bar (for fake lossless) */ +.repair-spectrum-bar { + margin-bottom: 10px; +} +.repair-spectrum-label { + font-size: 11px; + color: rgba(255, 255, 255, 0.5); + margin-bottom: 4px; +} +.repair-spectrum-track { + position: relative; + height: 10px; + background: rgba(255, 255, 255, 0.06); + border-radius: 5px; + overflow: visible; +} +.repair-spectrum-detected { + position: absolute; + top: 0; + left: 0; + height: 100%; + background: linear-gradient(90deg, #ef4444, #f97316); + border-radius: 5px; + opacity: 0.7; +} +.repair-spectrum-expected { + position: absolute; + top: -2px; + width: 2px; + height: 14px; + background: #22c55e; + border-radius: 1px; +} +.repair-spectrum-legend { + display: flex; + justify-content: space-between; + margin-top: 4px; + font-size: 10px; +} +.repair-spectrum-legend-detected { + color: #f97316; +} +.repair-spectrum-legend-expected { + color: #22c55e; +} + /* Score bars (for AcoustID) */ .repair-score-bar { display: flex;