Planespotters.net now requires a descriptive User-Agent with a contact
URL or email — generic strings return 403. Updated to comply with their
API policy.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- pyproject.toml: sync missing deps (flask-wtf, flask-compress,
simple-websocket, gunicorn, gevent, psutil, cryptography, meshcore,
pre-commit) so test_requirements integrity check passes
- tests/conftest.py: set INTERCEPT_DISABLE_AUTH=1 so auth routes
return 200 instead of 302 in tests
- routes/bluetooth_v2.py: add device_to_dict() helper that flattens
heuristics to top level for test_bluetooth_api serialization tests
- utils/bluetooth/heuristics.py: evaluate() now returns the device so
callers can chain; was returning None
- tests/test_satellite.py: reduce hours 48→2 in pass-prediction test
to prevent OOM kill on GitHub Actions 7GB runner at the 59% mark
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix app becoming unresponsive when two browser windows are open: the
root cause was HTTP/1.1 connection pool exhaustion (6-connection limit
per origin). VoiceAlerts was opening 3 SSE streams per window by
default, so two windows produced 8 connections and permanently starved
all regular HTTP requests.
- voice-alerts.js: default all streams to false (opt-in) to stay within
the browser connection limit; existing user preferences in localStorage
are preserved
- routes/alerts.py: replace direct AlertManager.stream_events() with
sse_stream_fanout so both windows receive every alert instead of
competing for the same queue
- routes/bluetooth_v2.py: same fanout fix via subscribe_fanout_queue,
preserving named SSE events (device_update, scan_started, etc.)
Also includes accumulated UI/theming changes: accent-cyan CSS variable
sweep across mode CSS/JS files, standalone dashboard pages, template
updates, satellite TLE data refresh, and tile provider default rename.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
contextlib.suppress(KeyError) around popitem prevents a crash in the SBS
parser thread if stop_adsb() calls clear() concurrently between the len()
check and the popitem call.
Two unit tests verify FIFO eviction semantics and duplicate-key no-op.
Replace four sequential list comprehensions (band → security → hidden → min_rssi)
with a single pass using a helper function. Reduces algorithmic complexity from O(4n)
to O(n) when multiple filters are applied. All WiFi tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Store last status message on MeshcoreClient so error details survive
beyond the SSE event (which isn't active during connecting state)
- Status endpoint now returns message field so the frontend can show
the real reason (e.g. 'Connection failed after retries: ...')
- Extend JS polling from 30s to 90s to outlast the backend's 65s
retry sequence (5+15+45s delays) before declaring timeout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Revert route to scan_ble() — scan_ble_sync() lives on AsyncWorker,
not MeshcoreClient; the 500 was caused by our previous fix
- MeshcoreClient.scan_ble() now runs a one-shot asyncio scan when no
worker is active, so Scan works before Connect is pressed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Device population: move refreshDroneDevices() inline to index.html
(same pattern as refreshTscmDevices) and call it from switchMode
alongside DroneMode.init(); remove _refreshDevices/populateSelect
from drone.js which was never guaranteed to run before lazy-load
completed, causing selects to stay on "Loading…" permanently
- IIFE pattern: change from named IIFE + window.DroneMode assignment
to var DroneMode = (function(){...return{...}})() matching OOK/
SpyStations convention
- Init guard: add _initialized flag (OOK state.initialized pattern);
re-entry after destroy() re-registers map/SSE cleanly without
duplicating click listeners on every mode switch
- Lifecycle: destroy() resets _initialized = false so map and SSE
are correctly rebuilt on re-entry
- Stop phase: add isDroneRunning tracking variable in index.html;
_setRunningUI() syncs it; switchMode stop phase now POSTs
/drone/stop when leaving drone mode while active, matching TSCM
- /drone/devices: add monitor_capable field to WiFi interfaces,
add running_as_root and warnings array to response (mirrors
/tscm/devices shape); add os import; show privilege warning div
in drone.html when not running as root
- drone.html: remove for= attribute from SDR label (plain <label>
inside .form-group matches TSCM convention); add droneDeviceWarnings
div for privilege warnings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add /drone/devices endpoint that enumerates available WiFi interfaces
(via iw/iwconfig) and RTL-SDR devices (via SDRFactory.detect_devices),
matching the pattern used by TSCM.
Sidebar WiFi interface and RTL-SDR inputs are now <select> elements
populated on init() from /drone/devices, consistent with how other
modes expose hardware selection. HackRF checkbox remains as a toggle
since it's a binary capability rather than an enumerated device list.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix stale DOM refs in fetchAircraftPhoto: elements were captured before
await fetch(), but showAircraftDetails rebuilds innerHTML on every RAF
update, leaving the async path writing to detached nodes. Now re-queries
the DOM after await, and the cache (synchronous) path queries inline so
refs are always fresh.
- Add thumbnail fallback in aircraft_photo route: fall back to thumbnail
when thumbnail_large.src is absent rather than returning null.
- Add Drone Intelligence to nav, help modal, cheat sheets, README, and docs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Sync with upstream main and fix required items from review:
- updateTimelineLabels() now uses InterceptTime API (getTimezone/getIANA)
instead of the stale selectedTimezone/TZ_MAP globals that were removed
during the earlier InterceptTime refactor — fixes ReferenceError on TZ
change and pass refresh.
- Remove profiles: [basic] from the intercept service in
docker-compose.yml so bare `docker compose up -d` still starts the
main service. Profile-gated services (intercept-history, adsb_db)
stay as-is.
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>
- 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>
* 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>
AIS:
- New optional NMEA UDP forwarding via AIS-catcher's -u flag, configurable
from the AIS sidebar (host + port). Lets OpenCPN and other NMEA tools
receive live vessel data directly. All SDR builders updated.
- New GET /ais/vessels endpoint — clean JSON snapshot of tracked vessels
for REST integration
ADS-B:
- New GET /adsb/aircraft endpoint — JSON snapshot of all tracked aircraft,
with optional ?icao= and ?military=true filters. Response includes a
reminder that port 30003 (SBS) is already available for tools like
Virtual Radar Server and OpenCPN's AIS/target plugin.
Closes#90
Adds a "Custom Range" sweep type that lets users specify start/end MHz
instead of using a fixed preset. Useful in dense RF environments where
a full or standard sweep returns too many signals and causes slowdown.
UI shows start/end MHz inputs when "Custom Range" is selected. Range is
validated (0 < start < end ≤ 6000 MHz) before the sweep starts.
Backend threads the ranges through to _scan_rf_signals(), which already
supports arbitrary frequency bands.
Closes#172
Strong passes at 40 dB (the previous default) cause RTL-SDR ADC clipping,
producing a distorted IQ stream that SatDump cannot lock onto. 30 dB is
a safer starting point that still captures weak passes cleanly.
Also adds a UI hint below the gain control explaining the saturation issue.
Closes#185
Timezone fixes:
- Add utils.js (InterceptTime) to adsb_dashboard.html — was completely
missing, causing all times to fall back to UTC regardless of setting
- Register onChange listener in nav.html so clock updates instantly
when timezone/format is changed in Settings
- Initialize timezone/format dropdowns on ADS-B dashboard page load
- Browser-verified: ET/12h ↔ UTC/24h switches instantly on ADS-B page
VDL2 correlation fix:
- Force ICAO hex to uppercase when promoting from VDL2 src.addr (dumpvdl2
may output lowercase, ADS-B stores uppercase — case mismatch prevented
correlator from matching)
- Move ICAO/addr promotion before ACARS field extraction so even
non-ACARS VDL2 frames (XID, connection mgmt) get correlated
Auth:
- Add INTERCEPT_DISABLE_AUTH env var to skip login for local/dev use
- Configurable via docker-compose.yml environment
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: dumpvdl2 outputs nested JSON (vdl2.avlc.acars.flight) but
FlightCorrelator only checks top-level fields. VDL2 messages were stored
in the correlator but never matched to any aircraft.
Fix: Promote identifying fields (flight, reg, tail, icao, addr, label,
text) from the nested VDL2 structure to top-level before storing in the
correlator. Also promote AVLC source address as ICAO when src.type is
"Aircraft".
Also fix VDL2 sidebar timestamps to use global InterceptTime setting.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 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>
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>
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.
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.
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.