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

@@ -3,11 +3,11 @@
<!-- Scan Mode Tabs -->
<div class="section">
<h3>Signal Source</h3>
<div class="wifi-scan-mode-tabs" style="display: flex; gap: 4px;">
<button id="wifiScanModeQuick" class="wifi-mode-tab active" style="flex: 1; padding: 8px; font-size: 11px; background: var(--accent-green); color: #000; border: none; border-radius: 4px; cursor: pointer;">
<div class="wifi-scan-mode-tabs" role="tablist" aria-label="Scan mode" style="display: flex; gap: 4px;">
<button id="wifiScanModeQuick" class="wifi-mode-tab active" role="tab" aria-selected="true" style="flex: 1; padding: 8px; font-size: 11px; background: var(--accent-green); color: #000; border: none; border-radius: 4px; cursor: pointer;">
Quick Scan
</button>
<button id="wifiScanModeDeep" class="wifi-mode-tab" style="flex: 1; padding: 8px; font-size: 11px; background: var(--bg-tertiary); color: #888; border: 1px solid var(--border-color); border-radius: 4px; cursor: pointer;">
<button id="wifiScanModeDeep" class="wifi-mode-tab" role="tab" aria-selected="false" style="flex: 1; padding: 8px; font-size: 11px; background: var(--bg-tertiary); color: #888; border: 1px solid var(--border-color); border-radius: 4px; cursor: pointer;">
Deep Scan
</button>
</div>
@@ -121,7 +121,7 @@
<label for="deauthCount">Deauth Count</label>
<input type="text" id="deauthCount" value="5" placeholder="5">
</div>
<button class="preset-btn" onclick="sendDeauth()" style="width: 100%; border-color: var(--accent-red); color: var(--accent-red);">
<button class="preset-btn btn-danger-outline" onclick="sendDeauth()" style="width: 100%;">
Send Deauth
</button>
</div>

View File

