diff --git a/static/css/modes/drone.css b/static/css/modes/drone.css
index 1e57e71..7941125 100644
--- a/static/css/modes/drone.css
+++ b/static/css/modes/drone.css
@@ -1,5 +1,80 @@
/* Drone Intelligence Styles */
+/* ── Main visuals panel ── */
+.drone-visuals-container {
+ display: none;
+ flex-direction: column;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ background: var(--bg-primary);
+}
+
+.drone-visuals-header {
+ flex-shrink: 0;
+ padding: 10px 16px;
+ border-bottom: 1px solid var(--border-color);
+ background: var(--bg-secondary);
+}
+
+.drone-visuals-stats {
+ display: flex;
+ gap: 24px;
+}
+
+.drone-vsstat {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 2px;
+}
+
+.drone-vsstat-value {
+ font-size: 22px;
+ font-weight: 700;
+ font-family: var(--font-mono);
+ color: var(--text-primary);
+ line-height: 1;
+}
+
+.drone-vsstat-label {
+ font-size: 9px;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: var(--text-dim);
+}
+
+.drone-visuals-body {
+ flex: 1;
+ min-height: 0;
+ display: flex;
+ overflow: hidden;
+}
+
+.drone-contact-panel {
+ width: 300px;
+ flex-shrink: 0;
+ overflow-y: auto;
+ border-right: 1px solid var(--border-color);
+ padding: 10px;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.drone-main-map {
+ flex: 1;
+ min-width: 0;
+}
+
+.drone-empty-state {
+ padding: 24px 12px;
+ text-align: center;
+ font-size: 11px;
+ color: var(--text-dim);
+ line-height: 1.6;
+}
+
.drone-vector-pills {
display: flex;
flex-wrap: wrap;
@@ -69,12 +144,6 @@
color: var(--accent-red);
}
-.drone-map {
- height: 280px;
- border-radius: 4px;
- border: 1px solid var(--border-color);
- margin: 0 12px 12px;
-}
.drone-marker-high-risk {
animation: dsc-distress-pulse 1.5s infinite;
diff --git a/static/js/modes/drone.js b/static/js/modes/drone.js
index dbc3fee..3c39e92 100644
--- a/static/js/modes/drone.js
+++ b/static/js/modes/drone.js
@@ -17,9 +17,9 @@
function _initMap() {
if (_map) return;
- const mapEl = document.getElementById('droneMap');
+ const mapEl = document.getElementById('droneMainMap');
if (!mapEl || typeof L === 'undefined') return;
- _map = L.map('droneMap', { zoomControl: true }).setView([20, 0], 2);
+ _map = L.map('droneMainMap', { zoomControl: true }).setView([20, 0], 2);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap',
maxZoom: 18,
@@ -64,7 +64,9 @@
function _upsertCard(contact) {
const listEl = document.getElementById('droneContactList');
+ const emptyEl = document.getElementById('droneContactEmpty');
if (!listEl) return;
+ if (emptyEl) emptyEl.style.display = 'none';
let card = document.getElementById('drone-card-' + contact.id);
if (!card) {
card = document.createElement('div');
@@ -80,8 +82,8 @@
const vectors = (contact.detection_vectors || []).map(function (v) {
return '' + v + '';
}).join('');
- const alt = contact.altitude_m != null ? contact.altitude_m.toFixed(0) + 'm' : '—';
- const spd = contact.speed_ms != null ? contact.speed_ms.toFixed(1) + 'm/s' : '—';
+ const alt = contact.altitude_m != null ? contact.altitude_m.toFixed(0) + ' m' : '—';
+ const spd = contact.speed_ms != null ? contact.speed_ms.toFixed(1) + ' m/s' : '—';
card.innerHTML = [
'
',
' ' + (contact.serial_number || contact.id) + '',
@@ -138,10 +140,13 @@
.then(function (r) { return r.json(); })
.then(function (contacts) {
const nonCompliant = contacts.filter(function (c) { return !c.compliant; }).length;
- const countEl = document.getElementById('droneContactCount');
- const ncEl = document.getElementById('droneNonCompliantCount');
- if (countEl) countEl.textContent = contacts.length;
- if (ncEl) ncEl.textContent = nonCompliant;
+ const highRisk = contacts.filter(function (c) { return c.risk_level === 'high'; }).length;
+ const set = function (id, val) { const el = document.getElementById(id); if (el) el.textContent = val; };
+ set('droneContactCount', contacts.length);
+ set('droneNonCompliantCount', nonCompliant);
+ set('droneVsContacts', contacts.length);
+ set('droneVsNonCompliant', nonCompliant);
+ set('droneVsHighRisk', highRisk);
})
.catch(function () {});
}
@@ -201,5 +206,9 @@
});
}
- window.DroneMode = { init: init, destroy: destroy };
+ function invalidateMap() {
+ if (_map) _map.invalidateSize();
+ }
+
+ window.DroneMode = { init: init, destroy: destroy, invalidateMap: invalidateMap };
})();
diff --git a/templates/index.html b/templates/index.html
index a2c862b..a283dcb 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -2212,6 +2212,35 @@
+
+
+
@@ -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;
diff --git a/templates/partials/modes/drone.html b/templates/partials/modes/drone.html
index 4f6ea23..188c2c4 100644
--- a/templates/partials/modes/drone.html
+++ b/templates/partials/modes/drone.html
@@ -17,20 +17,25 @@
-
RTL-SDR Device (433 MHz)
-
-
-
-
@@ -51,11 +56,4 @@
Non-compliant: 0
-
-
-
-