mirror of
https://github.com/smittix/intercept.git
synced 2026-05-27 02:04:45 -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',
|
title: 'View Aircraft Dashboard',
|
||||||
description: 'Open dedicated ADS-B dashboard page',
|
description: 'Open dedicated ADS-B dashboard page',
|
||||||
keyword: 'aircraft adsb dashboard',
|
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',
|
title: 'View Vessel Dashboard',
|
||||||
description: 'Open dedicated AIS dashboard page',
|
description: 'Open dedicated AIS dashboard page',
|
||||||
keyword: 'vessel ais dashboard',
|
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',
|
title: 'Kill All Running Processes',
|
||||||
|
|||||||
@@ -18,6 +18,18 @@
|
|||||||
if (menuLink) {
|
if (menuLink) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
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;
|
window.location.href = menuLink.href;
|
||||||
return;
|
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-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>
|
<span class="mode-name">SubGHz</span>
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -309,16 +313,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -3604,6 +3598,93 @@
|
|||||||
|
|
||||||
const LOCAL_STOP_TIMEOUT_MS = 2200;
|
const LOCAL_STOP_TIMEOUT_MS = 2200;
|
||||||
const REMOTE_STOP_TIMEOUT_MS = 8000;
|
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) {
|
function postStopRequest(url, timeoutMs = LOCAL_STOP_TIMEOUT_MS) {
|
||||||
const controller = (typeof AbortController !== 'undefined') ? new AbortController() : null;
|
const controller = (typeof AbortController !== 'undefined') ? new AbortController() : null;
|
||||||
@@ -3651,16 +3732,22 @@
|
|||||||
// Mode switching
|
// Mode switching
|
||||||
async function switchMode(mode, options = {}) {
|
async function switchMode(mode, options = {}) {
|
||||||
const { updateUrl = true } = options;
|
const { updateUrl = true } = options;
|
||||||
|
const switchStartMs = performance.now();
|
||||||
|
const previousMode = currentMode;
|
||||||
if (mode === 'listening') mode = 'waterfall';
|
if (mode === 'listening') mode = 'waterfall';
|
||||||
if (!validModes.has(mode)) mode = 'pager';
|
if (!validModes.has(mode)) mode = 'pager';
|
||||||
// Only stop local scans if in local mode (not agent mode)
|
// Only stop local scans if in local mode (not agent mode)
|
||||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||||
|
const stopPhaseStartMs = performance.now();
|
||||||
|
let stopTaskCount = 0;
|
||||||
if (!isAgentMode) {
|
if (!isAgentMode) {
|
||||||
|
const stopTasks = [];
|
||||||
|
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
await awaitStopAction('pager', () => stopDecoding(), LOCAL_STOP_TIMEOUT_MS);
|
stopTasks.push(awaitStopAction('pager', () => stopDecoding(), LOCAL_STOP_TIMEOUT_MS));
|
||||||
}
|
}
|
||||||
if (isSensorRunning) {
|
if (isSensorRunning) {
|
||||||
await awaitStopAction('sensor', () => stopSensorDecoding(), LOCAL_STOP_TIMEOUT_MS);
|
stopTasks.push(awaitStopAction('sensor', () => stopSensorDecoding(), LOCAL_STOP_TIMEOUT_MS));
|
||||||
}
|
}
|
||||||
const wifiScanActive = (
|
const wifiScanActive = (
|
||||||
typeof WiFiMode !== 'undefined'
|
typeof WiFiMode !== 'undefined'
|
||||||
@@ -3668,7 +3755,7 @@
|
|||||||
&& WiFiMode.isScanning()
|
&& WiFiMode.isScanning()
|
||||||
) || isWifiRunning;
|
) || isWifiRunning;
|
||||||
if (wifiScanActive) {
|
if (wifiScanActive) {
|
||||||
await awaitStopAction('wifi', () => stopWifiScan(), LOCAL_STOP_TIMEOUT_MS);
|
stopTasks.push(awaitStopAction('wifi', () => stopWifiScan(), LOCAL_STOP_TIMEOUT_MS));
|
||||||
}
|
}
|
||||||
const btScanActive = (typeof BluetoothMode !== 'undefined' &&
|
const btScanActive = (typeof BluetoothMode !== 'undefined' &&
|
||||||
typeof BluetoothMode.isScanning === 'function' &&
|
typeof BluetoothMode.isScanning === 'function' &&
|
||||||
@@ -3677,15 +3764,21 @@
|
|||||||
(currentMode === 'bluetooth' && mode === 'bt_locate') ||
|
(currentMode === 'bluetooth' && mode === 'bt_locate') ||
|
||||||
(currentMode === 'bt_locate' && mode === 'bluetooth');
|
(currentMode === 'bt_locate' && mode === 'bluetooth');
|
||||||
if (btScanActive && !isBtModeTransition && typeof stopBtScan === 'function') {
|
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) {
|
if (isAprsRunning) {
|
||||||
await awaitStopAction('aprs', () => stopAprs(), LOCAL_STOP_TIMEOUT_MS);
|
stopTasks.push(awaitStopAction('aprs', () => stopAprs(), LOCAL_STOP_TIMEOUT_MS));
|
||||||
}
|
}
|
||||||
if (isTscmRunning) {
|
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
|
// Clean up SubGHz SSE connection when leaving the mode
|
||||||
if (typeof SubGhz !== 'undefined' && currentMode === 'subghz' && mode !== 'subghz') {
|
if (typeof SubGhz !== 'undefined' && currentMode === 'subghz' && mode !== 'subghz') {
|
||||||
@@ -3955,6 +4048,19 @@
|
|||||||
if (mode !== 'waterfall' && typeof Waterfall !== 'undefined' && Waterfall.destroy) {
|
if (mode !== 'waterfall' && typeof Waterfall !== 'undefined' && Waterfall.destroy) {
|
||||||
Promise.resolve(Waterfall.destroy()).catch(() => {});
|
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)
|
// 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 #}
|
{# JavaScript stub for pages that don't have switchMode defined #}
|
||||||
<script>
|
<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)
|
// Ensure navigation functions exist (for dashboard pages that don't have the full JS)
|
||||||
if (typeof switchMode === 'undefined') {
|
if (typeof switchMode === 'undefined') {
|
||||||
window.switchMode = function(mode) {
|
window.switchMode = function(mode) {
|
||||||
|
|||||||
Reference in New Issue
Block a user