Improve mode transitions and add nav perf instrumentation

This commit is contained in:
Smittix
2026-02-23 18:14:31 +00:00
parent c31ed14041
commit 3acdab816a
4 changed files with 218 additions and 18 deletions

View File

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

View File

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

View File

@@ -214,6 +214,10 @@
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12h6l3-9 3 18 3-9h5"/></svg></span>
<span class="mode-name">SubGHz</span>
</button>
<button class="mode-card mode-card-sm" onclick="selectMode('waterfall')">
<span class="mode-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="M2 18h20" opacity="0.5"/><path d="M2 21h20" opacity="0.3"/></svg></span>
<span class="mode-name">Waterfall</span>
</button>
</div>
</div>
@@ -309,16 +313,6 @@
</div>
</div>
<!-- Signals (extended) -->
<div class="mode-category">
<h3 class="mode-category-title"><span class="mode-category-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"/></svg></span> Spectrum</h3>
<div class="mode-grid mode-grid-compact">
<button class="mode-card mode-card-sm" onclick="selectMode('waterfall')">
<span class="mode-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="M2 18h20" opacity="0.5"/><path d="M2 21h20" opacity="0.3"/></svg></span>
<span class="mode-name">Waterfall</span>
</button>
</div>
</div>
</div>
</div>
@@ -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)

View File

@@ -227,6 +227,62 @@
{# JavaScript stub for pages that don't have switchMode defined #}
<script>
(function () {
const NAV_PERF_KEY = 'intercept_nav_perf_v1';
const MAX_NAV_AGE_MS = 30000;
function parseNavPerf(raw) {
if (!raw) return null;
try {
return JSON.parse(raw);
} catch (_) {
return null;
}
}
if (!window.InterceptNavPerf) {
window.InterceptNavPerf = {
markStart(meta = {}) {
try {
const payload = {
startedAtEpochMs: Date.now(),
sourcePath: window.location.pathname + window.location.search,
sourceMode: document.body?.getAttribute('data-mode') || null,
...meta,
};
sessionStorage.setItem(NAV_PERF_KEY, JSON.stringify(payload));
} catch (_) {
// Ignore storage errors in private/incognito mode.
}
}
};
}
document.addEventListener('DOMContentLoaded', function () {
const payload = parseNavPerf(sessionStorage.getItem(NAV_PERF_KEY));
if (!payload || !payload.targetPath) return;
const ageMs = Date.now() - (payload.startedAtEpochMs || 0);
if (ageMs < 0 || ageMs > MAX_NAV_AGE_MS) {
try { sessionStorage.removeItem(NAV_PERF_KEY); } catch (_) { }
return;
}
if (window.location.pathname !== payload.targetPath) return;
console.info(
`[Perf] Nav ${payload.sourcePath || '(unknown)'} -> ${payload.targetPath} in ${Math.round(ageMs)}ms`,
{
trigger: payload.trigger || 'unknown',
sourceMode: payload.sourceMode || null,
activeScans: payload.activeScans || null,
}
);
try { sessionStorage.removeItem(NAV_PERF_KEY); } catch (_) { }
});
})();
// Ensure navigation functions exist (for dashboard pages that don't have the full JS)
if (typeof switchMode === 'undefined') {
window.switchMode = function(mode) {