mirror of
https://github.com/smittix/intercept.git
synced 2026-06-18 10:29:46 -07:00
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:
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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: '© OSM © CARTO'
|
||||
});
|
||||
tileLayer.addTo(map);
|
||||
}
|
||||
if (tileLayer && typeof tileLayer.on === 'function') {
|
||||
tileLayer.on('load', () => {
|
||||
scheduleMapStabilization(8);
|
||||
});
|
||||
}
|
||||
ensureHeatLayer();
|
||||
syncMovementLayer();
|
||||
syncHeatLayer();
|
||||
|
||||
@@ -43,6 +43,9 @@ var OokMode = (function () {
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
if (state.running) {
|
||||
stop();
|
||||
}
|
||||
disconnectSSE();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2344,7 +2344,11 @@ const WeatherSat = (function() {
|
||||
clearInterval(countdownInterval);
|
||||
countdownInterval = null;
|
||||
}
|
||||
stopStream();
|
||||
if (isRunning) {
|
||||
stop().catch(() => {});
|
||||
} else {
|
||||
stopStream();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -58,6 +58,9 @@ var WeFax = (function () {
|
||||
|
||||
function destroy() {
|
||||
closeImage();
|
||||
if (state.running) {
|
||||
stop();
|
||||
}
|
||||
disconnectSSE();
|
||||
stopScope();
|
||||
stopCountdownTimer();
|
||||
|
||||
+10
-13
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user