@@ -55,7 +55,7 @@
<nav class="mode-nav" id="mainNav">
{# Signals Group #}
<div class="mode-nav-dropdown" data-group="signals">
<button type="button" class="mode-nav-dropdown-btn"{% if is_index_page %} onclick="toggleNavDropdown('signals')"{% endif %}>
<button type="button" class="mode-nav-dropdown-btn" aria-expanded="false"{% if is_index_page %} onclick="toggleNavDropdown('signals')"{% endif %}>
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12h4l3-8 3 16 3-8h4"/><path d="M22 12h-1"/><path d="M1 12h1"/></svg></span>
<span class="nav-label">Signals</span>
<span class="dropdown-arrow icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
@@ -74,7 +74,7 @@
{# Tracking Group #}
<div class="mode-nav-dropdown" data-group="tracking">
<button type="button" class="mode-nav-dropdown-btn"{% if is_index_page %} onclick="toggleNavDropdown('tracking')"{% endif %}>
<button type="button" class="mode-nav-dropdown-btn" aria-expanded="false"{% if is_index_page %} onclick="toggleNavDropdown('tracking')"{% endif %}>
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg></span>
<span class="nav-label">Tracking</span>
<span class="dropdown-arrow icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
@@ -91,7 +91,7 @@
{# Space Group #}
<div class="mode-nav-dropdown" data-group="space">
<button type="button" class="mode-nav-dropdown-btn"{% if is_index_page %} onclick="toggleNavDropdown('space')"{% endif %}>
<button type="button" class="mode-nav-dropdown-btn" aria-expanded="false"{% if is_index_page %} onclick="toggleNavDropdown('space')"{% endif %}>
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/></svg></span>
<span class="nav-label">Space</span>
<span class="dropdown-arrow icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
@@ -114,7 +114,7 @@
{# Wireless Group #}
<div class="mode-nav-dropdown" data-group="wireless">
<button type="button" class="mode-nav-dropdown-btn"{% if is_index_page %} onclick="toggleNavDropdown('wireless')"{% endif %}>
<button type="button" class="mode-nav-dropdown-btn" aria-expanded="false"{% if is_index_page %} onclick="toggleNavDropdown('wireless')"{% endif %}>
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor" stroke="none"/></svg></span>
<span class="nav-label">Wireless</span>
<span class="dropdown-arrow icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
@@ -131,7 +131,7 @@
{# Intel Group #}
<div class="mode-nav-dropdown" data-group="intel">
<button type="button" class="mode-nav-dropdown-btn"{% if is_index_page %} onclick="toggleNavDropdown('intel')"{% endif %}>
<button type="button" class="mode-nav-dropdown-btn" aria-expanded="false"{% if is_index_page %} onclick="toggleNavDropdown('intel')"{% endif %}>
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></span>
<span class="nav-label">Intel</span>
<span class="dropdown-arrow icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
@@ -146,7 +146,7 @@
{# System Group #}
<div class="mode-nav-dropdown" data-group="system">
<button type="button" class="mode-nav-dropdown-btn"{% if is_index_page %} onclick="toggleNavDropdown('system')"{% endif %}>
<button type="button" class="mode-nav-dropdown-btn" aria-expanded="false"{% if is_index_page %} onclick="toggleNavDropdown('system')"{% endif %}>
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg></span>
<span class="nav-label">System</span>
<span class="dropdown-arrow icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
@@ -159,7 +159,7 @@
{# Dynamic dashboard button (shown when in satellite mode) #}
<div class="mode-nav-actions">
<a href="/satellite/dashboard" target="_blank" class="nav-action-btn" id="satelliteDashboardBtn">
<a href="/satellite/dashboard" target="_blank" class="nav-action-btn" id="satelliteDashboardBtn" style="display: none;">
<span class="nav-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg></span>
<span class="nav-label">Full Dashboard</span>
</a>
@@ -362,16 +362,84 @@
// Close other dropdowns
document.querySelectorAll('.mode-nav-dropdown.open').forEach(d => {
if (d !== dropdown) d.classList.remove('open');
if (d !== dropdown) {
d.classList.remove('open');
const btn = d.querySelector('.mode-nav-dropdown-btn');
if (btn) btn.setAttribute('aria-expanded', 'false');
}
});
dropdown.classList.toggle('open');
const isOpen = dropdown.classList.toggle('open');
const btn = dropdown.querySelector('.mode-nav-dropdown-btn');
if (btn) btn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
// Focus first menu item when opening
if (isOpen) {
const firstItem = dropdown.querySelector('.mode-nav-dropdown-menu button, .mode-nav-dropdown-menu a');
if (firstItem) firstItem.focus();
}
};
// Close dropdowns when clicking outside
document.addEventListener('click', function(e) {
if (!e.target.closest('.mode-nav-dropdown')) {
document.querySelectorAll('.mode-nav-dropdown.open').forEach(d => d.classList.remove('open'));
document.querySelectorAll('.mode-nav-dropdown.open').forEach(d => {
d.classList.remove('open');
const btn = d.querySelector('.mode-nav-dropdown-btn');
if (btn) btn.setAttribute('aria-expanded', 'false');
});
}
});
// Keyboard support for dropdown buttons and menu items
document.addEventListener('keydown', function(e) {
const dropdown = e.target.closest('.mode-nav-dropdown');
if (!dropdown) return;
const btn = dropdown.querySelector('.mode-nav-dropdown-btn');
const menu = dropdown.querySelector('.mode-nav-dropdown-menu');
if (!menu) return;
const items = Array.from(menu.querySelectorAll('button, a'));
if (e.target === btn || e.target.closest('.mode-nav-dropdown-btn')) {
// On the dropdown trigger button
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
const group = dropdown.getAttribute('data-group');
if (group) toggleNavDropdown(group);
} else if (e.key === 'ArrowDown') {
e.preventDefault();
if (!dropdown.classList.contains('open')) {
const group = dropdown.getAttribute('data-group');
if (group) toggleNavDropdown(group);
}
if (items.length) items[0].focus();
} else if (e.key === 'Escape') {
dropdown.classList.remove('open');
if (btn) {
btn.setAttribute('aria-expanded', 'false');
btn.focus();
}
}
} else if (items.includes(e.target)) {
// Inside the dropdown menu
const idx = items.indexOf(e.target);
if (e.key === 'ArrowDown') {
e.preventDefault();
items[(idx + 1) % items.length].focus();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
items[(idx - 1 + items.length) % items.length].focus();
} else if (e.key === 'Escape') {
e.preventDefault();
dropdown.classList.remove('open');
if (btn) {
btn.setAttribute('aria-expanded', 'false');
btn.focus();
}
} else if (e.key === 'Enter' || e.key === ' ') {
// Default behavior (click) is fine for links/buttons
}
}
});
}