fix: resolve two-window hang and sweep UI/theming updates

Fix app becoming unresponsive when two browser windows are open: the
root cause was HTTP/1.1 connection pool exhaustion (6-connection limit
per origin). VoiceAlerts was opening 3 SSE streams per window by
default, so two windows produced 8 connections and permanently starved
all regular HTTP requests.

- voice-alerts.js: default all streams to false (opt-in) to stay within
  the browser connection limit; existing user preferences in localStorage
  are preserved
- routes/alerts.py: replace direct AlertManager.stream_events() with
  sse_stream_fanout so both windows receive every alert instead of
  competing for the same queue
- routes/bluetooth_v2.py: same fanout fix via subscribe_fanout_queue,
  preserving named SSE events (device_update, scan_started, etc.)

Also includes accumulated UI/theming changes: accent-cyan CSS variable
sweep across mode CSS/JS files, standalone dashboard pages, template
updates, satellite TLE data refresh, and tile provider default rename.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
James Smith
2026-05-20 22:01:10 +01:00
parent 5100f55586
commit a3f2fa7b88
48 changed files with 1524 additions and 943 deletions
+3 -3
View File
@@ -216,9 +216,9 @@ const FirstRunSetup = (function() {
el.innerHTML = '<div style="display:flex;justify-content:space-between;font-size:7px;color:#aaa;font-family:monospace;"><span>INTERCEPT</span><span style="color:#4a4;">● LIVE</span></div><div style="font-size:7px;color:#888;font-family:monospace;margin-top:2px;">ADS-B ........... 247<br>TSCM ............ 3 ⚠</div>';
});
const enhancedBtn = makeTierBtn('enhanced', 'Enhanced', 'Amber military console — for desktop or laptop.', (el) => {
el.style.cssText += 'background:#080600;border:1px solid rgba(200,150,40,0.3);display:flex;flex-direction:column;justify-content:center;padding:6px;gap:3px;';
el.innerHTML = '<div style="display:flex;justify-content:space-between;font-size:7px;color:#c89628;font-family:monospace;letter-spacing:2px;"><span>INTERCEPT</span><span style="opacity:0.6;">14:27Z</span></div><div style="border-left:2px solid #c89628;padding-left:4px;margin-top:4px;font-size:8px;color:#c89628;font-family:monospace;font-weight:700;">247 ADS-B</div>';
const enhancedBtn = makeTierBtn('enhanced', 'Enhanced', 'Signals teal console — for desktop or laptop.', (el) => {
el.style.cssText += 'background:#000202;border:1px solid rgba(46,125,138,0.3);display:flex;flex-direction:column;justify-content:center;padding:6px;gap:3px;';
el.innerHTML = '<div style="display:flex;justify-content:space-between;font-size:7px;color:#2e7d8a;font-family:monospace;letter-spacing:2px;"><span>INTERCEPT</span><span style="opacity:0.6;">14:27Z</span></div><div style="border-left:2px solid #2e7d8a;padding-left:4px;margin-top:4px;font-size:8px;color:#2e7d8a;font-family:monospace;font-weight:700;">247 ADS-B</div>';
});
btnWrap.appendChild(leanBtn);
+7 -6
View File
@@ -16,17 +16,18 @@ const VoiceAlerts = (function () {
const PITCH_MIN = 0.5;
const PITCH_MAX = 2.0;
// Default config
// Default config — streams are opt-in to avoid saturating the browser's
// HTTP/1.1 per-origin connection limit (6) when multiple tabs are open.
let _config = {
rate: 1.1,
pitch: 0.9,
voiceName: '',
streams: {
pager: true,
tscm: true,
bluetooth: true,
adsb_military: true,
squawks: true,
pager: false,
tscm: false,
bluetooth: false,
adsb_military: false,
squawks: false,
},
};
+2 -21
View File
@@ -197,30 +197,11 @@ const BtLocate = (function() {
// Init map
const mapEl = document.getElementById('btLocateMap');
if (mapEl && typeof L !== 'undefined') {
map = L.map('btLocateMap', {
if (mapEl && typeof L !== 'undefined' && typeof MapUtils !== 'undefined') {
map = MapUtils.init('btLocateMap', {
center: [0, 0],
zoom: 2,
zoomControl: true,
});
let tileLayer = null;
// Use tile provider from user settings
if (typeof Settings !== 'undefined' && Settings.createTileLayer) {
tileLayer = Settings.createTileLayer();
tileLayer.addTo(map);
Settings.registerMap(map);
} else {
tileLayer = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
maxZoom: 19,
attribution: '&copy; OSM &copy; CARTO'
});
tileLayer.addTo(map);
}
if (tileLayer && typeof tileLayer.on === 'function') {
tileLayer.on('load', () => {
scheduleMapStabilization(8);
});
}
ensureHeatLayer();
syncMovementLayer();
syncHeatLayer();
+3
View File
@@ -43,6 +43,9 @@ var OokMode = (function () {
}
function destroy() {
if (state.running) {
stop();
}
disconnectSSE();
}
+5 -1
View File
@@ -874,7 +874,11 @@ const SSTVGeneral = (function() {
* Destroy — close SSE stream and stop scope animation for clean mode switching.
*/
function destroy() {
stopStream();
if (isRunning) {
stop().catch(() => {});
} else {
stopStream();
}
}
// Public API
+7 -3
View File
@@ -1428,9 +1428,13 @@ const SSTV = (function() {
* Destroy — close SSE stream and clear ISS tracking/countdown timers for clean mode switching.
*/
function destroy() {
if (eventSource) {
eventSource.close();
eventSource = null;
if (isRunning) {
stop().catch(() => {});
} else {
if (eventSource) {
eventSource.close();
eventSource = null;
}
}
stopIssTracking();
stopCountdown();
+10 -2
View File
@@ -3014,10 +3014,14 @@ const Waterfall = (function () {
}
// Backend stop is fire-and-forget; UI is already updated above.
const _audioStopCtrl = new AbortController();
const _audioStopTid = setTimeout(() => _audioStopCtrl.abort(), 3000);
try {
await fetch('/receiver/audio/stop', { method: 'POST' });
await fetch('/receiver/audio/stop', { method: 'POST', signal: _audioStopCtrl.signal });
} catch (_) {
// Ignore backend stop errors
} finally {
clearTimeout(_audioStopTid);
}
if (resumeWaterfall && _active) {
@@ -3222,10 +3226,14 @@ const Waterfall = (function () {
if (_es) {
_closeSseStream();
const _wfStopCtrl = new AbortController();
const _wfStopTid = setTimeout(() => _wfStopCtrl.abort(), 3000);
try {
await fetch('/receiver/waterfall/stop', { method: 'POST' });
await fetch('/receiver/waterfall/stop', { method: 'POST', signal: _wfStopCtrl.signal });
} catch (_) {
// Ignore fallback stop errors.
} finally {
clearTimeout(_wfStopTid);
}
}
+5 -1
View File
@@ -2344,7 +2344,11 @@ const WeatherSat = (function() {
clearInterval(countdownInterval);
countdownInterval = null;
}
stopStream();
if (isRunning) {
stop().catch(() => {});
} else {
stopStream();
}
}
/**
+15 -6
View File
@@ -260,7 +260,10 @@ function initWebsdrGlobe(mapEl) {
if (typeof window.Globe !== 'function' || !isWebglSupported()) return false;
mapEl.innerHTML = '';
mapEl.style.background = 'radial-gradient(circle at 30% 20%, rgba(14, 42, 68, 0.9), rgba(4, 9, 16, 0.95) 58%, rgba(2, 4, 9, 0.98) 100%)';
const _wsdrTier = document.documentElement.getAttribute('data-ui-tier') || 'enhanced';
mapEl.style.background = _wsdrTier === 'enhanced'
? 'radial-gradient(circle at 30% 20%, rgba(4, 18, 22, 0.92), rgba(2, 8, 10, 0.96) 58%, rgba(0, 2, 2, 0.99) 100%)'
: 'radial-gradient(circle at 30% 20%, rgba(14, 42, 68, 0.9), rgba(4, 9, 16, 0.95) 58%, rgba(2, 4, 9, 0.98) 100%)';
mapEl.style.cursor = 'grab';
const _wsdrAccent = getComputedStyle(document.documentElement).getPropertyValue('--accent-cyan').trim() || '#3bb9ff';
@@ -296,6 +299,8 @@ function initWebsdrGlobe(mapEl) {
ensureWebsdrGlobePopup(mapEl);
resizeWebsdrGlobe();
// Grid layout may not have settled on the first rAF; re-sync after one frame.
requestAnimationFrame(() => resizeWebsdrGlobe());
return true;
}
@@ -475,8 +480,10 @@ function ensureWebsdrGlobePopup(mapEl) {
websdrGlobePopup.style.maxWidth = '260px';
websdrGlobePopup.style.padding = '10px';
websdrGlobePopup.style.borderRadius = '8px';
websdrGlobePopup.style.border = '1px solid rgba(0, 212, 255, 0.35)';
websdrGlobePopup.style.background = 'rgba(5, 13, 20, 0.92)';
const _wsdrPopupRgb = getComputedStyle(document.documentElement).getPropertyValue('--accent-cyan-rgb').trim() || '0, 212, 255';
const _wsdrPopupTier = document.documentElement.getAttribute('data-ui-tier') || 'enhanced';
websdrGlobePopup.style.border = `1px solid rgba(${_wsdrPopupRgb}, 0.35)`;
websdrGlobePopup.style.background = _wsdrPopupTier === 'enhanced' ? 'rgba(2, 8, 10, 0.94)' : 'rgba(5, 13, 20, 0.92)';
websdrGlobePopup.style.backdropFilter = 'blur(4px)';
websdrGlobePopup.style.boxShadow = '0 8px 24px rgba(0, 0, 0, 0.4)';
websdrGlobePopup.style.color = 'var(--text-primary)';
@@ -574,8 +581,9 @@ function renderReceiverList(receivers) {
container.innerHTML = receivers.slice(0, 50).map((rx, idx) => {
const selected = idx === websdrSelectedReceiverIndex;
const baseBg = selected ? 'rgba(0,212,255,0.14)' : 'transparent';
const hoverBg = selected ? 'rgba(0,212,255,0.18)' : 'rgba(0,212,255,0.05)';
const _wsdrRxRgb = getComputedStyle(document.documentElement).getPropertyValue('--accent-cyan-rgb').trim() || '0, 212, 255';
const baseBg = selected ? `rgba(${_wsdrRxRgb},0.14)` : 'transparent';
const hoverBg = selected ? `rgba(${_wsdrRxRgb},0.18)` : `rgba(${_wsdrRxRgb},0.05)`;
return `
<div style="padding: 8px 8px 8px 10px; border-bottom: 1px solid rgba(255,255,255,0.05); cursor: pointer; transition: background 0.2s; border-left: 2px solid ${selected ? 'var(--accent-cyan)' : 'transparent'}; background: ${baseBg};"
onmouseover="this.style.background='${hoverBg}'" onmouseout="this.style.background='${baseBg}'"
@@ -951,13 +959,14 @@ function loadSpyStationPresets() {
return;
}
const _wsdrSpyRgb = getComputedStyle(document.documentElement).getPropertyValue('--accent-cyan-rgb').trim() || '0, 212, 255';
container.innerHTML = stations.slice(0, 30).map(s => {
const primaryFreq = s.frequencies?.find(f => f.primary) || s.frequencies?.[0];
const freqKhz = primaryFreq?.freq_khz || 0;
return `
<div style="padding: 6px 4px; border-bottom: 1px solid rgba(255,255,255,0.05); cursor: pointer; display: flex; justify-content: space-between; align-items: center;"
onclick="tuneToSpyStation('${escapeHtmlWebsdr(s.id)}', ${freqKhz})"
onmouseover="this.style.background='rgba(0,212,255,0.05)'" onmouseout="this.style.background='transparent'">
onmouseover="this.style.background='rgba(${_wsdrSpyRgb},0.05)'" onmouseout="this.style.background='transparent'">
<div>
<span style="color: var(--accent-cyan); font-weight: bold;">${escapeHtmlWebsdr(s.name)}</span>
<span style="color: var(--text-muted); font-size: 9px; margin-left: 4px;">${escapeHtmlWebsdr(s.nickname || '')}</span>
+3
View File
@@ -58,6 +58,9 @@ var WeFax = (function () {
function destroy() {
closeImage();
if (state.running) {
stop();
}
disconnectSSE();
stopScope();
stopCountdownTimer();
+10 -13
View File
@@ -155,25 +155,22 @@ const WiFiMode = (function() {
// ==========================================================================
function init() {
console.log('[WiFiMode] Initializing...');
// Capabilities and one-time component setup only on first call.
// Subsequent visits refresh scan state and re-render without redundant fetches.
const firstInit = capabilities === null;
// Cache DOM elements
cacheDOM();
// Check capabilities
checkCapabilities();
if (firstInit) {
checkCapabilities();
initScanModeTabs();
initNetworkFilters();
initSortControls();
initHeatmap();
}
// Initialize components
initScanModeTabs();
initNetworkFilters();
initSortControls();
initHeatmap();
scheduleRender({ table: true, stats: true, radar: true });
// Check if already scanning
checkScanStatus();
console.log('[WiFiMode] Initialized');
}
// DOM element cache