,
+ document.body,
+ );
+}
diff --git a/webui/src/components/UndockedPageExitGuard/index.jsx b/webui/src/components/UndockedPageExitGuard/index.jsx
deleted file mode 100644
index df83f0c6..00000000
--- a/webui/src/components/UndockedPageExitGuard/index.jsx
+++ /dev/null
@@ -1,66 +0,0 @@
-// Undocked Page Exit Guard
-// Purpose: Asks the browser to confirm document-level navigation while the current driver's rover is undocked.
-// Scope: Owns the native beforeunload lifecycle and queues the matching AlertFeed explanation when leaving is attempted.
-import { useEffect } from 'react';
-import { useControlSelector } from '../../controls/index.js';
-import { useSessionActions } from '../../context/SessionContext.jsx';
-import { useTelemetrySelector } from '../../context/TelemetryContext.jsx';
-
-const UNDOCKED_EXIT_ALERT_LIFETIME_MS = 15 * 1000;
-
-function selectHomeBaseState(frame) {
- const homeBase = frame?.sensors?.chargingSources?.homeBase;
-
- /*
- Missing telemetry must remain distinct from a real "not on the home base"
- reading. Treating the initial empty frame as undocked would enable the exit
- warning before the browser has received enough information to justify it.
- */
- return typeof homeBase === 'boolean' ? homeBase : null;
-}
-
-export default function UndockedPageExitGuard() {
- const roverId = useControlSelector((control) => control.state.roverId);
- const homeBaseState = useTelemetrySelector(roverId, selectHomeBaseState);
- const { pushAlert } = useSessionActions();
- const shouldConfirmExit = Boolean(roverId) && homeBaseState === false;
-
- useEffect(() => {
- if (!shouldConfirmExit) return undefined;
-
- function handleBeforeUnload(event) {
- /*
- Browsers intentionally own the wording and presentation of this dialog.
- preventDefault is the modern signal, while assigning returnValue keeps
- the confirmation working in browsers that still require the legacy part
- of the beforeunload contract.
- */
- event.preventDefault();
- event.returnValue = true;
-
- /*
- If the driver cancels the browser-owned confirmation, React remains
- mounted and AlertFeed presents the actionable reason for the warning.
- A stable id refreshes this one warning lane on another exit attempt
- instead of stacking duplicate cards over the driving interface.
- */
- pushAlert({
- id: 'undocked-page-exit-warning',
- kind: 'undocked-exit-warning',
- lifetimeMs: UNDOCKED_EXIT_ALERT_LIFETIME_MS,
- });
- }
-
- window.addEventListener('beforeunload', handleBeforeUnload);
- return () => {
- /*
- Removing the listener as soon as the rover docks or the assignment ends
- prevents ordinary spectators and completed drivers from seeing a stale
- leave-page confirmation.
- */
- window.removeEventListener('beforeunload', handleBeforeUnload);
- };
- }, [pushAlert, shouldConfirmExit]);
-
- return null;
-}
diff --git a/webui/src/driver/DriverPageRoot.jsx b/webui/src/driver/DriverPageRoot.jsx
index daefc339..4daebbc7 100644
--- a/webui/src/driver/DriverPageRoot.jsx
+++ b/webui/src/driver/DriverPageRoot.jsx
@@ -28,7 +28,6 @@ import useLayoutMode from '../hooks/useLayoutMode.js';
import { DriverHelpProvider } from '../layouts/driver/DriverHelpContext.jsx';
import { LayoutProvider } from '../layouts/LayoutContext.jsx';
import DriverLayoutRoot from '../layouts/driver/DriverLayoutRoot.jsx';
-import UndockedPageExitGuard from '../components/UndockedPageExitGuard/index.jsx';
/* Driver-page compositions live under layouts/driver. App retains only global providers, overlays, and route-level state. */
function DriverPageRoot({ oldDesktop = false }) {
@@ -92,7 +91,6 @@ function DriverPageContent({ layout, oldDesktop }) {
return (
-
diff --git a/webui/src/main.jsx b/webui/src/main.jsx
index fa001ab0..629aa090 100644
--- a/webui/src/main.jsx
+++ b/webui/src/main.jsx
@@ -2,7 +2,7 @@
// Purpose: Boots the React application and mounts global providers/router roots. Scope: Defines top-level route wiring and root render lifecycle for the browser app.
import { lazy, StrictMode, Suspense } from 'react'
import { createRoot } from 'react-dom/client'
-import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'
+import { createBrowserRouter, Route, RouterProvider, Routes } from 'react-router-dom'
import './index.css'
// Theme artwork is a separate style concern from global component utilities. Loading its dedicated
// entrypoint here keeps every route consistent without returning theme definitions to index.css.
@@ -22,6 +22,7 @@ import AnalyticsReporter from './analytics/AnalyticsReporter.jsx'
import PtzAppRoot from './ptz/PtzAppRoot.jsx'
import MicApp from './mic/MicApp.jsx'
import OperatingModeRouteSync from './components/OperatingModeRouteSync/index.jsx'
+import PageExitGuard from './components/PageExitGuard/index.jsx'
import InitialSessionOverlay from './components/InitialSessionOverlay/index.jsx'
import AppErrorBoundary from './components/AppErrorBoundary.jsx'
@@ -41,6 +42,42 @@ function lazyRoute(element, label) {
)
}
+// A data router provides one blocker for links, programmatic navigation, and history.
+const router = createBrowserRouter([{
+ path: '*',
+ element: (
+ <>
+
+
+
+
+ } />
+ {/* The retired desktop composition remains available for direct comparison
+ without duplicating the driver shell or changing canonical mobile layouts. */}
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ , 'setup')} />
+ , 'administration')} />
+ {/*
+ PTZ is a separate route so the driver layout and its replay
+ panel are not mounted behind the camera controller. This
+ also makes orientation changes a PTZ layout concern instead
+ of a local overlay-open state owned by the driver page.
+ */}
+ } />
+ , 'fleet reports')}
+ />
+
+ >
+ ),
+}])
+
createRoot(document.getElementById('root')).render(
@@ -55,34 +92,7 @@ createRoot(document.getElementById('root')).render(
-
-
-
-
- } />
- {/* The retired desktop composition remains available for direct comparison
- without duplicating the driver shell or changing canonical mobile layouts. */}
- } />
- } />
- } />
- } />
- } />
- } />
- , 'setup')} />
- , 'administration')} />
- {/*
- PTZ is a separate route so the driver layout and its replay
- panel are not mounted behind the camera controller. This
- also makes orientation changes a PTZ layout concern instead
- of a local overlay-open state owned by the driver page.
- */}
- } />
- , 'fleet reports')}
- />
-
-
+