Pārlūkot izejas kodu

Clean up live listen UI and remove dead mode toggles

master
Jan Svabenik pirms 1 dienas
vecāks
revīzija
609a216ddd
3 mainītis faili ar 173 papildinājumiem un 81 dzēšanām
  1. +92
    -35
      web/app.js
  2. +13
    -14
      web/index.html
  3. +68
    -32
      web/style.css

+ 92
- 35
web/app.js Parādīt failu

@@ -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 `<div class="signal-popover__row"><span>${label}</span><span class="signal-popover__bar"><span class="signal-popover__fill" style="width:${width}%"></span></span><span>${Number(value).toFixed(2)}</span></div>`;
}).join('');
signalPopover.innerHTML = `<div class="signal-popover__title">${signal.class?.mod_type || 'Signal'}${signal.class?.pll?.rds_station ? ' · ' + signal.class.pll.rds_station : ''}</div><div class="signal-popover__meta">${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' : ''}</div><div class="signal-popover__scores">${rows || '<div class="signal-popover__meta">No classifier scores</div>'}</div>`;
const popW = 220;
const primaryMode = getSignalPrimaryMode(signal);
const runtimeInfo = getSignalRuntimeSummary(signal);
signalPopover.innerHTML = `<div class="signal-popover__title">${primaryMode}${signal.class?.pll?.rds_station ? ' · ' + signal.class.pll.rds_station : ''}</div><div class="signal-popover__meta">${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' : ''}</div><div class="signal-popover__scores">${rows || '<div class="signal-popover__meta">No classifier scores</div>'}</div>`; 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 ? `<span class="item-meta" data-field="decision" style="opacity:0.75">${decFlags}${decFlags && decText ? ' · ' : ''}${decText}</span>` : '';
btn.innerHTML = `<div class="item-top"><span class="item-title" data-field="freq">${fmtMHz(s.center_hz, 6)}</span><span class="item-badge" data-field="snr" style="color:${snrColor(s.snr_db || 0)}">${(s.snr_db || 0).toFixed(1)} dB</span></div><div class="item-bottom"><span class="item-meta" data-field="mod" style="color:${mc.label};font-weight:600">${mod || 'carrier'}</span><span class="item-meta" data-field="bw" style="opacity:0.6">BW ${fmtKHz(s.bw_hz || 0)}</span>${rds ? `<span class="item-meta" data-field="rds" style="color:#fff;font-weight:700">${rds}</span>` : ''}${decMeta}</div>`;
btn.innerHTML = `<div class="item-top"><span class="item-title" data-field="freq">${fmtMHz(s.center_hz, 6)}</span><span class="item-badge" data-field="snr" style="color:${snrColor(s.snr_db || 0)}">${(s.snr_db || 0).toFixed(1)} dB</span></div><div class="item-bottom"><span class="signal-primary"><span class="item-meta item-meta--runtime" data-field="mode" style="color:${mc.label}">${primaryMode}</span>${runtime ? `<span class="item-meta item-meta--live" data-field="runtime">${runtime}</span>` : ''}</span><span class="signal-secondary"><span class="item-meta" data-field="bw" style="opacity:0.6">BW ${fmtKHz(s.bw_hz || 0)}</span>${audio ? `<span class="item-meta" data-field="audio">${audio}</span>` : ''}${rds ? `<span class="item-meta" data-field="rds" style="color:#fff;font-weight:700">${rds}</span>` : ''}${decMeta}</span></div>`;
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);




+ 13
- 14
web/index.html Parādīt failu

@@ -26,14 +26,7 @@
</div>
</div>

<nav class="topbar-center">
<div class="mode-strip" role="tablist" aria-label="Modes">
<button class="mode-btn active" data-mode="live" type="button"><span class="mode-dot"></span>Live</button>
<button class="mode-btn" data-mode="hunt" type="button">Hunt</button>
<button class="mode-btn" data-mode="review" type="button">Review</button>
<button class="mode-btn" data-mode="lab" type="button">Lab</button>
</div>
</nav>
<div class="topbar-center"></div>

<div class="topbar-right">
<div class="ws-badge" id="wsBadge">Connecting</div>
@@ -290,12 +283,18 @@
</div>
<button class="act-btn" id="liveListenBtn" type="button">Live Listen</button>
<div class="listen-meta" id="listenMeta">
<div class="listen-meta__title">Live listen status</div>
<div class="listen-meta__row"><span>Demod</span><span id="listenMetaDemod">-</span></div>
<div class="listen-meta__row"><span>Playback</span><span id="listenMetaPlayback">-</span></div>
<div class="listen-meta__row"><span>Stereo</span><span id="listenMetaStereo">-</span></div>
<div class="listen-meta__row"><span>Sample rate</span><span id="listenMetaRate">-</span></div>
<div class="listen-meta__row"><span>Channels</span><span id="listenMetaChannels">-</span></div>
<div class="listen-meta__head">
<div class="listen-meta__title">Live listen</div>
<div class="listen-meta__badge" id="listenMetaStatus">Idle</div>
</div>
<div class="listen-meta__primary">
<span class="listen-meta__mode" id="listenMetaPlayback">-</span>
<span class="listen-meta__stereo" id="listenMetaStereo">-</span>
</div>
<div class="listen-meta__secondary">
<span id="listenMetaDemod">Demod -</span>
<span id="listenMetaAudio">Audio -</span>
</div>
</div>
</section>



+ 68
- 32
web/style.css Parādīt failu

@@ -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; } }



Notiek ielāde…
Atcelt
Saglabāt