fix(modes): deep-linked mode scripts fail when body not yet parsed

ensureModeScript() used document.body.appendChild() to load lazy mode
scripts, but the preload for ?mode= query params runs in <head> before
<body> exists, causing all deep-linked modes to silently fail.

Also fix cross-mode handoffs (BT→BT Locate, WiFi→WiFi Locate,
Spy Stations→Waterfall) that assumed target module was already loaded.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-03-12 20:49:08 +00:00
parent e687862043
commit 90281b1535
87 changed files with 9128 additions and 8368 deletions

View File

@@ -12,6 +12,7 @@ from collections.abc import Generator
from flask import Blueprint, Response, jsonify, request
from utils.responses import api_success, api_error
from utils.bluetooth.irk_extractor import get_paired_irks
from utils.bt_locate import (
Environment,
@@ -33,18 +34,18 @@ def start_session():
"""
Start a locate session.
Request JSON:
- mac_address: Target MAC address (optional)
- name_pattern: Target name substring (optional)
- irk_hex: Identity Resolving Key hex string (optional)
- device_id: Device ID from Bluetooth scanner (optional)
- device_key: Stable device key from Bluetooth scanner (optional)
- fingerprint_id: Payload fingerprint ID from Bluetooth scanner (optional)
- known_name: Hand-off device name (optional)
- known_manufacturer: Hand-off manufacturer (optional)
- last_known_rssi: Hand-off last RSSI (optional)
- environment: 'FREE_SPACE', 'OUTDOOR', 'INDOOR', 'CUSTOM' (default: OUTDOOR)
- custom_exponent: Path loss exponent for CUSTOM environment (optional)
Request JSON:
- mac_address: Target MAC address (optional)
- name_pattern: Target name substring (optional)
- irk_hex: Identity Resolving Key hex string (optional)
- device_id: Device ID from Bluetooth scanner (optional)
- device_key: Stable device key from Bluetooth scanner (optional)
- fingerprint_id: Payload fingerprint ID from Bluetooth scanner (optional)
- known_name: Hand-off device name (optional)
- known_manufacturer: Hand-off manufacturer (optional)
- last_known_rssi: Hand-off last RSSI (optional)
- environment: 'FREE_SPACE', 'OUTDOOR', 'INDOOR', 'CUSTOM' (default: OUTDOOR)
- custom_exponent: Path loss exponent for CUSTOM environment (optional)
Returns:
JSON with session status.
@@ -52,47 +53,46 @@ def start_session():
data = request.get_json() or {}
# Build target
target = LocateTarget(
mac_address=data.get('mac_address'),
name_pattern=data.get('name_pattern'),
irk_hex=data.get('irk_hex'),
device_id=data.get('device_id'),
device_key=data.get('device_key'),
fingerprint_id=data.get('fingerprint_id'),
known_name=data.get('known_name'),
known_manufacturer=data.get('known_manufacturer'),
last_known_rssi=data.get('last_known_rssi'),
)
target = LocateTarget(
mac_address=data.get('mac_address'),
name_pattern=data.get('name_pattern'),
irk_hex=data.get('irk_hex'),
device_id=data.get('device_id'),
device_key=data.get('device_key'),
fingerprint_id=data.get('fingerprint_id'),
known_name=data.get('known_name'),
known_manufacturer=data.get('known_manufacturer'),
last_known_rssi=data.get('last_known_rssi'),
)
# At least one identifier required
if not any([
target.mac_address,
target.name_pattern,
target.irk_hex,
target.device_id,
target.device_key,
target.fingerprint_id,
]):
return jsonify({
'error': (
'At least one target identifier required '
'(mac_address, name_pattern, irk_hex, device_id, device_key, or fingerprint_id)'
)
}), 400
if not any([
target.mac_address,
target.name_pattern,
target.irk_hex,
target.device_id,
target.device_key,
target.fingerprint_id,
]):
return api_error(
'At least one target identifier required '
'(mac_address, name_pattern, irk_hex, device_id, device_key, or fingerprint_id)',
400
)
# Parse environment
env_str = data.get('environment', 'OUTDOOR').upper()
try:
environment = Environment[env_str]
except KeyError:
return jsonify({'error': f'Invalid environment: {env_str}'}), 400
return api_error(f'Invalid environment: {env_str}', 400)
custom_exponent = data.get('custom_exponent')
if custom_exponent is not None:
try:
custom_exponent = float(custom_exponent)
except (ValueError, TypeError):
return jsonify({'error': 'custom_exponent must be a number'}), 400
return api_error('custom_exponent must be a number', 400)
# Fallback coordinates when GPS is unavailable (from user settings)
fallback_lat = None
@@ -109,27 +109,21 @@ def start_session():
f"env={environment.name}, fallback=({fallback_lat}, {fallback_lon})"
)
try:
session = start_locate_session(
target, environment, custom_exponent, fallback_lat, fallback_lon
)
except RuntimeError as exc:
logger.warning(f"Unable to start BT Locate session: {exc}")
return jsonify({
'status': 'error',
'error': 'Bluetooth scanner could not be started. Check adapter permissions/capabilities.',
}), 503
except Exception as exc:
logger.exception(f"Unexpected error starting BT Locate session: {exc}")
return jsonify({
'status': 'error',
'error': 'Failed to start locate session',
}), 500
return jsonify({
'status': 'started',
'session': session.get_status(),
})
try:
session = start_locate_session(
target, environment, custom_exponent, fallback_lat, fallback_lon
)
except RuntimeError as exc:
logger.warning(f"Unable to start BT Locate session: {exc}")
return api_error('Bluetooth scanner could not be started. Check adapter permissions/capabilities.', 503)
except Exception as exc:
logger.exception(f"Unexpected error starting BT Locate session: {exc}")
return api_error('Failed to start locate session', 500)
return jsonify({
'status': 'started',
'session': session.get_status(),
})
@bt_locate_bp.route('/stop', methods=['POST'])
@@ -143,18 +137,18 @@ def stop_session():
return jsonify({'status': 'stopped'})
@bt_locate_bp.route('/status', methods=['GET'])
def get_status():
"""Get locate session status."""
session = get_locate_session()
if not session:
@bt_locate_bp.route('/status', methods=['GET'])
def get_status():
"""Get locate session status."""
session = get_locate_session()
if not session:
return jsonify({
'active': False,
'target': None,
})
include_debug = str(request.args.get('debug', '')).lower() in ('1', 'true', 'yes')
return jsonify(session.get_status(include_debug=include_debug))
'active': False,
'target': None,
})
include_debug = str(request.args.get('debug', '')).lower() in ('1', 'true', 'yes')
return jsonify(session.get_status(include_debug=include_debug))
@bt_locate_bp.route('/trail', methods=['GET'])
@@ -216,15 +210,15 @@ def test_resolve_rpa():
address = data.get('address', '')
if not irk_hex or not address:
return jsonify({'error': 'irk_hex and address are required'}), 400
return api_error('irk_hex and address are required', 400)
try:
irk = bytes.fromhex(irk_hex)
except ValueError:
return jsonify({'error': 'Invalid IRK hex string'}), 400
return api_error('Invalid IRK hex string', 400)
if len(irk) != 16:
return jsonify({'error': 'IRK must be exactly 16 bytes (32 hex characters)'}), 400
return api_error('IRK must be exactly 16 bytes (32 hex characters)', 400)
result = resolve_rpa(irk, address)
return jsonify({
@@ -239,14 +233,14 @@ def set_environment():
"""Update the environment on the active session."""
session = get_locate_session()
if not session:
return jsonify({'error': 'no active session'}), 400
return api_error('no active session', 400)
data = request.get_json() or {}
env_str = data.get('environment', '').upper()
try:
environment = Environment[env_str]
except KeyError:
return jsonify({'error': f'Invalid environment: {env_str}'}), 400
return api_error(f'Invalid environment: {env_str}', 400)
custom_exponent = data.get('custom_exponent')
if custom_exponent is not None:
@@ -268,11 +262,11 @@ def debug_matching():
"""Debug endpoint showing scanner devices and match results."""
session = get_locate_session()
if not session:
return jsonify({'error': 'no session'})
return api_error('no session')
scanner = session._scanner
if not scanner:
return jsonify({'error': 'no scanner'})
return api_error('no scanner')
devices = scanner.get_devices(max_age_seconds=30)
return jsonify({