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 @@ - -
-

Spectrum

-
- -
-
@@ -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 #}