// Main Application Shell
// Purpose: Composes the primary rover control interface and page-level layout. Scope: Orchestrates high-level panels, overlays, and feature modules for the default route.
import { useCallback, useEffect, useMemo, useState } from 'react';
import TelemetryPanel from './components/TelemetryPanel/index.jsx';
import PiHostStatsCard from './components/PiHostStatsCard/index.jsx';
import ReplaySourcesPanel from './components/ReplaySourcesPanel/index.jsx';
import AlertFeed from './components/AlertFeed/index.jsx';
import AuxColumn from './components/MobileControls/AuxColumn.jsx';
import MovementColumn from './components/MobileControls/MovementColumn.jsx';
import {
ControlSystemProvider,
KeyboardInputManager,
GamepadInputManager,
useControlSelector,
} from './controls/index.js';
import RoomCameraPanel from './components/RoomCameraPanel/index.jsx';
import KinectPanel from './components/KinectPanel/index.jsx';
import BalanceBoardPanel from './components/BalanceBoardPanel/index.jsx';
import DriverVideo from './components/DriverVideo/index.jsx';
import RightPaneTabs from './components/RightPaneTabs/index.jsx';
import ModeGateOverlay from './components/ModeGateOverlay/index.jsx';
import HomeAssistantControls from './components/HomeAssistantControls/index.jsx';
import TurnAlertListener from './components/TurnAlertListener/index.jsx';
import RawUserPilePanel from './components/RawUserPilePanel/index.jsx';
import OverseerPreferencePanel from './components/OverseerPreferencePanel/index.jsx';
import SocialButtonsGrid from './components/SocialButtonsGrid/index.jsx';
import ChatPanel from './components/ChatPanel/index.jsx';
import FullscreenPrompt from './components/FullscreenPrompt/index.jsx';
import FloatingFullscreenButton from './components/FloatingFullscreenButton/index.jsx';
import { useFullscreenPrompt } from './hooks/useFullscreenPrompt.js';
import { useSettingsNamespace } from './settings/index.js';
import HelpOverlay from './components/HelpOverlay/index.jsx';
import QuickstartOverlay from './components/QuickstartOverlay/index.jsx';
import HelpPanel from './components/HelpPanel/index.jsx';
import SettingsPanel from './components/SettingsPanel/index.jsx';
import Tabs, { Tab, TabList, TabPanel, TabPanels } from './components/Tabs/index.jsx';
import useDefaultNickname from './hooks/useDefaultNickname.js';
import useUserIdentitySync from './hooks/useUserIdentitySync.js';
import useIncomingInterInstanceTransfer from './hooks/useIncomingInterInstanceTransfer.js';
import GlobalObjectiveBanner from './components/GlobalObjectiveBanner/index.jsx';
import RoverQueuesPanel from './components/RoverQueuesPanel/index.jsx';
import PtzQueueCard from './components/PtzCamera/index.jsx';
import VipPanel from './components/VipPanel/index.jsx';
import { useSessionSelector } from './context/SessionContext.jsx';
import { useTelemetryVisualPolicy } from './context/TelemetryContext.jsx';
import ButtonBoxPanel from './components/ButtonBoxPanel/index.jsx';
import BarcodeGamesPanel from './components/BarcodeGamesPanel/index.jsx';
import OdometerPanel from './components/OdometerPanel/index.jsx';
import FleetReportsCard from './components/FleetReportsCard/index.jsx';
import LiftCard from './components/LiftCard/index.jsx';
import NeatoCard from './components/NeatoCard/index.jsx';
import RewardRunOverlay from './components/RewardRunOverlay/index.jsx';
import SocketConnectionPill from './components/SocketConnectionPill/index.jsx';
import DuplicateIdentityOverlay from './components/DuplicateIdentityOverlay/index.jsx';
import {
DEFAULT_PAGE_THEME_KEY,
getPageThemeClass,
themeGapClass,
themeStackClass,
} from './themes/index.js';
import useLayoutMode from './hooks/useLayoutMode.js';
function DesktopLayout({ layout, onOpenHelpOverlay }) {
return (
);
}
function MobileFeatureTabs({
layout,
onOpenHelpOverlay,
roomPanelId,
}) {
const [activeTab, setActiveTab] = useState('chat');
const isVerified = useSessionSelector((state) => Boolean(state.session?.isVerified));
const ownRoverId = useSessionSelector((state) => String(state.session?.assignment?.roverId || '').trim());
const ownAudioForward = useSessionSelector((state) => {
const roverId = String(state.session?.assignment?.roverId || '').trim();
return roverId ? state.session?.audioForward?.[roverId] || null : null;
});
const pttActive = useControlSelector((control) => Boolean(control.state.mic?.pttActive));
const { value: vipAudio } = useSettingsNamespace('vipAudio', { openMicEnabled: false, pttMode: 'live' });
const vipDotClass = isVerified ? 'bg-emerald-400' : 'bg-amber-400';
const openMicEnabled = Boolean(vipAudio?.openMicEnabled);
const pttMode = vipAudio?.pttMode === 'clip' ? 'clip' : 'live';
const vipMicActive = Boolean(
ownRoverId &&
isVerified &&
(pttMode === 'clip' ? pttActive : (openMicEnabled || pttActive)),
);
const vipClipPlaying = Boolean(
ownRoverId &&
isVerified &&
pttMode === 'clip' &&
ownAudioForward?.source === 'upload' &&
ownAudioForward?.state === 'playing',
);
const showOverseerPreferencePanel = useSessionSelector((state) => {
const vote = state.session?.overseerVote;
// Match the panel's server-owned voting gate so the mobile chat row can
// collapse to a single-column layout when voting is unavailable, including
// disabled service and direct-address mode.
return Boolean(vote?.votingEnabled);
});
const handleTabChange = useCallback(
(tab) => {
setActiveTab(tab);
},
[],
);
return (
Chat
Activities
VIP
Room Controls
Help
Settings
{/*
The overseer vote panel is server-vote-gated. When voting
is unavailable, the raw user pile should reclaim the row
instead of sitting in a half-empty two-column layout.
*/}
{showOverseerPreferencePanel ? : null}
{/* activities tab */}
{/* Fleet reports owns its optional feature gate and intentionally
remains last so Activities keeps lightweight controls first. */}
{/* {showTelemetry ? : null} */}
);
}
function MobilePortraitLayout({ onOpenHelpOverlay, swapMobileControlColumns = false }) {
const columnHeight = 'h-[min(60svh,24rem)]';
const firstColumn = swapMobileControlColumns
?
: ;
const secondColumn = swapMobileControlColumns
?
: ;
return (
{/* Portrait owns the two-column placement because the same reusable mobile
columns sit in different grid contexts in portrait and landscape. */}
{firstColumn}
{secondColumn}
{/*
*/}
);
}
function MobileLandscapeLayout({ onOpenHelpOverlay, swapMobileControlColumns = false }) {
const columnClass = 'self-start h-[min(100svh,32rem)]';
const firstColumn = swapMobileControlColumns
?
: ;
const secondColumn = swapMobileControlColumns
?
: ;
return (
{firstColumn}
{secondColumn}
);
}
function App() {
const layout = useLayoutMode();
const isDesktop = layout === 'desktop';
const fullscreen = useFullscreenPrompt(layout);
const { value: pageSettings } = useSettingsNamespace('page', {
backgroundTheme: DEFAULT_PAGE_THEME_KEY,
});
// Resolve the cookie value through the shared catalog before painting the page. This prevents
// an obsolete or hand-edited key from stripping the background class from every exposed seam.
const pageBackgroundClass = getPageThemeClass(pageSettings?.backgroundTheme);
return (
);
}
function AppWithProviders({ layout, isDesktop, fullscreen }) {
useDefaultNickname();
useIncomingInterInstanceTransfer();
useUserIdentitySync({ identitySurface: 'driver' });
useTelemetryVisualPolicy({ mobile: !isDesktop });
const {
visible: fullscreenVisible,
mode: fullscreenMode,
isIOS: fullscreenIsIOS,
nativeSupported: fullscreenNativeSupported,
enterFullscreen,
dismiss,
showPrompt,
} = fullscreen;
const {
value: helpSettings,
save: saveHelpSettings,
} = useSettingsNamespace('help', { showOnLoad: true });
const {
value: quickstartSettings,
status: quickstartStatus,
save: saveQuickstartSettings,
} = useSettingsNamespace('quickstart', { showOnLoad: true });
const { value: pageSettings } = useSettingsNamespace('page', {
swapMobileControlColumns: false,
});
const swapMobileControlColumns = Boolean(pageSettings?.swapMobileControlColumns);
const fullscreenButtonSide = swapMobileControlColumns ? 'left' : 'right';
const showFloatingFullscreenButton = !isDesktop && (fullscreenIsIOS || fullscreenNativeSupported);
const [helpVisible, setHelpVisible] = useState(false);
const [quickstartVisible, setQuickstartVisible] = useState(false);
useEffect(() => {
if (quickstartStatus === 'ready') {
setQuickstartVisible(quickstartSettings?.showOnLoad !== false);
}
}, [quickstartStatus, quickstartSettings?.showOnLoad]);
const openHelp = useCallback(() => {
setHelpVisible(true);
}, []);
const closeHelp = useCallback(() => {
setHelpVisible(false);
}, []);
const closeQuickstart = useCallback(() => {
setQuickstartVisible(false);
}, []);
const handleFloatingFullscreen = useCallback(async () => {
if (fullscreenIsIOS) {
showPrompt();
return;
}
const entered = await enterFullscreen();
if (!entered) {
showPrompt();
}
}, [enterFullscreen, fullscreenIsIOS, showPrompt]);
const setQuickstartShowOnLoad = useCallback(
(enabled) => {
const next = Boolean(enabled);
saveQuickstartSettings((current) => ({ ...(current ?? {}), showOnLoad: next }));
if (!next) {
setQuickstartVisible(false);
}
},
[saveQuickstartSettings],
);
const openHelpFromQuickstart = useCallback(() => {
setQuickstartVisible(false);
setHelpVisible(true);
}, []);
const handleFullscreenPromptEnter = useCallback(async () => {
const entered = await enterFullscreen();
return entered;
}, [enterFullscreen]);
const handleFullscreenPromptDismiss = useCallback(() => {
dismiss();
}, [dismiss]);
const renderedLayout = useMemo(
() =>
isDesktop
?
: layout === 'mobile-landscape'
?
: ,
[isDesktop, layout, openHelp, swapMobileControlColumns],
);
return (
{!isDesktop ? : null}
{renderedLayout}
saveHelpSettings((current) => ({ ...(current ?? {}), showOnLoad: Boolean(enabled) }))}
/>
{showFloatingFullscreenButton ? (
) : null}
);
}
export default App;