- Access module-level _sweep_running, _current_sweep_id, and tscm_queue
via explicit package import to avoid UnboundLocalError from closure
variable shadowing in route handlers
- Remove orphaned tscmProgressBar.style.width assignment in index.html
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix deep scan with 'All bands' never scanning 5GHz: band='all' now
correctly passes --band abg to airodump-ng (previously no flag was
added, causing airodump-ng to default to 2.4GHz-only)
- Fix APs first seen without channel info permanently stuck at
band='unknown': _update_access_point now backfills channel, frequency,
and band when a subsequent observation resolves the channel
- Fix legacy /wifi/scan/start combining mutually exclusive --band and -c
flags: --band is now only added when no explicit channel list is given,
and the interface is always placed as the last argument
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When using a remote SBS feed, no local SDR is needed. The pre-flight
device conflict check was running regardless and stopping whichever
mode had the selected SDR device claimed — even though ADS-B remote
mode never touches a local SDR. Skip the conflict check when remoteConfig is set.
check_tools() was using cmd_exists on 'auto_rx.py' which fails because
it's never in PATH — installed to /opt/radiosonde_auto_rx/. Now uses
the same file-based check as tool_is_installed(), consistent with
health check and status view.
Precise calculation showed mission-drawer (~1100px) was 213px taller
than command-rail content (~887px), leaving 213px of empty background
at the bottom of the right column.
Three targeted reductions to mission-drawer height (~234px total):
- drawer-actions: stacked 1-column → 3-column row (-76px)
- drawer-list: max-height 240px → 180px (-40px)
- drawer-info-grid: 1-column → 2-column for Quick Info (-118px)
Mission-drawer drops to ~866px, command-rail (~887px) now drives the
primary-layout row height — gap closes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The command-rail's 1fr last row caused the sky view panel to fill all
remaining column height (driven by the tall mission-drawer), showing a
large empty bordered space below the pass data strip.
Switch to all-auto rows with align-content: start so each panel is
exactly as tall as its content — the open background below the column
looks intentional rather than a panel with dead space inside it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove 'METEOR-M2' (NORAD 40069) from WEATHER_SAT_KEYS — it has no
entry in WEATHER_SATELLITES and no dropdown option, so the Capture
button was silently scheduling against the wrong satellite
- Dispatch a 'change' event after setting satSelect.value in preSelect()
and startPass() so any UI listeners (frequency display, mode info)
update correctly when the satellite is set programmatically
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1fr in the grid row caused the panel to fill the entire remaining page
height on mobile (~1000px+), leaving large gaps around the centred content.
max-height: min(55vh, 520px) keeps it proportionate on any screen size.
Also switch to justify-content: flex-start so the canvas+strip pack at
the top rather than floating in the middle of a large void.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fills the empty space below the sky view circle with a compact
three-column AOS / TCA / LOS readout (time + azimuth/elevation)
and a duration + max elevation footer line.
Populated by drawPolarPlot() when a pass is selected; shows a
placeholder prompt otherwise.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Setting both width:100% and height:100% made CSS ignore aspect-ratio,
stretching the drawing buffer non-uniformly into the tall container.
Fixed by keeping only width:100% + max-height:100% so aspect-ratio:1/1
clamps the height and the element stays square.
Draw functions now use canvas.offsetWidth for the square buffer size.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- command-rail last row changed from minmax(260px, 340px) to 1fr so the
polar plot expands to fill whatever vertical space remains after the
Next Pass and Live Telemetry panels
- polar-container made flex-column so panel-content can grow with flex: 1
- #polarPlot width/height 100% with aspect-ratio 1/1 — canvas fills the
available square area and stays proportional
- Remove align-items: start from the 1320px breakpoint primary-layout so
the command-rail stretches to match map height in the two-column layout
- Fix matching 2-column command-rail rows at 1320px breakpoint
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove align-self: start from .polar-container so the grid row's
minmax(260px, 340px) height is actually respected
- Switch #polarPlot to aspect-ratio: 1/1 so the canvas is always square
- Fix both draw functions to size canvas from getBoundingClientRect on
the canvas itself (not parent) using min(width, height) for a square plot
- Remove min-height from .dashboard to prevent empty space below content
on narrow/mobile screens where stacked panels are shorter than 720px
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move _passAbortController = null to after response.json() so the retry
scheduler cannot see a false idle state mid-parse, increment
_passRequestId, and discard the in-flight response — this was causing
non-ISS satellites to show no passes intermittently
- Add _computeSlantRange() helper using 3D ECEF geometry
- Update applyTelemetryPosition to compute slant range from SSE lat/lon/
altitude, giving distance updates at 1Hz instead of 5s HTTP poll rate
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two root-cause bugs causing the reported issues:
1. Tracker never sent ISS positions: _start_satellite_tracker fell back
to sat_name.replace(' ', '-').upper() as the TLE cache key when the
DB entry had null TLE lines. For 'ISS (ZARYA)' this produced
'ISS-(ZARYA)' which has no matching entry in _tle_cache (keyed as
'ISS'). ISS was silently skipped every loop tick, so no SSE positions
were ever emitted and the map marker never moved.
Fix: try _BUILTIN_NORAD_TO_KEY.get(norad_int) first before the
name-derived fallback so the NORAD-to-key mapping is always used.
2. Stale TLE pass prediction results were cached: if startup TLEs were
too old for Skyfield to find events in the 48h window, the empty
passes list was cached for 300s. A page refresh within that window
re-served the empty result, showing 'NO PASSES FOUND' persistently.
Fix: only cache non-empty pass results so the next request
recomputes once the TLE auto-refresh has populated fresh data.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
updateCountdown fell back to passes[0] even when it was in the past,
showing 00:00:00:00 with a stale satellite name indefinitely. Now
displays a clear 'NO UPCOMING PASSES' state with '--' for all fields
when no future pass exists in the current prediction window.
Ground track computation (90 Skyfield points per satellite) was blocking
the 1Hz tracker loop on every cache miss. On cold start with multiple
tracked satellites this could stall the SSE stream for several seconds.
Tracks are now computed in a 2-worker ThreadPoolExecutor. The tracker
loop emits position without groundTrack on cache miss; clients retain
the previous track via SSE merge until the new one is ready.
Consolidated to a single active-request guard with cleanup in finally.
The previous pattern had redundant null-checks across try and catch, and
an always-false check on a controller that was already null. Cancel-on-
new-request is now explicit before creating the new controller.
METEOR-M2 (NORAD 40069) is a weather satellite with LRPT downlink but
was missing from WEATHER_SAT_KEYS, so no capture button appeared in
the pass list. Adds it alongside M2-3 and M2-4.
Replace geocentric.distance().km - 6371 (fixed spherical radius) with
wgs84.subpoint(geocentric).elevation.km in the /position endpoint.
The SSE tracker was already fixed in the Task 1 commit.
Previously currentPos only had lat/lon, so the updateTelemetry fallback
(used before first live position arrives) always showed '---' for
altitude/elevation/azimuth/distance. currentPos now includes all fields
computed from the request observer location. updateTelemetry simplified
to delegate to applyTelemetryPosition.
TLE data was only refreshed once at startup. After each refresh, a new
24-hour timer is now scheduled in a finally block so it fires even on
refresh failure. threading moved to module-level import.
Adds a 'source' param to handleLivePositions. The SSE path ('sse') only
applies lat/lon/altitude/groundTrack since the server-side tracker has
no per-client location. The HTTP poll path ('poll') owns all observer-
relative data and the visible-count badge.
SSE runs server-wide with DEFAULT_LAT/LON defaults of 0,0. Emitting
elevation/azimuth/distance/visible from the tracker produced wrong
values (always visible:False) that overwrote correct data from the
per-client HTTP poll every second.
The HTTP poll (/satellite/position) owns all observer-relative data.
SSE now only emits lat/lon/altitude/groundTrack. Also removes the
unused DEFAULT_LATITUDE/DEFAULT_LONGITUDE import.
- Add authenticated client fixture to test_weather_sat_routes.py so
require_login() before_request doesn't redirect test clients to /login
- Save timer mock references before disable()/skip_pass() clear _timer = None
- Patch app.claim_sdr_device to return None in execute_capture and
scheduling cycle tests to avoid real USB hardware probing in CI
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When pip installs flask-sock into the venv, it finds simple-websocket
already satisfied in ~/.local (user site-packages from a prior install)
and skips installing it into the venv. The venv Python cannot import from
~/.local (user site-packages are disabled in venvs), so flask_sock's
top-level "from simple_websocket import Server" raises ImportError, and
all WebSocket features are silently disabled.
Fix: explicitly list simple-websocket>=0.5.1 as an install target in
setup.sh and requirements.txt so pip installs it into the venv
regardless of what is already present in user or system site-packages.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add immediate keepalive to /adsb/stream generator so the Werkzeug dev
server flushes response headers immediately on tracking start, preventing
the 30-second delay before the aircraft map begins receiving data
- Same fix for /controller/stream/all used by the ADSB dashboard in agent mode
- Widen WebSocket init exception guards in app.py from ImportError to
Exception so any startup failure (e.g. RuntimeError from flask-sock on
an unsupported WSGI server) is caught instead of propagating
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Werkzeug's dev server buffers SSE response headers until the first body byte
is written. With keepalive_interval=30s, opening two SSE connections on
DOMContentLoaded (satellite stream + new ground station stream) meant the
browser waited 30 seconds before receiving any response bytes from either
connection. Browsers keep their loading indicator active while connections are
pending, causing the satellite dashboard to appear stuck loading.
Fix: yield an immediate keepalive at the start of sse_stream_fanout so every
SSE endpoint flushes headers + first data to the browser instantly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- OBSERVATION PROFILES section with list of configured satellites
- + ADD button opens inline form pre-filled from currently selected satellite
and SatNOGS transmitter data (frequency, decoder type auto-detected)
- EDIT / ✕ buttons per profile row
- Form fields: frequency, decoder (FM/AFSK/GMSK/BPSK/IQ-only), min elevation,
gain, record IQ checkbox
- UPCOMING PASSES section below profiles with friendlier empty-state message
- gsOnSatelliteChange hook updates form when satellite dropdown changes
- CSS for .gs-form-row, .gs-profile-item, .gs-form-label
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add auto-refresh on window focus so the dropdown updates automatically when
switching back from the sidebar, plus a manual ↺ refresh button next to the
dropdown. Also preserves the current selection across refreshes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add --no-cache-dir and --timeout 120 to all pip calls to prevent hanging
on corrupt/stale pip HTTP cache (cachecontrol .pyc issue)
- Replace silent python -c import verification with pip show to avoid
import-time side effects hanging the installer
- Switch optional packages to --only-binary :all: to skip source compilation
on Python versions without pre-built wheels (prevents gevent/numpy hangs)
- Warn early when Python 3.13+ is detected that some packages may be skipped
- Add ground track caching with 30-minute TTL to satellite route
- Add live satellite position tracker background thread via SSE fanout
- Add satellite_predict, satellite_telemetry, and satnogs utilities
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On first boot, unattended-upgrades or apt-daily often holds the dpkg
lock, causing silent hangs with no user feedback. Added wait_for_apt_lock()
that polls for up to 120s with status messages, called before apt-get
update and inside apt_try_install_any.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Dispatch observer-location-changed event from settings manager and
listen for it in APRS mode so manual location saves propagate to
the map and distance calculations. Also refresh ObserverLocation in
initAprsMap() to catch changes between page load and first map use.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The core pip install was suppressing errors with 2>/dev/null, and the
verification check was finding packages in ~/.local/site-packages
instead of the venv. When run with sudo, ~/.local isn't visible,
causing the flask-compress warning.
- Remove --quiet and stderr suppression from core package install
- Use python -s flag in verification to ignore user site-packages
- Update health check to also verify flask-compress and flask-wtf
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
These packages were in requirements.txt but missing from the explicit
pip install commands in setup.sh, causing warnings on fresh installs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pass DEFAULT_LATITUDE/DEFAULT_LONGITUDE from config to both standalone
dashboard templates so observer-location.js uses .env values instead of
falling back to hardcoded London coordinates on first visit.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The APRS map initialisation only checked for a live GPS fix, falling
back to the centre of the US (39.8N, 98.6W) when none was available.
It never read the observer position configured in .env via
INTERCEPT_DEFAULT_LAT / INTERCEPT_DEFAULT_LON.
Seed aprsUserLocation from ObserverLocation.getShared() (or the
Jinja-injected defaults) on page load so the map centres on the
user's configured position and distance calculations work without GPS.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The APRS stop endpoint terminated two processes sequentially (up to 4s
with PROCESS_TERMINATE_TIMEOUT=2s each) while the frontend fetch timed
out at 2.2s. This caused console errors and left the SDR device claimed
in the registry until termination finished, making the status panel show
the device as active after the user clicked stop.
Fix: release the SDR device from the registry immediately inside the
lock, clear process references, then terminate processes in a background
thread so the HTTP response returns instantly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When dump1090 lacks native --enable-biast support, the system now falls
back to rtl_biast (RTL-SDR Blog drivers) to enable bias-t power before
starting dump1090. The Blog V4's built-in LNA requires bias-t to
receive ADS-B signals.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
HOST_NAME_MAX is Linux-specific and undefined on macOS, causing 3
compile errors in acarsdec.c. Now patched with #define HOST_NAME_MAX 255
before building. Also fixed deprecated -Ofast flag on all macOS archs
(was only patched for arm64).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
timeout (GNU coreutils) is not available on macOS, causing rtl_test to
silently fail and report no SDR device found. Now tries timeout, then
gtimeout (Homebrew coreutils), then falls back to background process
with manual kill.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
.logo span { display: inline } in dashboard CSS had specificity (0,1,1),
overriding .brand-i { display: inline-block } at (0,1,0). Inline elements
ignore width/height, so the SVG rendered at intrinsic size (~80px tall).
Added .logo .brand-i selector at (0,2,0) to retain inline-block display.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
get_setting() now catches sqlite3.OperationalError and returns the
default value. Previously, an inaccessible database (e.g. root-owned
instance/ from sudo) caused inject_offline_settings to crash every
page render with 500 Internal Server Error.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
read_env_var() grep pipeline failed under set -euo pipefail when .env
existed but didn't contain the requested key. grep returned 1 (no match),
pipefail propagated it, and set -e killed the script.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
On x86_64, explicitly pass -march=x86-64 so the compiler emits only
baseline instructions. SatDump's SIMD plugins still compile with their
own per-target flags and do runtime CPU detection, so AVX2 acceleration
remains available on capable hardware. ARM builds are unaffected.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The data/ directory became a Python package (oui.py, patterns.py, satellites.py)
in v2.26.0, but .dockerignore still blanket-excluded it as runtime data.
This caused ModuleNotFoundError: No module named 'data.oui' on container startup.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Apply the branded SVG "i" glyph to nav logo, hero heading, and footer
on the GitHub Pages landing page, matching the main app's branding.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Patch release for #186 — default ADMIN_PASSWORD now matches README,
and credential changes in config.py sync to DB on restart.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The default ADMIN_PASSWORD was an empty string, triggering random
password generation on first run — contradicting the README which
states admin:admin. Additionally, editing config.py after first run
had no effect since init_db() only seeded users on an empty table.
- Change default ADMIN_PASSWORD from '' to 'admin'
- Sync admin credentials from config on every startup so that
changes to config.py or env vars take effect without wiping the DB
Fixes#186
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix SSE fanout thread AttributeError when source queue is None during
interpreter shutdown by snapshotting to local variable with null guard
- Fix branded "i" logo rendering oversized on first page load (FOUC) by
adding inline width/height to SVG elements across 10 templates
- Bump version to 2.26.0 in config.py, pyproject.toml, and CHANGELOG.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Scale down the branded "i" to sit as a proper lowercase glyph beside
the uppercase "NTERCEPT" text, with the stem bottom on the baseline
and the dot just above cap height.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Scale the branded "i" glyph proportionally to each SVG's font size
(scale 0.94 for 64px, 1.24 for 84px) and align the stem bottom to
the text baseline so the glyph sits naturally beside "NTERCEPT".
Also adds brand-pack.html (logos, profiles, banners, stickers, release
templates) and wallpapers.html (12 themes, 8 resolutions, PNG export).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the plain cyan text "i" with the logo-style SVG glyph (green dot
+ cyan stem/bars) in both the README banner and social preview images.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The CSS ::after dot positioning was unreliable across fonts and sizes.
Switch to an inline SVG of the "i" glyph (green dot + cyan stem/bars)
extracted from the logo — renders pixel-perfect at any size.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The dotless i (ı) wasn't rendering in all fonts. Switch to a regular "i"
with the green dot CSS overlay positioned on top of the native dot.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Matches the logo icon — the "i" in iNTERCEPT now renders with a cyan
letter and green dot via CSS, consistent across the main header, welcome
card, dashboard headers, help modal, settings modal, and all popout pages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ensureModeScript() used document.body.appendChild() to load lazy mode
scripts, but the preload for ?mode= query params runs in <head> before
<body> exists, causing all deep-linked modes to silently fail.
Also fix cross-mode handoffs (BT→BT Locate, WiFi→WiFi Locate,
Spy Stations→Waterfall) that assumed target module was already loaded.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Backend sends rssi_current but frontend was reading net.signal || net.rssi,
causing RSSI to parse as NaN and silently skipping all meter/audio updates.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
acarsdec is not available in apt repos, so the apt_install attempt
always failed with a confusing error message before falling through
to the source build. Skip the apt attempt and go straight to compiling
from source on Linux.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add WiFi Locate mode for locating access points by BSSID with real-time
signal meter, distance estimation, RSSI history chart, and audio
proximity tones. Includes hand-off from WiFi detail drawer, environment
presets (Free Space/Outdoor/Indoor), and signal-lost detection.
Also includes:
- Mobile navigation reorganized into labeled groups (SIG/TRK/SPC/WIFI/INTEL/SYS)
- flask-limiter made optional with graceful degradation
- Fix radiosonde setup missing semver Python dependency
- Documentation updates (FEATURES, USAGE, UI_GUIDE, GitHub Pages site)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The OOK subprocess was spawned without start_new_session=True, so
process.terminate() only signalled the parent — child processes kept
running. Now uses os.killpg() to terminate the entire process group,
matching the pattern used by all other routes (ADS-B, AIS, ACARS, etc.).
Also fixes silent error swallowing in the frontend stop handler so the
UI resets even if the backend request fails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The airband start function was calling parseInt() directly on composite
device selector values like "rtlsdr:0", which always returned NaN and
fell back to device 0. This also meant sdr_type was never sent to the
backend, and could result in int(None) TypeError on the server.
Now properly splits the composite value (matching ADS-B/ACARS/VDL2
pattern) and sends both device index and sdr_type. Also hardened
backend int() parsing to use explicit None checks.
Fixes: "Airband Error: Invalid parameter: int() argument must be a
string, a bytes-like object or a real number, not 'NoneType'"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Addresses final upstream review — all backend-derived values (timestamp,
bit_count, rssi, hex, ascii) now use DOM methods instead of innerHTML
interpolation, closing the last XSS surface. Bumps cache-buster to ook2.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SatDump v1.2.2 has multiple GCC 15 build failures (sol2 templates,
libacars incompatible pointer types) that are difficult to patch
exhaustively. On distros where SatDump is available as a system
package (Ubuntu 24.10+, Debian Trixie+), install via apt instead
of building from source. Falls back to source build on older systems.
Closes#180
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Detect crashed rtl_433 process via poll() and clean up stale state
instead of permanently blocking restarts with 409
- Replace innerHTML+onclick preset rendering with createElement/addEventListener
to prevent XSS via crafted localStorage frequency values
- Normalize preset frequencies to toFixed(3) on save and render
- Add try/catch + shape validation to loadPresets() for corrupted localStorage
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Critical:
- Pass sdr_type_str to claim/release_sdr_device (was missing 3rd arg)
- Add ook_active_sdr_type module-level var for proper device registry tracking
- Add server-side range validation on all timing params via validate_positive_int
Major:
- Extract cleanup_ook() function for full teardown (stop_event, pipes, process,
SDR release) — called from both stop_ook() and kill_all()
- Replace Popen monkey-patching with module-level _ook_stop_event/_ook_parser_thread
- Fix XSS: define local _esc() fallback in ook.js, never use raw innerHTML
- Remove dead inversion code path in utils/ook.py (bytes.fromhex on same
string that already failed decode — could never produce a result)
Minor:
- Status event key 'status' → 'text' for consistency with other modules
- Parser thread logging: debug → warning for missing code field and errors
- Parser thread emits status:stopped on exit (normal EOF or crash)
- Add cache-busting ?v={{ version }}&r=ook1 to ook.js script include
- Fix gain/ppm comparison: != '0' (string) → != 0 (number)
Tests: 22 → 33 (added start success, stop with process, SSE stream,
timing range validation, stopped-on-exit event)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add client-side and server-side military aircraft detection using ICAO
hex ranges and callsign prefixes (matching live dashboard logic). History
table shows MIL/CIV badges with filtering dropdown, and exports respect
the classification filter.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add date range filtering, CSV export, and enhanced history page styling
for the ADS-B aircraft tracking history feature.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Users reported setup.sh appearing stuck during dump1090 installation on
Ubuntu 25.10. Added progress messages before APT package checks, build
dependency installation, and fallback clone steps.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add -Wno-template-body to CMAKE_CXX_FLAGS to suppress GCC 15's
-Wtemplate-body warning that breaks SatDump's bundled sol2/sol.hpp.
The flag is silently ignored by older GCC versions.
Closes#180
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add kill_all() handler for OOK process cleanup on global reset
- Fix stop_ook() to close pipes and join parser thread (prevents hangs)
- Add ook.css with CSS classes, replace inline styles in ook.html
- Register ook.css in lazy-load style map (INTERCEPT_MODE_STYLE_MAP)
- Fix frontend frequency min=24 to match backend validation
- Add 22 unit tests for decode_ook_frame, ook_parser_thread, and routes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Parse hackrf_info stderr (newer firmware) and handle non-zero exit codes
- Fix gain_max from 62 to 102 (combined LNA 40 + VGA 62)
- Apply resolved readsb binary path for all SDR types, not just RTL-SDR
- Add HackRF/SoapySDR-specific error messages in ADS-B startup
- Add HackRF waterfall support via rx_sdr IQ capture + FFT
- Add 17 tests for HackRF detection and command builder
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix XSS: escape ASCII output in innerHTML via escapeHtml()
- Fix deadlock: use put_nowait() for queue ops under ook_lock
- Fix SSE leak: add ook to moduleDestroyMap so switching modes
closes the EventSource
- Fix RSSI: explicit null check preserves valid zero values in
JSON export
- Add frame cap: trim oldest frames at 5000 to prevent unbounded
memory growth on busy bands
- Validate timing params: wrap int() casts in try/except, return
400 instead of 500 on invalid input
- Fix PWM hint: correct to short=0/long=1 matching rtl_433
OOK_PWM convention (UI, JS hints, and cheat sheet)
- Fix inversion docstring: clarify fallback only applies when
primary hex parse fails, not for valid decoded frames
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace hardcoded frequency buttons with localStorage-backed presets.
Default presets are standard ISM frequencies (433.920, 315, 868, 915 MHz).
Users can add custom frequencies, right-click to remove, and reset to
defaults — matching the pager module pattern.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Covers identifying modulation type (PWM/PPM/Manchester), finding
pulse timing via rtl_433 -A, common ISM frequencies and timings,
and troubleshooting tips for tolerance and bit order.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix double-scroll by switching ookOutputPanel to flex layout
- Keep decoded frames visible after stopping (persist for review)
- Wire global Clear/CSV/JSON status bar buttons to OOK functions
- Hide default output pane in OOK mode (uses own panel)
- Add command display showing the active rtl_433 command
- Add JSON export and auto-scroll support
- Fix 0x prefix stripping in OOK hex decoder
- Fix PWM encoding hint text
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Timing presets: five quick-fill buttons (300/600, 300/900, 400/800, 500/1500, 500 MC)
that populate all six pulse-timing fields at once — maps to CTF flag timing profiles
- RSSI per frame: add -M level to rtl_433 command; parse snr/rssi/level from JSON;
display dB SNR inline with each frame; include rssi_db column in CSV export
- Auto bit-order suggest: "Suggest" button counts printable chars across all stored
frames for MSB vs LSB, selects the winner, shows count — no decoder restart needed
- Pattern filter: live hex/ASCII filter input above the frame log; hides non-matching
frames and highlights matches in green; respects current bit order
- TSCM integration: "Decode (OOK)" button in RF signal device details panel switches
to OOK mode and pre-fills frequency — frontend-only, no backend changes needed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Users had no visibility into what was happening during silent apt/pip
installs. Added info messages before Python package installs, APT
package lists update, and PostgreSQL installation.
Replace the linear setup.sh with an interactive menu-driven installer:
- First-time wizard with OS detection and profile selection
- Install profiles: Core SIGINT, Maritime, Weather, RF Security, Full, Custom
- System health check (tools, SDR devices, ports, permissions, venv, PostgreSQL)
- Automated PostgreSQL setup for ADS-B history (creates DB, user, tables, indexes)
- Environment configurator for interactive INTERCEPT_* variable editing
- Update tools (rebuild source-built binaries)
- Uninstall/cleanup with granular options and double-confirm for destructive ops
- View status table of all tools with installed/missing state
- CLI flags: --non-interactive, --profile=, --health-check, --postgres-setup, --menu
- .env file helpers (read/write) with start.sh auto-sourcing
- Bash 3.2 compatible (no associative arrays) for macOS support
Update all documentation to reflect the new menu system:
- README.md: installation section with profiles, CLI flags, env config, health check
- CLAUDE.md: entry points and local setup commands
- docs/index.html: GitHub Pages install cards with profile mentions
- docs/HARDWARE.md: setup script section with profile table
- docs/TROUBLESHOOTING.md: health check and profile-based install guidance
- docs/DISTRIBUTED_AGENTS.md: controller quick start
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Runtime data (station config, logs) should not be tracked in version control.
Also removes duplicate "Local data" block in .gitignore.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SSE EventSources and running processes were not cleaned up during
dashboard navigation, saturating the browser's per-origin connection
limit. Extract moduleDestroyMap into shared getModuleDestroyFn() and
call destroyCurrentMode() before navigation. Also expand
stopActiveLocalScansForNavigation() to cover wefax, weathersat, sstv,
subghz, meshtastic, and gps modes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Several modes didn't pass sdr_type to claim_sdr_device(), defaulting to
'rtlsdr' and triggering an rtl_test USB probe that fails for HackRF with
a confusing "check that the RTL-SDR is connected" message.
- Add sdr_type to frontend start requests for rtlamr, weather-sat, sstv-general
- Read sdr_type in backend routes and pass to claim/release_sdr_device()
- Add early guard returning clear "not yet supported" error for non-RTL-SDR
hardware in modes that are hardcoded to RTL-SDR tools
- Make probe_rtlsdr_device error message device-type-agnostic
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The "Mute" button on pager cards persists muted addresses to
localStorage with no visible indicator, making it easy to
accidentally hide an address and forget about it. This caused
flag fragment messages on RIC 1337 to silently disappear.
- Add "X muted source(s) — Unmute All" indicator to sidebar
- Stop persisting hideToneOnly filter across sessions so the
default (show all) always applies on page load
- Remove default checked state from Tone Only filter checkbox
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Safety net for Windows developers whose git config (core.autocrlf=true)
converts LF to CRLF on checkout. Even with .gitattributes forcing eol=lf,
some git configurations can still produce CRLF working copies. The sed
pass after COPY ensures start.sh and other scripts always have Unix
line endings inside the container.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Docker containers crash on startup when shell scripts have CRLF line
endings (from Windows git checkout with core.autocrlf=true). The
start.sh gunicorn entrypoint fails with "$'\r': command not found".
Add .gitattributes forcing eol=lf for *.sh and Dockerfile so Docker
builds work regardless of the developer's git line ending config.
Also normalizes two scripts that were committed with CRLF.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cover all parse_multimon_output code paths:
- Alpha and Numeric content types across POCSAG baud rates
- Empty content and special characters (base64, punctuation)
- Catch-all pattern for non-standard content type labels
- Address-only (Tone) messages with trailing whitespace
- FLEX simple format and unrecognized input lines
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three issues caused POCSAG messages to be incorrectly hidden or
misclassified in the Device Intelligence panel:
1. detectEncryption used a narrow character class ([a-zA-Z0-9\s.,!?-])
to measure "printable ratio". Messages containing common printable
ASCII characters like : = / + @ fell below the 0.8 threshold and
returned null ("Unknown") instead of false ("Plaintext"). Simplified
to check all printable ASCII (\x20-\x7E) which correctly classifies
base64, structured data, and punctuation-heavy content.
2. The default hideToneOnly filter was true, hiding all address-only
(Tone) pager messages. When RF conditions cause multimon-ng to decode
the address but not the message content, the resulting Tone card was
silently filtered. Changed default to false so users see all traffic
and can opt-in to filtering.
3. The multimon-ng output parser only recognized "Alpha" and "Numeric"
content type labels. Added a catch-all pattern to capture any
additional content type labels that future multimon-ng versions or
forks might emit, rather than dropping them to raw output.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Radiosonde route now runs a quick import check before launching the full
subprocess, catching missing Python dependencies immediately with a clear
message instead of a truncated traceback. Error messages are context-aware:
import errors suggest re-running setup.sh rather than checking SDR connections.
Increased stderr truncation limit from 200 to 500 chars and added full stderr
logging via logger.error() across all affected routes (radiosonde, ais, aprs,
acars, vdl2) for easier debugging.
Closes#173
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the /devices fetch hasn't completed or fails, parseInt on an empty
select returns NaN which JSON-serializes to null. The backend then calls
int(None) and raises TypeError. Fix both layers: frontend falls back to
0 on NaN, backend uses `or` defaults so null values don't bypass the
fallback.
Also adds a short TTL cache to detect_all_devices() so multiple
concurrent callers on the same page load don't each spawn blocking
subprocess probes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three compounding bugs prevented flask-sock (and other C-extension
packages) from installing and hid the actual errors:
- Add python3-dev to Debian apt installs so Python.h is available for
building gevent, cryptography, etc.
- Remove 2>/dev/null from optional packages pip loop so install errors
are visible and diagnosable
- Surface pip/setuptools/wheel upgrade failures with a warning instead
of silently swallowing them
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add dropout tolerance (2 blocks ~40ms) to bridge brief signal gaps that
caused the state machine to chop dahs into multiple dits. Also fix scope
SNR display to use actual noise_ref instead of noise_floor.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add theme-aware severity/neon CSS variables and replace hardcoded hex
colors (#fff, #000, #00ff88, #ffcc00, etc.) with var() references
across 26 files so text remains readable in both dark and light themes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Nav active labels used color: var(--bg-primary) which resolved to
near-white on light backgrounds. Run-state chips and buttons had
hardcoded dark RGBA backgrounds. Added light-theme overrides for
readable text and appropriate light backgrounds.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use postMessage from parent page to notify the satellite dashboard
iframe of visibility changes, preventing unnecessary POST requests
to /satellite/position when the user isn't viewing satellite mode.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Backend: Add /prefetch-images endpoint that warms the image cache in
parallel using a thread pool, skipping already-cached images.
Frontend: Trigger prefetch on mode init so images load instantly.
Replace per-request Date.now() cache-bust with a 5-minute rotating
key to allow browser caching aligned with backend max-age.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When navigating from another mode (e.g. pager) to the ADS-B dashboard,
the old process could still hold the USB device. Two fixes:
1. routes/adsb.py: If dump1090 starts but SBS port never comes up,
kill the process and return a DEVICE_BUSY error instead of silently
claiming success with no data.
2. templates/adsb_dashboard.html: Pre-flight conflict check in
toggleTracking() queries /devices/status and auto-stops any
conflicting mode before starting ADS-B, with a 1.5s USB release
delay.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Stock rtl-sdr packages don't support the -T bias-tee flag (only
RTL-SDR Blog builds do). Passing -T to stock rtl_sdr causes an
immediate exit, breaking meteor scatter and waterfall modes.
Now probes the tool's --help output before adding -T, with a regex
that avoids false-matching "DVB-T" in the description text.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Meteor: onopen callback used closure variable _ws instead of `this`,
so a double-click during CONNECTING state sent on the wrong socket.
Also clean up any in-progress connection on re-start, not just running ones.
Setup: make apt-get update non-fatal so third-party repo errors
(e.g. stale PPAs on Debian) don't abort the entire install.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Match gunicorn's patch_all() args exactly (remove subprocess=False),
filter the MonkeyPatchWarning from the unavoidable double-patch, and
wrap gevent's _ForkHooks.after_fork_in_child to catch the spurious
AssertionError that fires when subprocesses fork after double-patching.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Gunicorn's gevent worker deadlocks during init_process() on Raspberry Pi
(ARM) before it can apply its own monkey-patching. Patching in post_fork
runs immediately after fork and before worker init, avoiding the race.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Full-stack meteor scatter monitoring mode that captures IQ data from
an RTL-SDR, computes FFT waterfall frames via WebSocket, and runs a
real-time detection engine to identify transient VHF reflections from
meteor ionization trails (e.g. GRAVES radar at 143.050 MHz).
Backend: MeteorDetector with EMA noise floor, SNR threshold state
machine (IDLE/DETECTING/ACTIVE/COOLDOWN), hysteresis, and CSV/JSON
export. WebSocket at /ws/meteor for binary waterfall frames, SSE at
/meteor/stream for detection events and stats.
Frontend: spectrum + waterfall + timeline canvases, event table with
SNR/duration/confidence, stats strip, turbo colour LUT. Uses shared
SDR device selection panel with conflict tracking.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SSTV mode was slow to populate next-pass countdown and ISS location map
due to uncached skyfield computation and sequential JS API calls.
- Cache ISS position (10s TTL) and schedule (15min TTL, keyed by rounded lat/lon)
- Cache skyfield timescale object (expensive to create on every request)
- Reduce external API timeouts from 5s to 3s
- Fire checkStatus, loadImages, loadIssSchedule, updateIssPosition in parallel via Promise.all
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The /space-weather/data endpoint made 13 sequential HTTP requests, each
with a 15s timeout, causing 30-195s load times on cold cache. Now uses
ThreadPoolExecutor to fetch all sources concurrently, reducing worst-case
latency to ~15s (single slowest request).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add flex-shrink: 0 so the strip holds its intrinsic height instead of
being distorted by the parent flex container.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move tracking state, balloon count, last update, and waveform from the
sidebar into a stats strip above the map, matching the APRS strip pattern.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reverts the incorrect assumption from f8e5d61 that -c expects a
directory. The auto_rx -c flag expects the full path to station.cfg.
Passing the directory caused "Config file ... does not exist!" on start.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Running via sudo creates data/radiosonde/ as root. On next run the
config write fails with an unhandled OSError, Flask returns an HTML 500,
and the frontend shows a cryptic JSON parse error.
Three-layer fix:
- start.sh: pre-create known data dirs before chown, add certs/ to the
list, export INTERCEPT_SUDO_UID/GID for runtime use
- generate_station_cfg: catch OSError with actionable message, chown
newly created files to the real user via _fix_data_ownership()
- start_radiosonde: wrap config generation in try/except so it returns
JSON instead of letting Flask emit an HTML error page
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reusable SVG bar waveform (SignalWaveform.Live) that animates in response
to incoming SSE data — idle breathing when stopped, active oscillation
proportional to telemetry update frequency, smooth decay on signal loss.
Integrated into radiosonde Status section with ping() on each balloon
message and stop() on tracking stop. Also hardens the fetch error path
to show a readable message instead of a JSON parse error when the server
returns HTML.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The -c flag expects a directory containing station.cfg, but we were
passing the full file path, so auto_rx could never find its config.
Also fix sonde_type priority to prefer subtype over type.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The visual refresh layer hardcoded dark rgba() gradients that overrode
variable-based backgrounds. Added [data-theme="light"] overrides for
visual refresh CSS variables and comprehensive component backgrounds
in index.css and global-nav.css.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move flask-sock and websocket-client from the batch core install (where
failures are silently swallowed) to the optional packages loop so users
see a clear warning if either package fails to build on ARM.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove section hover shift, fix broken NOAA PDF link, reorder sections
to match Weather Satellite pattern, and fix text alignment spacing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SSE EventSource connections for AIS, ACARS, VDL2, and radiosonde were
not closed when switching modes, causing fd exhaustion after repeated
switches. Also fixes socket leaks on exception paths in AIS/ADS-B
stream parsers, closes subprocess pipes in safe_terminate/cleanup, and
caches skyfield timescale at module level to avoid per-request fd churn.
Closes#169
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
METEOR-M2-4 was defined as an active weather satellite but had no
orbital data, so pass predictions always returned empty. Added TLE
entry and CelesTrak name mapping for automatic refresh.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add SSLZeroReturnError and SSLError to gevent's NOT_ERROR list so
dropped TLS handshakes (browser preflight, plain HTTP to HTTPS port)
don't print scary tracebacks to the console.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolves the machine's LAN IP via hostname -I so users see a
clickable URL they can use from other devices on the network.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
sqlite3.connect() opens read-only files without error — the failure
only surfaces on the first write (INSERT). Add an upfront os.access()
check on both the directory and file, with a clear error showing the
owner and the exact chown command to fix it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When start.sh runs via sudo, chown instance/ and data/ back to the
invoking user so the SQLite DB stays accessible without sudo. Also
adds a clear error message in get_connection() when the DB can't be
opened due to permissions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The FFTW3 dev package was listed twice in the build stage and both
copies were removed during cleanup, taking the runtime .so with them.
Switching the duplicate to libfftw3-bin ensures libfftw3f.so.3 persists.
Fixes#166
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Escape ac.icao, callsign, typeCode with escapeHtml() in aircraft card (XSS)
- Add linking comments between duplicated IATA_TO_ICAO mappings
- VDL2 sidebar: single-click selects aircraft, double-click opens modal
- Remove stale ICAOs from acarsAircraftIcaos in cleanupOldAircraft()
- Add null guard to drawPolarPlot() in weather-satellite.js
- Move deferred imports (translate_message, get_flight_correlator) to module level
- Check all frequency checkboxes by default on initial load
- Remove extra blank lines and uncertain MC/MCO airline code entry
- Add TODO comments linking duplicated renderAcarsCard implementations
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extends the rtl_tcp support (added in c1339b6 for APRS, Morse, DSC) to
the weather satellite mode. When a remote SDR host is provided, SatDump
uses --source rtltcp instead of --source rtlsdr, local device claiming
is skipped, and the frontend sends rtl_tcp params via getRemoteSDRConfig().
Closes#166
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When stopping gunicorn with Ctrl+C, the gevent worker's handle_quit()
calls sys.exit(0) inside a greenlet, causing gevent to print a
SystemExit traceback. Add a gunicorn config with post_worker_init hook
that marks SystemExit as a non-error in gevent's hub.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Closes#164. Only pager and sensor routes supported rtl_tcp connections.
Now aprs, morse, and dsc routes follow the same pattern: extract
rtl_tcp_host/port from the request, skip local device claiming for
remote connections, and use SDRFactory.create_network_device(). DSC also
refactored from manual rtl_fm command building to use SDRFactory's
builder abstraction. Frontend wired up for all three modes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Blueprint registration and database init run synchronously (essential
for routing). Process cleanup, database cleanup scheduling, and TLE
satellite updates are deferred to a background thread with a 1-second
delay so the gevent worker can start serving HTTP requests right away.
Previously all init ran synchronously during module import, blocking
the single gevent worker for minutes while TLE data was fetched from
CelesTrak.
Also removes duplicate TLE update — init_tle_auto_refresh() already
schedules its own background fetch.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All other docs already reference sudo ./start.sh but the inline usage
comments in start.sh itself and the --help example in USAGE.md were
missing it, which could lead users to run without root privileges.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The custom SIGINT/SIGTERM handler in utils/process.py overrode
gunicorn's own signal management, causing KeyboardInterrupt to fire
inside the gevent worker on Ctrl+C instead of allowing gunicorn's
graceful shutdown. Now detects if another signal manager (gunicorn)
has already installed handlers and defers to it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Blueprint registration, database init, cleanup, and websocket setup
were all inside main() which only runs via 'python intercept.py'.
When gunicorn imports app:app, it got a bare Flask app with no routes,
causing every endpoint to return 404.
Extracted initialization into _init_app() called at module level with
a guard to prevent double-init when main() is also used.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rate limiting on login is a security requirement, not optional.
Reverts the no-op fallback — if flask-limiter is missing, the app
will fail fast with a clear import error rather than silently
running without rate limiting.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- psutil was in requirements.txt but missing from setup.sh optional list
- Verification check no longer hard-fails on flask-limiter since app.py
now handles it as optional with a no-op fallback
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
flask-limiter may not be installed (e.g. RPi venv). The hard import
crashed the gunicorn gevent worker on startup, causing all routes to
return 404 with no visible error. Now falls back to a no-op limiter.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Long-lived SSE connections prevent the gevent worker from exiting on
SIGINT. --graceful-timeout 5 force-kills the worker after 5 seconds.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Gunicorn's gevent worker (-k gevent) handles monkey-patching internally.
The manual patch_all() in app.py ran in the master process before worker
fork, preventing the worker from booting (no 'Booting worker' log line,
server unreachable).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolves ModuleNotFoundError when running outside a venv by auto-detecting
the venv/bin/python relative to the script, falling back to VIRTUAL_ENV
or system python3.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add start.sh as the recommended production entry point with:
- gunicorn + gevent worker for concurrent SSE/WebSocket handling
- CLI flags for port, host, debug, HTTPS, and dependency checks
- Auto-fallback to Flask dev server if gunicorn not installed
- Conditional gevent monkey-patch in app.py via INTERCEPT_USE_GEVENT env var
- Docker CMD updated to use start.sh
- Updated all docs, setup.sh, and requirements.txt accordingly
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Change 'already_running' to 'already_scanning' status in bluetooth_v2
so frontend recognizes the response and connects the SSE stream
- Hide pagerScopePanel and sensorScopePanel in switchMode() to prevent
audio waveform bars leaking into other modes
- Clear devices Map, pendingDeviceIds Set, and UI in BluetoothMode.destroy()
to prevent memory accumulation on repeated mode switches
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously all HackRF devices were hardcoded as "HackRF One" regardless
of actual hardware variant. Now parses the Board ID line from hackrf_info
to correctly identify HackRF Pro, HackRF One, and other variants.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Pass observer location and gpsd status to radiosonde_auto_rx station config
- Add station marker on radiosonde map with GPS live position updates
- Display distance from station to each balloon in cards and popups
- Update aircraft database
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolved conflicts:
- routes/acars.py: keep /messages and /clear endpoints for history reload
- routes/vdl2.py: keep /messages and /clear endpoints for history reload
- templates/adsb_dashboard.html: keep removal of hardcoded device-1
defaults for ACARS/VDL2 selectors (users pick their own device)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Mode modules were leaking EventSource connections, setInterval timers,
and setTimeout timers on every mode switch, causing progressive browser
sluggishness. Added destroy() to 8 modules missing it (meshtastic,
bluetooth, wifi, bt_locate, sstv, sstv-general, websdr, spy-stations)
and centralized all destroy calls in switchMode() via a moduleDestroyMap
that cleanly tears down only the previous mode.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Re-implements envelope detection on top of the rewritten Morse decoder.
Addresses PR #160 review feedback:
- Rebase: rebuilt on current upstream/main (lifecycle state machine)
- Gap thresholds: 2.0/5.0 for envelope only; goertzel keeps 2.6/6.0
- Frequency validation: max_mhz=1766 for envelope, 30 for goertzel
- Tests: EnvelopeDetector unit tests + envelope-mode decoder test
- Envelope uses direct magnitude threshold (no SNR/noise ref)
- Goertzel path completely unchanged
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
rtl_test opens the USB device during probing. After killing the
process, the kernel may not release the USB interface immediately.
dump1090 then fails with usb_claim_interface error -6. Add a 0.5s
delay after probe cleanup to allow the kernel to fully release the
device before the actual decoder opens it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
rtl_test -t often exits non-zero after finding a device (e.g.
"No E4000 tuner found, aborting" with R820T tuners). The return
code fallback was firing even when the "Found N device(s)" success
message had already been matched. Track device_found separately
and only use return code as fallback when no success was seen.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The success check ('Found' in line and 'device' in line) matched
"No supported devices found" since both keywords appear. Add a
pre-check for negative device messages, a return code fallback,
and a clearer error message.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The stop button appeared unresponsive because UI updates waited for the
server response. If the fetch hung or errored, the user saw nothing.
Now the UI updates immediately (matching the pager stop pattern) and
the server request happens in the background.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Auto_rx reads many config keys without defaults and crashes if they're
missing, even for disabled features like email. Include every section
and key from the example config to prevent missing-key errors.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The config format changed significantly: SDR settings moved to [sdr_1],
[positioning] became [location], and many sections are now required.
Also enable payload_summary UDP output so telemetry reaches our listener.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Pass config file path (not directory) to auto_rx -c flag
- Use absolute paths in generated station.cfg since auto_rx runs
with cwd set to its install directory
- Teach dependency checker about auto_rx.py at /opt install path
so the "missing dependency" banner no longer appears
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The setup.sh skip check only looked for auto_rx.py, so a previous
incomplete install (Python files but no compiled binaries) would be
treated as fully installed. Now also checks for dft_detect binary.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
setup.sh and Dockerfile were installing the Python package and copying
files but skipping the build.sh step that compiles the C decoders.
This caused "Binary dft_detect does not exist" at runtime.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The subprocess was launched with bare 'python' which on Debian doesn't
exist (python3 only) and wouldn't have access to the venv-installed
radiosonde dependencies anyway. Using sys.executable ensures the same
interpreter (with all installed packages) is used.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move adsb_active_device/sdr_type assignment to immediately after
claim_sdr_device so stop_adsb() can always release the device, even
during startup. Sync sdr_type_str after SDRType fallback to prevent
claim/release key mismatch. Clear active device on all error paths.
Replace blind 3s sleep for dump1090 readiness with port-polling loop
(100ms intervals, 3s max). Replace subprocess.run() in rtl_test probe
with Popen + select-based early termination on success/error detection.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Avoids PEP 668 externally-managed-environment error on Debian Bookworm
by using the project's venv/bin/pip instead of system pip3.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Integrate radiosonde_auto_rx for automatic weather balloon detection and
decoding on 400-406 MHz. Includes UDP telemetry parsing, Leaflet map with
altitude-colored markers and trajectory tracks, SDR device registry
integration, setup script installation, and Docker support.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The registry used plain int keys (device index), so HackRF at index 0
and RTL-SDR at index 0 would collide. Changed to composite string keys
("sdr_type:index") so each SDR type+index pair is tracked independently.
Updated all route callers, frontend device selectors, and session restore.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Switch direwolf subprocess output from PIPE to PTY (pseudo-terminal),
forcing line-buffered output so packets arrive immediately instead of
waiting for a 4-8KB pipe buffer to fill. Matches the proven pattern
used by pager mode.
Also enhances direwolf config with FIX_BITS error correction and
disables unused AGWPE/KISS server ports.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The globe wasn't rendering because initGlobe() used setTimeout(100)
which can race with the display:none removal by switchMode(). Both
GPS and WebSDR modes use requestAnimationFrame to wait for the browser
to compute layout before initializing Globe.gl.
- Replace setTimeout with RAF-based retry loop (up to 8 frames)
- Add try-catch around Globe() init with fallback message
- Match the proven pattern from GPS and WebSDR modes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix globe destroyed on re-render by preserving canvas DOM node across
renderLocationCard() calls instead of recreating from scratch
- Reduce globe.gl camera minDistance (180->120) so globe is visible in
200px container
- Clear stale globeInstance ref when canvas is gone
- Enlarge CPU gauge (90->110px), percentage label (18->22px), core bars
(24->48px height), and detail text (11->12px)
- JS fetchLocation() now supplements server response with client-side
ObserverLocation.getShared() from localStorage when server returns
'default' or 'none', picking up manual coordinates from settings modal
- Location priority: GPS > config env vars > manual (localStorage) > default
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add third location fallback to utils/constants (London 51.5074/-0.1278)
so location always resolves even without GPS or env vars configured
- Remove min-height from sys-card to eliminate wasted space
- Switch System Info to vertical key-value layout filling the card
- Clean up OS string (strip glibc suffix), use locale date for boot time
- Bump info grid font size from 11px to 12px for readability
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace broken app.gps_state lookup with utils.gps.get_current_position()
and return GPS metadata (fix quality, satellites, accuracy). Shrink location
card to single-column with 200px globe, move System Info into row 2.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add SVG arc gauge, per-core CPU bars, temperature sparkline, network
interface monitoring with bandwidth deltas, disk I/O rates, 3D globe
with observer location, weather overlay, battery/fan/throttle support,
and process grid layout. New /system/location and /system/weather endpoints.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Clear sidebar highlights and ACARS message timer when stale selected
aircraft is removed in cleanupOldAircraft()
- Escape all user-controlled strings in renderAcarsCard(),
addAcarsMessage(), and renderAcarsMainCard() before innerHTML insertion
- Remove dead duplicate H1 check in classify_message_type
- Move _d label from link_test set to handshake return path
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Real-time dashboard for host metrics (CPU, memory, disk, temperatures),
active decoder process status, and SDR device enumeration via SSE streaming.
Auto-connects when entering the mode with graceful psutil fallback.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Switch decoder subprocess from text mode to binary mode and decode
each line with errors='replace' so corrupted radio bytes (e.g. 0xf7)
are substituted instead of raising UnicodeDecodeError after long runs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The satellite dropdown had no change listener, so selecting a different
satellite never updated the pass list, timeline, countdown, or polar plot.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The .btn, .btn-sm, and .btn-ghost classes used by morse mode buttons
(TXT, CSV, Copy, Clear) were defined in core/components.css but the
stylesheet was never loaded in index.html, causing unstyled buttons.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the stop POST timed out (5s), lifecycle was set to 'idle' on error,
allowing checkStatus to see running=true and reconnect SSE. Now:
- stop .then() stays in 'stopping' on timeout/error instead of going idle
- checkStatus skips reconnect when lifecycle is 'stopping' post-timeout
but still transitions to idle when server confirms running=false
- LOCAL_STOP_TIMEOUT_MS raised from 5s to 12s to match server cleanup time
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous stopPromise guard only prevented new polls from being
dispatched. Polls already in-flight before stop was clicked could still
return with running=true and override the stopping lifecycle, causing
SSE reconnection and an apparent restart loop. Add a second guard in
the .then() handler to check stopPromise/lifecycle before acting.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Guard checkStatus() against in-flight stop to prevent status poller
from overriding stopping state and reconnecting SSE. Lower SNR floor
from 1.3 to 1.15 to accommodate weaker CW signals. Add SNR/noise_ref
to scope events and metrics for real-time threshold debugging.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Widen noise detector offset from ±100Hz to ±200Hz to reduce spectral
leakage into the noise reference, and scale threshold_multiplier for
SNR space (2.8 → 1.54) so real CW signals reliably trigger tone
detection instead of producing all-E's at 60 WPM.
Fix misleading "decoder startup" timeout message on stop requests and
increase stop timeout from 2.2s to 5s.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous magnitude-based threshold couldn't distinguish CW tone from
AGC-amplified inter-element silence — the Goertzel level stayed above
threshold permanently, preventing any tone OFF transitions and thus zero
character decodes.
Switch tone detection to use SNR (tone_mag / adjacent_band_noise_ref).
Both bands are equally amplified by AGC, so the ratio is gain-invariant.
Also replace the conditional noise_ref guard with unconditional blending
so the noise floor tracks actual ambient levels continuously.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Filter decoder-thread 'stopped' status events that race with the route
lifecycle, causing the frontend to drop back to idle on first start.
Pull noise floor upward using adjacent-frequency Goertzel reference when
warmup calibration runs before AGC converges, preventing permanent
tone-on with zero character decodes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The multimon-ng MORSE_CW decoder never reliably decoded characters.
Switch live decode to use the existing morse_decoder_thread() which
wraps MorseDecoder with Goertzel tone detection, adaptive thresholds,
and proper timing estimation — eliminating multimon-ng, PTY plumbing,
and the relay thread from the CW pipeline entirely.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Step-by-step instructions for running multiple RTL-SDR dongles:
serial burning, udev symlinks, USB power, and Docker passthrough.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add bufsize=0 to Popen for raw FileIO instead of BufferedReader, and
start decoder/stderr threads immediately before sleep+poll so stdout
is read without delay — matching the working pager pattern.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
BufferedReader.read(n) on non-interactive streams (Python 3.14) blocks
until the full n bytes accumulate, starving the decoder of real-time
PCM data. Use os.read() on the raw file descriptor instead, which
returns as soon as any data is available. Falls back to .read() for
file-like objects without fileno() (e.g. BytesIO in tests).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace select.select()+os.read() with a blocking reader thread feeding
a queue, matching pager's working pattern. The select() approach fails
to detect available data on Python 3.14's BufferedReader-wrapped pipes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
rtl_fm prints device info, tuning, and errors to stderr but the morse
route only logged these server-side. Now stderr lines are forwarded to
the morse queue as info events, displayed in a compact diagnostic log
below the scope canvas. After 10s with no audio data, the scope text
escalates to prompt the user to check the SDR log.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace blocking rtl_stdout.read() with select()+os.read() so the
decoder thread emits diagnostic heartbeat scope events when rtl_fm
produces no PCM data (common in direct sampling mode). Add waiting-state
rendering in the scope canvas and hide the generic placeholder/status
bar for morse mode.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add automatic gain control (AGC) before Goertzel processing to normalize
quiet audio from direct sampling mode where the -g gain flag has no effect.
Fix broken adaptive threshold bootstrap by adding a 50-block warm-up phase
that collects magnitude statistics before seeding noise floor and signal peak.
Lower threshold ratio from 50% to 30% for better weak-CW sensitivity.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The -D flag is only available in newer rtl_fm builds. Docker and distro
packages use the older -E direct / -E direct2 flags instead, which are
universally supported.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Enable direct sampling (-D 2) for RTL-SDR at HF frequencies below 24 MHz
so rtl_fm can actually receive CW signals. Add startup health check to
detect immediate rtl_fm failures. Push stopped status event from decoder
thread on EOF so the frontend auto-resets. Add frequency placeholder and
help text. Fix stop button silently swallowing errors.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
validate_frequency() defaults to 24-1766 MHz (VHF/UHF range), but Morse/CW
operates on HF bands (0.5-30 MHz). Pass explicit min/max to allow HF frequencies.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolved conflict in static/js/modes/weather-satellite.js:
- Kept allPasses state variable and applyPassFilter() for satellite pass filtering
- Kept satellite select dropdown listener for filter feature
- Adopted upstream's optimistic stop() UI pattern for better responsiveness
- Kept optional chaining (pass?.trajectory) since drawPolarPlot can receive null
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Closes#155 — users can now access settings directly from the welcome
screen without entering a mode first.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix "What's New" section shifting up/down on smaller screens (#157) by
isolating the logo pulse animation to its own compositing layer, stabilizing
the scrollbar gutter, and pinning the welcome container dimensions.
Morse mode improvements: relocate scope and decoded output panels to the
main content area, use shared SDR device controls, and reduce panel heights
for better layout.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Correct modulation parameters (1200 bps, 2100/1300 Hz tones), replace
invented format codes with the six ITU-defined specifiers {102, 112,
114, 116, 120, 123}, accept all valid EOS symbols (117, 122, 127),
add parser validation (format, MMSI, raw field, telecommand range),
and fix truthiness bugs that dropped zero-valued fields.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add synchronous startup verification after Popen() — sleep 0.5s and poll
the process before returning to the caller. If SatDump exits immediately
(missing device, bad args), raise RuntimeError with the actual error
message instead of returning status: 'started'. Keep a shorter (2s) async
backup check for slower failures.
Also fix --source_id handling: omit the flag entirely when no serial number
is found instead of passing "0" which SatDump may reject. Change start()
and start_from_file() to return (bool, str|None) tuples so error messages
propagate through to the HTTP response.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use run-btn/stop-btn classes and bottom placement instead of
btn-primary/btn-danger in a flex section, and preset-btn class
for band presets. Aligns with all other mode panels.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
TSCM RF scan now auto-detects HackRF via SDRFactory and uses
hackrf_sweep as an alternative to rtl_power. Also includes
improvements to listening post, rtlamr, weather satellite,
SubGHz, Meshtastic, SSTV, WeFax, and process monitor modules.
Fixes#154
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New signal mode for decoding Morse code (CW) transmissions via SDR.
Includes route blueprint, utility decoder, frontend UI, and tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When an error occurred with an out-of-range span (e.g. 30 MHz on
RTL-SDR), the span input kept the invalid value. Track the last
effective span from successful starts and reset the input on error.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace hardcoded "rtl_fm" references in wefax.py with the actual SDR
tool name so error messages correctly show "rx_fm" for non-RTL devices.
Use get_tool_path('rx_fm') in all SoapySDR command builders to match
the pattern already used for rx_sdr.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add pre-flight check for I/Q capture binary before spawning process
- Capture stderr from I/Q process for better error diagnostics
- Sync effective span value back to UI when backend adjusts it
- Use get_tool_path('rx_sdr') in Airspy, HackRF, LimeSDR, and SDRPlay
command builders to support custom install locations
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Make stopAprs() async and await backend stop completion before
re-enabling the Start button, preventing race where a late stop
request kills newly started processes
- Add cache-buster param to EventSource URL to prevent browser
SSE connection reuse between stop/start cycles
- Capture aprs_active_device locally in stream_aprs_output so the
old thread's finally block doesn't release a device claimed by
a new session
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace hardcoded rtl_fm with SDRFactory abstraction layer so WeFax works
with any supported SDR hardware, matching the pattern used by APRS and
other modes. RTL-SDR direct sampling flag preserved for HF reception.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Number(null) evaluates to 0 which passes Number.isFinite(),
causing aprsHasValidCoordinates(null, null) to return true.
This made initAprsMap() center the map at [0,0] (Gulf of Guinea)
at zoom 8 instead of the US default, hiding all station markers
off-screen.
Add null guards (lat != null && lon != null) to reject null/undefined
while still accepting 0 as a valid equator coordinate.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add flex-shrink: 0 to .section, .run-btn, and .stop-btn so flex
children maintain natural height and the sidebar scrolls instead
of compressing content on 1080p displays.
Fixes#151
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The default 1 MHz sample rate was too low for SatDump's meteor_m2-x_lrpt
pipeline, causing NOSYNC and 0.000dB SNR. Bumped to 2.4 MHz (SatDump
recommendation) and wired up the WEATHER_SAT_SAMPLE_RATE config value
so it actually gets passed to decoder.start() from both the auto-scheduler
and manual start route.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Timer threads now log on fire and catch all exceptions so scheduled
captures never die silently. Frontend connects SSE when the scheduler
is enabled (not only on manual Start) and polls /wefax/status every 10s
as a fallback so the UI stays in sync with auto-fired captures.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace window.open() with a fullscreen modal matching the SSTV
pattern: toolbar with download/delete SVG buttons, close button,
click-outside-to-close, and confirmation before delete.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
stop() was returning before the decode thread could save any partial
image to disk, so the frontend loadImages() call found nothing new.
Join the decode thread (2s timeout) before returning — with select()-
based reads the thread exits within ~0.5s so this stays responsive.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace blocking stdout.read() with select()-based non-blocking reads
so the decode thread responds to stop within 0.5s
- Make stop() non-blocking by releasing the lock before terminating the
process and removing the redundant wait()
- Move initial scanning SSE event from start() into the decode thread so
it fires after the frontend EventSource connects
- Update frontend stop() to give immediate UI feedback before the fetch
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
rtl_fm subprocess failures (missing tool, no SDR hardware) were silent —
add tool-path check and post-spawn health check in _start_pipeline(),
show errors prominently in the strip status bar (red text + red dot),
and include error detail in scheduler skip events.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Flash the Start button itself with amber pulse when clicked without a
station selected, and show "Select Station" in the strip status text
right next to the button so the error is immediately visible.
Add a 24-hour timeline bar with broadcast window markers, red UTC time
cursor, and countdown boxes (HRS/MIN/SEC) that tick down to the next
broadcast. Broadcasts show as amber blocks on the timeline track with
imminent/active visual states matching the weather satellite pattern.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix silent failure when starting without station/frequency selected by
flashing amber on status text and dropdowns. Add auto-capture scheduler
that uses fixed UTC broadcast schedules from station data to
automatically start/stop WeFax decoding at broadcast times.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement HF radiofax decoding with custom Python DSP pipeline
(rtl_fm USB → Goertzel/Hilbert demodulation), 33-station database
with broadcast schedules, audio waveform scope, live image preview,
and decoded image gallery. Amber/gold UI theme for HF distinction.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove profiles: [basic] from intercept service so docker compose up -d
works without --profile flag (fixes breaking change for existing deployments)
- Add missing Any import to routes/acars.py and routes/vdl2.py
- Reset last_message_time to None in ACARS and VDL2 clear endpoints
- Restore 131.725 and 131.825 to default ACARS frequencies (major US carriers)
- Copy VDL2 ACARS enrichment fields to top-level data dict instead of mutating
nested acars_payload (consistent with ACARS route pattern)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The shared audio queue (maxsize reduced from 80 to 20) was not flushed
when the monitor frequency changed — only when the monitor was disabled.
This caused up to 4 seconds of stale old-frequency audio to play after
clicking to tune, making click-to-tune appear non-functional.
Now flushes the queue whenever the VFO frequency changes, so audio at
the new frequency begins within ~50ms (one FFT frame).
1. Stop Monitor button was disabled during shared monitor retunes
because _syncMonitorButtons disabled the button whenever
_startingMonitor was true, even if the monitor was already active.
Now only disables during initial start (not retunes).
2. Click-to-tune was inconsistent because the shared monitor retune
(rearm after capture restart) captured the center frequency early
in _startMonitorInternal, then sent it via POST to /audio/start.
If the user clicked a new frequency during the async reconnect,
the POST carried the stale frequency and could override the click.
Now retunes use the live _monitorFreqMhz and send a WS tune sync
after reconnecting to ensure the backend has the latest VFO.
Two root causes for the waterfall/monitor lockup when scrolling past the
2.4 MHz RTL-SDR span:
1. safe_terminate() sent SIGKILL but never called wait(), leaving a
zombie process that kept the USB device handle open. The subsequent
capture restart failed the USB probe and the monitor could not use
the shared IQ path, falling back to a process-based monitor that
stole the SDR from the waterfall.
2. When the frontend created a new WebSocket after a failure, the old
handler's finally block called _set_shared_capture_state(running=False)
which could race with the new handler's running=True, making the
shared monitor path unavailable. Added a generation counter so only
the owning handler can clear the shared state.
When restarting capture for a new frequency, the USB handle from the
just-killed process may not be released by the kernel in time for the
rtl_test probe inside claim_sdr_device. Add retry logic (up to 4
attempts with 0.4s backoff) matching the pattern already used by the
audio start endpoint.
Also clean up stale shared-monitor state in the frontend error handler
so the monitor button is not left disabled when the capture restart
fails.
When changing frequency with shared monitor active, the monitor retune
could be silently dropped if a previous retune was still in-flight,
leaving the UI stuck on "Starting <freq>". After stopping and restarting
the waterfall, the monitor button could remain disabled because
_startingMonitor was never reset and _monitorRetuneTimer was not cleared.
- Cancel in-flight monitor start when queuing a new retune
- Always clear _pendingSharedMonitorRearm in started handler
- Clear _monitorRetuneTimer and reset _startingMonitor in stop()
Auto-generates a self-signed certificate into data/certs/ when
INTERCEPT_HTTPS=true, or accepts custom cert/key paths via
INTERCEPT_SSL_CERT and INTERCEPT_SSL_KEY. Resolves 400 errors
from browsers sending TLS ClientHello to the plain HTTP server.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
- _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>
- 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>
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>
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>
When dashboards (satellite, ADS-B, AIS) are loaded via iframe with
?embedded=true, the full navigation bar was still rendered, creating
a "UI in UI" effect. Pass the embedded query param from route handlers
to templates and conditionally skip the nav include.
Fixes#144
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Hit area: was Math.max(dotSize * 2, 15) — up to 24px radius around a 4px
dot. Now the CSS hover-flicker is fixed the large hit area is unnecessary
and was the reason dots activated when merely nearby. Changed to dotSize + 4
(proportional, 4px padding around the visual circle).
Overlap spread: compute all band positions first, then run an iterative
push-apart pass (spreadOverlappingDots) that nudges any two dots whose
arc gap is smaller than 2 * maxHitArea + 2px apart. Positions within a
band are stable across renders (same hash angle, same band = same output
before spreading) so dots don't shuffle on every update.
Z-order: sort visible devices by rssi_current ascending before rendering
so the strongest signal lands last in SVG order and receives clicks when
dots stack.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The root cause was in proximity-viz.css, not the JS:
.radar-device:hover { transform: scale(1.2); }
When the cursor entered a .radar-device, the 1.2x scale physically moved
the hit-area boundary, pushing the cursor outside it. The browser then
fired mouseout, the scale reverted, the cursor was back inside, mouseover
fired again, and the scale reapplied — a rapid enter/exit loop that looked
like the dot jumping and dancing.
Replace the geometry-changing scale with a brightness filter on the dot
circle only. filter: brightness() does not affect pointer-event hit testing
so there is no feedback loop, and the hover still gives clear visual
feedback. Also removes the transition: transform rule that was animating
the scale and contributing to the flicker.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace continuous estimated_distance_m-based radius with proximity band
snapping (immediate/near/far/unknown → fixed radius ratios of 0.15/0.40/
0.70/0.90). The proximity_band is computed server-side from rssi_ema which
is already smoothed, so it changes infrequently — dots now only move when
a device genuinely crosses a band boundary rather than on every RSSI
fluctuation.
Also removes the client-side EMA and positionCache added in the previous
commit, and reverts CSS style.transform back to SVG transform attribute to
avoid coordinate-system mismatch when the SVG is displayed at a scaled size.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The remaining jitter after the in-place DOM rewrite was caused by RSSI
fluctuations propagating directly into dot positions on every 200ms
update cycle.
Two fixes:
1. Client-side EMA (alpha=0.25) on x/y coordinates per device. Each
render blends 25% toward the new raw position and retains 75% of the
smoothed position, filtering high-frequency RSSI noise without hiding
genuine distance changes. positionCache is keyed by device_key and
cleared on device removal or radar reset.
2. CSS transition (transform 0.6s ease-out) on each wrapper element.
Switching from SVG transform attribute to style.transform enables
native CSS transitions, so any remaining position change (e.g. a band
crossing) animates smoothly rather than snapping.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of rebuilding devicesGroup.innerHTML on every render, mutate
existing SVG elements in-place (update transforms, attributes, class
names) and only create/remove elements when devices genuinely appear
or disappear from the visible set.
This eliminates the root cause of both the jitter and the blank-radar
regression: hover state can never be disrupted by a render because the
DOM elements under the cursor are never destroyed. The isHovered /
renderPending / interactionLockUntil state machine and its associated
mouseover/mouseout listeners are removed entirely — they are no longer
needed. A shared buildSelectRing() helper deduplicates the animated
selection ring construction used by renderDevices() and
applySelectionToElement(). Closes#143.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace capture-phase mouseenter/mouseleave with bubbling mouseover/mouseout
for tracking hover state in the ProximityRadar component.
The capture-phase approach caused two problems:
1. Moving between sibling child elements (hit-area → dot circle) fired
mouseleave, prematurely clearing isHovered and triggering a DOM rebuild
that caused visible jitter.
2. When renderDevices() rebuilt innerHTML, the browser fired mouseleave for
the destroyed element with relatedTarget pointing at the newly created
element at the same position, leaving isHovered permanently stuck at true
and suppressing all future renders.
The fix uses mouseover/mouseout (which bubble) with devicesGroup.contains()
to reliably detect whether the cursor genuinely left the device group, immune
to innerHTML rebuilds. Fixes both WiFi and Bluetooth proximity radars as they
share this component. Closes#143.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Persist ACARS/VDL2 messages across page refresh via new /acars/messages
and /vdl2/messages endpoints backed by FlightCorrelator
- Add clear buttons to ACARS/VDL2 sidebars and right-panel datalink section
with /acars/clear and /vdl2/clear endpoints
- Fix right-panel DATALINK MESSAGES flickering by diffing innerHTML before
updating, with opacity transition for smooth refreshes
- Add aircraft deselect toggle (click selected aircraft again to deselect)
- Enrich VDL2 messages with ACARS label translation (label_description,
message_type, parsed fields) matching existing ACARS translator
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ACARS and VDL2 conflict warnings were hardcoded to check device === '0'
instead of comparing against the actual ADS-B device (adsbActiveDevice).
This caused false warnings when ADS-B used a different device index.
Also removes hardcoded device-1 defaults for ACARS/VDL2 selectors —
users should pick their own device based on their antenna setup.
Adds profiles: [basic] to the intercept service in docker-compose so it
doesn't port-conflict with intercept-history when using --profile history.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ACARS messages use IATA codes (e.g. UA2412) while ADS-B uses ICAO
callsigns (e.g. UAL2412). Add a translation layer so the two can
match, enabling click-to-highlight and datalink message correlation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Click an ACARS message in the left sidebar to zoom the map to the
matching aircraft and open its detail panel. Aircraft with ACARS
activity show a DLK badge in the tracked list. Default NA frequency
changed to only check 131.550 on initial load.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add ACARS label translation, message classification, and field parsers
so decoded messages show human-readable descriptions instead of raw
label codes (H1, DF, _d, 5Z, etc.). Integrate translated ACARS
messages into the ADS-B aircraft detail panel and add a live message
feed to the standalone ACARS mode.
- New utils/acars_translator.py with ~50 label codes, type classifier,
and parsers for position reports, engine data, weather, and OOOI
- Enrich messages at ingest in routes/acars.py with translation fields
- Backfill translation in /adsb/aircraft/<icao>/messages endpoint
- ADS-B dashboard: DATALINK MESSAGES section in aircraft detail panel
with auto-refresh, color-coded type badges, and parsed field display
- Standalone ACARS mode: scrollable live message feed (max 30 cards)
- Fix default N. America ACARS frequencies to 131.550/130.025/129.125
- Unit tests covering all translator functions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The slant correction was severely under-correcting because bwd=50 caused
the sync deviation measurements to saturate after only ~25 lines (for a
2-sample/line SDR clock drift). Lines 25-256 all reported deviation=-50,
pulling the linear regression slope toward zero.
Increase bwd and fwd to 800 samples each — sufficient to track cumulative
drift from up to ~±200 ppm SDR clock offset across the full 256-line image.
Also use a full-sync-length (432-sample) Goertzel window instead of 1/3
length, giving ~111 Hz frequency resolution to cleanly separate the 1200 Hz
sync tone from 1500 Hz pixel data. Search is stepped at 5 samples (~0.1 ms)
for efficiency, keeping the goertzel_batch batch size at ~320 windows/line.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previous attempts to correct slant by altering R-channel placement and
buffer consumption caused cascading failures: a false positive in B pixel
data would misplace R, then the wrong consumed value misaligned the next
line's G, and the error compounded across all 256 lines.
New approach (safe by design):
- Sync search is measurement-only: never touches pos or consumed, so
a noisy or wrong measurement cannot corrupt the current or future lines.
- Per-line deviation (measured sync position minus expected) is recorded
in self._sync_deviations throughout the decode.
- get_image() fits a line through the deviations (linear regression) to
estimate the per-line SDR clock drift rate, then applies a horizontal
shear to the assembled PIL image: each row is shifted by
-round(row × drift_rate × width / channel_samples) pixels.
- Worst case (all measurements fail): no correction applied, image
quality identical to the pre-change baseline.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The step-49 coarse scan introduced up to ±24 sample uncertainty in R
channel placement. When accumulated SDR clock drift pushed the actual
sync 35+ samples early in the search region, the step-49 windows could
land on the B-pixel tail and return position 0, misplacing R by ~50
samples (~16 pixel colour shift) — worse than no correction at all.
Replace with a vectorised goertzel_batch sliding-window scan at step=1
over a short window (sync_duration / 3 ≈ 3 ms), giving single-sample
accuracy. Use consumed=pos (instead of max(pos,line_samples)) when the
sync is found, so the next line starts at its correct separator and
per-line timing errors stop accumulating entirely.
Falls back to the fixed-offset path whenever the sync is not found
(e.g. noisy signal), preserving the pre-change baseline quality.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous sync search used search_margin = line_samples/10 (~306
samples for Scottie2), reaching deep into B channel pixel data behind
pos and well past the expected sync end ahead of pos.
When _find_sync returned a position in the late portion of that wide
region, pos + R_channel_samples exceeded the buffer length. The
buffer-too-short guard in _decode_line then returned early without
consuming data or advancing the line counter, causing the stall guard
in feed() to permanently break the decode loop.
Fix: use a 50-sample backward margin (covers >130 ppm SDR drift) and
a forward margin capped to whatever the current buffer can safely
support for the R channel. A final candidate-position check before
committing pos ensures no overflow is possible.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Scottie modes place their horizontal sync pulse between the Blue and Red
channels. The decoder was using a fixed offset to skip over it, so any
SDR clock error accumulated line-by-line and produced a visible diagonal
slant in the decoded image.
Fix: search for the actual 1200 Hz sync pulse in a ±10% window around
the expected position before decoding the Red channel, then align to the
real pulse. This resets accumulated clock drift on every scanline, the
same way Martin and Robot modes already handle their front-of-line sync.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix wrong VIS codes for PD90 (96→99), PD120 (93→95), PD180 (95→97),
PD240 (113→96), and ScottieDX (55→76). This caused PD180 to be detected
as PD90 and PD120 to fail entirely.
Replace batch Goertzel pixel decoding with analytic signal (Hilbert
transform) FM demodulation. The Goertzel approach used 96-sample windows
with ~500 Hz resolution — wider than the 800 Hz pixel frequency range —
making accurate pixel decoding impossible for fast modes like Martin2
and Scottie2. The Hilbert method computes per-sample instantaneous
frequency, matching the approach used by QSSTV and other professional
SSTV decoders.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tolerate intermittent ambiguous windows during leader detection (up to
3 consecutive misses), use energy-based break detection when tone
classification fails at leader-break boundary, and add single-bit VIS
error correction for parity-bit and data-bit corruption on noisy HF.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
VIS detection recognized these modes but ALL_MODES had no decoder specs,
causing silent decode failures on common HF frequencies like 14.230 MHz.
Also emit a user-visible SSE event when an unsupported VIS code is detected.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous fix (f29ae3d) introduced a regression: when VIS parity
check failed or the VIS code was unrecognized, the detector entered
DETECTED state permanently and never resumed scanning. Now it resets
to IDLE on validation failure and only enters DETECTED on success.
Also resets partial image progress counter between consecutive decodes
and adds SDR device claiming to general SSTV route to prevent conflicts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
VISDetector._process_window() was calling self.reset() inside the
STOP_BIT handler, wiping self._buffer before feed() could advance
past the triggering window. All audio samples buffered after the
VIS STOP_BIT (the start of the first scan line) were silently
discarded, causing the image decoder to begin decoding mid-stream
with no alignment reference. The result was every scan line being
desynchronised from the first, producing the diagonal stripes and
scrambled colours seen in decoded images.
Fix: remove the premature reset() from _process_window(). The
STOP_BIT handler now sets state=DETECTED and returns the result.
A new remaining_buffer property exposes the post-VIS samples.
_decode_audio_stream() and decode_file() capture those samples
before calling reset(), then immediately feed them into the newly
created SSTVImageDecoder so decoding begins from sample 0 of
the first sync pulse.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
If dump1090-mutability was installed by a previous run and later
removed (e.g. by apt removing it as a reverse dep), the symlink at
/usr/local/sbin/dump1090 is left pointing at a non-existent target.
cmd_exists finds the broken symlink and treats dump1090 as installed,
so the real install is skipped and running dump1090 gives
"No such file or directory".
Before the install check, resolve the command path and delete it if
it exists in PATH but is not executable (broken symlink).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The apt-removal approach caused cascading failures: removing librtlsdr0
swept out dump1090-mutability and other reverse deps, then source builds
reinstalled librtlsdr-dev (pulling librtlsdr0 back), and the dump1090
subshell crashed because kill "" (empty progress_pid after progress_pid=)
returned non-zero and fired the global ERR trap.
Switch to a targeted ldconfig priority file instead:
- Write /etc/ld.so.conf.d/00-local-first.conf containing /usr/local/lib
- Files named 00-* sort before aarch64-linux-gnu.conf alphabetically,
so ldconfig lists /usr/local/lib/librtlsdr.so.0 (Blog) first
- apt librtlsdr0, rtl-sdr, dump1090-mutability etc. are never touched
- Source build functions keep their unconditional apt_install librtlsdr-dev
Also fix the dump1090 EXIT trap: guard kill/wait against empty
progress_pid so it does not fire the ERR trap after a clean exit 0.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The global ERR trap (trap 'on_error $LINENO' ERR) fires on any non-zero
exit. After `kill $progress_pid`, `wait $progress_pid` returns 143
(128+SIGTERM), triggering the trap and aborting the build even when
make itself succeeded. Add `|| true` to all five wait calls in
install_dump1090_from_source_debian (inline and EXIT trap).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When Blog drivers are installed, apt rtl-sdr/librtlsdr0/librtlsdr-dev
are removed to ensure the Blog library in /usr/local/lib is the only
one ldconfig sees. But four source-build functions each called
`apt_install librtlsdr-dev`, which re-pulled librtlsdr0 from apt and
immediately re-shadowed the Blog library.
Fix: each function now checks `pkg-config --exists librtlsdr` first;
if the Blog drivers (or any other /usr/local install) already provide
the headers and .pc file the apt install is skipped entirely.
Also add a post-removal guard in install_rtlsdr_blog_drivers_debian:
after apt removes librtlsdr0 it may silently sweep out dump1090-mutability
as a reverse dep. The guard detects this and rebuilds dump1090 from
source immediately, using the Blog drivers' headers via pkg-config.
Affected functions:
- install_dump1090_from_source_debian
- install_acarsdec_from_source_debian
- install_dumpvdl2_from_source_debian
- install_aiscatcher_from_source_debian
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removing only the rtl-sdr binary package left librtlsdr0 (the library)
installed at /lib/aarch64-linux-gnu/librtlsdr.so.0. ldconfig lists the
multiarch path before /usr/local/lib, so even the Blog driver binary
(/usr/local/bin/rtl_test) was loading the old apt library — which has
no R828D/V4 tuner support — causing the PLL-not-locked / deaf dongle
symptom.
Now remove rtl-sdr, librtlsdr0, and librtlsdr-dev together so the only
librtlsdr.so.0 in the ldconfig cache is the Blog drivers' copy in
/usr/local/lib.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs caused RTL-SDR dongles to be deaf after setup on Raspberry Pi:
1. The apt `rtl-sdr` package was left installed alongside the Blog
drivers, creating a binary/library ambiguity. Anything linking or
calling the apt binaries in /usr/bin used the non-V4-aware library
from /usr/lib instead of the Blog drivers in /usr/local. Fix: remove
the apt package immediately after a successful Blog driver build.
2. `blacklist_kernel_drivers_debian` returned early with "already
present" without ever running `modprobe -r`, so dvb_usb_rtl28xxu
could remain loaded and hold the device in DVB mode (rtl_test sees
the USB device but the tuner is unconfigured). Fix: always run the
module unload loop regardless of whether the blacklist file is new.
Also add `update-initramfs -u` so the blacklist survives reboots.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous logic installed rtl-sdr via apt first, then gated the Blog
driver install on cmd_exists rtl_test — which was always true, so V4
drivers were never installed. Replace with a yes/no prompt (default y,
backward-compatible) guarded by IS_DRAGONOS for pre-configured distros.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add missing setThemePreference() and setAnimationsEnabled() functions
to settings-manager.js; sync theme/animations dropdowns in _updateUI
- Fix base.html toggleTheme() saving to wrong localStorage key ('theme'
instead of 'intercept-theme'), causing theme not to persist in ADS-B
and AIS dashboards; also sync button icon and persist to server
- Add [data-theme="light"] CSS variable overrides to adsb_dashboard.css
and ais_dashboard.css so the dashboards respond to light theme
- Fix GPS sky view canvas (gps.js) to read grid/label colours from CSS
variables instead of hardcoded dark hex values; add MutationObserver
to redraw immediately on theme change
- Fix satellite_dashboard.html polar plot functions to read background,
accent and text colours from CSS variables
Closes#139
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The .icon base class (global-nav.css) forces display:inline-flex and
width/height of 18-20px, overriding the intended 48px size and causing
the SVG to render inline inside the h2 rather than as a block above it.
Override with display:block, explicit 48px dimensions, and auto margins
so the icon renders centred above the DISCLAIMER heading.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the | tail -5 filter with pip --quiet and 2>/dev/null to
silence 'Requirement already satisfied' lines and the harmless
send2trash metadata warning that were leaking to the terminal.
The import verification step still catches real install failures.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
If lua_utils.cpp has no trailing newline the closing pragma was appended
directly to the last line (};#pragma GCC diagnostic pop), causing a
stray '#' compile error on GCC 13+ / Raspberry Pi OS Bookworm.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On Raspberry Pi OS Bookworm the package is libvolk2-dev, not libvolk-dev.
Also soft-fail optional SDR hardware libs (libjemalloc, libnng, SoapySDR,
HackRF, LimeSuite) so a missing package no longer aborts the SatDump build.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Weather Satellite:
- Fix duplicate event listeners on mode re-entry via locationListenersAttached guard
- Add suspend() to stop countdown/SSE stream when switching away from the mode
- Call WeatherSat.suspend() in switchMode() when leaving weathersat
- Fix toggleScheduler() to take the checkbox element as source of truth,
preventing both checkboxes fighting each other
- Reset isRunning/UI state after auto-capture completes (scheduler path)
- Always re-select first pass and reset selectedPassIndex after loadPasses()
- Keep timeline cursor in sync inside selectPass()
- Add seconds to pass ID format to avoid collisions on concurrent passes
- Improve predict_passes() comment clarity; fix trajectory comment
ADS-B dashboard:
- Batch altitude-colour trail segments into runs of same-colour polylines,
reducing Leaflet layer count from O(trail length) to O(colour changes)
for significantly better rendering performance with many aircraft
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Single-threaded make on a Raspberry Pi 5 could take 5-10+ minutes
with no output, making the setup appear hung. Now uses all available
CPU cores and prints a "still compiling" heartbeat every 20s.
Also prints build log tail on failure for easier debugging.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the old SDR/RF, Wireless, Security, Space layout with a cleaner
five-group structure. Tracking (Aircraft, Vessels, APRS, GPS) becomes its
own top-level group; Meshtastic moves to Wireless; WebSDR and Spy Stations
move to Intel. Also fixes BT Locate overflow/min-height CSS.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New mode providing real-time space weather data from NOAA SWPC, NASA SDO,
and HamQSL APIs. Includes Kp index, solar wind, X-ray flux charts, HF band
conditions, D-RAP absorption maps, aurora forecast, solar imagery, flare
probability, and active solar regions. No SDR hardware required.
Bumps version to 2.20.0. Updates all documentation including README, FEATURES,
USAGE, UI_GUIDE, help modal, and GitHub Pages site.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update default ACARS frequencies for North America to 131.725/131.825 MHz and add ISS (145.825 MHz) as a selectable APRS frequency region.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix SatDump crash reported as "Capture complete" by collecting exit
status via process.wait() before checking returncode
- Fix PTY file descriptor double-close race between stop() and reader
thread by adding thread-safe _close_pty() helper with dedicated lock
- Fix image watcher missing final images by doing post-exit scans after
SatDump process ends, using threading.Event for fast wakeup
- Fix failed image copy permanently skipping file by only marking as
known after successful copy
- Fix frontend error handler not resetting isRunning, preventing new
captures after a crash
- Fix console auto-hide timer leak on rapid complete/error events
- Fix ground track and auto-scheduler ignoring shared ObserverLocation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The button is unnecessary since analytics expands the sidebar to
full width with no output panel to reveal.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sidebar sections are collapsed by default on DOMContentLoaded. When
switching to analytics mode, expand all its sections so the dashboard
content is visible immediately.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Analytics is a sidebar-only mode with no visuals container, so the
output panel was rendering on top of the analytics content. Add
analytics-active class to expand the sidebar full-width and hide
the output panel when in analytics mode.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The decoder output panel was not being hidden when entering analytics
mode, causing it to render on top of the analytics dashboard.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extend cross-mode analytics to include ACARS/VDL2 message counts, APRS
stations, and Meshtastic messages. Refactor count helpers into reusable
_safe_len() and _safe_route_attr() utilities. Add health checks for
rtlamr, dmr, and meshtastic modes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The analytics summary, health, and export were only reading from legacy
DataStores (app_module.wifi_networks, bt_devices) which the v2 WiFi and
Bluetooth scanners don't populate. Now checks v2 scanner singletons
first and falls back to legacy stores.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a unified analytics mode under the Security nav group that aggregates
data across all signal modes. Includes emergency squawk alerting (7700/7600/7500),
vertical rate anomaly detection, ACARS/VDL2-to-ADS-B flight correlation,
geofence zones with enter/exit detection for aircraft/vessels/APRS stations,
temporal pattern detection, RSSI history tracking, Meshtastic topology mapping,
and JSON/CSV data export.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When navigating away from the dashboard and back, the page reloads with
no knowledge of running decoders. Add status checks on page load to sync
UI state and reconnect SSE streams. Also add auto-reconnect on SSE error
with guard conditions to prevent loops when intentionally stopped.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Check process exit code when SatDump terminates — non-zero exit now
emits an error status with the exit code instead of falsely reporting
a successful capture completion.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:11:33 +00:00
402 changed files with 86091 additions and 33055 deletions
All notable changes to iNTERCEPT will be documented in this file.
## [2.26.11] - 2026-03-14
### Fixed
- **APRS map ignores configured observer position** — The APRS map always fell back to the centre of the US (39.8°N, 98.6°W) when no live GPS fix was available, ignoring the observer position configured in `.env` (`INTERCEPT_DEFAULT_LAT` / `INTERCEPT_DEFAULT_LON`). Now seeds the APRS user location from the shared observer location on page load, so the map centres correctly and distance calculations work. (#193)
---
## [2.26.10] - 2026-03-14
### Fixed
- **APRS stop timeout and inverted SDR device status** — The APRS stop endpoint terminated two processes sequentially (up to 4s) while the frontend timed out at 2.2s, causing console errors and the SDR status panel to show stale state (active after stop, idle during use). Now releases the SDR device immediately and terminates processes in a background thread so the response returns instantly. (#194)
---
## [2.26.9] - 2026-03-14
### Fixed
- **ADS-B bias-t support for RTL-SDR Blog V4** — When dump1090 lacks native `--enable-biast` support, the system now falls back to `rtl_biast` (from RTL-SDR Blog drivers) to enable bias-t power before starting dump1090. The Blog V4's built-in LNA requires bias-t to receive ADS-B signals. (#195)
---
## [2.26.8] - 2026-03-14
### Fixed
- **acarsdec build failure on macOS** — `HOST_NAME_MAX` is Linux-specific (`<limits.h>`) and undefined on macOS, causing 3 compile errors in `acarsdec.c`. Now patched with `#define HOST_NAME_MAX 255` before building. Also fixed deprecated `-Ofast` flag warning on all macOS architectures (was only patched for arm64). (#187)
---
## [2.26.7] - 2026-03-14
### Fixed
- **Health check SDR detection on macOS** — `timeout` (GNU coreutils) is not available on macOS, causing `rtl_test` to silently fail and report "No RTL-SDR device found" even when one is connected. Now tries `timeout`, then `gtimeout` (Homebrew coreutils), then falls back to a background process with manual kill. (#188)
---
## [2.26.6] - 2026-03-14
### Fixed
- **Oversized branded 'i' logo on dashboards** — `.logo span { display: inline }` in dashboard CSS had higher specificity (0,1,1) than `.brand-i { display: inline-block }` (0,1,0), forcing the branded "i" SVG to render as inline which ignores width/height. Added `.logo .brand-i` selector (0,2,0) to retain `inline-block` display. (#189)
---
## [2.26.5] - 2026-03-14
### Fixed
- **Database errors crash entire UI** — `get_setting()` now catches `sqlite3.OperationalError` and returns the default value instead of propagating the exception. Previously, if the database was inaccessible (e.g. root-owned `instance/` directory from running with `sudo`), the `inject_offline_settings` context processor would crash every page render with a 500 Internal Server Error. (#190)
---
## [2.26.4] - 2026-03-14
### Fixed
- **Environment Configurator crash** — `read_env_var()` crashed with "Setup failed at line 2333" when `.env` existed but didn't contain the variable being looked up. `grep` returned exit code 1 (no match), which `pipefail` propagated and `set -e` turned into a fatal error. Fixed by appending `|| true` to the pipeline. (#191)
---
## [2.26.3] - 2026-03-13
### Fixed
- **SatDump AVX2 crash** — SatDump now compiles with `-march=x86-64` on x86_64 platforms (Docker and `setup.sh`), preventing "Illegal instruction" crashes on CPUs without AVX2. SIMD plugins still use runtime detection for acceleration on capable hardware. (#185)
---
## [2.26.2] - 2026-03-13
### Fixed
- **Docker startup crash** — `.dockerignore` excluded the entire `data/` directory, which is now a Python package (`data.oui`, `data.patterns`, `data.satellites`). Caused `ModuleNotFoundError: No module named 'data.oui'` on container startup. Fixed by only excluding non-code files from `data/`.
---
## [2.26.1] - 2026-03-13
### Fixed
- **Default admin credentials** — Default `ADMIN_PASSWORD` changed from empty string to `admin`, matching the README documentation (`admin:admin`)
- **Config credential sync** — Admin password changes in `config.py` or via `INTERCEPT_ADMIN_PASSWORD` env var now sync to the database on restart, without needing to delete the DB
---
## [2.26.0] - 2026-03-13
### Fixed
- **SSE fanout crash** - `_run_fanout` daemon thread no longer crashes with `AttributeError: 'NoneType' object has no attribute 'get'` when source queue becomes None during interpreter shutdown
- **Branded logo FOUC** - Added inline `width`/`height` to branded "i" SVG elements across 10 templates to prevent oversized rendering before CSS loads; refresh no longer needed
---
## [2.25.0] - 2026-03-12
### Added
- **SSEManager** - Centralized SSE connection management with exponential backoff reconnection and visual connection status indicator
- **Loading button states** - `withLoadingButton()` utility for async action buttons across all modes
- **Actionable error reporting** - `reportActionableError()` added to 5 mode JS files for user-friendly error messages
- SSE fanout backlog causing delayed updates across all modes
- SSE reconnect packet loss during client reconnection
- Waterfall monitor tuning race conditions
- Mode FOUC (flash of unstyled content) on initial navigation
- Various Morse decoder stability and lifecycle fixes
---
## [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
# Initial setup (installs dependencies and configures SDR tools)
# First-time setup (interactive wizard with install profiles)
./setup.sh
# Run the application (requires sudo for SDR/network access)
# Or headless full install
./setup.sh --non-interactive
# Or install specific profiles
./setup.sh --profile=core,weather
# Run with production server (gunicorn + gevent, handles concurrent SSE/WebSocket)
sudo ./start.sh
# Or for quick local dev (Flask dev server)
sudo -E venv/bin/python intercept.py
# Or activate venv first
source venv/bin/activate
sudo -E python intercept.py
# Other setup utilities
./setup.sh --health-check # Verify installation
./setup.sh --postgres-setup # Set up ADS-B history database
./setup.sh --menu # Force interactive menu
```
### Testing
@@ -69,8 +79,10 @@ mypy .
## Architecture
### Entry Points
-`intercept.py` - Main entry point script
-`app.py` - Flask application initialization, global state management, process lifecycle, SSE streaming infrastructure
-`setup.sh` - Menu-driven installer with profile system (wizard, health check, PostgreSQL setup, env configurator, update, uninstall). Sources `.env` on startup via `start.sh`.
-`start.sh` - Production startup script (gunicorn + gevent auto-detection, CLI flags, HTTPS, `.env` sourcing, fallback to Flask dev server)
-`intercept.py` - Direct Flask dev server entry point (quick local development)
-`app.py` - Flask application initialization, global state management, process lifecycle, SSE streaming infrastructure, conditional gevent monkey-patch
### Route Blueprints (routes/)
Each signal type has its own Flask blueprint:
@@ -121,7 +133,7 @@ Each signal type has its own Flask blueprint:
### Key Patterns
**Server-Sent Events (SSE)**: All real-time features stream via SSE endpoints (`/stream_pager`, `/stream_sensor`, etc.). Pattern uses `queue.Queue` with timeout and keepalive messages.
**Server-Sent Events (SSE)**: All real-time features stream via SSE endpoints (`/stream_pager`, `/stream_sensor`, etc.). Pattern uses `queue.Queue` with timeout and keepalive messages. Under gunicorn + gevent, each SSE connection is a lightweight greenlet instead of an OS thread.
**Process Management**: External decoders run as subprocesses with output threads feeding queues. Use `safe_terminate()` for cleanup. Global locks prevent race conditions.
@@ -152,7 +164,7 @@ Each signal type has its own Flask blueprint:
- **Mode Integration**: Each mode needs entries in `index.html` at ~12 points: CSS include, welcome card, partial include, visuals container, JS include, `validModes` set, `modeGroups` map, classList toggle, `modeNames`, visuals display toggle, titles, and init call in `switchMode()`
### Docker
-`Dockerfile` - Single-stage build with all SDR tools compiled from source (dump1090, AIS-catcher, slowrx, SatDump, etc.)
-`Dockerfile` - Single-stage build with all SDR tools compiled from source (dump1090, AIS-catcher, slowrx, SatDump, etc.). CMD runs `start.sh` (gunicorn + gevent)
-`docker-compose.yml` - Two profiles: `basic` (standalone) and `history` (with Postgres for ADS-B)
-`build-multiarch.sh` - Multi-arch build script for amd64 + arm64 (RPi5)
- Data persisted via `./data:/app/data` volume mount
- **BT Locate** - SAR Bluetooth device location with GPS-tagged signal trail mapping and proximity alerts
- **WiFi Locate** - Locate WiFi access points by BSSID with real-time signal meter, distance estimation, and proximity audio
- **GPS** - Real-time GPS position tracking with live map, speed, altitude, and satellite info
- **TSCM** - Counter-surveillance with RF baseline comparison and threat detection
- **Meshtastic** - LoRa mesh network integration
- **Space Weather** - Real-time solar and geomagnetic data from NOAA SWPC, NASA SDO, and HamQSL (no SDR required)
- **Spy Stations** - Number stations and diplomatic HF network database
- **Remote Agents** - Distributed SIGINT with remote sensor nodes
- **Offline Mode** - Bundled assets for air-gapped/field deployments
---
## Installation / Debian / Ubuntu / MacOS
## CW / Morse Decoder Notes
```
Live backend:
- Uses `rtl_fm` piped into `multimon-ng` (`MORSE_CW`) for real-time decode.
Recommended baseline settings:
- **Tone**: `700 Hz`
- **Bandwidth**: `200 Hz` (use `100 Hz` for crowded bands, `400 Hz` for drifting signals)
- **Threshold Mode**: `Auto`
- **WPM Mode**: `Auto`
Auto Tone Track behavior:
- Continuously measures nearby tone energy around the configured CW pitch.
- Steers the detector toward the strongest valid CW tone when signal-to-noise is sufficient.
- Use **Hold Tone Lock** to freeze tracking once the desired signal is centered.
Troubleshooting (no decode / noisy decode):
- Confirm demod path is **USB/CW-compatible** and frequency is tuned correctly.
- If multiple SDRs are connected and the selected one has no PCM output, Morse startup now auto-tries other detected SDR devices and reports the active device/serial in status logs.
- Match **tone** and **bandwidth** to the actual sidetone/pitch.
- Try **Threshold Auto** first; if needed, switch to manual threshold and recalibrate.
- Use **Reset/Calibrate** after major frequency or band condition changes.
- Raise **Minimum Signal Gate** to suppress random noise keying.
./setup.sh# Interactive menu (first run launches setup wizard)
sudo ./start.sh
```
On first run, `setup.sh` launches a **guided wizard** that detects your OS, lets you choose install profiles, sets up the Python environment, and optionally configures environment variables and PostgreSQL.
On subsequent runs, it opens an **interactive menu**:
```
INTERCEPT Setup Menu
════════════════════════════════════════
1) Install / Add Modules
2) System Health Check
3) Database Setup (ADS-B History)
4) Update Tools
5) Environment Configurator
6) Uninstall / Cleanup
7) View Status
0) Exit
```
> **Production vs Dev server:** `start.sh` auto-detects gunicorn + gevent and runs a production server with cooperative greenlets — handles multiple SSE/WebSocket clients without blocking. Falls back to Flask dev server if gunicorn is not installed. For quick local development, you can still use `sudo -E venv/bin/python intercept.py` directly.
### Install Profiles
Choose what to install during the wizard or via menu option 1:
The ADS-B history feature persists aircraft messages to Postgres for long-term analysis.
Use the **Environment Configurator** (menu option 5) to interactively set any `INTERCEPT_*` variable. Settings are saved to a `.env` file that `start.sh` sources automatically on startup.
You can also create or edit `.env` manually:
```bash
# .env (auto-loaded by start.sh)
INTERCEPT_PORT=5050
INTERCEPT_ADSB_AUTO_START=true
INTERCEPT_DEFAULT_LAT=51.5074
INTERCEPT_DEFAULT_LON=-0.1278
```
### ADS-B History (Optional)
The ADS-B history feature persists aircraft messages to PostgreSQL for long-term analysis.
**Automated setup (local install):**
```bash
./setup.sh --postgres-setup
# Or use menu option 3: Database Setup
```
This will install PostgreSQL if needed, create the database/user/tables, and write the connection settings to `.env`.
**Docker:**
```bash
# Start with ADS-B history and Postgres
docker compose --profile history up -d
```
Set the following environment variables (for example in a`.env` file):
Set the following environment variables (in`.env`):
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.