diff --git a/web/app.js b/web/app.js index 26afb1d..9acd865 100644 --- a/web/app.js +++ b/web/app.js @@ -95,6 +95,7 @@ const recordingAudioLink = qs('recordingAudioLink'); const followBtn = qs('followBtn'); const fitBtn = qs('fitBtn'); const resetMaxBtn = qs('resetMaxBtn'); +const debugOverlayToggle = qs('debugOverlayToggle'); const timelineFollowBtn = qs('timelineFollowBtn'); const timelineFreezeBtn = qs('timelineFreezeBtn'); @@ -150,6 +151,8 @@ let timelineRects = []; let liveSignalRects = []; let recordings = []; let recordingsFetchInFlight = false; +let showDebugOverlay = true; +let showDebugOverlay = true; const GAIN_MAX = 60; const timelineWindowMs = 5 * 60 * 1000; @@ -181,6 +184,27 @@ function fmtMs(ms) { return `${(ms / 1000).toFixed(2)} s`; } +function renderScoreBars(scores) { + if (!classifierScoreBarsEl) return; + if (!scores || typeof scores !== 'object') { + classifierScoreBarsEl.innerHTML = ''; + return; + } + const entries = Object.entries(scores) + .filter(([, v]) => Number.isFinite(Number(v))) + .sort((a, b) => Number(b[1]) - Number(a[1])) + .slice(0, 6); + if (!entries.length) { + classifierScoreBarsEl.innerHTML = ''; + return; + } + const maxVal = Math.max(...entries.map(([, v]) => Number(v)), 1e-6); + classifierScoreBarsEl.innerHTML = entries.map(([label, value]) => { + const width = Math.max(4, (Number(value) / maxVal) * 100); + return `
`; + }).join(''); +} + function colorMap(v) { const x = Math.max(0, Math.min(1, v)); const r = Math.floor(255 * Math.pow(x, 0.55)); @@ -225,14 +249,16 @@ function sampleOverlayAtX(overlay, x, width, centerHz, sampleRate) { } function drawThresholdOverlay(ctx, w, h, minDb, maxDb) { - if (!latest?.thresholds?.length) return; + if (!showDebugOverlay) return; + const thresholds = latest?.debug?.thresholds; + if (!Array.isArray(thresholds) || thresholds.length === 0) return; ctx.save(); ctx.strokeStyle = 'rgba(255, 196, 92, 0.9)'; ctx.lineWidth = 1.25; if (ctx.setLineDash) ctx.setLineDash([6, 4]); ctx.beginPath(); for (let x = 0; x < w; x++) { - const v = sampleOverlayAtX(latest.thresholds, x, w, latest.center_hz, latest.sample_rate); + const v = sampleOverlayAtX(thresholds, x, w, latest.center_hz, latest.sample_rate); if (v == null || Number.isNaN(v)) continue; const y = h - ((v - minDb) / (maxDb - minDb)) * (h - 18) - 6; if (x === 0) ctx.moveTo(x, y); @@ -1063,6 +1089,7 @@ function openDrawer(ev) { .map(([k, v]) => `${k}:${v.toFixed(2)}`) .join(' · '); classifierScoresEl.textContent = rows ? `Classifier scores: ${rows}` : 'Classifier scores: -'; + renderScoreBars(scores); } else { const liveScores = (latest?.debug?.scores || []).find((s) => Math.abs((s.center_hz || 0) - (ev.center_hz || 0)) < Math.max(500, (ev.bandwidth_hz || 0))); if (liveScores?.scores) { @@ -1072,8 +1099,10 @@ function openDrawer(ev) { .map(([k, v]) => `${k}:${Number(v).toFixed(2)}`) .join(' · '); classifierScoresEl.textContent = rows ? `Classifier scores: ${rows}` : 'Classifier scores: -'; + renderScoreBars(liveScores.scores); } else { classifierScoresEl.textContent = 'Classifier scores: -'; + renderScoreBars(null); } } } @@ -1363,6 +1392,11 @@ maxHoldToggle.addEventListener('change', () => { maxSpectrum = null; markSpectrumDirty(); }); +if (debugOverlayToggle) debugOverlayToggle.addEventListener('change', () => { + showDebugOverlay = debugOverlayToggle.checked; + markSpectrumDirty(); + updateHeroMetrics(); +}); resetMaxBtn.addEventListener('click', () => { maxSpectrum = null; markSpectrumDirty(); @@ -1521,6 +1555,8 @@ if (recordingList) { }); } +if (debugOverlayToggle) debugOverlayToggle.checked = showDebugOverlay; + window.addEventListener('keydown', (ev) => { if (ev.target && ['INPUT', 'SELECT', 'TEXTAREA'].includes(ev.target.tagName)) return; if (ev.key === ' ') { diff --git a/web/index.html b/web/index.html index 31104cf..add056e 100644 --- a/web/index.html +++ b/web/index.html @@ -58,6 +58,7 @@ + diff --git a/web/style.css b/web/style.css index c87d5d9..6fba36f 100644 --- a/web/style.css +++ b/web/style.css @@ -413,6 +413,12 @@ input[type="range"]::-moz-range-thumb { .insp-viz { height: 200px; } .insp-note { font-size: 0.72rem; color: var(--text-mute); } +.score-bars { display: grid; gap: 6px; } +.score-bar { display: grid; grid-template-columns: 52px 1fr 44px; gap: 8px; align-items: center; font-family: var(--mono); font-size: 0.68rem; } +.score-bar-label { color: var(--text-dim); } +.score-bar-track { position: relative; height: 8px; border-radius: 999px; background: rgba(148, 163, 184, 0.14); overflow: hidden; } +.score-bar-fill { position: absolute; inset: 0 auto 0 0; background: linear-gradient(90deg, rgba(0,255,200,0.72), rgba(0,144,255,0.8)); border-radius: 999px; } +.score-bar-value { color: var(--text-mute); text-align: right; } /* ═══════════ KEYBOARD OVERLAY ═══════════ */ .kb-overlay {