- ${detailLines(item)}
const form = document.querySelector('#analysis-form'); const button = document.querySelector('#analyze-button'); const errorBox = document.querySelector('#error-message'); const results = document.querySelector('#dashboard'); const infoButtons = document.querySelectorAll('.info-button'); const drillTriggers = document.querySelectorAll('[data-detail]'); const drilldownTitle = document.querySelector('#drilldown-title'); const drilldownCount = document.querySelector('#drilldown-count'); const drilldownSummary = document.querySelector('#drilldown-summary'); const drilldownList = document.querySelector('#drilldown-list'); const repairPanel = document.querySelector('#duplicate-repair'); const repairChoice = document.querySelector('#repair-choice'); const repairPreview = document.querySelector('#repair-preview'); const repairMessage = document.querySelector('#repair-message'); const previewRepairButton = document.querySelector('#preview-repair'); const applyRepairButton = document.querySelector('#apply-repair'); const restoreRepairButton = document.querySelector('#restore-repair'); let latestAnalysis = null; let selectedDuplicateGroup = null; let previewedRepair = null; let latestBackup = null; function expandHome(path) { return path.trim(); } function escapeHtml(value) { return String(value ?? '').replace(/[&<>"']/g, (character) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', }[character])); } function detailLines(item) { if (item.files) { return item.files.map((file) => `
${previewedRepair.replaced.length} duplicate file(s) will be backed up, then replaced with shortcuts to the keeper. ${previewedRepair.metadata_backups} Serato metadata file(s) will also be copied into the rollback snapshot. Database V2 will not be changed.
`; repairPreview.hidden = false; applyRepairButton.disabled = false; repairMessage.textContent = 'Preview complete. Nothing has changed yet.'; } catch (error) { repairMessage.textContent = error.message; } }); applyRepairButton.addEventListener('click', async () => { if (!previewedRepair) return; applyRepairButton.disabled = true; repairMessage.textContent = 'Creating the backup before making changes…'; try { const result = await requestRepair('/api/duplicates/apply'); latestBackup = result.backup; repairMessage.textContent = `Cleanup complete. Restore backup: ${result.backup}`; previewRepairButton.disabled = true; restoreRepairButton.hidden = false; } catch (error) { repairMessage.textContent = error.message; applyRepairButton.disabled = false; } }); restoreRepairButton.addEventListener('click', async () => { if (!latestBackup) return; restoreRepairButton.disabled = true; repairMessage.textContent = 'Restoring the duplicate files…'; try { const response = await fetch('/api/backups/restore', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({serato: expandHome(document.querySelector('#serato-path').value), backup: latestBackup})}); const result = await response.json(); if (!response.ok) throw new Error(result.error || 'Restore failed'); repairMessage.textContent = `Restore complete. ${result.restored.length} original file(s) returned.`; restoreRepairButton.hidden = true; } catch (error) { repairMessage.textContent = error.message; restoreRepairButton.disabled = false; } }); repairChoice.addEventListener('change', () => { previewedRepair = null; applyRepairButton.disabled = true; repairPreview.hidden = true; repairMessage.textContent = 'Keeper changed. Preview the plan again.'; }); drilldownList.addEventListener('click', (event) => { const item = event.target.closest('[data-duplicate-index]'); if (item) chooseDuplicate(document.querySelector('.drill-trigger.selected')?.dataset.detail, Number(item.dataset.duplicateIndex)); }); drilldownList.addEventListener('keydown', (event) => { if (event.key !== 'Enter' && event.key !== ' ') return; const item = event.target.closest('[data-duplicate-index]'); if (item) { event.preventDefault(); chooseDuplicate(document.querySelector('.drill-trigger.selected')?.dataset.detail, Number(item.dataset.duplicateIndex)); } }); function render(data) { latestAnalysis = data; document.querySelectorAll('[data-field]').forEach((element) => { const value = data[element.dataset.field]; element.textContent = value ?? '—'; }); const score = data.score; document.querySelector('#health-score').textContent = score == null ? '—' : `${score}%`; document.querySelector('#score-ring').style.setProperty('--score', score ?? 0); document.querySelector('#health-message').textContent = score == null ? 'Not enough data yet' : score >= 95 ? 'Looking excellent' : score >= 80 ? 'A few things need attention' : 'Review recommended'; document.querySelector('#score-basis').textContent = data.score_basis; document.querySelector('#analysis-time').textContent = `Completed ${new Date().toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'})}`; renderDetail(data.database_missing_paths > 0 ? 'database_missing_tracks' : 'old_crate_references'); results.hidden = false; results.scrollIntoView({behavior: 'smooth', block: 'start'}); } function closeInfoButtons(except = null) { infoButtons.forEach((infoButton) => { if (infoButton !== except) { infoButton.classList.remove('open'); infoButton.setAttribute('aria-expanded', 'false'); } }); } infoButtons.forEach((infoButton) => { infoButton.addEventListener('click', (event) => { event.stopPropagation(); const willOpen = !infoButton.classList.contains('open'); closeInfoButtons(infoButton); infoButton.classList.toggle('open', willOpen); infoButton.setAttribute('aria-expanded', String(willOpen)); }); }); drillTriggers.forEach((trigger) => { trigger.addEventListener('click', () => renderDetail(trigger.dataset.detail)); trigger.addEventListener('keydown', (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); renderDetail(trigger.dataset.detail); } }); }); document.addEventListener('click', () => closeInfoButtons()); document.addEventListener('keydown', (event) => { if (event.key === 'Escape') closeInfoButtons(); }); form.addEventListener('submit', async (event) => { event.preventDefault(); errorBox.hidden = true; button.disabled = true; button.querySelector('span').textContent = 'Analyzing safely…'; const roots = document.querySelector('#reference-roots').value .split('\n').map((value) => value.trim()).filter(Boolean); try { const response = await fetch('/api/analyze', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ serato: expandHome(document.querySelector('#serato-path').value), music: expandHome(document.querySelector('#music-path').value), reference_roots: roots, }), }); const data = await response.json(); if (!response.ok) throw new Error(data.error || 'Analysis failed'); render(data); } catch (error) { errorBox.textContent = error.message; errorBox.hidden = false; } finally { button.disabled = false; button.querySelector('span').textContent = 'Analyze library'; } });