SensorDashboard.applyViewState was resetting output.style.display=''
in the else branch, undoing switchMode's modesWithVisuals hide for
waterfall, morse, ook, and every other mode with its own visuals.
Only touch #output when mode === 'sensor'.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a bottomleft grid button (MapUtils.addGraticuleControl) to every
map in the app — Meshtastic, MeshCore, Drone, SSTV/ISS, BT Locate,
WebSDR, and Weather Satellite — defaulting to visible. The weather
satellite map's bespoke addStyledGridOverlay() is removed in favour of
the shared implementation. Also updates map-utils.css with button
styles and map-utils.js with the new addGraticuleControl() method.
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>
All files used hardcoded rgba(74, 163/158, 255, X) values in actual CSS
rules that CSS variable overrides couldn't touch. Solution: add
--accent-cyan-rgb triplet to variables.css root/light/enhanced blocks,
then replace every rgba(74,1xx,255,) occurrence across all CSS files
with rgba(var(--accent-cyan-rgb),). Enhanced tier sets the triplet to
200, 150, 40 (amber), so tscm.css panel bg, index.css card borders,
and all other tinted surfaces go amber automatically.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each dashboard is a separate HTML page that doesn't inherit the main SPA's
localStorage restore. Add a synchronous tier-restore script before CSS loads
so html[data-ui-tier] selectors fire on first paint.
Also add enhanced/lean tier override blocks to each dashboard CSS to remap
the dashboard-local variables (--bg-dark, --bg-panel, --radar-cyan, etc.)
that variables.css doesn't cover, and add lean-mode scanline/bg hide rules
since components.css is not loaded on these pages.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Brand logo SVGs (.logo, .welcome-logo, .brand-i) now follow --accent-cyan
via CSS rules that override SVG presentation attributes
- proximity-radar.js: sweep, center dot, gradient stops, and selection rings
all use var(--accent-cyan) in style attrs or read getComputedStyle at runtime
- system.js updateGlobePosition: observer point color reads CSS variable
- .bt-detail-address MAC address text uses var(--accent-cyan)
- Enhanced tier gets --visual-edge-cyan/--visual-glow-cyan amber overrides
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Globe.gl WebGL cannot be styled via CSS; read the computed accent color
at init time so Enhanced tier's amber (#c89628) is applied correctly.
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>
- 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>
Remove the intermediate #meshcoreMode wrapper div that was breaking the
flex height chain. Strip and body are now direct children of
#meshcoreVisuals (matching the Meshtastic pattern), so flex: 1 propagates
correctly and the content fills the full panel height.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The sidebar-hiding CSS lives only in meshtastic.css, which is lazily
loaded and may not be present when switching directly to Meshcore mode.
Duplicating the three rules into meshcore.css ensures the generic
sidebar is correctly hidden and the output panel fills the screen
regardless of load order.
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>
meshcoreMode partial was inside the generic .sidebar which gets hidden
when meshcore mode is active. Moved the include into meshcoreVisuals
(inside the output panel) — matching the same pattern as Meshtastic.
Also overrides mesh-visuals-container's column/padding defaults so the
meshcore sidebar+main row layout renders correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
meshcore.css was missing the .active display rule, so the meshcoreMode
div (display:none inline) was never made visible when the mode was
selected, leaving only the generic sidebar visible.
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>
Replace height:100% with flex:1+min-height:0 on .drone-visuals-container
so it fills the flex-column .output-panel correctly (height:100% collapses
inside a scroll container). Add min-height:400px to .drone-main-map so
Leaflet has pixel dimensions to render into.
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>
- 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>