- 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>
- Scan button shows 'Scanning...' and disables during fetch; shows
'No devices found' or 'Scan failed' on empty/error result; auto-
selects device if only one is returned
- Disconnect button now enabled during 'connecting' state so users
can cancel a stuck connection and retry with a different device
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After 30s of polling with no response, update UI to 'Connection timed
out' instead of silently leaving the dot stuck on Connecting...
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add base flex properties to #meshcoreVisuals so it fills full panel
height when meshtastic.css hasn't been lazily loaded yet
- Poll /meshcore/status every 2s after Connect click so the UI
transitions out of "Connecting..." when the backend is ready
- Fix Add Contact and Traceroute modals to use .show class pattern
(signal-details-modal uses opacity/visibility transitions, not display)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaced inner-sidebar layout (which collided with the generic app
sidebar) with a Meshtastic-style top connection strip + body row.
Contacts/nodes panel sits left of the tabbed content area, matching
the established pattern. Map now uses Settings.createTileLayer() with
a dark CartoDB fallback instead of plain OSM light tiles.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace hardcoded OSM tiles with Settings.createTileLayer() + registerMap()
so the drone map respects the user's map theme preference and switches
automatically with light/dark theme changes. Falls back to CartoDB dark_all
if Settings is unavailable.
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>
- Sidebar inputs now use form-group/label pattern matching other modes
- Move map and contact list out of sidebar into a dedicated droneVisuals
main panel (same pattern as tscm, spystations, etc.)
- droneVisuals: stats header (contacts / non-compliant / high-risk),
left contact card panel, and full-height Leaflet map on the right
- Wire droneVisuals into switchMode display toggle and modesWithVisuals
so the shared signal-feed output is hidden when drone mode is active
- Add invalidateMap() to force Leaflet to recalculate after the
container becomes visible
- Stats now update both sidebar counts and main panel values
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove inline style="display: none;" that was preventing the droneMode
panel from becoming visible when the active class was toggled — inline
styles override CSS class rules without !important. Add RTL-SDR device
index and HackRF toggle inputs that the backend already accepted but
were never surfaced in the UI; wire them through to the /drone/start
POST body.
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>
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>
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>
Replace static channel bar chart and security dots with a scrolling
2.4 GHz channel heatmap (up to 10 scan snapshots) and an SVG donut
security ring showing WPA2/WPA3/WEP/Open network distribution.
Replaces the 7-column <table> network list with flex div rows featuring
two-line layout (SSID + security badges on top, signal bar + meta on
bottom), coloured left-border threat indicators, and new sort controls.
Renames selectedNetwork → selectedBssid and updateNetworkTable → renderNetworks throughout wifi.js.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Global time preferences (Settings > Display > Time & Timezone):
- InterceptTime utility in core/utils.js with timezone + 12h/24h support
- Timezone options: UTC, Local, Eastern, Central, Mountain, Pacific
- Time format: 12-hour (AM/PM) or 24-hour toggle
- Defaults to US/Eastern + 12-hour
- Header nav clock updates to use selected timezone and format
- Weather satellite mode delegates to global InterceptTime
- Settings persist via localStorage, change listeners notify all modes
Weather satellite improvements:
- Satellite dropdown defaults to "All Meteor Satellites" showing all passes
- Can still filter to specific satellite (M2-3, M2-4, M2-4-80K)
- Capture button on pass cards auto-selects the correct satellite
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pass prediction improvements:
- Widen prediction window to 48h at 5° min elevation (was 24h/15°)
- Add AOS/TCA/LOS pass geometry detail panel with times and bearings
- Fix duration display (was showing seconds labeled as minutes)
- Enhanced pass cards with AOS/LOS times, bearings, and directions
- Add REFRESH button in passes panel header
- Better empty state with clear "set your location" prompt and icon
Countdown and visual:
- Pulse animation on countdown when pass is imminent or active
- Countdown numbers scale up and change color for urgency
Sidebar getting started guide:
- New "Getting Started" section explaining what Meteor satellites are,
polar orbits, 4-8 passes/day, step-by-step workflow
- "When to look" tips (elevation, day vs night, pass direction)
- "What you need" equipment table with costs
- Collapsed antenna guide by default to reduce initial overwhelm
- Improved offline decode section with clear instructions on where
to get IQ recordings
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Docker fixes:
- Add missing COPY for /usr/local/share/ (pipeline definitions were never
reaching the runtime image — root cause of silent SatDump failures)
- Add libfftw3-double3 and libfftw3-single3 runtime dependencies
- Handle arm64 vs x86 install path differences (/usr vs /usr/local)
- Split SatDump compile and staging into separate layers for better caching
- Add build-time assertions to catch missing pipelines early
UI enhancements:
- Timezone selector (UTC, Local, Eastern, Central, Mountain, Pacific)
with localStorage persistence — all time displays update instantly
- Pass analysis bar showing 24h quality breakdown and best upcoming pass
- Enhanced pass cards with cardinal direction (NW→SE), BEST badge
- Console timestamps, log level filters (ALL/SIGNAL/PROG/ERR), COPY/CLR
- Pass count in stats strip
- Demo data mode for UI testing without SDR or live satellite pass
- Meteor M2-4 80k baud fallback pipeline option
Co-Authored-By: Claude Opus 4.6 (1M context) <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>
- 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>
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>
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>
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>
- 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>