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
+15 -3
View File
@@ -2094,13 +2094,25 @@ ACARS: ${r.statistics.acarsMessages} messages`;
const existing = document.getElementById('aircraftDbBanner');
if (existing) existing.remove();
const _dbTier = document.documentElement.getAttribute('data-ui-tier') || 'enhanced';
const _dbBg = _dbTier === 'enhanced'
? (type === 'not_installed' ? 'rgba(4, 22, 26, 0.97)' : 'rgba(4, 22, 26, 0.97)')
: (type === 'not_installed' ? 'rgba(59, 130, 246, 0.95)' : 'rgba(34, 197, 94, 0.95)');
const _dbBorder = _dbTier === 'enhanced'
? (type === 'not_installed' ? '1px solid rgba(46, 125, 138, 0.45)' : '1px solid rgba(46, 125, 138, 0.45)')
: 'none';
const _dbBtnColor = _dbTier === 'enhanced'
? (type === 'not_installed' ? '#2e7d8a' : '#38c180')
: (type === 'not_installed' ? '#3b82f6' : '#22c55e');
const banner = document.createElement('div');
banner.id = 'aircraftDbBanner';
banner.style.cssText = `
position: fixed;
top: 70px;
right: 20px;
background: ${type === 'not_installed' ? 'rgba(59, 130, 246, 0.95)' : 'rgba(34, 197, 94, 0.95)'};
background: ${_dbBg};
border: ${_dbBorder};
color: white;
padding: 12px 16px;
border-radius: 8px;
@@ -2115,14 +2127,14 @@ ACARS: ${r.statistics.acarsMessages} messages`;
banner.innerHTML = `
<div style="font-weight: bold; margin-bottom: 6px;">Aircraft Database Not Installed</div>
<div style="margin-bottom: 10px; font-size: 11px; opacity: 0.9;">Download to see aircraft types, registrations, and model info.</div>
<button onclick="downloadAircraftDb()" style="background: white; color: #3b82f6; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-weight: 500; font-size: 11px;">Download Database</button>
<button onclick="downloadAircraftDb()" style="background: rgba(0,0,0,0.35); color: ${_dbBtnColor}; border: 1px solid ${_dbBtnColor}; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-weight: 500; font-size: 11px;">Download Database</button>
<button onclick="this.parentElement.remove()" style="position: absolute; top: 6px; right: 8px; background: none; border: none; color: white; cursor: pointer; font-size: 14px;">×</button>
`;
} else {
banner.innerHTML = `
<div style="font-weight: bold; margin-bottom: 6px;">Database Update Available</div>
<div style="margin-bottom: 10px; font-size: 11px; opacity: 0.9;">New version: ${version || 'latest'}</div>
<button onclick="downloadAircraftDb()" style="background: white; color: #22c55e; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-weight: 500; font-size: 11px;">Update Now</button>
<button onclick="downloadAircraftDb()" style="background: rgba(0,0,0,0.35); color: ${_dbBtnColor}; border: 1px solid ${_dbBtnColor}; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-weight: 500; font-size: 11px;">Update Now</button>
<button onclick="this.parentElement.remove()" style="position: absolute; top: 6px; right: 8px; background: none; border: none; color: white; cursor: pointer; font-size: 14px;">×</button>
`;
}
+3 -1
View File
@@ -1,14 +1,16 @@
<!DOCTYPE html>
<html lang="en" class="{% if offline_settings.tile_provider in ['cartodb_dark', 'cartodb_dark_cyan'] %}map-cyber-enabled{% endif %}">
<head>
<script>(function(){var t=localStorage.getItem('intercept-ui-tier')||'enhanced';document.documentElement.setAttribute('data-ui-tier',t);})();</script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ADS-B History // INTERCEPT</title>
{% if offline_settings.fonts_source == 'local' %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
{% else %}
<link href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&family=Roboto+Condensed:wght@300;400;500;600;700&display=swap" rel="stylesheet">
{% endif %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/variables.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/layout.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}?v={{ version }}&r=maptheme17">
+3
View File
@@ -5,7 +5,10 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>iNTERCEPT // Remote Agents</title>
<script>(function(){var t=localStorage.getItem('intercept-ui-tier')||'enhanced';document.documentElement.setAttribute('data-ui-tier',t);})();</script>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&family=Roboto+Condensed:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/variables.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/base.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
+14 -9
View File
@@ -42,7 +42,7 @@
{% if offline_settings.fonts_source == 'local' %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
{% else %}
<link href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&family=Roboto+Condensed:wght@300;400;500;600;700&display=swap" rel="stylesheet">
{% endif %}
<!-- Leaflet CSS -->
{% if offline_settings.assets_source == 'local' %}
@@ -1478,7 +1478,7 @@
<div id="tscmVisuals" class="tscm-dashboard" style="display: none; padding: 16px;">
<!-- Legal Disclaimer Banner -->
<div class="tscm-legal-banner"
style="margin-bottom: 12px; padding: 8px 12px; background: rgba(74, 158, 255, 0.1); border: 1px solid rgba(74, 158, 255, 0.3); border-radius: 4px; font-size: 10px; color: #ffffff;">
style="margin-bottom: 12px; padding: 8px 12px; background: rgba(var(--accent-cyan-rgb), 0.1); border: 1px solid rgba(var(--accent-cyan-rgb), 0.3); border-radius: 4px; font-size: 10px; color: #ffffff;">
<strong>TSCM Screening Tool:</strong> This system identifies wireless and RF anomalies.
Findings are indicators, NOT confirmed surveillance devices.
No content is intercepted or decoded. Professional verification required.
@@ -2172,7 +2172,7 @@
<div style="display: grid; grid-template-columns: 3fr 1fr; gap: 12px; flex: 1; min-height: 0; overflow: hidden;">
<!-- Map -->
<div class="radio-module-box" style="padding: 0; overflow: hidden; position: relative; min-height: 0;">
<div id="websdrMap" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0;"></div>
<div id="websdrMap" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; display: flex; align-items: center; justify-content: center; overflow: hidden;"></div>
</div>
<!-- Receiver List -->
<div style="display: flex; flex-direction: column; gap: 12px; min-width: 0; min-height: 0; overflow: hidden;">
@@ -4639,15 +4639,17 @@
return;
}
if (!validModes.has(mode)) mode = 'pager';
const _modeAssetTimeout = (p) =>
Promise.race([p, new Promise((r) => setTimeout(r, 5000))]);
const styleReadyPromise = (typeof window.ensureModeStyles === 'function')
? Promise.resolve(window.ensureModeStyles(mode)).catch((err) => {
? _modeAssetTimeout(Promise.resolve(window.ensureModeStyles(mode)).catch((err) => {
console.warn(`[ModeSwitch] style load failed for ${mode}: ${err?.message || err}`);
})
}))
: Promise.resolve();
const scriptReadyPromise = (typeof window.ensureModeScript === 'function')
? Promise.resolve(window.ensureModeScript(mode)).catch((err) => {
? _modeAssetTimeout(Promise.resolve(window.ensureModeScript(mode)).catch((err) => {
console.warn(`[ModeSwitch] script load failed for ${mode}: ${err?.message || err}`);
})
}))
: Promise.resolve();
// Only stop local scans if in local mode (not agent mode)
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
@@ -4697,6 +4699,9 @@
if (isDroneRunning) {
stopTasks.push(awaitStopAction('drone', () => fetch('/drone/stop', { method: 'POST' }), LOCAL_STOP_TIMEOUT_MS));
}
if (isRtlamrRunning) {
stopTasks.push(awaitStopAction('rtlamr', () => stopRtlamrDecoding(), LOCAL_STOP_TIMEOUT_MS));
}
if (stopTasks.length) {
await Promise.allSettled(stopTasks);
@@ -16616,7 +16621,7 @@
<!-- Cheat Sheet Modal -->
<div id="cheatSheetModal" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.7); z-index:10000; align-items:center; justify-content:center; padding:20px;" onclick="if(event.target===this)CheatSheets.hide()">
<div style="background:var(--bg-card, #1a1f2e); border:1px solid rgba(255,255,255,0.15); border-radius:12px; max-width:480px; width:100%; max-height:80vh; overflow-y:auto; padding:20px; position:relative;">
<div style="background:var(--bg-card, #1a1f2e); border:1px solid var(--border-color); border-radius:12px; max-width:480px; width:100%; max-height:80vh; overflow-y:auto; padding:20px; position:relative;">
<button onclick="CheatSheets.hide()" style="position:absolute; top:12px; right:12px; background:none; border:none; color:var(--text-dim); cursor:pointer; font-size:18px; line-height:1;"></button>
<div id="cheatSheetContent"></div>
</div>
@@ -16624,7 +16629,7 @@
<!-- Keyboard Shortcuts Modal -->
<div id="kbShortcutsModal" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.7); z-index:10000; align-items:center; justify-content:center; padding:20px;" onclick="if(event.target===this)KeyboardShortcuts.hideHelp()">
<div style="background:var(--bg-card, #1a1f2e); border:1px solid rgba(255,255,255,0.15); border-radius:12px; max-width:520px; width:100%; max-height:80vh; overflow-y:auto; padding:20px; position:relative;">
<div style="background:var(--bg-card, #1a1f2e); border:1px solid var(--border-color); border-radius:12px; max-width:520px; width:100%; max-height:80vh; overflow-y:auto; padding:20px; position:relative;">
<button onclick="KeyboardShortcuts.hideHelp()" style="position:absolute; top:12px; right:12px; background:none; border:none; color:var(--text-dim); cursor:pointer; font-size:18px; line-height:1;"></button>
<h2 style="margin:0 0 16px; font-size:16px; color:var(--accent-cyan, #4aa3ff); font-family:var(--font-mono);">Keyboard Shortcuts</h2>
<table style="width:100%; border-collapse:collapse; font-family:var(--font-mono); font-size:12px;">
+1 -1
View File
@@ -10,7 +10,7 @@
{% if offline_settings and offline_settings.fonts_source == 'local' %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
{% else %}
<link href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&family=Roboto+Condensed:wght@300;400;500;600;700&display=swap" rel="stylesheet">
{% endif %}
{# Core CSS (Design System) #}
+2
View File
@@ -12,6 +12,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>iNTERCEPT // Restricted Access</title>
<script src="{{ url_for('static', filename='js/core/login.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&family=Roboto+Condensed:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/index.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', filename='css/login.css') }}" />
</head>
+28 -9
View File
@@ -1,14 +1,16 @@
<!DOCTYPE html>
<html lang="en" class="{% if offline_settings.tile_provider in ['cartodb_dark', 'cartodb_dark_cyan'] %}map-cyber-enabled{% endif %}">
<head>
<script>(function(){var t=localStorage.getItem('intercept-ui-tier')||'enhanced';document.documentElement.setAttribute('data-ui-tier',t);})();</script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Network Monitor // INTERCEPT</title>
{% if offline_settings.fonts_source == 'local' %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
{% else %}
<link href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&family=Roboto+Condensed:wght@300;400;500;600;700&display=swap" rel="stylesheet">
{% endif %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/variables.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/agents.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/layout.css') }}">
@@ -18,8 +20,8 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--font-sans: 'Roboto Condensed', 'Arial Narrow', Roboto, 'Helvetica Neue', Arial, sans-serif;
--font-mono: 'Roboto Condensed', 'Arial Narrow', Roboto, 'Helvetica Neue', Arial, sans-serif;
--font-sans: 'Inter', 'Roboto Condensed', 'Helvetica Neue', Arial, sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', 'Source Code Pro', Consolas, monospace;
--bg-primary: #0a0c10;
--bg-secondary: #0f1218;
--bg-tertiary: #151a23;
@@ -28,12 +30,29 @@
--text-dim: #4b5563;
--border-color: #1f2937;
--accent-cyan: #4a9eff;
--accent-cyan-rgb: 74, 158, 255;
--accent-green: #22c55e;
--accent-red: #ef4444;
--accent-orange: #f59e0b;
--accent-purple: #a855f7;
}
html[data-ui-tier="enhanced"] {
--bg-primary: #000000;
--bg-secondary: #020404;
--bg-tertiary: #040808;
--border-color: rgba(46, 125, 138, 0.18);
--accent-cyan: #2e7d8a;
--accent-cyan-rgb: 46, 125, 138;
}
html[data-ui-tier="lean"] {
--bg-primary: #111111;
--bg-secondary: #181818;
--bg-tertiary: #1f1f1f;
--border-color: #2a2a2a;
}
body {
font-family: var(--font-sans);
background: var(--bg-primary);
@@ -161,7 +180,7 @@
.panel-count {
font-size: 10px;
padding: 2px 8px;
background: rgba(74, 158, 255, 0.2);
background: rgba(var(--accent-cyan-rgb), 0.2);
color: var(--accent-cyan);
border-radius: 10px;
font-family: var(--font-mono);
@@ -192,7 +211,7 @@
}
.panel-tab.active {
background: rgba(74, 158, 255, 0.1);
background: rgba(var(--accent-cyan-rgb), 0.1);
color: var(--accent-cyan);
border-color: var(--accent-cyan);
}
@@ -230,7 +249,7 @@
}
.data-table tr:hover {
background: rgba(74, 158, 255, 0.05);
background: rgba(var(--accent-cyan-rgb), 0.05);
}
.mono {
@@ -249,7 +268,7 @@
gap: 4px;
padding: 2px 6px;
font-size: 9px;
background: rgba(74, 158, 255, 0.15);
background: rgba(var(--accent-cyan-rgb), 0.15);
color: var(--accent-cyan);
border-radius: 8px;
font-family: var(--font-mono);
@@ -439,7 +458,7 @@
gap: 6px;
margin-top: 4px;
padding: 3px 8px;
background: rgba(74, 158, 255, 0.1);
background: rgba(var(--accent-cyan-rgb), 0.1);
border-radius: 4px;
font-size: 10px;
}
@@ -497,7 +516,7 @@
letter-spacing: 0.5px;
}
.type-badge.type-wifi { background: rgba(74, 158, 255, 0.2); color: var(--accent-cyan); }
.type-badge.type-wifi { background: rgba(var(--accent-cyan-rgb), 0.2); color: var(--accent-cyan); }
.type-badge.type-bluetooth { background: rgba(168, 85, 247, 0.2); color: var(--accent-purple); }
.type-badge.type-adsb { background: rgba(34, 197, 94, 0.2); color: var(--accent-green); }
.type-badge.type-ais { background: rgba(245, 158, 11, 0.2); color: var(--accent-orange); }
+10 -8
View File
@@ -563,7 +563,7 @@
}
.location-select:focus {
outline: none;
border-color: #00d4ff;
border-color: var(--accent-cyan);
}
.location-status-dot {
width: 8px;
@@ -823,14 +823,16 @@
]
};
const _satAccent = getComputedStyle(document.documentElement).getPropertyValue('--accent-cyan').trim() || '#00ffff';
let satellites = {
25544: { name: 'ISS (ZARYA)', color: '#00ffff' },
25544: { name: 'ISS (ZARYA)', color: _satAccent },
40069: { name: 'METEOR-M2', color: '#9370DB' },
57166: { name: 'METEOR-M2-3', color: '#ff00ff' },
59051: { name: 'METEOR-M2-4', color: '#00ff88' }
};
const satColors = ['#00ffff', '#9370DB', '#ff00ff', '#00ff00', '#ff6600', '#ffff00', '#ff69b4', '#7b68ee'];
const satColors = [_satAccent, '#9370DB', '#ff00ff', '#00ff00', '#ff6600', '#ffff00', '#ff69b4', '#7b68ee'];
async function fetchJsonWithTimeout(url, options = {}, timeoutMs = SAT_DRAWER_FETCH_TIMEOUT_MS) {
const controller = new AbortController();
@@ -1102,7 +1104,7 @@
drawPolarPlotWithPosition(
normalized.azimuth ?? normalized.az,
normalized.elevation ?? normalized.el,
satellites[selectedSatellite]?.color || '#00d4ff'
satellites[selectedSatellite]?.color || _satAccent
);
}
renderMapTrackOverlays({ refreshPass: false, refreshLive: true });
@@ -1547,7 +1549,7 @@
const track = Array.isArray(pass?.groundTrack) ? pass.groundTrack : [];
if (!track.length) return null;
const color = pass.color || satellites[selectedSatellite]?.color || '#00d4ff';
const color = pass.color || satellites[selectedSatellite]?.color || _satAccent;
const layer = L.layerGroup();
const segments = splitAtAntimeridian(track);
const bounds = [];
@@ -1687,7 +1689,7 @@
bounds.push(...liveTrack.map(p => [p.lat, p.lon]));
}
const satColor = satellites[selectedSatellite]?.color || '#00d4ff';
const satColor = satellites[selectedSatellite]?.color || _satAccent;
const currentPos = latestLivePosition?.lat != null && latestLivePosition?.lon != null
? { lat: latestLivePosition.lat, lon: latestLivePosition.lon }
: (pass?.currentPos?.lat != null && pass?.currentPos?.lon != null
@@ -2328,7 +2330,7 @@
// Pass trajectory
if (pass && pass.trajectory) {
ctx.strokeStyle = pass.color || '#00d4ff';
ctx.strokeStyle = pass.color || _satAccent;
ctx.lineWidth = 3;
ctx.setLineDash([8, 4]);
ctx.beginPath();
@@ -2356,7 +2358,7 @@
if (maxElPoint) {
ctx.beginPath();
ctx.arc(maxElPoint.x, maxElPoint.y, 8, 0, Math.PI * 2);
ctx.fillStyle = pass.color || '#00d4ff';
ctx.fillStyle = pass.color || _satAccent;
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;