mirror of
https://github.com/smittix/intercept.git
synced 2026-07-15 04:58:10 -07:00
fix(drone): conform to established SPA patterns throughout
- 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>
This commit is contained in:
+32
-3
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import platform
|
import platform
|
||||||
import queue
|
import queue
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -80,7 +81,12 @@ def devices():
|
|||||||
if "Device:" in lines[j]:
|
if "Device:" in lines[j]:
|
||||||
dev = lines[j].split("Device:")[1].strip()
|
dev = lines[j].split("Device:")[1].strip()
|
||||||
result["wifi_interfaces"].append(
|
result["wifi_interfaces"].append(
|
||||||
{"name": dev, "display_name": f"{port} ({dev})", "type": "internal"}
|
{
|
||||||
|
"name": dev,
|
||||||
|
"display_name": f"{port} ({dev})",
|
||||||
|
"type": "internal",
|
||||||
|
"monitor_capable": False,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
except (FileNotFoundError, subprocess.TimeoutExpired, subprocess.SubprocessError):
|
except (FileNotFoundError, subprocess.TimeoutExpired, subprocess.SubprocessError):
|
||||||
@@ -100,6 +106,7 @@ def devices():
|
|||||||
"name": current,
|
"name": current,
|
||||||
"display_name": f"{current} ({iface_type})",
|
"display_name": f"{current} ({iface_type})",
|
||||||
"type": iface_type,
|
"type": iface_type,
|
||||||
|
"monitor_capable": True,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
current = None
|
current = None
|
||||||
@@ -110,7 +117,12 @@ def devices():
|
|||||||
if "IEEE 802.11" in line:
|
if "IEEE 802.11" in line:
|
||||||
iface = line.split()[0]
|
iface = line.split()[0]
|
||||||
result["wifi_interfaces"].append(
|
result["wifi_interfaces"].append(
|
||||||
{"name": iface, "display_name": f"{iface} (managed)", "type": "managed"}
|
{
|
||||||
|
"name": iface,
|
||||||
|
"display_name": f"{iface} (managed)",
|
||||||
|
"type": "managed",
|
||||||
|
"monitor_capable": True,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
except (FileNotFoundError, subprocess.TimeoutExpired, subprocess.SubprocessError):
|
except (FileNotFoundError, subprocess.TimeoutExpired, subprocess.SubprocessError):
|
||||||
pass
|
pass
|
||||||
@@ -130,7 +142,24 @@ def devices():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return jsonify({"status": "ok", "devices": result})
|
running_as_root = os.geteuid() == 0
|
||||||
|
warnings = []
|
||||||
|
if not running_as_root:
|
||||||
|
warnings.append(
|
||||||
|
{
|
||||||
|
"type": "privileges",
|
||||||
|
"message": "Not running as root — WiFi monitor mode may be unavailable.",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"devices": result,
|
||||||
|
"running_as_root": running_as_root,
|
||||||
|
"warnings": warnings,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@drone_bp.route("/status")
|
@drone_bp.route("/status")
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drone-visuals-header {
|
.drone-visuals-header {
|
||||||
|
|||||||
+59
-104
@@ -1,83 +1,25 @@
|
|||||||
(function DroneMode() {
|
var DroneMode = (function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
let _sse = null;
|
var _sse = null;
|
||||||
let _map = null;
|
var _map = null;
|
||||||
let _markers = {};
|
var _markers = {};
|
||||||
let _trails = {};
|
var _trails = {};
|
||||||
let _running = false;
|
var _initialized = false;
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
|
if (_initialized) {
|
||||||
|
_refreshStatus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_initialized = true;
|
||||||
document.getElementById('droneStartBtn')?.addEventListener('click', _start);
|
document.getElementById('droneStartBtn')?.addEventListener('click', _start);
|
||||||
document.getElementById('droneStopBtn')?.addEventListener('click', _stop);
|
document.getElementById('droneStopBtn')?.addEventListener('click', _stop);
|
||||||
_refreshDevices();
|
|
||||||
_initMap();
|
_initMap();
|
||||||
_connectSSE();
|
_connectSSE();
|
||||||
_refreshStatus();
|
_refreshStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function _refreshDevices() {
|
|
||||||
fetch('/drone/devices')
|
|
||||||
.then(function (r) { return r.json(); })
|
|
||||||
.then(function (data) {
|
|
||||||
const devs = data.devices || {};
|
|
||||||
_populateSelect(
|
|
||||||
'droneWifiIface',
|
|
||||||
devs.wifi_interfaces || [],
|
|
||||||
function (i) { return i.name; },
|
|
||||||
function (i) { return i.display_name || i.name; },
|
|
||||||
'No WiFi interfaces found'
|
|
||||||
);
|
|
||||||
_populateSelect(
|
|
||||||
'droneRtlIndex',
|
|
||||||
devs.sdr_devices || [],
|
|
||||||
function (d) { return d.index; },
|
|
||||||
function (d) { return d.display_name || d.name; },
|
|
||||||
'No SDR devices found'
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch(function () {
|
|
||||||
_setSelectError('droneWifiIface', 'Failed to load interfaces');
|
|
||||||
_setSelectError('droneRtlIndex', 'Failed to load devices');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function _populateSelect(id, items, valFn, labelFn, emptyMsg) {
|
|
||||||
const sel = document.getElementById(id);
|
|
||||||
if (!sel) return;
|
|
||||||
sel.innerHTML = '';
|
|
||||||
if (!items.length) {
|
|
||||||
const opt = document.createElement('option');
|
|
||||||
opt.value = '';
|
|
||||||
opt.textContent = emptyMsg;
|
|
||||||
sel.appendChild(opt);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
items.forEach(function (item) {
|
|
||||||
const opt = document.createElement('option');
|
|
||||||
opt.value = valFn(item);
|
|
||||||
opt.textContent = labelFn(item);
|
|
||||||
sel.appendChild(opt);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function _setSelectError(id, msg) {
|
|
||||||
const sel = document.getElementById(id);
|
|
||||||
if (!sel) return;
|
|
||||||
sel.innerHTML = '<option value="">' + msg + '</option>';
|
|
||||||
}
|
|
||||||
|
|
||||||
function _initMap() {
|
|
||||||
if (_map) return;
|
|
||||||
const mapEl = document.getElementById('droneMainMap');
|
|
||||||
if (!mapEl || typeof L === 'undefined') return;
|
|
||||||
_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,
|
|
||||||
}).addTo(_map);
|
|
||||||
}
|
|
||||||
|
|
||||||
function destroy() {
|
function destroy() {
|
||||||
_disconnectSSE();
|
_disconnectSSE();
|
||||||
if (_map) {
|
if (_map) {
|
||||||
@@ -86,6 +28,22 @@
|
|||||||
}
|
}
|
||||||
_markers = {};
|
_markers = {};
|
||||||
_trails = {};
|
_trails = {};
|
||||||
|
_initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function invalidateMap() {
|
||||||
|
if (_map) _map.invalidateSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _initMap() {
|
||||||
|
if (_map) return;
|
||||||
|
var mapEl = document.getElementById('droneMainMap');
|
||||||
|
if (!mapEl || typeof L === 'undefined') return;
|
||||||
|
_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,
|
||||||
|
}).addTo(_map);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _connectSSE() {
|
function _connectSSE() {
|
||||||
@@ -93,7 +51,7 @@
|
|||||||
_sse = new EventSource('/drone/stream');
|
_sse = new EventSource('/drone/stream');
|
||||||
_sse.addEventListener('message', function (e) {
|
_sse.addEventListener('message', function (e) {
|
||||||
try {
|
try {
|
||||||
const msg = JSON.parse(e.data);
|
var msg = JSON.parse(e.data);
|
||||||
if (msg.type === 'contact') _handleContact(msg.data);
|
if (msg.type === 'contact') _handleContact(msg.data);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
});
|
});
|
||||||
@@ -115,11 +73,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _upsertCard(contact) {
|
function _upsertCard(contact) {
|
||||||
const listEl = document.getElementById('droneContactList');
|
var listEl = document.getElementById('droneContactList');
|
||||||
const emptyEl = document.getElementById('droneContactEmpty');
|
var emptyEl = document.getElementById('droneContactEmpty');
|
||||||
if (!listEl) return;
|
if (!listEl) return;
|
||||||
if (emptyEl) emptyEl.style.display = 'none';
|
if (emptyEl) emptyEl.style.display = 'none';
|
||||||
let card = document.getElementById('drone-card-' + contact.id);
|
var card = document.getElementById('drone-card-' + contact.id);
|
||||||
if (!card) {
|
if (!card) {
|
||||||
card = document.createElement('div');
|
card = document.createElement('div');
|
||||||
card.id = 'drone-card-' + contact.id;
|
card.id = 'drone-card-' + contact.id;
|
||||||
@@ -128,14 +86,14 @@
|
|||||||
listEl.prepend(card);
|
listEl.prepend(card);
|
||||||
}
|
}
|
||||||
card.className = 'drone-contact-card ' + contact.risk_level + '-risk';
|
card.className = 'drone-contact-card ' + contact.risk_level + '-risk';
|
||||||
const complianceLabel = contact.compliant
|
var complianceLabel = contact.compliant
|
||||||
? '<span class="drone-compliance-badge compliant">Remote ID</span>'
|
? '<span class="drone-compliance-badge compliant">Remote ID</span>'
|
||||||
: '<span class="drone-compliance-badge non-compliant">No Remote ID</span>';
|
: '<span class="drone-compliance-badge non-compliant">No Remote ID</span>';
|
||||||
const vectors = (contact.detection_vectors || []).map(function (v) {
|
var vectors = (contact.detection_vectors || []).map(function (v) {
|
||||||
return '<span class="drone-vector-pill active">' + v + '</span>';
|
return '<span class="drone-vector-pill active">' + v + '</span>';
|
||||||
}).join('');
|
}).join('');
|
||||||
const alt = contact.altitude_m != null ? contact.altitude_m.toFixed(0) + ' m' : '—';
|
var alt = contact.altitude_m != null ? contact.altitude_m.toFixed(0) + ' m' : '—';
|
||||||
const spd = contact.speed_ms != null ? contact.speed_ms.toFixed(1) + ' m/s' : '—';
|
var spd = contact.speed_ms != null ? contact.speed_ms.toFixed(1) + ' m/s' : '—';
|
||||||
card.innerHTML = [
|
card.innerHTML = [
|
||||||
'<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:6px;">',
|
'<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>',
|
' <span style="font-family:var(--font-mono); font-size:11px; color:var(--accent-cyan);">' + (contact.serial_number || contact.id) + '</span>',
|
||||||
@@ -148,15 +106,15 @@
|
|||||||
|
|
||||||
function _upsertMapMarker(contact) {
|
function _upsertMapMarker(contact) {
|
||||||
if (!_map) return;
|
if (!_map) return;
|
||||||
const lat = contact.position[0];
|
var lat = contact.position[0];
|
||||||
const lon = contact.position[1];
|
var lon = contact.position[1];
|
||||||
if (_markers[contact.id]) {
|
if (_markers[contact.id]) {
|
||||||
_markers[contact.id].setLatLng([lat, lon]);
|
_markers[contact.id].setLatLng([lat, lon]);
|
||||||
} else {
|
} else {
|
||||||
const color = contact.risk_level === 'high' ? 'var(--accent-red)' :
|
var color = contact.risk_level === 'high' ? 'var(--accent-red)' :
|
||||||
contact.risk_level === 'medium' ? 'var(--accent-yellow)' :
|
contact.risk_level === 'medium' ? 'var(--accent-yellow)' :
|
||||||
'var(--accent-cyan)';
|
'var(--accent-cyan)';
|
||||||
const icon = L.divIcon({
|
var icon = L.divIcon({
|
||||||
className: 'drone-map-icon' + (contact.risk_level === 'high' ? ' drone-marker-high-risk' : ''),
|
className: 'drone-map-icon' + (contact.risk_level === 'high' ? ' drone-marker-high-risk' : ''),
|
||||||
html: '<div style="width:10px;height:10px;border-radius:50%;background:' + color + ';border:2px solid #fff;"></div>',
|
html: '<div style="width:10px;height:10px;border-radius:50%;background:' + color + ';border:2px solid #fff;"></div>',
|
||||||
iconSize: [10, 10],
|
iconSize: [10, 10],
|
||||||
@@ -166,7 +124,7 @@
|
|||||||
.addTo(_map)
|
.addTo(_map)
|
||||||
.bindPopup('<b>' + (contact.serial_number || contact.id) + '</b><br>Risk: ' + contact.risk_level);
|
.bindPopup('<b>' + (contact.serial_number || contact.id) + '</b><br>Risk: ' + contact.risk_level);
|
||||||
}
|
}
|
||||||
const trailPoints = (contact.position_history || []).map(function (p) {
|
var trailPoints = (contact.position_history || []).map(function (p) {
|
||||||
return [p.lat, p.lon];
|
return [p.lat, p.lon];
|
||||||
});
|
});
|
||||||
if (_trails[contact.id]) {
|
if (_trails[contact.id]) {
|
||||||
@@ -191,9 +149,9 @@
|
|||||||
fetch('/drone/contacts')
|
fetch('/drone/contacts')
|
||||||
.then(function (r) { return r.json(); })
|
.then(function (r) { return r.json(); })
|
||||||
.then(function (contacts) {
|
.then(function (contacts) {
|
||||||
const nonCompliant = contacts.filter(function (c) { return !c.compliant; }).length;
|
var nonCompliant = contacts.filter(function (c) { return !c.compliant; }).length;
|
||||||
const highRisk = contacts.filter(function (c) { return c.risk_level === 'high'; }).length;
|
var 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; };
|
var set = function (id, val) { var el = document.getElementById(id); if (el) el.textContent = val; };
|
||||||
set('droneContactCount', contacts.length);
|
set('droneContactCount', contacts.length);
|
||||||
set('droneNonCompliantCount', nonCompliant);
|
set('droneNonCompliantCount', nonCompliant);
|
||||||
set('droneVsContacts', contacts.length);
|
set('droneVsContacts', contacts.length);
|
||||||
@@ -207,7 +165,6 @@
|
|||||||
fetch('/drone/status')
|
fetch('/drone/status')
|
||||||
.then(function (r) { return r.json(); })
|
.then(function (r) { return r.json(); })
|
||||||
.then(function (data) {
|
.then(function (data) {
|
||||||
_running = data.running;
|
|
||||||
_setRunningUI(data.running);
|
_setRunningUI(data.running);
|
||||||
_updateVectorPills(data.vectors || []);
|
_updateVectorPills(data.vectors || []);
|
||||||
})
|
})
|
||||||
@@ -215,11 +172,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _start() {
|
function _start() {
|
||||||
const ifaceVal = document.getElementById('droneWifiIface')?.value || '';
|
var ifaceVal = document.getElementById('droneWifiIface')?.value || '';
|
||||||
const iface = ifaceVal || null;
|
var iface = ifaceVal || null;
|
||||||
const rtlVal = document.getElementById('droneRtlIndex')?.value;
|
var rtlVal = document.getElementById('droneRtlIndex')?.value;
|
||||||
const rtlIndex = rtlVal !== '' && rtlVal != null ? parseInt(rtlVal, 10) : 0;
|
var rtlIndex = rtlVal !== '' && rtlVal != null ? parseInt(rtlVal, 10) : 0;
|
||||||
const useHackrf = document.getElementById('droneUseHackrf')?.checked ?? true;
|
var useHackrf = document.getElementById('droneUseHackrf')?.checked ?? true;
|
||||||
fetch('/drone/start', {
|
fetch('/drone/start', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -237,32 +194,30 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _setRunningUI(running) {
|
function _setRunningUI(running) {
|
||||||
const startBtn = document.getElementById('droneStartBtn');
|
var startBtn = document.getElementById('droneStartBtn');
|
||||||
const stopBtn = document.getElementById('droneStopBtn');
|
var stopBtn = document.getElementById('droneStopBtn');
|
||||||
const statusEl = document.getElementById('droneStatusText');
|
var statusEl = document.getElementById('droneStatusText');
|
||||||
if (startBtn) startBtn.disabled = running;
|
if (startBtn) startBtn.disabled = running;
|
||||||
if (stopBtn) stopBtn.disabled = !running;
|
if (stopBtn) stopBtn.disabled = !running;
|
||||||
if (statusEl) {
|
if (statusEl) {
|
||||||
statusEl.textContent = running ? 'Active' : 'Standby';
|
statusEl.textContent = running ? 'Active' : 'Standby';
|
||||||
statusEl.style.color = running ? 'var(--accent-green)' : 'var(--accent-yellow)';
|
statusEl.style.color = running ? 'var(--accent-green)' : 'var(--accent-yellow)';
|
||||||
}
|
}
|
||||||
|
// Sync global state for switchMode stop phase
|
||||||
|
if (typeof isDroneRunning !== 'undefined') isDroneRunning = running;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _updateVectorPills(activeVectors) {
|
function _updateVectorPills(activeVectors) {
|
||||||
const pillMap = {
|
var pillMap = {
|
||||||
'REMOTE_ID': 'dronePillRemoteId',
|
'REMOTE_ID': 'dronePillRemoteId',
|
||||||
'RTL433': 'dronePill433',
|
'RTL433': 'dronePill433',
|
||||||
'HACKRF': 'dronePillHackrf',
|
'HACKRF': 'dronePillHackrf',
|
||||||
};
|
};
|
||||||
Object.entries(pillMap).forEach(function ([key, id]) {
|
Object.keys(pillMap).forEach(function (key) {
|
||||||
const el = document.getElementById(id);
|
var el = document.getElementById(pillMap[key]);
|
||||||
if (el) el.classList.toggle('active', activeVectors.some(function (v) { return v.includes(key); }));
|
if (el) el.classList.toggle('active', activeVectors.some(function (v) { return v.includes(key); }));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function invalidateMap() {
|
return { init: init, destroy: destroy, invalidateMap: invalidateMap };
|
||||||
if (_map) _map.invalidateSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
window.DroneMode = { init: init, destroy: destroy, invalidateMap: invalidateMap };
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -4694,6 +4694,9 @@
|
|||||||
if (isTscmRunning) {
|
if (isTscmRunning) {
|
||||||
stopTasks.push(awaitStopAction('tscm', () => stopTscmSweep(), LOCAL_STOP_TIMEOUT_MS));
|
stopTasks.push(awaitStopAction('tscm', () => stopTscmSweep(), LOCAL_STOP_TIMEOUT_MS));
|
||||||
}
|
}
|
||||||
|
if (isDroneRunning) {
|
||||||
|
stopTasks.push(awaitStopAction('drone', () => fetch('/drone/stop', { method: 'POST' }), LOCAL_STOP_TIMEOUT_MS));
|
||||||
|
}
|
||||||
|
|
||||||
if (stopTasks.length) {
|
if (stopTasks.length) {
|
||||||
await Promise.allSettled(stopTasks);
|
await Promise.allSettled(stopTasks);
|
||||||
@@ -4919,6 +4922,11 @@
|
|||||||
refreshTscmDevices();
|
refreshTscmDevices();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize Drone mode when selected
|
||||||
|
if (mode === 'drone') {
|
||||||
|
refreshDroneDevices();
|
||||||
|
}
|
||||||
|
|
||||||
// Module destroy is now handled by moduleDestroyMap above.
|
// Module destroy is now handled by moduleDestroyMap above.
|
||||||
|
|
||||||
// Show/hide Device Intelligence for modes that use it (not for satellite/aircraft/tscm)
|
// Show/hide Device Intelligence for modes that use it (not for satellite/aircraft/tscm)
|
||||||
@@ -12056,6 +12064,64 @@
|
|||||||
|
|
||||||
// Scanner and receiver logic are handled by Waterfall mode.
|
// Scanner and receiver logic are handled by Waterfall mode.
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Drone Intelligence Functions
|
||||||
|
// ============================================
|
||||||
|
var isDroneRunning = false;
|
||||||
|
|
||||||
|
async function refreshDroneDevices() {
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/drone/devices');
|
||||||
|
const data = await resp.json();
|
||||||
|
const devs = data.devices || {};
|
||||||
|
|
||||||
|
const wifiSel = document.getElementById('droneWifiIface');
|
||||||
|
if (wifiSel) {
|
||||||
|
wifiSel.innerHTML = '';
|
||||||
|
if (devs.wifi_interfaces && devs.wifi_interfaces.length > 0) {
|
||||||
|
devs.wifi_interfaces.forEach(iface => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = iface.name;
|
||||||
|
opt.textContent = iface.display_name || iface.name;
|
||||||
|
wifiSel.appendChild(opt);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
wifiSel.innerHTML = '<option value="">No WiFi interfaces found</option>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sdrSel = document.getElementById('droneRtlIndex');
|
||||||
|
if (sdrSel) {
|
||||||
|
sdrSel.innerHTML = '';
|
||||||
|
if (devs.sdr_devices && devs.sdr_devices.length > 0) {
|
||||||
|
devs.sdr_devices.forEach(dev => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = dev.index !== undefined ? dev.index : 0;
|
||||||
|
opt.textContent = dev.display_name || dev.name || 'SDR Device';
|
||||||
|
sdrSel.appendChild(opt);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
sdrSel.innerHTML = '<option value="">No SDR devices found</option>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const warnEl = document.getElementById('droneDeviceWarnings');
|
||||||
|
if (warnEl) {
|
||||||
|
if (!data.running_as_root) {
|
||||||
|
warnEl.textContent = 'Not running as root — WiFi monitor mode may be unavailable.';
|
||||||
|
warnEl.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
warnEl.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
const wifiSel = document.getElementById('droneWifiIface');
|
||||||
|
if (wifiSel) wifiSel.innerHTML = '<option value="">Failed to load interfaces</option>';
|
||||||
|
const sdrSel = document.getElementById('droneRtlIndex');
|
||||||
|
if (sdrSel) sdrSel.innerHTML = '<option value="">Failed to load devices</option>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// TSCM (Counter-Surveillance) Functions
|
// TSCM (Counter-Surveillance) Functions
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<div class="section">
|
<div class="section">
|
||||||
<h3>WiFi Interface</h3>
|
<h3>WiFi Interface</h3>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="droneWifiIface">Interface (monitor mode)</label>
|
<label>Interface (monitor mode)</label>
|
||||||
<select id="droneWifiIface">
|
<select id="droneWifiIface">
|
||||||
<option value="">Loading interfaces…</option>
|
<option value="">Loading interfaces…</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<div class="section">
|
<div class="section">
|
||||||
<h3>SDR Settings</h3>
|
<h3>SDR Settings</h3>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="droneRtlIndex">RTL-SDR Device (433 MHz)</label>
|
<label>RTL-SDR Device (433 MHz)</label>
|
||||||
<select id="droneRtlIndex">
|
<select id="droneRtlIndex">
|
||||||
<option value="">Loading devices…</option>
|
<option value="">Loading devices…</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -42,6 +42,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="droneDeviceWarnings" class="info-text" style="display:none; color:var(--accent-yellow); font-size:10px; padding: 0 4px;"></div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div style="display:flex; gap:8px;">
|
<div style="display:flex; gap:8px;">
|
||||||
<button id="droneStartBtn" class="run-btn" style="flex:1;">Start</button>
|
<button id="droneStartBtn" class="run-btn" style="flex:1;">Start</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user