Data pipeline (critical): scanners/detectors now write to a separate _obs_queue;
a relay thread reads observations and calls correlator.process(), which emits
processed DroneContact dicts to drone_queue for SSE. Without this the SSE stream
received raw unserializable dataclass objects causing JSON errors.
Frontend (critical):
- Add droneContactList container to drone.html so contact cards render
- Add droneMap container and initialize Leaflet in drone.js init()
- Define dsc-distress-pulse keyframes in drone.css (was referenced but missing)
- Fix SSE reconnect: null _sse before setTimeout to prevent _connectSSE no-op loop
Other fixes:
- Validate rtl_sdr_index with validate_device_index(), return 400 on bad input
- Move _ensure_workers() inside _drone_lock to prevent double-initialization race
- Add double-call guard to RemoteIDScanner.start()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Concurrent POST /drone/start under gevent would race on _drone_running;
lock mirrors the ais_lock / dsc_lock pattern used throughout the codebase.
Null guards prevent AttributeError if worker constructors fail.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements Task 5: creates routes/drone.py with /status, /contacts,
/start, /stop, and /stream (SSE fanout) endpoints; registers the
drone_bp blueprint in routes/__init__.py; adds drone_queue to app.py;
adds opendroneid>=1.0 to requirements.txt. All 39 drone tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace _running bool with threading.Event for correct cross-thread visibility
- Add _proc_lock to guard _rtl_proc/_hackrf_proc across worker/main threads
- Use register_process + safe_terminate (pipe close + SIGKILL fallback on timeout)
- Compute HackRF frequency as band midpoint (hz_low+hz_high)//2, not hz_low
- Guard start() for idempotency — double-call no longer leaks threads
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements RFDetector class that wraps rtl_433 (433/868MHz) and hackrf_sweep
(2.4/5.8GHz) subprocesses, emitting RFObservation objects onto a shared queue.
Includes signature matching, frequency band validation, and power thresholding.
- _handle_rtl433_line(): Parse JSON output, filter drone bands, emit observations
- _handle_hackrf_line(): Parse CSV output, average power levels, threshold at -90dBm
- start()/stop(): Manage subprocess threads for concurrent RF detection
- Graceful handling of missing tools (rtl_433, hackrf_sweep)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevents a non-running sniffer object being stored when start() raises
(e.g. permission denied or interface not found).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two root causes behind HackRF showing as unavailable when tools are installed:
1. get_tool_path() didn't search /usr/local/bin on Linux. HackRF tools built
from source (as in the Dockerfile) land there, but the path wasn't checked
when sudo/service environments have a restricted PATH.
2. check_hackrf() only tested hackrf_transfer, but the health check tests
hackrf_info — both come from the same apt package but a user could have one
visible and not the other. Now either binary confirms the tools are present.
hackrf_transfer is still required for actual RX/TX operations.
Fixes#212
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
data/*.json was excluded by .dockerignore, so wefax_stations.json was
never copied into the container image. The volume mounts in docker-compose
only cover subdirectories (weather_sat, adsb, etc.), leaving the stations
file inaccessible at runtime — causing the /wefax/stations route to 500
and the station/frequency dropdowns to appear empty.
Also adds a graceful file-existence check in load_stations() so a missing
file logs a warning and returns an empty list instead of an unhandled
FileNotFoundError.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. iss_schedule() was importing TLE_SATELLITES directly from data/satellites.py
(hardcoded, 446 days stale) instead of the live _tle_cache kept fresh by
the 24h auto-refresh. Add get_cached_tle() to satellite.py and use it.
2. Ground track was a fake sine wave (inclination * sin(phase)) that mapped
longitude offset directly to orbital phase, ignoring Earth's rotation under
the satellite (~23° westward shift per orbit). Replace with a /sstv/iss-track
endpoint that propagates the orbit via skyfield SGP4 over ±90 minutes, and
update the frontend to call it. Past/future track rendered with separate
polylines (dim solid vs bright dashed).
3. refresh_tle_data() updated _tle_cache in memory but never persisted back to
data/satellites.py, so every restart reloaded the stale hardcoded TLE. Add
_persist_tle_cache() called after each successful refresh.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove backdrop-filter: blur(5px) from .card and .panel — on ARM/Linux
Chromium this is software-rendered, causing severe CPU overhead at 42+
instances. The opaque surface gradient makes blur imperceptible anyway.
- Remove inset vignette box-shadow from .panel added in 51c1014
- Rewrite panel-pulse keyframes to animate opacity only (was box-shadow,
which triggers CPU repaint every frame; opacity is compositor-only)
- Gate body::before and .visuals-container::after scanline pseudo-elements
under [data-animations="off"] — the toggle was blind to both
- Gate panel-indicator pulse under [data-animations="off"] for consistency
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace hardcoded L.map() + CartoCD dark tile layer with MapUtils.init()
and add tactical overlays. Adds test verifying the cartocdn URL is gone.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace custom createFallbackGridLayer/upgradeRadarTilesFromSettings with
MapUtils.init(), add range ring + reticle + HUD panel overlays via
MapUtils.addTacticalOverlays(), and wire updateCount/updateReticle into
the SSE aircraft handler and drawRangeRings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add offline.stadia_key to OFFLINE_DEFAULTS in routes/offline.py
- Add stadia_dark and tactical tile providers to Settings.tileProviders
- Update getTileConfig() to inject Stadia API key or fall back to CartoDB dark
- Add setStadiaKey() method for saving and applying the API key
- Show/hide Stadia key row in setTileProvider() and _updateUI()
- Add Stadia options to tile provider select in settings modal
- Add Stadia API key input row to settings modal
- Add TDD tests for stadia_key backend
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds initNavGroupState() and saveNavGroupState() functions so the
open/closed state of each .mode-nav-dropdown survives page reloads.
Active groups are never force-closed even if localStorage says closed.
Adds test_nav_state.py with two tests verifying presence of the
functions and data-group attributes on all five nav groups.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
6-task plan covering token deepening, nav active state glow, panel pulse
animation, scanline texture, and localStorage nav group persistence.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mission Control aesthetic, maps overhaul, mode polish sprint for 12 modes,
and 5 new features (Spectrum Overview, Alerts Engine, Signal Recording,
Signal ID, Mobile PWA) — decomposed into 4 sequential sub-projects.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds ruff pre-commit hook that auto-fixes and formats on every commit,
preventing lint CI failures from reaching GitHub.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(adsb): disable bias-T on stop and warn when toggled while running
The RTL-SDR bias-T hardware register persists after the device is closed,
so toggling bias-T off in the UI and stopping the SDR had no effect on the
actual hardware — verified with a multimeter in issue #205.
- Add disable_bias_t_via_rtl_biast() to rtlsdr.py (mirrors enable, uses -b 0)
- Track adsb_bias_t_active in adsb.py; call disable on stop_adsb() so the
hardware register is cleared when ADS-B is stopped
- Show an inline warning in the UI when the bias-T checkbox is toggled while
any SDR mode is active, since the setting only takes effect at start time
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(lint): remove unused imports in tscm sweep.py
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Four list-trimming loops used querySelectorAll (static NodeList) inside a
while condition, so .length never decreased — causing infinite loops that
froze the page, or repeated removeChild calls on already-removed nodes
(TypeError: parameter 1 is not of type 'Node').
Also replaces blocking alert() with showInfo() for start errors and adds
a .catch() handler to the start_sensor fetch so network failures surface
cleanly instead of leaving the UI in a broken state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>