Files
serato-doctor/serato_doctor/webui/app.js
T
2026-07-01 09:17:25 -07:00

148 lines
5.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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) => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;',
}[character]));
}
function detailLines(item) {
if (item.files) {
return item.files.map((file) => `<li>${escapeHtml(file)}</li>`).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 Seratos missing list.');
return lines.map((line) => `<li>${escapeHtml(line)}</li>`).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 = '<div class="empty-detail">Nothing to review here. Tiny victory parade, very tasteful.</div>';
return;
}
drilldownList.innerHTML = detail.items.map((item) => `
<article class="detail-item">
<strong>${escapeHtml(item.filename || item.path || 'Untitled item')}</strong>
<ul>${detailLines(item)}</ul>
</article>
`).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';
}
});