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:
James Smith
2026-05-13 09:33:38 +01:00
parent 4ba8a40af9
commit 410225d54d
5 changed files with 163 additions and 109 deletions
+32 -3
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import logging
import os
import platform
import queue
import subprocess
@@ -80,7 +81,12 @@ def devices():
if "Device:" in lines[j]:
dev = lines[j].split("Device:")[1].strip()
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
except (FileNotFoundError, subprocess.TimeoutExpired, subprocess.SubprocessError):
@@ -100,6 +106,7 @@ def devices():
"name": current,
"display_name": f"{current} ({iface_type})",
"type": iface_type,
"monitor_capable": True,
}
)
current = None
@@ -110,7 +117,12 @@ def devices():
if "IEEE 802.11" in line:
iface = line.split()[0]
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):
pass
@@ -130,7 +142,24 @@ def devices():
except Exception:
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")