Compare commits

...

9 Commits

Author SHA1 Message Date
Smittix 367048e853 chore: bump version to 2.22.3 and update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 21:03:27 +00:00
Smittix 406ca28304 fix: suppress stale WebSocket close message after stopping waterfall
stop() sets _ws = null before the async onclose fires, so the handler
now early-returns when _ws is null instead of showing the misleading
"WebSocket closed before ready" retry message.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 21:01:59 +00:00
Smittix f889c53d92 fix: waterfall monitor audio delay and unresponsive stop button
- _waitForPlayback now only succeeds on playing/timeupdate events, not
  loadeddata/canplay which fire from just the WAV header before real
  audio arrives
- stopMonitor() pauses audio and updates UI immediately instead of
  blocking on the backend stop request (1+ second delay)
- Reduced backend audio stop sleep from 1.0s to 0.15s; the start
  retry loop already handles USB contention

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 20:59:40 +00:00
Smittix b0af1d16d2 chore: bump pyproject.toml version to 2.22.2
Was missed during previous 2.22.x release bumps.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 20:27:35 +00:00
Smittix 4e67b77714 fix: first-load rendering for Waterfall CSS and WebSDR globe
- Waterfall: load waterfall.css eagerly in <head> instead of lazily on
  mode switch; the lazy inject raced with the panel becoming visible,
  leaving unstyled HTML for up to 20 s on cold cache
- WebSDR: await a requestAnimationFrame before calling Globe()(mapEl) so
  the browser has committed the display:flex layout and clientWidth/
  clientHeight are non-zero; previously the globe WebGL renderer was
  created at 0×0 (especially on warm-cache refreshes) and could not
  recover via the deferred resize calls
