feat(drone): add main visuals panel with map and contact list

- 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>
This commit is contained in:
James Smith
2026-05-13 09:16:54 +01:00
parent 2475e5dd5a
commit 6523686aca
4 changed files with 145 additions and 35 deletions
+36 -2
View File
@@ -2212,6 +2212,35 @@
</div>
</div>
<!-- Drone Intelligence Dashboard -->
<div id="droneVisuals" class="drone-visuals-container" style="display: none;">
<div class="drone-visuals-header">
<div class="drone-visuals-stats">
<div class="drone-vsstat">
<span class="drone-vsstat-value" id="droneVsContacts">0</span>
<span class="drone-vsstat-label">Contacts</span>
</div>
<div class="drone-vsstat">
<span class="drone-vsstat-value" id="droneVsNonCompliant" style="color:var(--accent-red);">0</span>
<span class="drone-vsstat-label">Non-Compliant</span>
</div>
<div class="drone-vsstat">
<span class="drone-vsstat-value" id="droneVsHighRisk" style="color:var(--accent-orange);">0</span>
<span class="drone-vsstat-label">High Risk</span>
</div>
</div>
</div>
<div class="drone-visuals-body">
<div class="drone-contact-panel">
<div id="droneContactList"></div>
<div id="droneContactEmpty" class="drone-empty-state">
No contacts detected — start detection to begin scanning.
</div>
</div>
<div id="droneMainMap" class="drone-main-map"></div>
</div>
</div>
<!-- Meshtastic Messages Dashboard -->
<div id="meshtasticVisuals" class="mesh-visuals-container" style="display: none;">
<!-- Compact Status Strip -->
@@ -4787,6 +4816,7 @@
const radiosondeVisuals = document.getElementById('radiosondeVisuals');
const meteorVisuals = document.getElementById('meteorVisuals');
const systemVisuals = document.getElementById('systemVisuals');
const droneVisuals = document.getElementById('droneVisuals');
if (wifiLayoutContainer) wifiLayoutContainer.classList.toggle('active', mode === 'wifi');
if (btLayoutContainer) btLayoutContainer.classList.toggle('active', mode === 'bluetooth');
if (satelliteVisuals) satelliteVisuals.style.display = mode === 'satellite' ? 'block' : 'none';
@@ -4831,10 +4861,11 @@
if (radiosondeVisuals) radiosondeVisuals.style.display = mode === 'radiosonde' ? 'flex' : 'none';
if (meteorVisuals) meteorVisuals.style.display = mode === 'meteor' ? 'flex' : 'none';
if (systemVisuals) systemVisuals.style.display = mode === 'system' ? 'flex' : 'none';
if (droneVisuals) droneVisuals.style.display = mode === 'drone' ? 'flex' : 'none';
// Hide the signal feed output for modes that have their own visuals
const outputEl = document.getElementById('output');
const modesWithVisuals = ['satellite', 'sstv', 'weathersat', 'sstv_general', 'wefax', 'aprs', 'wifi', 'bluetooth', 'tscm', 'spystations', 'meshtastic', 'meshcore', 'websdr', 'subghz', 'spaceweather', 'bt_locate', 'wifi_locate', 'waterfall', 'morse', 'meteor', 'system', 'ook', 'radiosonde', 'gps'];
const modesWithVisuals = ['satellite', 'sstv', 'weathersat', 'sstv_general', 'wefax', 'aprs', 'wifi', 'bluetooth', 'tscm', 'spystations', 'meshtastic', 'meshcore', 'websdr', 'subghz', 'spaceweather', 'bt_locate', 'wifi_locate', 'waterfall', 'morse', 'meteor', 'system', 'ook', 'radiosonde', 'gps', 'drone'];
if (outputEl) outputEl.style.display = modesWithVisuals.includes(mode) ? 'none' : 'block';
// Prevent Leaflet heatmap redraws on hidden BT Locate map containers.
@@ -5042,7 +5073,10 @@
} else if (mode === 'ook') {
OokMode.init();
} else if (mode === 'drone') {
if (typeof DroneMode !== 'undefined') DroneMode.init();
if (typeof DroneMode !== 'undefined') {
DroneMode.init();
setTimeout(() => { DroneMode.invalidateMap?.(); }, 100);
}
}
if (requestId !== modeSwitchRequestId) return;
+16 -18
View File
@@ -17,20 +17,25 @@
</div>
<div class="section">
<h3>WiFi Interface <span style="font-weight:400; font-size:11px; color:var(--text-dim)">(monitor mode)</span></h3>
<input type="text" id="droneWifiIface" placeholder="e.g. wlan0mon" style="width:100%;">
<h3>WiFi Interface</h3>
<div class="form-group">
<label for="droneWifiIface">Interface (monitor mode)</label>
<input type="text" id="droneWifiIface" placeholder="e.g. wlan0mon">
</div>
</div>
<div class="section">
<h3>RTL-SDR Device <span style="font-weight:400; font-size:11px; color:var(--text-dim)">(433 MHz)</span></h3>
<input type="number" id="droneRtlIndex" value="0" min="0" max="7" style="width:100%;" placeholder="Device index">
</div>
<div class="section">
<label class="inline-checkbox">
<input type="checkbox" id="droneUseHackrf" checked>
Use HackRF <span style="font-size:10px; color:var(--text-dim);">(2.4 / 5.8 GHz)</span>
</label>
<h3>SDR Settings</h3>
<div class="form-group">
<label for="droneRtlIndex">RTL-SDR Device Index (433 MHz)</label>
<input type="number" id="droneRtlIndex" value="0" min="0" max="7" placeholder="Device index">
</div>
<div class="form-group">
<label class="inline-checkbox">
<input type="checkbox" id="droneUseHackrf" checked>
Use HackRF (2.4 / 5.8 GHz)
</label>
</div>
</div>
<div class="section">
@@ -51,11 +56,4 @@
Non-compliant: <span id="droneNonCompliantCount" style="color:var(--accent-red);">0</span>
</p>
</div>
<div class="section">
<h3>Detected Contacts</h3>
<div id="droneContactList"></div>
</div>
<div id="droneMap" class="drone-map"></div>
</div>