mirror of
https://github.com/smittix/intercept.git
synced 2026-06-08 14:11:54 -07:00
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:
@@ -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;
|
||||
|
||||
@@ -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 '<span class="drone-vector-pill active">' + v + '</span>';
|
||||
}).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 = [
|
||||
'<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:6px;">',
|
||||
' <span style="font-family:var(--font-mono); font-size:11px; color:var(--accent-cyan);">' + (contact.serial_number || contact.id) + '</span>',
|
||||
@@ -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 };
|
||||
})();
|
||||
|
||||
+36
-2
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user