Remove legacy RF modes and add SignalID route/tests

This commit is contained in:
Smittix
2026-02-23 13:34:00 +00:00
parent 5f480caa3f
commit 7ea06caaa2
35 changed files with 3883 additions and 6920 deletions

View File

@@ -18,7 +18,7 @@
document.write('<style id="disclaimer-gate">.welcome-overlay{display:none !important}</style>');
window._showDisclaimerOnLoad = true;
}
// If navigating with a mode param (e.g. /?mode=listening), hide welcome immediately
// If navigating with a mode param (e.g. /?mode=waterfall), hide welcome immediately
// to prevent flash of welcome screen before JS applies the mode
else if (new URLSearchParams(window.location.search).get('mode')) {
document.write('<style id="mode-gate">.welcome-overlay{display:none !important}</style>');
@@ -79,20 +79,34 @@
subghz: "{{ url_for('static', filename='css/modes/subghz.css') }}?v={{ version }}&r=subghz_layout9",
bt_locate: "{{ url_for('static', filename='css/modes/bt_locate.css') }}?v={{ version }}&r=btlocate4",
spaceweather: "{{ url_for('static', filename='css/modes/space-weather.css') }}",
waterfall: "{{ url_for('static', filename='css/modes/waterfall.css') }}?v={{ version }}&r=wfdeck10",
rfheatmap: "{{ url_for('static', filename='css/modes/rfheatmap.css') }}",
fingerprint: "{{ url_for('static', filename='css/modes/fingerprint.css') }}"
waterfall: "{{ url_for('static', filename='css/modes/waterfall.css') }}?v={{ version }}&r=wfdeck19"
};
window.INTERCEPT_MODE_STYLE_LOADED = {};
window.ensureModeStyles = function(mode) {
const href = window.INTERCEPT_MODE_STYLE_MAP ? window.INTERCEPT_MODE_STYLE_MAP[mode] : null;
if (!href) return;
if (window.INTERCEPT_MODE_STYLE_LOADED[href]) return;
window.INTERCEPT_MODE_STYLE_LOADED[href] = true;
const absHref = new URL(href, window.location.href).href;
const existing = Array.from(document.querySelectorAll('link[data-mode-style]'))
.find((link) => link.href === absHref);
if (existing) {
window.INTERCEPT_MODE_STYLE_LOADED[href] = 'loaded';
return;
}
if (window.INTERCEPT_MODE_STYLE_LOADED[href] === 'loading') return;
window.INTERCEPT_MODE_STYLE_LOADED[href] = 'loading';
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = href;
link.dataset.modeStyle = mode;
link.onload = () => {
window.INTERCEPT_MODE_STYLE_LOADED[href] = 'loaded';
};
link.onerror = () => {
delete window.INTERCEPT_MODE_STYLE_LOADED[href];
try {
link.remove();
} catch (_) {}
};
document.head.appendChild(link);
};
</script>
@@ -196,10 +210,6 @@
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg></span>
<span class="mode-name">Meters</span>
</button>
<button class="mode-card mode-card-sm" onclick="selectMode('listening')">
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 21V9"/></svg></span>
<span class="mode-name">Listening Post</span>
</button>
<button class="mode-card mode-card-sm" onclick="selectMode('subghz')">
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12h6l3-9 3 18 3-9h5"/></svg></span>
<span class="mode-name">SubGHz</span>
@@ -296,14 +306,6 @@
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg></span>
<span class="mode-name">WebSDR</span>
</button>
<button class="mode-card mode-card-sm" onclick="selectMode('rfheatmap')">
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg></span>
<span class="mode-name">RF Heatmap</span>
</button>
<button class="mode-card mode-card-sm" onclick="selectMode('fingerprint')">
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12C2 6.5 6.5 2 12 2a10 10 0 0 1 8 4"/><path d="M5 19.5C5.5 18 6 15 6 12c0-.7.12-1.37.34-2"/><path d="M14 13.12c0 2.38 0 6.38-1 8.88"/></svg></span>
<span class="mode-name">RF Fingerprint</span>
</button>
</div>
</div>
@@ -622,8 +624,6 @@
{% include 'partials/modes/space-weather.html' %}
{% include 'partials/modes/listening-post.html' %}
{% include 'partials/modes/tscm.html' %}
{% include 'partials/modes/ais.html' %}
@@ -638,8 +638,6 @@
{% include 'partials/modes/bt_locate.html' %}
{% include 'partials/modes/waterfall.html' %}
{% include 'partials/modes/rfheatmap.html' %}
{% include 'partials/modes/fingerprint.html' %}
@@ -1193,9 +1191,11 @@
<!-- Sky View Polar Plot -->
<div class="gps-skyview-panel">
<h4>Satellite Sky View</h4>
<div class="gps-skyview-canvas-wrap">
<canvas id="gpsSkyCanvas" width="400" height="400"></canvas>
<div class="gps-skyview-canvas-wrap" id="gpsSkyViewWrap">
<canvas id="gpsSkyCanvas" width="400" height="400" aria-label="GPS satellite sky globe"></canvas>
<div class="gps-sky-overlay" id="gpsSkyOverlay" aria-hidden="true"></div>
</div>
<div class="gps-sky-hint">Drag to orbit | Scroll to zoom</div>
<div class="gps-legend">
<div class="gps-legend-item"><span class="gps-legend-dot" style="background:#00d4ff;"></span> GPS</div>
<div class="gps-legend-item"><span class="gps-legend-dot" style="background:#00ff88;"></span> GLONASS</div>
@@ -1248,442 +1248,6 @@
</div>
</div>
<!-- Listening Post Visualizations - Professional Ham Radio Scanner -->
<div class="wifi-visuals" id="listeningPostVisuals" style="display: none;">
<!-- WATERFALL FUNCTION BAR -->
<div class="function-strip listening-strip" style="grid-column: span 4;">
<div class="function-strip-inner">
<span class="strip-title">WATERFALL</span>
<div class="strip-divider"></div>
<!-- Span display -->
<div class="strip-stat">
<span class="strip-value" id="waterfallZoomSpan">20.0 MHz</span>
<span class="strip-label">SPAN</span>
</div>
<div class="strip-divider"></div>
<!-- Frequency inputs -->
<div class="strip-control">
<span class="strip-input-label">START</span>
<input type="number" id="waterfallStartFreq" class="strip-input wide" value="88" step="0.1">
</div>
<div class="strip-control">
<span class="strip-input-label">END</span>
<input type="number" id="waterfallEndFreq" class="strip-input wide" value="108" step="0.1">
</div>
<div class="strip-divider"></div>
<!-- Zoom buttons -->
<button type="button" class="strip-btn" onclick="zoomWaterfall('out')"></button>
<button type="button" class="strip-btn" onclick="zoomWaterfall('in')">+</button>
<div class="strip-divider"></div>
<!-- FFT Size -->
<div class="strip-control">
<span class="strip-input-label">FFT</span>
<select id="waterfallFftSize" class="strip-select">
<option value="512">512</option>
<option value="1024" selected>1024</option>
<option value="2048">2048</option>
<option value="4096">4096</option>
</select>
</div>
<!-- Gain -->
<div class="strip-control">
<span class="strip-input-label">GAIN</span>
<input type="number" id="waterfallGain" class="strip-input" value="40" min="0" max="50">
</div>
<div class="strip-divider"></div>
<!-- Start / Stop -->
<button type="button" class="strip-btn primary" id="startWaterfallBtn" onclick="startWaterfall()">▶ START</button>
<button type="button" class="strip-btn stop" id="stopWaterfallBtn" onclick="stopWaterfall()" style="display: none;">◼ STOP</button>
<!-- Status -->
<div class="strip-status">
<div class="status-dot inactive" id="waterfallStripDot"></div>
<span id="waterfallFreqRange">STANDBY</span>
</div>
</div>
</div>
<!-- WATERFALL / SPECTROGRAM PANEL -->
<div id="waterfallPanel" class="radio-module-box" style="grid-column: span 4; padding: 10px; display: none;">
<div class="module-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; font-size: 10px;">
<span>WATERFALL / SPECTROGRAM</span>
<span id="waterfallFreqRangeHeader" style="font-size: 9px; color: var(--accent-cyan);"></span>
</div>
<canvas id="spectrumCanvas" width="800" height="120" style="width: 100%; height: 120px; border-radius: 4px; background: rgba(0,0,0,0.8);"></canvas>
<canvas id="waterfallCanvas" width="800" height="400" style="width: 100%; height: 400px; border-radius: 4px; margin-top: 4px; background: rgba(0,0,0,0.9);"></canvas>
</div>
<!-- TOP: FREQUENCY DISPLAY PANEL -->
<div class="radio-module-box scanner-main" style="grid-column: span 4; padding: 12px;">
<div style="display: flex; gap: 15px; align-items: stretch;">
<!-- Main Frequency Display -->
<div
style="flex: 1; text-align: center; padding: 15px 20px; background: rgba(0,0,0,0.6); border-radius: 6px; border: 1px solid var(--border-color);">
<div class="freq-status" id="mainScannerModeLabel"
style="font-size: 10px; color: var(--text-muted); margin-bottom: 4px; letter-spacing: 3px;">
STOPPED</div>
<div style="display: flex; justify-content: center; align-items: baseline; gap: 8px;">
<div class="freq-digits" id="mainScannerFreq"
style="font-size: 52px; font-weight: bold; color: var(--accent-cyan); text-shadow: 0 0 30px var(--accent-cyan); font-family: var(--font-mono); letter-spacing: 3px;">
118.000</div>
<span class="freq-unit"
style="font-size: 20px; color: var(--text-secondary); font-weight: 500;">MHz</span>
</div>
<div
style="display: flex; justify-content: center; align-items: center; margin-top: 6px;">
<span class="freq-mode-badge" id="mainScannerMod"
style="background: var(--accent-cyan); color: #000; padding: 3px 12px; border-radius: 4px; font-size: 12px; font-weight: bold;">AM</span>
</div>
<!-- Progress bar -->
<div id="mainScannerProgress" style="display: none; margin-top: 12px;">
<div
style="display: flex; justify-content: space-between; font-size: 9px; color: var(--text-muted); margin-bottom: 3px;">
<span id="mainRangeStart">--</span>
<span id="mainRangeEnd">--</span>
</div>
<div
style="height: 6px; background: rgba(0,0,0,0.5); border-radius: 3px; overflow: hidden;">
<div class="scan-bar" id="mainProgressBar"
style="height: 100%; background: linear-gradient(90deg, var(--accent-cyan), var(--accent-green)); width: 0%; transition: width 0.1s;">
</div>
</div>
</div>
</div>
<!-- Synthesizer + Audio Output Panel -->
<div style="width: 320px; display: flex; flex-direction: column; gap: 8px;">
<!-- Synthesizer Display -->
<div
style="flex: 1; padding: 10px; background: rgba(0,0,0,0.6); border-radius: 6px; border: 1px solid var(--border-color);">
<div
style="font-size: 8px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 6px;">
Synthesizer</div>
<canvas id="synthesizerCanvas"
style="width: 100%; height: 60px; background: rgba(0,0,0,0.4); border-radius: 4px;"></canvas>
</div>
<!-- Audio Output -->
<div
style="padding: 10px; background: rgba(0,0,0,0.4); border-radius: 6px; border: 1px solid var(--border-color);">
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 8px;">
<div
style="font-size: 8px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px;">
Audio Output</div>
</div>
<audio id="scannerAudioPlayer" style="width: 100%; height: 28px;" controls></audio>
<button id="audioUnlockBtn" type="button"
style="display: none; margin-top: 8px; width: 100%; padding: 6px 10px; font-size: 10px; letter-spacing: 1px; background: var(--accent-cyan); color: #000; border: 1px solid var(--accent-cyan); border-radius: 4px; cursor: pointer;">
CLICK TO ENABLE AUDIO
</button>
<div style="display: flex; align-items: center; gap: 8px; margin-top: 8px;">
<span style="font-size: 7px; color: var(--text-muted);">LEVEL</span>
<div
style="flex: 1; height: 8px; background: rgba(0,0,0,0.5); border-radius: 4px; overflow: hidden;">
<div id="audioSignalBar"
style="height: 100%; background: linear-gradient(90deg, var(--accent-green), var(--accent-cyan), var(--accent-orange), var(--accent-red)); width: 0%; transition: width 0.1s;">
</div>
</div>
<span id="audioSignalDb"
style="font-size: 8px; color: var(--text-muted); min-width: 40px; text-align: right;">--
dB</span>
</div>
<div style="display: flex; align-items: center; gap: 8px; margin-top: 8px;">
<span style="font-size: 7px; color: var(--text-muted); letter-spacing: 1px;">SNR THRESH</span>
<input type="range" id="snrThresholdSlider" min="6" max="20" step="1" value="8"
style="flex: 1;" />
<span id="snrThresholdValue"
style="font-size: 8px; color: var(--text-muted); min-width: 26px; text-align: right;">8</span>
</div>
<!-- Signal Alert inline -->
<div id="mainSignalAlert"
style="display: none; background: rgba(0, 255, 100, 0.2); border: 1px solid var(--accent-green); border-radius: 4px; padding: 5px; text-align: center; margin-top: 8px;">
<span style="font-size: 9px; color: var(--accent-green); font-weight: bold;">
SIGNAL</span>
<button class="tune-btn" onclick="skipSignal()"
style="margin-left: 8px; padding: 2px 8px; font-size: 8px;">Skip</button>
</div>
</div>
</div>
</div>
</div>
<!-- CONTROL PANEL: Tuning Section | Mode/Band | Action Buttons -->
<div class="radio-module-box" style="grid-column: span 4; padding: 12px;">
<div style="display: flex; gap: 15px; align-items: stretch;">
<!-- LEFT: Tuning Section -->
<div
style="flex: 1; display: flex; align-items: center; justify-content: center; gap: 20px; padding: 10px 15px; background: rgba(0,0,0,0.3); border-radius: 8px; border: 1px solid var(--border-color);">
<!-- Fine Tune Buttons (Left of dial) -->
<div style="display: flex; flex-direction: column; gap: 4px;">
<button class="tune-btn" onclick="tuneFreq(-1)"
style="padding: 8px 12px; font-size: 11px;">-1</button>
<button class="tune-btn" onclick="tuneFreq(-0.1)"
style="padding: 8px 12px; font-size: 11px;">-.1</button>
<button class="tune-btn" onclick="tuneFreq(-0.05)"
style="padding: 8px 12px; font-size: 11px;">-.05</button>
<button class="tune-btn" onclick="tuneFreq(-0.005)"
style="padding: 8px 12px; font-size: 10px;">-.005</button>
</div>
<!-- Main Tuning Dial -->
<div style="display: flex; flex-direction: column; align-items: center;">
<div class="tuning-dial" id="mainTuningDial" data-value="118" data-min="24"
data-max="1800" data-step="0.1" style="width: 100px; height: 100px;"></div>
<div
style="font-size: 9px; color: var(--text-muted); margin-top: 6px; text-transform: uppercase; letter-spacing: 1px;">
Tune</div>
<div
style="font-size: 8px; color: var(--text-muted); margin-top: 3px; opacity: 0.6;"
title="Arrow keys: &#177;0.05/0.1 MHz&#10;Shift+Arrow: &#177;0.005/1 MHz">
&#9000; arrow keys</div>
</div>
<!-- Fine Tune Buttons (Right of dial) -->
<div style="display: flex; flex-direction: column; gap: 4px;">
<button class="tune-btn" onclick="tuneFreq(1)"
style="padding: 8px 12px; font-size: 11px;">+1</button>
<button class="tune-btn" onclick="tuneFreq(0.1)"
style="padding: 8px 12px; font-size: 11px;">+.1</button>
<button class="tune-btn" onclick="tuneFreq(0.05)"
style="padding: 8px 12px; font-size: 11px;">+.05</button>
<button class="tune-btn" onclick="tuneFreq(0.005)"
style="padding: 8px 12px; font-size: 10px;">+.005</button>
</div>
<!-- Divider -->
<div style="width: 1px; height: 80px; background: var(--border-color);"></div>
<!-- Settings: Step & Dwell -->
<div style="display: flex; flex-direction: column; gap: 8px; min-width: 90px;">
<div
style="display: flex; justify-content: space-between; align-items: center; font-size: 10px;">
<span style="color: var(--text-muted);">Step</span>
<select id="radioScanStep"
style="background: var(--bg-secondary); border: 1px solid var(--border-color); color: var(--text-primary); padding: 3px 5px; border-radius: 3px; font-size: 9px;">
<option value="8.33">8.33k</option>
<option value="12.5">12.5k</option>
<option value="25" selected>25k</option>
<option value="50">50k</option>
<option value="100">100k</option>
</select>
</div>
<div
style="display: flex; justify-content: space-between; align-items: center; font-size: 10px;">
<span style="color: var(--text-muted);">Dwell</span>
<select id="radioScanDwell"
style="background: var(--bg-secondary); border: 1px solid var(--border-color); color: var(--text-primary); padding: 3px 5px; border-radius: 3px; font-size: 9px;">
<option value="2">2s</option>
<option value="5">5s</option>
<option value="10" selected>10s</option>
<option value="30">30s</option>
</select>
</div>
</div>
<!-- Divider -->
<div style="width: 1px; height: 80px; background: var(--border-color);"></div>
<!-- SQL, Gain, Vol Knobs -->
<div style="display: flex; gap: 15px;">
<div class="knob-container">
<div class="radio-knob" id="radioSquelchKnob" data-value="0" data-min="0"
data-max="100" data-step="1"></div>
<div class="knob-label">SQL</div>
<div class="knob-value" id="radioSquelchValue">0</div>
</div>
<div class="knob-container">
<div class="radio-knob" id="radioGainKnob" data-value="40" data-min="0"
data-max="50" data-step="1"></div>
<div class="knob-label">GAIN</div>
<div class="knob-value" id="radioGainValue">40</div>
</div>
<div class="knob-container">
<div class="radio-knob" id="radioVolumeKnob" data-value="80" data-min="0"
data-max="100" data-step="1"></div>
<div class="knob-label">VOL</div>
<div class="knob-value" id="radioVolumeValue">80</div>
</div>
</div>
</div>
<!-- CENTER: Mode & Band (Stacked) -->
<div
style="width: 130px; display: flex; flex-direction: column; gap: 10px; justify-content: center;">
<div>
<div
style="font-size: 8px; color: var(--text-muted); margin-bottom: 4px; text-transform: uppercase; letter-spacing: 1px;">
Modulation</div>
<div class="radio-button-bank compact" id="modBtnBank"
style="flex-wrap: wrap; gap: 2px;">
<button class="radio-btn active" data-mod="am" onclick="setModulation('am')"
style="padding: 6px 10px; font-size: 10px;">AM</button>
<button class="radio-btn" data-mod="fm" onclick="setModulation('fm')"
style="padding: 6px 10px; font-size: 10px;">NFM</button>
<button class="radio-btn" data-mod="wfm" onclick="setModulation('wfm')"
style="padding: 6px 10px; font-size: 10px;">WFM</button>
<button class="radio-btn" data-mod="usb" onclick="setModulation('usb')"
style="padding: 6px 10px; font-size: 10px;">USB</button>
<button class="radio-btn" data-mod="lsb" onclick="setModulation('lsb')"
style="padding: 6px 10px; font-size: 10px;">LSB</button>
</div>
</div>
<div>
<div
style="font-size: 8px; color: var(--text-muted); margin-bottom: 4px; text-transform: uppercase; letter-spacing: 1px;">
Band</div>
<div class="radio-button-bank compact" id="bandBtnBank"
style="flex-wrap: wrap; gap: 2px;">
<button class="radio-btn" data-band="fm" onclick="setBand('fm')"
style="padding: 6px 10px; font-size: 10px;">FM</button>
<button class="radio-btn active" data-band="air" onclick="setBand('air')"
style="padding: 6px 10px; font-size: 10px;">AIR</button>
<button class="radio-btn" data-band="marine" onclick="setBand('marine')"
style="padding: 6px 10px; font-size: 10px;">MAR</button>
<button class="radio-btn" data-band="amateur2m" onclick="setBand('amateur2m')"
style="padding: 6px 10px; font-size: 10px;">2M</button>
<button class="radio-btn" data-band="amateur70cm"
onclick="setBand('amateur70cm')"
style="padding: 6px 10px; font-size: 10px;">70CM</button>
</div>
</div>
</div>
<!-- RIGHT: Scan Range + Action Buttons -->
<div style="width: 175px; display: flex; flex-direction: column; gap: 8px;">
<!-- Frequency Range - Prominent -->
<div
style="background: rgba(0,0,0,0.4); border: 1px solid var(--border-color); border-radius: 6px; padding: 10px;">
<div
style="font-size: 8px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; text-align: center;">
Scan Range (MHz)</div>
<div style="display: flex; align-items: center; gap: 6px;">
<div style="flex: 1;">
<div style="font-size: 7px; color: var(--text-muted); margin-bottom: 2px;">
START</div>
<input type="number" id="radioScanStart" value="118" step="0.1"
class="radio-input"
style="width: 100%; font-size: 16px; padding: 8px 6px; text-align: center; font-family: var(--font-mono); font-weight: bold; color: var(--accent-cyan);">
</div>
<span
style="color: var(--text-muted); font-size: 16px; padding-top: 12px;"></span>
<div style="flex: 1;">
<div style="font-size: 7px; color: var(--text-muted); margin-bottom: 2px;">
END</div>
<input type="number" id="radioScanEnd" value="137" step="0.1"
class="radio-input"
style="width: 100%; font-size: 16px; padding: 8px 6px; text-align: center; font-family: var(--font-mono); font-weight: bold; color: var(--accent-cyan);">
</div>
</div>
</div>
<!-- Action Buttons -->
<button class="radio-action-btn scan" id="radioScanBtn" onclick="toggleScanner()">
<span class="icon icon--sm" style="margin-right: 4px;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"/></svg></span>SCAN
</button>
<button class="radio-action-btn listen" id="radioListenBtn" onclick="toggleDirectListen()">
<span class="icon icon--sm" style="margin-right: 4px;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 18v-6a9 9 0 0 1 18 0v6"/><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/></svg></span>LISTEN
</button>
</div>
</div>
</div>
<!-- QUICK TUNE BAR -->
<div class="radio-module-box" style="grid-column: span 4; padding: 8px 12px;">
<div style="display: flex; align-items: center; gap: 10px;">
<span
style="font-size: 9px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px;">Quick
Tune:</span>
<button class="preset-freq-btn" onclick="quickTune(121.5, 'am')">121.5 GUARD</button>
<button class="preset-freq-btn" onclick="quickTune(156.8, 'fm')">156.8 CH16</button>
<button class="preset-freq-btn" onclick="quickTune(145.5, 'fm')">145.5 2M</button>
<button class="preset-freq-btn" onclick="quickTune(98.1, 'wfm')">98.1 FM</button>
<button class="preset-freq-btn" onclick="quickTune(462.5625, 'fm')">462.56 FRS</button>
<button class="preset-freq-btn" onclick="quickTune(446.0, 'fm')">446.0 PMR</button>
</div>
</div>
<!-- SIGNAL HITS -->
<div class="radio-module-box" style="grid-column: span 2; padding: 10px;">
<div class="module-header"
style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; font-size: 10px;">
<span>SIGNAL HITS</span>
<span
style="font-size: 10px; color: var(--accent-cyan); background: rgba(0,212,255,0.1); padding: 2px 8px; border-radius: 3px;"
id="scannerHitCount">0 signals</span>
</div>
<div id="scannerHitsList" style="overflow-y: auto; max-height: 100px;">
<table style="width: 100%; font-size: 10px; border-collapse: collapse;">
<thead>
<tr style="color: var(--text-muted); border-bottom: 1px solid var(--border-color);">
<th style="text-align: left; padding: 4px;">Time</th>
<th style="text-align: left; padding: 4px;">Frequency</th>
<th style="text-align: left; padding: 4px;">SNR</th>
<th style="text-align: left; padding: 4px;">Mod</th>
<th style="text-align: center; padding: 4px; width: 60px;">Action</th>
</tr>
</thead>
<tbody id="scannerHitsBody">
<tr style="color: var(--text-muted);">
<td colspan="5" style="padding: 15px; text-align: center; font-size: 10px;">No
signals detected</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- STATS PANEL -->
<div class="radio-module-box" style="grid-column: span 1; padding: 10px;">
<div class="module-header" style="margin-bottom: 8px; font-size: 10px;">
<span>STATS</span>
</div>
<div style="display: flex; flex-direction: column; gap: 8px;">
<div style="display: flex; align-items: center; justify-content: space-between;">
<span style="font-size: 9px; color: var(--text-muted);">SIGNALS</span>
<span
style="color: var(--accent-green); font-size: 18px; font-weight: bold; font-family: var(--font-mono);"
id="mainSignalCount">0</span>
</div>
<div style="display: flex; align-items: center; justify-content: space-between;">
<span style="font-size: 9px; color: var(--text-muted);">SCANNED</span>
<span
style="color: var(--accent-cyan); font-size: 18px; font-weight: bold; font-family: var(--font-mono);"
id="mainFreqsScanned">0</span>
</div>
<div style="display: flex; align-items: center; justify-content: space-between;">
<span style="font-size: 9px; color: var(--text-muted);">CYCLES</span>
<span
style="color: var(--accent-orange); font-size: 18px; font-weight: bold; font-family: var(--font-mono);"
id="mainScanCycles">0</span>
</div>
</div>
</div>
<!-- ACTIVITY LOG -->
<div class="radio-module-box" style="grid-column: span 1; padding: 10px;">
<div class="module-header"
style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; font-size: 10px;">
<span>LOG</span>
<div style="display: flex; gap: 4px;">
<button class="tune-btn" onclick="exportScannerLog()"
style="padding: 2px 6px; font-size: 8px;">Export</button>
<button class="tune-btn" onclick="clearScannerLog()"
style="padding: 2px 6px; font-size: 8px;">Clear</button>
</div>
</div>
<div class="log-content" id="scannerActivityLog"
style="max-height: 100px; overflow-y: auto; font-size: 9px; background: rgba(0,0,0,0.2); border-radius: 3px; padding: 6px;">
<div class="scanner-log-entry" style="color: var(--text-muted);">Ready</div>
</div>
</div>
<!-- SIGNAL ACTIVITY TIMELINE -->
<div class="radio-module-box" style="grid-column: span 4; padding: 10px;">
<div id="listeningPostTimelineContainer"></div>
</div>
</div>
<!-- Satellite Dashboard (Embedded) -->
<div id="satelliteVisuals" class="satellite-dashboard-embed" style="display: none;">
<iframe id="satelliteDashboardFrame" src="/satellite/dashboard?embedded=true" frameborder="0"
@@ -3200,6 +2764,10 @@
<button class="wf-step-btn" onclick="Waterfall.stepFreq && Waterfall.stepFreq(1)" title="Step up"></button>
<button class="wf-step-btn" onclick="Waterfall.stepFreq && Waterfall.stepFreq(10)" title="Step up ×10">»</button>
<div class="wf-freq-bar-sep"></div>
<span class="wf-freq-bar-label">ZOOM</span>
<button class="wf-step-btn wf-zoom-btn" onclick="Waterfall.zoomOut && Waterfall.zoomOut()" title="Zoom out (wider span)">-</button>
<button class="wf-step-btn wf-zoom-btn" onclick="Waterfall.zoomIn && Waterfall.zoomIn()" title="Zoom in (narrower span)">+</button>
<div class="wf-freq-bar-sep"></div>
<span class="wf-freq-bar-label">STEP</span>
<select id="wfStepSize" class="wf-step-select">
<option value="0.001">1 kHz</option>
@@ -3224,6 +2792,8 @@
<div class="wf-tune-line" id="wfTuneLineSpec"></div>
</div>
<div class="wf-band-strip" id="wfBandStrip"></div>
<!-- Drag handle to resize spectrum vs waterfall -->
<div class="wf-resize-handle" id="wfResizeHandle">
<div class="wf-resize-grip"></div>
@@ -3241,20 +2811,6 @@
</div>
</div>
<!-- RF Heatmap Visuals -->
<div id="rfheatmapVisuals" style="display: none; flex-direction: column; flex: 1; min-height: 0; overflow: hidden;">
<div class="rfhm-map-container" style="flex: 1; min-height: 0; position: relative;">
<div id="rfheatmapMapEl" style="width: 100%; height: 100%;"></div>
</div>
</div>
<!-- Fingerprint Visuals -->
<div id="fingerprintVisuals" style="display: none; flex-direction: column; flex: 1; min-height: 0; overflow: hidden; padding: 10px; gap: 10px;">
<div class="fp-chart-container" style="flex: 1; min-height: 200px;">
<canvas id="fpChartCanvas"></canvas>
</div>
</div>
<!-- Device Intelligence Dashboard (above waterfall for prominence) -->
<div class="recon-panel collapsed" id="reconPanel">
<div class="recon-header" onclick="toggleReconCollapse()" style="cursor: pointer;">
@@ -3369,7 +2925,6 @@
<!-- WiFi v2 components -->
<script src="{{ url_for('static', filename='js/components/channel-chart.js') }}"></script>
<script src="{{ url_for('static', filename='js/modes/wifi.js') }}"></script>
<script src="{{ url_for('static', filename='js/modes/listening-post.js') }}"></script>
<script src="{{ url_for('static', filename='js/modes/spy-stations.js') }}"></script>
<script src="{{ url_for('static', filename='js/modes/meshtastic.js') }}"></script>
<script src="{{ url_for('static', filename='js/modes/sstv.js') }}"></script>
@@ -3380,12 +2935,10 @@
<script src="{{ url_for('static', filename='js/modes/subghz.js') }}?v={{ version }}&r=subghz_layout9"></script>
<script src="{{ url_for('static', filename='js/modes/bt_locate.js') }}?v={{ version }}&r=btlocate4"></script>
<script src="{{ url_for('static', filename='js/modes/space-weather.js') }}"></script>
<script src="{{ url_for('static', filename='js/core/voice-alerts.js') }}?v={{ version }}&r=voicefix1"></script>
<script src="{{ url_for('static', filename='js/core/voice-alerts.js') }}?v={{ version }}&r=voicefix2"></script>
<script src="{{ url_for('static', filename='js/core/keyboard-shortcuts.js') }}"></script>
<script src="{{ url_for('static', filename='js/core/cheat-sheets.js') }}"></script>
<script src="{{ url_for('static', filename='js/modes/waterfall.js') }}?v={{ version }}&r=wfdeck10"></script>
<script src="{{ url_for('static', filename='js/modes/rfheatmap.js') }}"></script>
<script src="{{ url_for('static', filename='js/modes/fingerprint.js') }}"></script>
<script src="{{ url_for('static', filename='js/modes/waterfall.js') }}?v={{ version }}&r=wfdeck20"></script>
<script>
// ============================================
@@ -3432,15 +2985,6 @@
collapsed: true
}
},
'listening': {
container: 'listeningPostTimelineContainer',
config: typeof RFTimelineAdapter !== 'undefined' ? RFTimelineAdapter.getListeningPostConfig() : {
title: 'Signal Activity',
mode: 'listening-post',
visualMode: 'enriched',
collapsed: false
}
},
'bluetooth': {
container: 'bluetoothTimelineContainer',
config: typeof BluetoothTimelineAdapter !== 'undefined' ? BluetoothTimelineAdapter.getBluetoothConfig() : {
@@ -3496,7 +3040,11 @@
}
// Selected mode from welcome screen
let selectedStartMode = localStorage.getItem('intercept.default_mode') || 'pager';
const savedDefaultMode = localStorage.getItem('intercept.default_mode');
let selectedStartMode = savedDefaultMode === 'listening' ? 'waterfall' : (savedDefaultMode || 'pager');
if (savedDefaultMode === 'listening') {
localStorage.setItem('intercept.default_mode', 'waterfall');
}
// Mode selection from welcome page
function selectMode(mode) {
@@ -3522,7 +3070,6 @@
pager: { label: 'Pager', indicator: 'PAGER', outputTitle: 'Pager Decoder', group: 'signals' },
sensor: { label: '433MHz', indicator: '433MHZ', outputTitle: '433MHz Sensor Monitor', group: 'signals' },
rtlamr: { label: 'Meters', indicator: 'METERS', outputTitle: 'Utility Meter Monitor', group: 'signals' },
listening: { label: 'Listening Post', indicator: 'LISTENING POST', outputTitle: 'Listening Post', group: 'signals' },
subghz: { label: 'SubGHz', indicator: 'SUBGHZ', outputTitle: 'SubGHz Transceiver', group: 'signals' },
aprs: { label: 'APRS', indicator: 'APRS', outputTitle: 'APRS Tracker', group: 'tracking' },
gps: { label: 'GPS', indicator: 'GPS', outputTitle: 'GPS Receiver', group: 'tracking' },
@@ -3539,15 +3086,14 @@
spystations: { label: 'Spy Stations', indicator: 'SPY STATIONS', outputTitle: 'Spy Stations', group: 'intel' },
websdr: { label: 'WebSDR', indicator: 'WEBSDR', outputTitle: 'HF/Shortwave WebSDR', group: 'intel' },
waterfall: { label: 'Waterfall', indicator: 'WATERFALL', outputTitle: 'Spectrum Waterfall', group: 'signals' },
rfheatmap: { label: 'RF Heatmap', indicator: 'RF HEATMAP', outputTitle: 'RF Signal Heatmap', group: 'intel' },
fingerprint: { label: 'Fingerprint', indicator: 'RF FINGERPRINT', outputTitle: 'Signal Fingerprinting', group: 'intel' },
};
const validModes = new Set(Object.keys(modeCatalog));
window.interceptModeCatalog = Object.assign({}, modeCatalog);
function getModeFromQuery() {
const params = new URLSearchParams(window.location.search);
const mode = params.get('mode');
const requestedMode = params.get('mode');
const mode = requestedMode === 'listening' ? 'waterfall' : requestedMode;
if (!mode || !validModes.has(mode)) return null;
return mode;
}
@@ -4059,6 +3605,8 @@
// Mode switching
function switchMode(mode, options = {}) {
const { updateUrl = true } = options;
if (mode === 'listening') mode = 'waterfall';
if (!validModes.has(mode)) mode = 'pager';
// Only stop local scans if in local mode (not agent mode)
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
if (!isAgentMode) {
@@ -4122,7 +3670,6 @@
document.getElementById('wifiMode')?.classList.toggle('active', mode === 'wifi');
document.getElementById('bluetoothMode')?.classList.toggle('active', mode === 'bluetooth');
document.getElementById('btLocateMode')?.classList.toggle('active', mode === 'bt_locate');
document.getElementById('listeningPostMode')?.classList.toggle('active', mode === 'listening');
document.getElementById('aprsMode')?.classList.toggle('active', mode === 'aprs');
document.getElementById('tscmMode')?.classList.toggle('active', mode === 'tscm');
document.getElementById('aisMode')?.classList.toggle('active', mode === 'ais');
@@ -4132,8 +3679,6 @@
document.getElementById('subghzMode')?.classList.toggle('active', mode === 'subghz');
document.getElementById('spaceWeatherMode')?.classList.toggle('active', mode === 'spaceweather');
document.getElementById('waterfallMode')?.classList.toggle('active', mode === 'waterfall');
document.getElementById('rfheatmapMode')?.classList.toggle('active', mode === 'rfheatmap');
document.getElementById('fingerprintMode')?.classList.toggle('active', mode === 'fingerprint');
const pagerStats = document.getElementById('pagerStats');
@@ -4161,7 +3706,6 @@
const wifiLayoutContainer = document.getElementById('wifiLayoutContainer');
const btLayoutContainer = document.getElementById('btLayoutContainer');
const satelliteVisuals = document.getElementById('satelliteVisuals');
const listeningPostVisuals = document.getElementById('listeningPostVisuals');
const aprsVisuals = document.getElementById('aprsVisuals');
const tscmVisuals = document.getElementById('tscmVisuals');
const spyStationsVisuals = document.getElementById('spyStationsVisuals');
@@ -4175,12 +3719,9 @@
const btLocateVisuals = document.getElementById('btLocateVisuals');
const spaceWeatherVisuals = document.getElementById('spaceWeatherVisuals');
const waterfallVisuals = document.getElementById('waterfallVisuals');
const rfheatmapVisuals = document.getElementById('rfheatmapVisuals');
const fingerprintVisuals = document.getElementById('fingerprintVisuals');
if (wifiLayoutContainer) wifiLayoutContainer.style.display = mode === 'wifi' ? 'flex' : 'none';
if (btLayoutContainer) btLayoutContainer.style.display = mode === 'bluetooth' ? 'flex' : 'none';
if (satelliteVisuals) satelliteVisuals.style.display = mode === 'satellite' ? 'block' : 'none';
if (listeningPostVisuals) listeningPostVisuals.style.display = mode === 'listening' ? 'grid' : 'none';
if (aprsVisuals) aprsVisuals.style.display = mode === 'aprs' ? 'flex' : 'none';
if (tscmVisuals) tscmVisuals.style.display = mode === 'tscm' ? 'flex' : 'none';
if (spyStationsVisuals) spyStationsVisuals.style.display = mode === 'spystations' ? 'flex' : 'none';
@@ -4193,9 +3734,7 @@
if (subghzVisuals) subghzVisuals.style.display = mode === 'subghz' ? 'flex' : 'none';
if (btLocateVisuals) btLocateVisuals.style.display = mode === 'bt_locate' ? 'flex' : 'none';
if (spaceWeatherVisuals) spaceWeatherVisuals.style.display = mode === 'spaceweather' ? 'flex' : 'none';
if (waterfallVisuals) waterfallVisuals.style.display = (mode === 'waterfall' || mode === 'listening') ? 'flex' : 'none';
if (rfheatmapVisuals) rfheatmapVisuals.style.display = mode === 'rfheatmap' ? 'flex' : 'none';
if (fingerprintVisuals) fingerprintVisuals.style.display = mode === 'fingerprint' ? 'flex' : 'none';
if (waterfallVisuals) waterfallVisuals.style.display = mode === 'waterfall' ? 'flex' : 'none';
// Prevent Leaflet heatmap redraws on hidden BT Locate map containers.
if (typeof BtLocate !== 'undefined' && BtLocate.setActiveMode) {
@@ -4245,7 +3784,7 @@
const reconBtn = document.getElementById('reconBtn');
const intelBtn = document.querySelector('[onclick="exportDeviceDB()"]');
const reconPanel = document.getElementById('reconPanel');
if (mode === 'satellite' || mode === 'sstv' || mode === 'weathersat' || mode === 'sstv_general' || mode === 'gps' || mode === 'listening' || mode === 'aprs' || mode === 'tscm' || mode === 'spystations' || mode === 'meshtastic' || mode === 'websdr' || mode === 'subghz' || mode === 'spaceweather' || mode === 'waterfall' || mode === 'rfheatmap' || mode === 'fingerprint') {
if (mode === 'satellite' || mode === 'sstv' || mode === 'weathersat' || mode === 'sstv_general' || mode === 'gps' || mode === 'aprs' || mode === 'tscm' || mode === 'spystations' || mode === 'meshtastic' || mode === 'websdr' || mode === 'subghz' || mode === 'spaceweather' || mode === 'waterfall') {
if (reconPanel) reconPanel.style.display = 'none';
if (reconBtn) reconBtn.style.display = 'none';
if (intelBtn) intelBtn.style.display = 'none';
@@ -4260,19 +3799,12 @@
// Show agent selector for modes that support remote agents
const agentSection = document.getElementById('agentSection');
const agentModes = ['pager', 'sensor', 'rtlamr', 'listening', 'aprs', 'wifi', 'bluetooth', 'aircraft', 'tscm', 'ais'];
const agentModes = ['pager', 'sensor', 'rtlamr', 'aprs', 'wifi', 'bluetooth', 'aircraft', 'tscm', 'ais'];
if (agentSection) agentSection.style.display = agentModes.includes(mode) ? 'block' : 'none';
// Show RTL-SDR device section for modes that use it
const rtlDeviceSection = document.getElementById('rtlDeviceSection');
if (rtlDeviceSection) rtlDeviceSection.style.display = (mode === 'pager' || mode === 'sensor' || mode === 'rtlamr' || mode === 'listening' || mode === 'aprs' || mode === 'sstv' || mode === 'weathersat' || mode === 'sstv_general') ? 'block' : 'none';
// Show waterfall panel if running in listening mode
const waterfallPanel = document.getElementById('waterfallPanel');
if (waterfallPanel) {
const running = (typeof isWaterfallRunning !== 'undefined' && isWaterfallRunning);
waterfallPanel.style.display = (mode === 'listening' && running) ? 'block' : 'none';
}
if (rtlDeviceSection) rtlDeviceSection.style.display = (mode === 'pager' || mode === 'sensor' || mode === 'rtlamr' || mode === 'aprs' || mode === 'sstv' || mode === 'weathersat' || mode === 'sstv_general') ? 'block' : 'none';
// Toggle mode-specific tool status displays
const toolStatusPager = document.getElementById('toolStatusPager');
@@ -4283,7 +3815,7 @@
// Hide output console for modes with their own visualizations
const outputEl = document.getElementById('output');
const statusBar = document.querySelector('.status-bar');
if (outputEl) outputEl.style.display = (mode === 'satellite' || mode === 'sstv' || mode === 'weathersat' || mode === 'sstv_general' || mode === 'aprs' || mode === 'wifi' || mode === 'bluetooth' || mode === 'listening' || mode === 'tscm' || mode === 'spystations' || mode === 'meshtastic' || mode === 'websdr' || mode === 'subghz' || mode === 'spaceweather' || mode === 'bt_locate' || mode === 'waterfall' || mode === 'rfheatmap' || mode === 'fingerprint') ? 'none' : 'block';
if (outputEl) outputEl.style.display = (mode === 'satellite' || mode === 'sstv' || mode === 'weathersat' || mode === 'sstv_general' || mode === 'aprs' || mode === 'wifi' || mode === 'bluetooth' || mode === 'tscm' || mode === 'spystations' || mode === 'meshtastic' || mode === 'websdr' || mode === 'subghz' || mode === 'spaceweather' || mode === 'bt_locate' || mode === 'waterfall') ? 'none' : 'block';
if (statusBar) statusBar.style.display = (mode === 'satellite' || mode === 'websdr' || mode === 'subghz' || mode === 'spaceweather' || mode === 'waterfall') ? 'none' : 'flex';
// Restore sidebar when leaving Meshtastic mode (user may have collapsed it)
@@ -4316,12 +3848,6 @@
} else if (mode === 'satellite') {
initPolarPlot();
initSatelliteList();
} else if (mode === 'listening') {
// Check for incoming tune requests from Spy Stations
if (typeof checkIncomingTuneRequest === 'function') {
checkIncomingTuneRequest();
}
if (typeof Waterfall !== 'undefined') Waterfall.init();
} else if (mode === 'spystations') {
SpyStations.init();
} else if (mode === 'meshtastic') {
@@ -4360,17 +3886,10 @@
SpaceWeather.init();
} else if (mode === 'waterfall') {
if (typeof Waterfall !== 'undefined') Waterfall.init();
} else if (mode === 'rfheatmap') {
if (typeof RFHeatmap !== 'undefined') {
RFHeatmap.init();
setTimeout(() => RFHeatmap.invalidateMap(), 100);
}
} else if (mode === 'fingerprint') {
if (typeof Fingerprint !== 'undefined') Fingerprint.init();
}
// Destroy Waterfall WebSocket when leaving SDR receiver modes
if (mode !== 'waterfall' && mode !== 'listening' && typeof Waterfall !== 'undefined' && Waterfall.destroy) {
if (mode !== 'waterfall' && typeof Waterfall !== 'undefined' && Waterfall.destroy) {
Promise.resolve(Waterfall.destroy()).catch(() => {});
}
}
@@ -10793,7 +10312,7 @@
}
});
// NOTE: Scanner and Audio Receiver code moved to static/js/modes/listening-post.js
// Scanner and receiver logic are handled by Waterfall mode.
// ============================================
// TSCM (Counter-Surveillance) Functions
@@ -12376,7 +11895,7 @@
</button>
</div>
<div style="font-size: 10px; color: var(--text-secondary); margin-top: 8px;">
${protocol === 'rf' ? 'Listen buttons open Listening Post. ' : ''}Known devices are excluded from threat scoring in future sweeps.
${protocol === 'rf' ? 'Listen buttons open Spectrum Waterfall. ' : ''}Known devices are excluded from threat scoring in future sweeps.
</div>
</div>
`;
@@ -13154,18 +12673,18 @@
// Close the modal
closeTscmDeviceModal();
// Switch to listening post mode
switchMode('listening');
// Switch to spectrum waterfall mode
switchMode('waterfall');
// Wait a moment for the mode to switch, then tune to the frequency
setTimeout(() => {
if (typeof tuneToFrequency === 'function') {
tuneToFrequency(frequency, modulation);
if (typeof Waterfall !== 'undefined' && typeof Waterfall.quickTune === 'function') {
Waterfall.quickTune(frequency, modulation);
} else {
// Fallback: manually update the frequency input
const freqInput = document.getElementById('radioScanStart');
// Fallback: update Waterfall center control directly
const freqInput = document.getElementById('wfCenterFreq');
if (freqInput) {
freqInput.value = frequency.toFixed(1);
freqInput.value = frequency.toFixed(4);
}
alert(`Tune to ${frequency.toFixed(3)} MHz (${modulation.toUpperCase()}) to listen`);
}
@@ -15107,8 +14626,6 @@
});
</script>
<!-- Scanner/Audio code moved to static/js/modes/listening-post.js -->
{% include 'partials/help-modal.html' %}
@@ -15317,8 +14834,6 @@
<table style="width:100%; border-collapse:collapse; font-family:var(--font-mono); font-size:12px;">
<tbody>
<tr style="border-bottom:1px solid rgba(255,255,255,0.06);"><td style="padding:6px 8px; color:var(--accent-cyan);">Alt+W</td><td style="padding:6px 8px; color:var(--text-secondary);">Switch to Waterfall</td></tr>
<tr style="border-bottom:1px solid rgba(255,255,255,0.06);"><td style="padding:6px 8px; color:var(--accent-cyan);">Alt+H</td><td style="padding:6px 8px; color:var(--text-secondary);">Switch to RF Heatmap</td></tr>
<tr style="border-bottom:1px solid rgba(255,255,255,0.06);"><td style="padding:6px 8px; color:var(--accent-cyan);">Alt+N</td><td style="padding:6px 8px; color:var(--text-secondary);">Switch to Fingerprint</td></tr>
<tr style="border-bottom:1px solid rgba(255,255,255,0.06);"><td style="padding:6px 8px; color:var(--accent-cyan);">Alt+M</td><td style="padding:6px 8px; color:var(--text-secondary);">Toggle voice mute</td></tr>
<tr style="border-bottom:1px solid rgba(255,255,255,0.06);"><td style="padding:6px 8px; color:var(--accent-cyan);">Alt+S</td><td style="padding:6px 8px; color:var(--text-secondary);">Toggle sidebar</td></tr>
<tr style="border-bottom:1px solid rgba(255,255,255,0.06);"><td style="padding:6px 8px; color:var(--accent-cyan);">Alt+K / ?</td><td style="padding:6px 8px; color:var(--text-secondary);">Show keyboard shortcuts</td></tr>