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'); let latestAnalysis = 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) => `
  • ${escapeHtml(file)}
  • `).join(''); } const lines = []; if (item.artist || item.title) lines.push(`${item.artist || 'Unknown artist'} — ${item.title || item.filename}`); if (item.crate) lines.push(`Crate: ${item.crate}`); if (item.saved_path) lines.push(`Saved path: ${item.saved_path}`); if (item.candidate) lines.push(`Candidate: ${item.candidate}`); if (item.path) lines.push(`File: ${item.path}`); if (item.target) lines.push(`Target: ${item.target}`); if (item.score || item.reason) lines.push(`${item.score || 'Match'} · ${item.reason || 'Candidate found'}`); if (item.repeated_filename) lines.push('Same filename appears more than once in Serato’s missing list.'); return lines.map((line) => `
  • ${escapeHtml(line)}
  • `).join(''); } function renderDetail(key) { const detail = latestAnalysis?.details?.[key]; if (!detail) return; drillTriggers.forEach((trigger) => { trigger.classList.toggle('selected', trigger.dataset.detail === key); }); drilldownTitle.textContent = detail.title; drilldownCount.textContent = `${detail.total ?? 0} found`; drilldownSummary.textContent = detail.summary; if (!detail.items?.length) { drilldownList.innerHTML = '
    Nothing to review here. Tiny victory parade, very tasteful.
    '; return; } drilldownList.innerHTML = detail.items.map((item) => `
    ${escapeHtml(item.filename || item.path || 'Untitled item')}
    `).join(''); } 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'; } });