diff --git a/static/js/core/command-palette.js b/static/js/core/command-palette.js
index 9bc1fd0..b128f36 100644
--- a/static/js/core/command-palette.js
+++ b/static/js/core/command-palette.js
@@ -187,13 +187,39 @@ const CommandPalette = (function() {
title: 'View Aircraft Dashboard',
description: 'Open dedicated ADS-B dashboard page',
keyword: 'aircraft adsb dashboard',
- run: () => { window.location.href = '/adsb/dashboard'; }
+ run: () => {
+ if (window.InterceptNavPerf && typeof window.InterceptNavPerf.markStart === 'function') {
+ window.InterceptNavPerf.markStart({
+ targetPath: '/adsb/dashboard',
+ trigger: 'command-palette',
+ sourceMode: (typeof currentMode === 'string' && currentMode) ? currentMode : null,
+ activeScans: (typeof getActiveScanSummary === 'function') ? getActiveScanSummary() : null,
+ });
+ }
+ if (typeof stopActiveLocalScansForNavigation === 'function') {
+ stopActiveLocalScansForNavigation();
+ }
+ window.location.href = '/adsb/dashboard';
+ }
},
{
title: 'View Vessel Dashboard',
description: 'Open dedicated AIS dashboard page',
keyword: 'vessel ais dashboard',
- run: () => { window.location.href = '/ais/dashboard'; }
+ run: () => {
+ if (window.InterceptNavPerf && typeof window.InterceptNavPerf.markStart === 'function') {
+ window.InterceptNavPerf.markStart({
+ targetPath: '/ais/dashboard',
+ trigger: 'command-palette',
+ sourceMode: (typeof currentMode === 'string' && currentMode) ? currentMode : null,
+ activeScans: (typeof getActiveScanSummary === 'function') ? getActiveScanSummary() : null,
+ });
+ }
+ if (typeof stopActiveLocalScansForNavigation === 'function') {
+ stopActiveLocalScansForNavigation();
+ }
+ window.location.href = '/ais/dashboard';
+ }
},
{
title: 'Kill All Running Processes',
diff --git a/static/js/core/global-nav.js b/static/js/core/global-nav.js
index 280df7a..34fe83e 100644
--- a/static/js/core/global-nav.js
+++ b/static/js/core/global-nav.js
@@ -18,6 +18,18 @@
if (menuLink) {
event.preventDefault();
event.stopPropagation();
+ try {
+ const target = new URL(menuLink.href, window.location.href);
+ if (window.InterceptNavPerf && typeof window.InterceptNavPerf.markStart === 'function') {
+ window.InterceptNavPerf.markStart({
+ targetPath: target.pathname,
+ trigger: 'global-nav',
+ sourceMode: document.body?.getAttribute('data-mode') || null,
+ });
+ }
+ } catch (_) {
+ // Ignore malformed link targets.
+ }
window.location.href = menuLink.href;
return;
}
diff --git a/templates/index.html b/templates/index.html
index 0e032eb..8b27a9e 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -214,6 +214,10 @@
SubGHz
+
@@ -309,16 +313,6 @@
-
-
@@ -3604,6 +3598,93 @@
const LOCAL_STOP_TIMEOUT_MS = 2200;
const REMOTE_STOP_TIMEOUT_MS = 8000;
+ const DASHBOARD_NAV_PATHS = new Set([
+ '/adsb/dashboard',
+ '/ais/dashboard',
+ '/satellite/dashboard',
+ ]);
+
+ function getActiveScanSummary() {
+ return {
+ pager: Boolean(isRunning),
+ sensor: Boolean(isSensorRunning),
+ wifi: Boolean(
+ ((typeof WiFiMode !== 'undefined' && typeof WiFiMode.isScanning === 'function' && WiFiMode.isScanning()) || isWifiRunning)
+ ),
+ bluetooth: Boolean(
+ ((typeof BluetoothMode !== 'undefined' && typeof BluetoothMode.isScanning === 'function' && BluetoothMode.isScanning()) || isBtRunning)
+ ),
+ aprs: Boolean(typeof isAprsRunning !== 'undefined' && isAprsRunning),
+ tscm: Boolean(typeof isTscmRunning !== 'undefined' && isTscmRunning),
+ };
+ }
+
+ function stopActiveLocalScansForNavigation() {
+ const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
+ if (isAgentMode) return;
+
+ if (isRunning && typeof stopDecoding === 'function') {
+ Promise.resolve(stopDecoding()).catch(() => { });
+ }
+ if (isSensorRunning && typeof stopSensorDecoding === 'function') {
+ Promise.resolve(stopSensorDecoding()).catch(() => { });
+ }
+
+ const wifiScanActive = (
+ typeof WiFiMode !== 'undefined'
+ && typeof WiFiMode.isScanning === 'function'
+ && WiFiMode.isScanning()
+ ) || isWifiRunning;
+ if (wifiScanActive && typeof stopWifiScan === 'function') {
+ Promise.resolve(stopWifiScan()).catch(() => { });
+ }
+
+ const btScanActive = (
+ typeof BluetoothMode !== 'undefined'
+ && typeof BluetoothMode.isScanning === 'function'
+ && BluetoothMode.isScanning()
+ ) || isBtRunning;
+ if (btScanActive && typeof stopBtScan === 'function') {
+ Promise.resolve(stopBtScan()).catch(() => { });
+ }
+
+ if (typeof isAprsRunning !== 'undefined' && isAprsRunning && typeof stopAprs === 'function') {
+ Promise.resolve(stopAprs()).catch(() => { });
+ }
+ if (typeof isTscmRunning !== 'undefined' && isTscmRunning && typeof stopTscmSweep === 'function') {
+ Promise.resolve(stopTscmSweep()).catch(() => { });
+ }
+ }
+
+ if (!window._dashboardNavigationStopHookBound) {
+ window._dashboardNavigationStopHookBound = true;
+ document.addEventListener('click', (event) => {
+ if (event.defaultPrevented || event.button !== 0) return;
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
+
+ const link = event.target && event.target.closest
+ ? event.target.closest('a[href]')
+ : null;
+ if (!link || link.target === '_blank') return;
+
+ try {
+ const href = new URL(link.href, window.location.href);
+ if (href.origin !== window.location.origin) return;
+ if (!DASHBOARD_NAV_PATHS.has(href.pathname)) return;
+ if (window.InterceptNavPerf && typeof window.InterceptNavPerf.markStart === 'function') {
+ window.InterceptNavPerf.markStart({
+ targetPath: href.pathname,
+ trigger: 'index-link',
+ sourceMode: currentMode,
+ activeScans: getActiveScanSummary(),
+ });
+ }
+ stopActiveLocalScansForNavigation();
+ } catch (_) {
+ // Ignore malformed hrefs.
+ }
+ });
+ }
function postStopRequest(url, timeoutMs = LOCAL_STOP_TIMEOUT_MS) {
const controller = (typeof AbortController !== 'undefined') ? new AbortController() : null;
@@ -3651,16 +3732,22 @@
// Mode switching
async function switchMode(mode, options = {}) {
const { updateUrl = true } = options;
+ const switchStartMs = performance.now();
+ const previousMode = currentMode;
if (mode === 'listening') mode = 'waterfall';
if (!validModes.has(mode)) mode = 'pager';
// Only stop local scans if in local mode (not agent mode)
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
+ const stopPhaseStartMs = performance.now();
+ let stopTaskCount = 0;
if (!isAgentMode) {
+ const stopTasks = [];
+
if (isRunning) {
- await awaitStopAction('pager', () => stopDecoding(), LOCAL_STOP_TIMEOUT_MS);
+ stopTasks.push(awaitStopAction('pager', () => stopDecoding(), LOCAL_STOP_TIMEOUT_MS));
}
if (isSensorRunning) {
- await awaitStopAction('sensor', () => stopSensorDecoding(), LOCAL_STOP_TIMEOUT_MS);
+ stopTasks.push(awaitStopAction('sensor', () => stopSensorDecoding(), LOCAL_STOP_TIMEOUT_MS));
}
const wifiScanActive = (
typeof WiFiMode !== 'undefined'
@@ -3668,7 +3755,7 @@
&& WiFiMode.isScanning()
) || isWifiRunning;
if (wifiScanActive) {
- await awaitStopAction('wifi', () => stopWifiScan(), LOCAL_STOP_TIMEOUT_MS);
+ stopTasks.push(awaitStopAction('wifi', () => stopWifiScan(), LOCAL_STOP_TIMEOUT_MS));
}
const btScanActive = (typeof BluetoothMode !== 'undefined' &&
typeof BluetoothMode.isScanning === 'function' &&
@@ -3677,15 +3764,21 @@
(currentMode === 'bluetooth' && mode === 'bt_locate') ||
(currentMode === 'bt_locate' && mode === 'bluetooth');
if (btScanActive && !isBtModeTransition && typeof stopBtScan === 'function') {
- await awaitStopAction('bluetooth', () => stopBtScan(), LOCAL_STOP_TIMEOUT_MS);
+ stopTasks.push(awaitStopAction('bluetooth', () => stopBtScan(), LOCAL_STOP_TIMEOUT_MS));
}
if (isAprsRunning) {
- await awaitStopAction('aprs', () => stopAprs(), LOCAL_STOP_TIMEOUT_MS);
+ stopTasks.push(awaitStopAction('aprs', () => stopAprs(), LOCAL_STOP_TIMEOUT_MS));
}
if (isTscmRunning) {
- await awaitStopAction('tscm', () => stopTscmSweep(), LOCAL_STOP_TIMEOUT_MS);
+ stopTasks.push(awaitStopAction('tscm', () => stopTscmSweep(), LOCAL_STOP_TIMEOUT_MS));
}
+
+ if (stopTasks.length) {
+ await Promise.allSettled(stopTasks);
+ }
+ stopTaskCount = stopTasks.length;
}
+ const stopPhaseMs = Math.round(performance.now() - stopPhaseStartMs);
// Clean up SubGHz SSE connection when leaving the mode
if (typeof SubGhz !== 'undefined' && currentMode === 'subghz' && mode !== 'subghz') {
@@ -3955,6 +4048,19 @@
if (mode !== 'waterfall' && typeof Waterfall !== 'undefined' && Waterfall.destroy) {
Promise.resolve(Waterfall.destroy()).catch(() => {});
}
+
+ const totalMs = Math.round(performance.now() - switchStartMs);
+ console.info(
+ `[Perf] switchMode ${previousMode} -> ${mode}: stop=${stopPhaseMs}ms tasks=${stopTaskCount} total=${totalMs}ms`,
+ {
+ updateUrl,
+ agentMode: isAgentMode,
+ }
+ );
+ requestAnimationFrame(() => {
+ const firstFrameMs = Math.round(performance.now() - switchStartMs);
+ console.info(`[Perf] switchMode ${previousMode} -> ${mode}: first-frame=${firstFrameMs}ms`);
+ });
}
// Handle window resize for maps (especially important on mobile orientation change)
diff --git a/templates/partials/nav.html b/templates/partials/nav.html
index d6372cc..2ff2d84 100644
--- a/templates/partials/nav.html
+++ b/templates/partials/nav.html
@@ -227,6 +227,62 @@
{# JavaScript stub for pages that don't have switchMode defined #}