- Bump version to 2.22.2

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 20:25:05 +00:00
Smittix b1993847b5 docs: remove RF Heatmap references — feature was not shipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 19:40:15 +00:00
Smittix cde79f4619 fix: use official favicon.svg logo for all PWA and app icons
Regenerates icon-192.png, icon-512.png, apple-touch-icon.png, and
favicon-32.png from the official iNTERCEPT logo (favicon.svg) instead
of the placeholder icon.svg. Also replaces icon.svg with the official
logo so the SVG manifest entry is consistent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 19:37:44 +00:00
Smittix cc271819ad chore: bump version to 2.22.1 and update changelog
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 19:36:55 +00:00
Smittix 8cd64ce3ca fix: PWA install prompt - add PNG icons and fix apple-touch-icon
Browsers require PNG icons (192x192, 512x512) in the manifest to show
the install prompt. SVG-only manifests are not sufficient. Also adds the
180x180 apple-touch-icon PNG for iOS home screen, bumps SW cache to v3,
and adds scope to the manifest.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 19:36:28 +00:00
14 changed files with 643 additions and 576 deletions
+20 -1
View File
@@ -2,13 +2,32 @@
All notable changes to iNTERCEPT will be documented in this file. All notable changes to iNTERCEPT will be documented in this file.
## [2.22.3] - 2026-02-23
### Fixed
- Waterfall control panel rendered as unstyled text for up to 20 seconds on first visit — CSS is now loaded eagerly with the rest of the page assets
- WebSDR globe failed to render on first page load — initialization now waits for a layout frame before mounting the WebGL renderer, ensuring the container has non-zero dimensions
- Waterfall monitor audio took minutes to start — `_waitForPlayback` now only reports success on actual audio playback (`playing`/`timeupdate`), not from the WAV header alone (`loadeddata`/`canplay`)
- Waterfall monitor could not be stopped — `stopMonitor()` now pauses audio and updates the UI immediately instead of waiting for the backend stop request (which blocked for 1+ seconds during SDR process cleanup)
- Stopping the waterfall no longer shows a stale "WebSocket closed before ready" message — the `onclose` handler now detects intentional closes
---
## [2.22.1] - 2026-02-23
### Fixed
- PWA install prompt not appearing — manifest now includes required PNG icons (192×192, 512×512)
- Apple touch icon updated to PNG for iOS Safari compatibility
- Service worker cache bumped to bust stale cached assets
---
## [2.22.0] - 2026-02-23 ## [2.22.0] - 2026-02-23
### Added ### Added
- **Waterfall Receiver Overhaul** - WebSocket-based I/Q streaming with server-side FFT, click-to-tune, zoom controls, and auto-scaling - **Waterfall Receiver Overhaul** - WebSocket-based I/Q streaming with server-side FFT, click-to-tune, zoom controls, and auto-scaling
- **Voice Alerts** - Configurable text-to-speech event notifications across modes - **Voice Alerts** - Configurable text-to-speech event notifications across modes
- **Signal Fingerprinting** - RF device identification and pattern analysis mode - **Signal Fingerprinting** - RF device identification and pattern analysis mode
- **RF Heatmap** - Geographic signal density visualization with Leaflet heatmap overlay
- **SignalID** - Automatic signal classification via SigIDWiki API integration - **SignalID** - Automatic signal classification via SigIDWiki API integration
- **PWA Support** - Installable web app with service worker caching and manifest - **PWA Support** - Installable web app with service worker caching and manifest
- **Real-time Signal Scope** - Live signal visualization for pager, sensor, and SSTV modes - **Real-time Signal Scope** - Live signal visualization for pager, sensor, and SSTV modes
+13 -3
View File
@@ -7,18 +7,28 @@ import os
import sys import sys
# Application version # Application version
VERSION = "2.22.0" VERSION = "2.22.3"
# Changelog - latest release notes (shown on welcome screen) # Changelog - latest release notes (shown on welcome screen)
CHANGELOG = [ CHANGELOG = [
{ {
"version": "2.22.0", "version": "2.22.3",
"date": "February 2026",
"highlights": [
"Waterfall control panel no longer shows as unstyled text on first visit",
"WebSDR globe renders correctly on first page load without requiring a refresh",
"Waterfall monitor audio no longer takes minutes to start — playback detection now waits for real audio data instead of just the WAV header",
"Waterfall monitor stop is now instant — audio pauses and UI updates immediately instead of waiting for backend cleanup",
"Stopping the waterfall no longer shows a stale 'WebSocket closed before ready' message",
]
},
{
"version": "2.22.1",
"date": "February 2026", "date": "February 2026",
"highlights": [ "highlights": [
"Waterfall receiver overhaul: WebSocket I/Q streaming with server-side FFT, click-to-tune, and zoom controls", "Waterfall receiver overhaul: WebSocket I/Q streaming with server-side FFT, click-to-tune, and zoom controls",
"Voice alerts for configurable event notifications across modes", "Voice alerts for configurable event notifications across modes",
"Signal fingerprinting mode for RF device identification and pattern analysis", "Signal fingerprinting mode for RF device identification and pattern analysis",
"RF Heatmap for geographic signal density visualization",
"SignalID integration via SigIDWiki API for automatic signal classification", "SignalID integration via SigIDWiki API for automatic signal classification",
"PWA support: installable web app with service worker and manifest", "PWA support: installable web app with service worker and manifest",
"Mode stop responsiveness improvements with faster timeout handling", "Mode stop responsiveness improvements with faster timeout handling",
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "intercept" name = "intercept"
version = "2.21.1" version = "2.22.3"
description = "Signal Intelligence Platform - Pager/433MHz/ADS-B/Satellite/WiFi/Bluetooth" description = "Signal Intelligence Platform - Pager/433MHz/ADS-B/Satellite/WiFi/Bluetooth"
readme = "README.md" readme = "README.md"
requires-python = ">=3.9" requires-python = ">=3.9"
+486 -484
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 919 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

+19 -20
View File
@@ -1,21 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<rect width="512" height="512" fill="#0b1118" rx="80"/> <!-- Background -->
<!-- Signal wave arcs radiating from center-left --> <rect width="100" height="100" fill="#0a0a0f"/>
<g fill="none" stroke="#4aa3ff" stroke-linecap="round">
<!-- Inner arc --> <!-- Signal brackets - left side -->
<path stroke-width="22" d="M 160 256 Q 192 210 192 256 Q 192 302 160 256" opacity="0.5"/> <path d="M15 30 Q5 50, 15 70" stroke="#00d4ff" stroke-width="4" fill="none" stroke-linecap="round" opacity="0.5"/>
<!-- Small arc --> <path d="M22 35 Q14 50, 22 65" stroke="#00d4ff" stroke-width="3.5" fill="none" stroke-linecap="round" opacity="0.7"/>
<path stroke-width="22" d="M 130 256 Q 180 185 180 256 Q 180 327 130 256" opacity="0.65"/> <path d="M29 40 Q23 50, 29 60" stroke="#00d4ff" stroke-width="3" fill="none" stroke-linecap="round"/>
<!-- Medium arc -->
<path stroke-width="24" d="M 100 256 Q 175 155 175 256 Q 175 357 100 256" opacity="0.8"/> <!-- Signal brackets - right side -->
<!-- Large arc --> <path d="M85 30 Q95 50, 85 70" stroke="#00d4ff" stroke-width="4" fill="none" stroke-linecap="round" opacity="0.5"/>
<path stroke-width="26" d="M 68 256 Q 170 120 170 256 Q 170 392 68 256" opacity="0.95"/> <path d="M78 35 Q86 50, 78 65" stroke="#00d4ff" stroke-width="3.5" fill="none" stroke-linecap="round" opacity="0.7"/>
</g> <path d="M71 40 Q77 50, 71 60" stroke="#00d4ff" stroke-width="3" fill="none" stroke-linecap="round"/>
<!-- Horizontal beam line -->
<line x1="190" y1="256" x2="420" y2="256" stroke="#4aa3ff" stroke-width="20" stroke-linecap="round"/> <!-- The 'i' letter -->
<!-- Signal dot at origin --> <circle cx="50" cy="22" r="7" fill="#00ff88"/>
<circle cx="190" cy="256" r="18" fill="#4aa3ff"/> <rect x="43" y="35" width="14" height="45" rx="2" fill="#00d4ff"/>
<!-- Target reticle at end --> <rect x="36" y="35" width="28" height="5" rx="1" fill="#00d4ff"/>
<circle cx="420" cy="256" r="28" fill="none" stroke="#4aa3ff" stroke-width="14"/> <rect x="36" y="75" width="28" height="5" rx="1" fill="#00d4ff"/>
<circle cx="420" cy="256" r="8" fill="#4aa3ff"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

+25 -10
View File
@@ -900,19 +900,27 @@ const Waterfall = (function () {
resolve(ok); resolve(ok);
}; };
const onReady = () => finish(true); // Only treat actual playback as success. `loadeddata` and
// `canplay` fire when just the WAV header arrives — before any
// real audio samples have been decoded — which caused the
// monitor to report "started" while the stream was still silent.
const onReady = () => {
if (player.currentTime > 0 || (!player.paused && player.readyState >= 4)) {
finish(true);
}
};
const onFail = () => finish(false); const onFail = () => finish(false);
const events = ['playing', 'timeupdate', 'canplay', 'loadeddata']; const events = ['playing', 'timeupdate'];
const failEvents = ['error', 'abort', 'stalled', 'ended']; const failEvents = ['error', 'abort', 'stalled', 'ended'];
events.forEach((evt) => player.addEventListener(evt, onReady)); events.forEach((evt) => player.addEventListener(evt, onReady));
failEvents.forEach((evt) => player.addEventListener(evt, onFail)); failEvents.forEach((evt) => player.addEventListener(evt, onFail));
timer = setTimeout(() => { timer = setTimeout(() => {
finish(!player.paused && (player.currentTime > 0 || player.readyState >= 2)); finish(!player.paused && player.currentTime > 0);
}, timeoutMs); }, timeoutMs);
if (!player.paused && (player.currentTime > 0 || player.readyState >= 2)) { if (!player.paused && player.currentTime > 0) {
finish(true); finish(true);
} }
}); });
@@ -2571,6 +2579,7 @@ const Waterfall = (function () {
} }
if (attempt < maxAttempts) { if (attempt < maxAttempts) {
_setMonitorState(`Waiting for audio stream (attempt ${attempt}/${maxAttempts})...`);
await _wait(220 * attempt); await _wait(220 * attempt);
continue; continue;
} }
@@ -2813,12 +2822,9 @@ const Waterfall = (function () {
clearTimeout(_monitorRetuneTimer); clearTimeout(_monitorRetuneTimer);
_audioConnectNonce += 1; _audioConnectNonce += 1;
try { // Immediately pause audio and update the UI so the user gets instant
await fetch('/receiver/audio/stop', { method: 'POST' }); // feedback. The backend cleanup (which can block for 1-2 s while the
} catch (_) { // SDR process group is reaped) happens afterwards.
// Ignore backend stop errors
}
_stopSmeter(); _stopSmeter();
_setUnlockVisible(false); _setUnlockVisible(false);
_audioUnlockRequired = false; _audioUnlockRequired = false;
@@ -2836,6 +2842,13 @@ const Waterfall = (function () {
_setVisualStatus('READY'); _setVisualStatus('READY');
} }
// Backend stop is fire-and-forget; UI is already updated above.
try {
await fetch('/receiver/audio/stop', { method: 'POST' });
} catch (_) {
// Ignore backend stop errors
}
if (resumeWaterfall && _active) { if (resumeWaterfall && _active) {
_resumeWaterfallAfterMonitor = false; _resumeWaterfallAfterMonitor = false;
await start(); await start();
@@ -2983,6 +2996,8 @@ const Waterfall = (function () {
}; };
_ws.onclose = () => { _ws.onclose = () => {
// stop() sets _ws = null before the async onclose fires.
if (!_ws) return;
if (!_wsOpened && _active) { if (!_wsOpened && _active) {
// Wait for timeout-based fallback; avoid flapping to SSE on brief close/retry. // Wait for timeout-based fallback; avoid flapping to SSE on brief close/retry.
_setStatus('WebSocket closed before ready. Waiting to retry/fallback...'); _setStatus('WebSocket closed before ready. Waiting to retry/fallback...');
+11
View File
@@ -51,6 +51,17 @@ async function initWebSDR() {
if (!mapEl) return; if (!mapEl) return;
const globeReady = await ensureWebsdrGlobeLibrary(); const globeReady = await ensureWebsdrGlobeLibrary();
// Wait for a paint frame so the browser computes layout after the
// display:flex change in switchMode. Without this, Globe()(mapEl) can
// run before clientWidth/clientHeight are non-zero (especially when
// scripts are served from cache and resolve before the first layout pass).
await new Promise(resolve => requestAnimationFrame(resolve));
// If the mode was switched away while scripts were loading, abort so
// websdrInitialized stays false and we retry cleanly next time.
if (!mapEl.clientWidth || !mapEl.clientHeight) return;
if (globeReady && initWebsdrGlobe(mapEl)) { if (globeReady && initWebsdrGlobe(mapEl)) {
websdrMapType = 'globe'; websdrMapType = 'globe';
} else if (typeof L !== 'undefined' && await initWebsdrLeaflet(mapEl)) { } else if (typeof L !== 'undefined' && await initWebsdrLeaflet(mapEl)) {
+11
View File
@@ -3,10 +3,21 @@
"short_name": "INTERCEPT", "short_name": "INTERCEPT",
"description": "Unified SIGINT platform for software-defined radio analysis", "description": "Unified SIGINT platform for software-defined radio analysis",
"start_url": "/", "start_url": "/",
"scope": "/",
"display": "standalone", "display": "standalone",
"background_color": "#0b1118", "background_color": "#0b1118",
"theme_color": "#0b1118", "theme_color": "#0b1118",
"icons": [ "icons": [
{
"src": "/static/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/static/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png"
},
{ {
"src": "/static/icons/icon.svg", "src": "/static/icons/icon.svg",
"sizes": "any", "sizes": "any",
+53 -53
View File
@@ -1,22 +1,22 @@
/* INTERCEPT Service Worker — cache-first static, network-only for API/SSE/WS */ /* INTERCEPT Service Worker — cache-first static, network-only for API/SSE/WS */
const CACHE_NAME = 'intercept-v2'; const CACHE_NAME = 'intercept-v3';
const NETWORK_ONLY_PREFIXES = [ const NETWORK_ONLY_PREFIXES = [
'/stream', '/ws/', '/api/', '/gps/', '/wifi/', '/bluetooth/', '/stream', '/ws/', '/api/', '/gps/', '/wifi/', '/bluetooth/',
'/adsb/', '/ais/', '/acars/', '/aprs/', '/tscm/', '/satellite/', '/adsb/', '/ais/', '/acars/', '/aprs/', '/tscm/', '/satellite/',
'/meshtastic/', '/bt_locate/', '/receiver/', '/sensor/', '/pager/', '/meshtastic/', '/bt_locate/', '/receiver/', '/sensor/', '/pager/',
'/sstv/', '/weather-sat/', '/subghz/', '/rtlamr/', '/dsc/', '/vdl2/', '/sstv/', '/weather-sat/', '/subghz/', '/rtlamr/', '/dsc/', '/vdl2/',
'/spy/', '/space-weather/', '/websdr/', '/analytics/', '/correlation/', '/spy/', '/space-weather/', '/websdr/', '/analytics/', '/correlation/',
'/recordings/', '/controller/', '/ops/', '/recordings/', '/controller/', '/ops/',
]; ];
const STATIC_PREFIXES = [ const STATIC_PREFIXES = [
'/static/css/', '/static/css/',
'/static/js/', '/static/js/',
'/static/icons/', '/static/icons/',
'/static/fonts/', '/static/fonts/',
]; ];
const CACHE_EXACT = ['/manifest.json']; const CACHE_EXACT = ['/manifest.json'];
function isHttpRequest(req) { function isHttpRequest(req) {
@@ -29,9 +29,9 @@ function isNetworkOnly(req) {
const accept = req.headers.get('Accept') || ''; const accept = req.headers.get('Accept') || '';
if (accept.includes('text/event-stream')) return true; if (accept.includes('text/event-stream')) return true;
const url = new URL(req.url); const url = new URL(req.url);
return NETWORK_ONLY_PREFIXES.some(p => url.pathname.startsWith(p)); return NETWORK_ONLY_PREFIXES.some(p => url.pathname.startsWith(p));
} }
function isStaticAsset(req) { function isStaticAsset(req) {
const url = new URL(req.url); const url = new URL(req.url);
if (CACHE_EXACT.includes(url.pathname)) return true; if (CACHE_EXACT.includes(url.pathname)) return true;
@@ -62,19 +62,19 @@ function fallbackResponse(req, status = 503) {
headers: { 'Content-Type': 'text/plain; charset=utf-8' }, headers: { 'Content-Type': 'text/plain; charset=utf-8' },
}); });
} }
self.addEventListener('install', (e) => { self.addEventListener('install', (e) => {
self.skipWaiting(); self.skipWaiting();
}); });
self.addEventListener('activate', (e) => { self.addEventListener('activate', (e) => {
e.waitUntil( e.waitUntil(
caches.keys().then(keys => caches.keys().then(keys =>
Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k))) Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k)))
).then(() => self.clients.claim()) ).then(() => self.clients.claim())
); );
}); });
self.addEventListener('fetch', (e) => { self.addEventListener('fetch', (e) => {
const req = e.request; const req = e.request;
@@ -90,18 +90,18 @@ self.addEventListener('fetch', (e) => {
); );
return; return;
} }
// Cache-first for static assets // Cache-first for static assets
if (isStaticAsset(req)) { if (isStaticAsset(req)) {
e.respondWith( e.respondWith(
caches.open(CACHE_NAME).then(cache => caches.open(CACHE_NAME).then(cache =>
cache.match(req).then(cached => { cache.match(req).then(cached => {
if (cached) { if (cached) {
// Revalidate in background // Revalidate in background
fetch(req).then(res => { fetch(req).then(res => {
if (res && res.status === 200) cache.put(req, res.clone()); if (res && res.status === 200) cache.put(req, res.clone());
}).catch(() => {}); }).catch(() => {});
return cached; return cached;
} }
return fetch(req).then(res => { return fetch(req).then(res => {
if (res && res.status === 200) cache.put(req, res.clone()); if (res && res.status === 200) cache.put(req, res.clone());
@@ -111,12 +111,12 @@ self.addEventListener('fetch', (e) => {
) )
); );
return; return;
} }
// Network-first for HTML pages // Network-first for HTML pages
e.respondWith( e.respondWith(
fetch(req).catch(() => fetch(req).catch(() =>
caches.match(req).then(cached => cached || new Response('Offline', { status: 503 })) caches.match(req).then(cached => cached || new Response('Offline', { status: 503 }))
) )
); );
}); });
+4 -4
View File
@@ -10,7 +10,7 @@
<meta name="theme-color" content="#0b1118"> <meta name="theme-color" content="#0b1118">
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<link rel="apple-touch-icon" href="/static/icons/icon.svg"> <link rel="apple-touch-icon" href="/static/icons/apple-touch-icon.png">
<!-- Disclaimer gate - must accept before seeing welcome page --> <!-- Disclaimer gate - must accept before seeing welcome page -->
<script> <script>
// Check BEFORE page renders - if disclaimer not accepted, hide welcome page // Check BEFORE page renders - if disclaimer not accepted, hide welcome page
@@ -66,6 +66,7 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/components/function-strip.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/components/function-strip.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/components/toast.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/components/toast.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/components/ux-platform.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/components/ux-platform.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/waterfall.css') }}?v={{ version }}&r=wfdeck19">
<script> <script>
window.INTERCEPT_MODE_STYLE_MAP = { window.INTERCEPT_MODE_STYLE_MAP = {
aprs: "{{ url_for('static', filename='css/modes/aprs.css') }}", aprs: "{{ url_for('static', filename='css/modes/aprs.css') }}",
@@ -78,8 +79,7 @@
gps: "{{ url_for('static', filename='css/modes/gps.css') }}", gps: "{{ url_for('static', filename='css/modes/gps.css') }}",
subghz: "{{ url_for('static', filename='css/modes/subghz.css') }}?v={{ version }}&r=subghz_layout9", 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", 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') }}", spaceweather: "{{ url_for('static', filename='css/modes/space-weather.css') }}"
waterfall: "{{ url_for('static', filename='css/modes/waterfall.css') }}?v={{ version }}&r=wfdeck19"
}; };
window.INTERCEPT_MODE_STYLE_LOADED = {}; window.INTERCEPT_MODE_STYLE_LOADED = {};
window.ensureModeStyles = function(mode) { window.ensureModeStyles = function(mode) {
@@ -2936,7 +2936,7 @@
<script src="{{ url_for('static', filename='js/core/voice-alerts.js') }}?v={{ version }}&r=voicefix2"></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/keyboard-shortcuts.js') }}"></script>
<script src="{{ url_for('static', filename='js/core/cheat-sheets.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=wfdeck20"></script> <script src="{{ url_for('static', filename='js/modes/waterfall.js') }}?v={{ version }}&r=wfdeck21"></script>
<script> <script>
// ============================================ // ============================================