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>
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
- Pager and sensor gain inputs changed from unvalidated text fields to
number inputs with min/max/step constraints
- ADS-B dashboard now exposes a gain input in the tracking strip;
previously gain was hardcoded to 40 dB with no user control
- validate_gain() ceiling raised from 50 to 102 dB to support HackRF
(LNA 40 + VGA 62 = 102 dB combined) and LimeSDR (73 dB)
- sdrCapabilities gain_max values corrected: HackRF 62→102, Airspy 21→45
- onSDRTypeChanged() now propagates gain_max to all mode gain inputs so
HTML constraints match the selected SDR's actual range
Closes#162
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
/health now includes sdr_claims: a dict mapping 'sdr_type:device_index'
to the mode currently using that device (e.g. {"rtlsdr:0": "pager"}).
Empty when no devices are in use.
/devices/status already existed and returns the full device list with
in_use/used_by per device — documented in the issue response.
Closes#158
Adds three new icon shapes (widebody, bizjet, turboprop) to the existing
set (jet, prop, helicopter, military, glider), giving 8 distinct silhouettes.
Classification covers common ICAO type codes: widebodies (744, 777, A380 etc.),
business jets (Citation, Gulfstream, Learjet etc.), turboprops (ATR, DH8 etc.),
and light GA piston aircraft.
Hover tooltip now shows aircraft type description (e.g. "Airbus A320-200")
when available from the aircraft DB, in addition to callsign and altitude.
Closes#201
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
Remove the split fast-path in doLocateHandoff that called BtLocate.handoff()
directly when the module was already loaded. That path relied on handoff()
internally calling switchMode, causing a double switchMode in the lazy-load
path and no guaranteed mode switch in the fast path.
Now doLocateHandoff always calls switchMode('bt_locate') first (lazy-loading
script/styles as needed), then calls BtLocate.handoff() in .then(). Removed
the redundant switchMode call from BtLocate.handoff() since the caller owns
the mode transition.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the requestAnimationFrame loop in proximity-radar.js with a
CSS @keyframes rotation on .bt-radar-sweep, mirroring the WiFi radar
pattern. Adds two trailing arc paths for a glow effect and updates
setPaused() to toggle animationPlayState instead of the rAF flag.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>