From 609a216ddd92ffa8179d578c532b8fedb6f07b54 Mon Sep 17 00:00:00 2001 From: Jan Svabenik Date: Sun, 22 Mar 2026 22:43:00 +0100 Subject: [PATCH] Clean up live listen UI and remove dead mode toggles --- web/app.js | 127 +++++++++++++++++++++++++++++++++++-------------- web/index.html | 27 +++++------ web/style.css | 100 +++++++++++++++++++++++++------------- 3 files changed, 173 insertions(+), 81 deletions(-) diff --git a/web/app.js b/web/app.js index 9d94b27..16b2fe2 100644 --- a/web/app.js +++ b/web/app.js @@ -127,8 +127,8 @@ const listenModeSelect = qs('listenMode'); const listenMetaDemod = qs('listenMetaDemod'); const listenMetaPlayback = qs('listenMetaPlayback'); const listenMetaStereo = qs('listenMetaStereo'); -const listenMetaRate = qs('listenMetaRate'); -const listenMetaChannels = qs('listenMetaChannels'); +const listenMetaStatus = qs('listenMetaStatus'); +const listenMetaAudio = qs('listenMetaAudio'); let latest = null; let currentConfig = null; @@ -289,8 +289,9 @@ class LiveListenWS { } const liveListenDefaults = { + status: 'Idle', demod: '-', - playback_mode: 'Inactive', + playback_mode: '-', stereo_state: '-', sample_rate: null, channels: null @@ -301,23 +302,51 @@ function formatListenMetaValue(value, fallback = '-') { return String(value); } +function isListeningSignal(signal) { + return !!(signal && liveListenTarget && matchesListenTarget(signal)); +} + +function getSignalPrimaryMode(signal) { + if (isListeningSignal(signal) && liveListenInfo?.playback_mode && liveListenInfo.playback_mode !== '-') { + return liveListenInfo.playback_mode; + } + if (signal?.class?.mod_type) return signal.class.mod_type; + return 'carrier'; +} + +function getSignalRuntimeSummary(signal) { + if (!isListeningSignal(signal)) return ''; + const bits = []; + if (liveListenInfo?.status && !['Idle', '-'].includes(liveListenInfo.status)) bits.push(liveListenInfo.status); + if (liveListenInfo?.stereo_state && liveListenInfo.stereo_state !== '-') bits.push(liveListenInfo.stereo_state); + return bits.join(' · '); +} + +function getSignalAudioSummary(signal) { + if (!isListeningSignal(signal)) return ''; + const rate = Number.isFinite(liveListenInfo?.sample_rate) && liveListenInfo.sample_rate > 0 ? fmtHz(liveListenInfo.sample_rate) : ''; + const ch = Number.isFinite(liveListenInfo?.channels) && liveListenInfo.channels > 0 ? `${liveListenInfo.channels} ch` : ''; + return [rate, ch].filter(Boolean).join(' · '); +} + function renderLiveListenMeta(info) { if (!listenMetaDemod) return; + const status = formatListenMetaValue(info?.status, 'Idle'); const demod = formatListenMetaValue(info?.demod); - const playback = formatListenMetaValue(info?.playback_mode, 'Inactive'); + const playback = formatListenMetaValue(info?.playback_mode); const stereo = formatListenMetaValue(info?.stereo_state); const sampleRate = Number.isFinite(info?.sample_rate) && info.sample_rate > 0 ? fmtHz(info.sample_rate) : '-'; const channels = Number.isFinite(info?.channels) && info.channels > 0 - ? String(info.channels) + ? `${info.channels} ch` : '-'; - listenMetaDemod.textContent = demod; + if (listenMetaStatus) listenMetaStatus.textContent = status; listenMetaPlayback.textContent = playback; listenMetaStereo.textContent = stereo; - listenMetaRate.textContent = sampleRate; - listenMetaChannels.textContent = channels; + listenMetaDemod.textContent = `Demod ${demod}`; + if (listenMetaAudio) listenMetaAudio.textContent = `Audio ${sampleRate}${channels !== '-' ? ` · ${channels}` : ''}`; } function resetLiveListenMeta() { @@ -332,7 +361,7 @@ function updateLiveListenMeta(partial) { function handleLiveListenAudioInfo(info) { if (!info || typeof info !== 'object') return; - const partial = {}; + const partial = { status: 'Live' }; if (info.demod) partial.demod = info.demod; if (info.playback_mode) partial.playback_mode = info.playback_mode; if (info.stereo_state) partial.stereo_state = info.stereo_state; @@ -349,6 +378,15 @@ function resolveListenMode(detectedMode) { return detectedMode || 'NFM'; } +function syncListeningVisuals() { + signalList?.querySelectorAll('.signal-item').forEach(el => { + const center = parseFloat(el.dataset.center || '0'); + const bw = parseFloat(el.dataset.bw || '0'); + const fakeSignal = { center_hz: center, bw_hz: bw }; + el.classList.toggle('listening', matchesListenTarget(fakeSignal)); + }); +} + function setLiveListenUI(active) { if (liveListenBtn) { liveListenBtn.textContent = active ? '■ Stop' : 'Live Listen'; @@ -369,6 +407,7 @@ function stopLiveListen() { liveListenTarget = null; setLiveListenUI(false); resetLiveListenMeta(); + syncListeningVisuals(); } function startLiveListen(freq, bw, detectedMode) { @@ -397,10 +436,12 @@ function startLiveListen(freq, bw, detectedMode) { }); liveListenWS.start(); setLiveListenUI(true); + syncListeningVisuals(); const startingInfo = { + status: 'Connecting', demod: mode || '-', - playback_mode: 'Starting', + playback_mode: mode || '-', stereo_state: mode === 'WFM_STEREO' ? 'searching' : 'mono', sample_rate: 48000, channels: mode === 'WFM_STEREO' ? 2 : 1 @@ -526,8 +567,9 @@ function renderSignalPopover(rect, signal) { const width = Math.max(4, (Number(value) / maxVal) * 100); return `
${label}${Number(value).toFixed(2)}
`; }).join(''); - signalPopover.innerHTML = `
${signal.class?.mod_type || 'Signal'}${signal.class?.pll?.rds_station ? ' · ' + signal.class.pll.rds_station : ''}
${fmtMHz(signal.class?.pll?.exact_hz || signal.center_hz, 5)} · ${fmtKHz(signal.bw_hz || 0)} · ${(signal.snr_db || 0).toFixed(1)} dB SNR${signal.class?.pll?.locked ? ` · PLL ${signal.class.pll.method} LOCK` : ''}${signal.class?.pll?.stereo ? ' · STEREO' : ''}
${rows || '
No classifier scores
'}
`; - const popW = 220; + const primaryMode = getSignalPrimaryMode(signal); + const runtimeInfo = getSignalRuntimeSummary(signal); + signalPopover.innerHTML = `
${primaryMode}${signal.class?.pll?.rds_station ? ' ' + signal.class.pll.rds_station : ''}
${fmtMHz(signal.class?.pll?.exact_hz || signal.center_hz, 5)} ${fmtKHz(signal.bw_hz || 0)} ${(signal.snr_db || 0).toFixed(1)} dB SNR${runtimeInfo ? ` ${runtimeInfo}` : ''}${signal.class?.pll?.locked ? ` PLL ${signal.class.pll.method} LOCK` : ''}${signal.class?.pll?.stereo ? ' STEREO' : ''}
${rows || '
No classifier scores
'}
`; const popW = 220; const left = rect.x + rect.w + 8; const top = rect.y + 8; const maxLeft = Math.max(8, spectrumCanvas.width - popW - 8); @@ -1157,12 +1199,14 @@ function renderSpectrum() { const x2 = ((right - startHz) / (endHz - startHz)) * w; const boxW = Math.max(2, x2 - x1); const mod = s.class?.mod_type || ''; - const mc = modColor(mod); + const primaryMode = getSignalPrimaryMode(s); + const mc = modColor(primaryMode); const rdsName = s.class?.pll?.rds_station || ''; + const runtimeInfo = getSignalRuntimeSummary(s); // Signal box with modulation-based color - ctx.fillStyle = modColorStr(mod, 0.10); - ctx.strokeStyle = modColorStr(mod, 0.75); + ctx.fillStyle = modColorStr(primaryMode, 0.10); + ctx.strokeStyle = modColorStr(primaryMode, 0.75); ctx.lineWidth = 1.5; ctx.fillRect(x1, 10, boxW, h - 28); ctx.strokeRect(x1, 10, boxW, h - 28); @@ -1179,10 +1223,11 @@ function renderSpectrum() { const freqStr = `${(s.center_hz / 1e6).toFixed(4)} MHz`; // Badge background - const badgeH = rdsName ? 42 : (mod ? 30 : 16); + const badgeH = rdsName ? 42 : ((primaryMode || runtimeInfo) ? 30 : 16); const freqW = ctx.measureText ? 0 : 0; // will measure below ctx.font = '11px Inter, sans-serif'; - const textW = Math.max(ctx.measureText(freqStr).width, mod ? ctx.measureText(mod).width : 0, rdsName ? ctx.measureText(rdsName).width : 0) + 8; + const line2 = runtimeInfo || primaryMode; + const textW = Math.max(ctx.measureText(freqStr).width, line2 ? ctx.measureText(line2).width : 0, rdsName ? ctx.measureText(rdsName).width : 0) + 8; ctx.fillStyle = 'rgba(7, 16, 24, 0.82)'; ctx.fillRect(labelX - 3, baseY, textW, badgeH); @@ -1191,11 +1236,11 @@ function renderSpectrum() { ctx.font = '11px Inter, sans-serif'; ctx.fillText(freqStr, labelX, baseY + 11); - // Line 2: Mod type (modulation color) - if (mod) { + // Line 2: runtime status or primary mode + if (runtimeInfo || primaryMode) { ctx.fillStyle = mc.label; ctx.font = 'bold 10px Inter, sans-serif'; - ctx.fillText(mod, labelX, baseY + 23); + ctx.fillText(runtimeInfo || primaryMode, labelX, baseY + 23); } // Line 3: RDS station name (white bold) @@ -1465,13 +1510,16 @@ function _createSignalItem(s) { btn.dataset.class = s.class?.mod_type || ''; btn.dataset.id = s.id || 0; const mod = s.class?.mod_type || ''; - const mc = modColor(mod); + const primaryMode = getSignalPrimaryMode(s); + const mc = modColor(primaryMode); const rds = s.class?.pll?.rds_station || ''; + const runtime = getSignalRuntimeSummary(s); + const audio = getSignalAudioSummary(s); const dec = decisionIndex.get(String(s.id || 0)); const decText = dec?.reason ? `${dec.reason}` : ''; const decFlags = dec ? `${dec.record ? 'REC' : ''}${dec.decode ? (dec.record ? '+DEC' : 'DEC') : ''}` : ''; const decMeta = decText || decFlags ? `${decFlags}${decFlags && decText ? ' · ' : ''}${decText}` : ''; - btn.innerHTML = `
${fmtMHz(s.center_hz, 6)}${(s.snr_db || 0).toFixed(1)} dB
${mod || 'carrier'}BW ${fmtKHz(s.bw_hz || 0)}${rds ? `${rds}` : ''}${decMeta}
`; + btn.innerHTML = `
${fmtMHz(s.center_hz, 6)}${(s.snr_db || 0).toFixed(1)} dB
${primaryMode}${runtime ? `${runtime}` : ''}BW ${fmtKHz(s.bw_hz || 0)}${audio ? `${audio}` : ''}${rds ? `${rds}` : ''}${decMeta}
`; btn.style.borderLeftColor = mc.label; btn.style.borderLeftWidth = '3px'; btn.style.borderLeftStyle = 'solid'; @@ -1483,23 +1531,39 @@ function _patchSignalItem(el, s) { const freqEl = el.querySelector('[data-field="freq"]'); const snrEl = el.querySelector('[data-field="snr"]'); const bwEl = el.querySelector('[data-field="bw"]'); - const modEl = el.querySelector('[data-field="mod"]'); + const modeEl = el.querySelector('[data-field="mode"]'); + const runtimeEl = el.querySelector('[data-field="runtime"]'); + const audioEl = el.querySelector('[data-field="audio"]'); const rdsEl = el.querySelector('[data-field="rds"]'); const mod = s.class?.mod_type || ''; - const mc = modColor(mod); + const primaryMode = getSignalPrimaryMode(s); + const mc = modColor(primaryMode); + const runtime = getSignalRuntimeSummary(s); + const audio = getSignalAudioSummary(s); const rds = s.class?.pll?.rds_station || ''; if (freqEl) freqEl.textContent = fmtMHz(s.center_hz, 6); if (snrEl) { snrEl.textContent = `${(s.snr_db || 0).toFixed(1)} dB`; snrEl.style.color = snrColor(s.snr_db || 0); } if (bwEl) bwEl.textContent = `BW ${fmtKHz(s.bw_hz || 0)}`; - if (modEl) { modEl.textContent = mod || 'carrier'; modEl.style.color = mc.label; } - if (rdsEl) { rdsEl.textContent = rds; } else if (rds && !rdsEl) { + if (modeEl) { modeEl.textContent = primaryMode; modeEl.style.color = mc.label; } + if (runtimeEl) { + runtimeEl.textContent = runtime; + runtimeEl.style.display = runtime ? '' : 'none'; + } + if (audioEl) { + audioEl.textContent = audio; + audioEl.style.display = audio ? '' : 'none'; + } + if (rdsEl) { + rdsEl.textContent = rds; + rdsEl.style.display = rds ? '' : 'none'; + } else if (rds && !rdsEl) { const span = document.createElement('span'); span.className = 'item-meta'; span.dataset.field = 'rds'; span.style.color = '#fff'; span.style.fontWeight = '700'; span.textContent = rds; - el.querySelector('.item-bottom')?.appendChild(span); + el.querySelector('.signal-secondary')?.appendChild(span); } el.dataset.center = s.center_hz; el.dataset.bw = s.bw_hz || 0; @@ -2178,14 +2242,6 @@ railTabs.forEach((tab) => { }); }); -modeButtons.forEach((btn) => { - btn.addEventListener('click', () => { - modeButtons.forEach(b => b.classList.toggle('active', b === btn)); - document.body.classList.remove('mode-live', 'mode-hunt', 'mode-review', 'mode-lab'); - document.body.classList.add(`mode-${btn.dataset.mode}`); - }); -}); -document.body.classList.add('mode-live'); drawerCloseBtn.addEventListener('click', closeDrawer); exportEventBtn.addEventListener('click', exportSelectedEvent); @@ -2385,3 +2441,4 @@ setInterval(loadSignals, 1500); setInterval(loadDecoders, 10000); + diff --git a/web/index.html b/web/index.html index 969c85c..122200f 100644 --- a/web/index.html +++ b/web/index.html @@ -26,14 +26,7 @@ - +
Connecting
@@ -290,12 +283,18 @@
-
Live listen status
-
Demod-
-
Playback-
-
Stereo-
-
Sample rate-
-
Channels-
+
+
Live listen
+
Idle
+
+
+ - + - +
+
+ Demod - + Audio - +
diff --git a/web/style.css b/web/style.css index 1800183..c7d71a8 100644 --- a/web/style.css +++ b/web/style.css @@ -92,21 +92,8 @@ button, input, select { font: inherit; } .topbar-center { display: flex; justify-content: center; } .topbar-right { display: flex; align-items: center; justify-content: flex-end; gap: 10px; } -/* Mode Strip */ -.mode-strip { display: flex; gap: 3px; background: var(--bg2); border: 1px solid var(--line); border-radius: 12px; padding: 3px; } -.mode-btn { - font-family: var(--mono); font-size: 0.72rem; font-weight: 500; - padding: 5px 14px; border-radius: 9px; - background: transparent; border: 1px solid transparent; - color: var(--text-dim); cursor: pointer; transition: all 0.15s; - display: flex; align-items: center; gap: 5px; -} -.mode-btn:hover { color: var(--text); background: rgba(255,255,255,0.03); } -.mode-btn.active { - background: rgba(0, 255, 200, 0.08); border-color: rgba(0, 255, 200, 0.2); - color: var(--accent); box-shadow: 0 0 12px rgba(0, 255, 200, 0.06); -} -.mode-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); animation: pulse 2s ease-in-out infinite; } +/* topbar center intentionally left available for future status content */ +.topbar-center { min-height: 1px; } .ws-badge { font-family: var(--mono); font-size: 0.68rem; font-weight: 500; @@ -375,12 +362,18 @@ input[type="range"]::-moz-range-thumb { .listen-meta { margin-top: 8px; - padding: 8px 10px; + padding: 10px; border-radius: var(--r); border: 1px solid var(--line); - background: rgba(5, 8, 14, 0.6); + background: rgba(5, 8, 14, 0.72); display: grid; - gap: 4px; + gap: 6px; +} +.listen-meta__head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; } .listen-meta__title { font-family: var(--mono); @@ -389,20 +382,69 @@ input[type="range"]::-moz-range-thumb { letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-mute); - margin-bottom: 2px; } -.listen-meta__row { +.listen-meta__badge { + font-family: var(--mono); + font-size: 0.62rem; + font-weight: 700; + letter-spacing: 0.04em; + padding: 2px 8px; + border-radius: 999px; + border: 1px solid var(--line); + background: rgba(15, 25, 40, 0.8); + color: var(--text-dim); +} +.listen-meta__primary { display: flex; - align-items: center; + align-items: baseline; justify-content: space-between; - gap: 8px; + gap: 10px; +} +.listen-meta__mode { + font-family: var(--mono); + font-size: 0.9rem; + font-weight: 700; + color: var(--text); +} +.listen-meta__stereo { + font-family: var(--mono); + font-size: 0.68rem; + font-weight: 700; + color: var(--accent); + text-transform: uppercase; +} +.listen-meta__secondary { + display: flex; + flex-wrap: wrap; + gap: 10px; font-family: var(--mono); - font-size: 0.7rem; + font-size: 0.66rem; color: var(--text-dim); } -.listen-meta__row span:last-child { - color: var(--text); - font-weight: 600; + +.signal-primary { + display: inline-flex; + align-items: center; + gap: 6px; + min-width: 0; +} +.signal-secondary { + display: inline-flex; + align-items: center; + gap: 8px; + min-width: 0; + flex-wrap: wrap; +} +.item-meta--runtime { + color: var(--accent); + font-weight: 700; +} +.item-meta--classifier { + opacity: 0.72; +} +.item-meta--live { + color: #fff; + font-weight: 700; } /* Health Grid */ @@ -492,12 +534,6 @@ input[type="range"]::-moz-range-thumb { .kb-grid span { font-size: 0.82rem; color: var(--text-dim); } .kb-close { width: 100%; justify-content: center; display: flex; } -/* ═══════════ MODE VARIANTS ═══════════ */ -body.mode-hunt .tl-panel { grid-row: 2; } -body.mode-review .stage { grid-template-rows: auto auto auto minmax(0, 0.76fr); } -body.mode-review .tl-panel { box-shadow: 0 0 0 1px rgba(0, 144, 255, 0.12), var(--shadow); } -body.mode-lab .hero-metrics { grid-template-columns: repeat(3, minmax(0, 1fr)); } - /* ═══════════ ANIMATIONS ═══════════ */ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }