mirror of
https://github.com/smittix/intercept.git
synced 2026-06-13 16:23:34 -07:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 367048e853 | |||
| 406ca28304 | |||
| f889c53d92 | |||
| b0af1d16d2 | |||
| 4e67b77714 | |||
| b1993847b5 | |||
| cde79f4619 | |||
| cc271819ad | |||
| 8cd64ce3ca |
+20
-1
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
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
@@ -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 |
@@ -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...');
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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
@@ -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 }))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
Reference in New Issue
Block a user