Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 367048e853 | |||
| 406ca28304 | |||
| f889c53d92 | |||
| b0af1d16d2 | |||
| 4e67b77714 | |||
| b1993847b5 | |||
| cde79f4619 | |||
| cc271819ad |
@@ -2,13 +2,32 @@
|
||||
|
||||
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
|
||||
|
||||
### Added
|
||||
- **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
|
||||
- **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
|
||||
- **PWA Support** - Installable web app with service worker caching and manifest
|
||||
- **Real-time Signal Scope** - Live signal visualization for pager, sensor, and SSTV modes
|
||||
|
||||
@@ -7,18 +7,28 @@ import os
|
||||
import sys
|
||||
|
||||
# Application version
|
||||
VERSION = "2.22.0"
|
||||
VERSION = "2.22.3"
|
||||
|
||||
# Changelog - latest release notes (shown on welcome screen)
|
||||
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",
|
||||
"highlights": [
|
||||
"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",
|
||||
"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",
|
||||
"PWA support: installable web app with service worker and manifest",
|
||||
"Mode stop responsiveness improvements with faster timeout handling",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "intercept"
|
||||
version = "2.21.1"
|
||||
version = "2.22.3"
|
||||
description = "Signal Intelligence Platform - Pager/433MHz/ADS-B/Satellite/WiFi/Bluetooth"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9"
|
||||
|
||||
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 750 B After Width: | Height: | Size: 919 B |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 16 KiB |
@@ -1,21 +1,20 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<rect width="512" height="512" fill="#0b1118" rx="80"/>
|
||||
<!-- Signal wave arcs radiating from center-left -->
|
||||
<g fill="none" stroke="#4aa3ff" stroke-linecap="round">
|
||||
<!-- Inner arc -->
|
||||
<path stroke-width="22" d="M 160 256 Q 192 210 192 256 Q 192 302 160 256" opacity="0.5"/>
|
||||
<!-- Small arc -->
|
||||
<path stroke-width="22" d="M 130 256 Q 180 185 180 256 Q 180 327 130 256" opacity="0.65"/>
|
||||
<!-- Medium arc -->
|
||||
<path stroke-width="24" d="M 100 256 Q 175 155 175 256 Q 175 357 100 256" opacity="0.8"/>
|
||||
<!-- Large arc -->
|
||||
<path stroke-width="26" d="M 68 256 Q 170 120 170 256 Q 170 392 68 256" opacity="0.95"/>
|
||||
</g>
|
||||
<!-- Horizontal beam line -->
|
||||
<line x1="190" y1="256" x2="420" y2="256" stroke="#4aa3ff" stroke-width="20" stroke-linecap="round"/>
|
||||
<!-- Signal dot at origin -->
|
||||
<circle cx="190" cy="256" r="18" fill="#4aa3ff"/>
|
||||
<!-- Target reticle at end -->
|
||||
<circle cx="420" cy="256" r="28" fill="none" stroke="#4aa3ff" stroke-width="14"/>
|
||||
<circle cx="420" cy="256" r="8" fill="#4aa3ff"/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<!-- Background -->
|
||||
<rect width="100" height="100" fill="#0a0a0f"/>
|
||||
|
||||
<!-- Signal brackets - left side -->
|
||||
<path d="M15 30 Q5 50, 15 70" stroke="#00d4ff" stroke-width="4" fill="none" stroke-linecap="round" opacity="0.5"/>
|
||||
<path d="M22 35 Q14 50, 22 65" stroke="#00d4ff" stroke-width="3.5" fill="none" stroke-linecap="round" opacity="0.7"/>
|
||||
<path d="M29 40 Q23 50, 29 60" stroke="#00d4ff" stroke-width="3" fill="none" stroke-linecap="round"/>
|
||||
|
||||
<!-- Signal brackets - right side -->
|
||||
<path d="M85 30 Q95 50, 85 70" stroke="#00d4ff" stroke-width="4" fill="none" stroke-linecap="round" opacity="0.5"/>
|
||||
<path d="M78 35 Q86 50, 78 65" stroke="#00d4ff" stroke-width="3.5" fill="none" stroke-linecap="round" opacity="0.7"/>
|
||||
<path d="M71 40 Q77 50, 71 60" stroke="#00d4ff" stroke-width="3" fill="none" stroke-linecap="round"/>
|
||||
|
||||
<!-- The 'i' letter -->
|
||||
<circle cx="50" cy="22" r="7" fill="#00ff88"/>
|
||||
<rect x="43" y="35" width="14" height="45" rx="2" fill="#00d4ff"/>
|
||||
<rect x="36" y="35" width="28" height="5" rx="1" fill="#00d4ff"/>
|
||||
<rect x="36" y="75" width="28" height="5" rx="1" fill="#00d4ff"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -900,19 +900,27 @@ const Waterfall = (function () {
|
||||
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 events = ['playing', 'timeupdate', 'canplay', 'loadeddata'];
|
||||
const events = ['playing', 'timeupdate'];
|
||||
const failEvents = ['error', 'abort', 'stalled', 'ended'];
|
||||
|
||||
events.forEach((evt) => player.addEventListener(evt, onReady));
|
||||
failEvents.forEach((evt) => player.addEventListener(evt, onFail));
|
||||
|
||||
timer = setTimeout(() => {
|
||||
finish(!player.paused && (player.currentTime > 0 || player.readyState >= 2));
|
||||
finish(!player.paused && player.currentTime > 0);
|
||||
}, timeoutMs);
|
||||
|
||||
if (!player.paused && (player.currentTime > 0 || player.readyState >= 2)) {
|
||||
if (!player.paused && player.currentTime > 0) {
|
||||
finish(true);
|
||||
}
|
||||
});
|
||||
@@ -2571,6 +2579,7 @@ const Waterfall = (function () {
|
||||
}
|
||||
|
||||
if (attempt < maxAttempts) {
|
||||
_setMonitorState(`Waiting for audio stream (attempt ${attempt}/${maxAttempts})...`);
|
||||
await _wait(220 * attempt);
|
||||
continue;
|
||||
}
|
||||
@@ -2813,12 +2822,9 @@ const Waterfall = (function () {
|
||||
clearTimeout(_monitorRetuneTimer);
|
||||
_audioConnectNonce += 1;
|
||||
|
||||
try {
|
||||
await fetch('/receiver/audio/stop', { method: 'POST' });
|
||||
} catch (_) {
|
||||
// Ignore backend stop errors
|
||||
}
|
||||
|
||||
// Immediately pause audio and update the UI so the user gets instant
|
||||
// feedback. The backend cleanup (which can block for 1-2 s while the
|
||||
// SDR process group is reaped) happens afterwards.
|
||||
_stopSmeter();
|
||||
_setUnlockVisible(false);
|
||||
_audioUnlockRequired = false;
|
||||
@@ -2836,6 +2842,13 @@ const Waterfall = (function () {
|
||||
_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) {
|
||||
_resumeWaterfallAfterMonitor = false;
|
||||
await start();
|
||||
@@ -2983,6 +2996,8 @@ const Waterfall = (function () {
|
||||
};
|
||||
|
||||
_ws.onclose = () => {
|
||||
// stop() sets _ws = null before the async onclose fires.
|
||||
if (!_ws) return;
|
||||
if (!_wsOpened && _active) {
|
||||
// Wait for timeout-based fallback; avoid flapping to SSE on brief close/retry.
|
||||
_setStatus('WebSocket closed before ready. Waiting to retry/fallback...');
|
||||
|
||||
@@ -51,6 +51,17 @@ async function initWebSDR() {
|
||||
if (!mapEl) return;
|
||||
|
||||
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)) {
|
||||
websdrMapType = 'globe';
|
||||
} else if (typeof L !== 'undefined' && await initWebsdrLeaflet(mapEl)) {
|
||||
|
||||
@@ -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/toast.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>
|
||||
window.INTERCEPT_MODE_STYLE_MAP = {
|
||||
aprs: "{{ url_for('static', filename='css/modes/aprs.css') }}",
|
||||
@@ -78,8 +79,7 @@
|
||||
gps: "{{ url_for('static', filename='css/modes/gps.css') }}",
|
||||
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",
|
||||
spaceweather: "{{ url_for('static', filename='css/modes/space-weather.css') }}",
|
||||
waterfall: "{{ url_for('static', filename='css/modes/waterfall.css') }}?v={{ version }}&r=wfdeck19"
|
||||
spaceweather: "{{ url_for('static', filename='css/modes/space-weather.css') }}"
|
||||
};
|
||||
window.INTERCEPT_MODE_STYLE_LOADED = {};
|
||||
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/keyboard-shortcuts.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>
|
||||
// ============================================
|
||||
|
||||