mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40:00 -07:00
Improve mode transitions and add nav perf instrumentation
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user