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
This commit is contained in:
parent
7a706e8c11
commit
0594673cad
2 changed files with 187 additions and 18 deletions
|
|
@ -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' ?
|
||||
`<span class="repair-finding-status-badge ${f.status}">${f.status}</span>` : '';
|
||||
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 = `<span class="repair-finding-status-badge ${f.status}">${actionText}</span>`;
|
||||
}
|
||||
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 += `<div class="repair-spectrum-bar">
|
||||
<div class="repair-spectrum-label">Spectral Analysis</div>
|
||||
<div class="repair-spectrum-track">
|
||||
<div class="repair-spectrum-detected" style="width:${cutoffPct}%"></div>
|
||||
<div class="repair-spectrum-expected" style="left:${expectedPct}%"></div>
|
||||
</div>
|
||||
<div class="repair-spectrum-legend">
|
||||
<span class="repair-spectrum-legend-detected">${cutoff} kHz detected</span>
|
||||
<span class="repair-spectrum-legend-expected">${expectedMin} kHz expected min</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
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 `<div class="repair-detail-sublist">${d.tracks.map((t, i) => `
|
||||
<div class="repair-detail-subitem">
|
||||
<strong>Copy ${i + 1}: ${_escFinding(t.title)} by ${_escFinding(t.artist)}</strong>
|
||||
<span>Album: ${_escFinding(t.album || 'Unknown')}</span>
|
||||
${t.bitrate ? `<span>Bitrate: ${t.bitrate} kbps${t.duration ? ` · Duration: ${Math.round(t.duration)}s` : ''}</span>` : ''}
|
||||
// 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 `<div class="repair-detail-sublist">${d.tracks.map((t, i) => {
|
||||
const isBest = t.id === bestDup.id;
|
||||
return `<div class="repair-detail-subitem ${isBest ? 'best' : 'removable'}">
|
||||
<strong>
|
||||
${isBest ? '<span class="repair-keep-badge">KEEP</span>' : '<span class="repair-remove-badge">REMOVE</span>'}
|
||||
${_escFinding(t.title)} by ${_escFinding(t.artist)}
|
||||
</strong>
|
||||
<span>Album: ${_escFinding(t.album || 'Unknown')}${t.bitrate ? ` · ${t.bitrate} kbps` : ''}${t.duration ? ` · ${Math.round(t.duration)}s` : ''}</span>
|
||||
${t.file_path ? `<span class="mono">${_escFinding(t.file_path)}</span>` : ''}
|
||||
</div>`).join('')}</div>`;
|
||||
</div>`;
|
||||
}).join('')}</div>`;
|
||||
|
||||
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 += `<div class="repair-completion-bar">
|
||||
<div class="repair-completion-label">${actual} of ${expected} tracks (${pct}%)</div>
|
||||
<div class="repair-completion-track"><div class="repair-completion-fill" style="width:${pct}%"></div></div>
|
||||
</div>`;
|
||||
}
|
||||
if (d.missing_tracks && d.missing_tracks.length) {
|
||||
return _gridRows(rows) + `<div class="repair-detail-sublist">${d.missing_tracks.map(t => `
|
||||
incHtml += `<div class="repair-detail-sublist">${d.missing_tracks.map(t => `
|
||||
<div class="repair-detail-subitem">
|
||||
<strong>#${t.track_number || '?'} ${_escFinding(t.name || t.title || 'Unknown')}</strong>
|
||||
${t.duration_ms ? `<span>Duration: ${Math.round(t.duration_ms / 1000)}s</span>` : ''}
|
||||
</div>`).join('')}</div>`;
|
||||
}
|
||||
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 += `<div class="repair-artwork-preview">
|
||||
<img src="${_escFinding(d.found_artwork_url)}" alt="Album artwork"
|
||||
onerror="this.parentElement.innerHTML='<span>Image failed to load</span>'" />
|
||||
<span class="repair-artwork-label">Found artwork</span>
|
||||
</div>`;
|
||||
}
|
||||
return artHtml;
|
||||
|
||||
case 'track_number_mismatch':
|
||||
if (d.matched_title) rows.push(['Matched To', d.matched_title]);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue