import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { SettingsProvider } from '../settings/index.js'; import { useSession } from '../context/SessionContext.jsx'; import { useTelemetryFrames } from '../context/TelemetryContext.jsx'; import { useVideoRequests } from '../hooks/useVideoRequests.js'; import { useRoverSnapshots } from '../hooks/useRoverSnapshots.js'; import { useSpectatorMode } from '../hooks/useSpectatorMode.js'; import VideoTile from '../components/VideoTile.jsx'; import TopDownMap from '../components/TopDownMap.jsx'; import AlertFeed from '../components/AlertFeed.jsx'; import useDefaultNickname from '../hooks/useDefaultNickname.js'; import BatteryBar from '../components/BatteryBar.jsx'; import { buildBatteryVisual } from '../lib/battery.js'; const ROTATE_MS = 20000; const HARD_REFRESH_MS = 1 * 60 * 60 * 1000; function formatDriverLabel({ roverId, session }) { const activeDriverId = session?.activeDrivers?.[roverId] || null; const user = (session?.users || []).find((entry) => entry.socketId === activeDriverId); const label = user?.nickname || (activeDriverId ? activeDriverId.slice(0, 6) : 'No driver'); const mode = session?.mode; const turnInfo = session?.turnQueues?.[roverId]; return mode === 'turns' && turnInfo?.current ? `${label}` : label; } function getBatteryVisual({ rover, frame }) { const charge = frame?.sensors?.batteryChargeMah ?? null; const config = rover?.battery ?? null; return buildBatteryVisual({ batteryState: rover?.batteryState ?? null, charge, config }); } function InfoColumn({ rover, frame, driverLabel, sessionInfo, videoMode = 'snapshot', snapshotFeed, withDivider = false, showPreview = true, variant = 'stacked', }) { const batteryVisual = getBatteryVisual({ rover, frame }); const batteryPercent = batteryVisual?.available ? batteryVisual.percentDisplay : null; const isActiveView = variant === 'active'; return (
{isActiveView ? (
{rover.name || rover.id}
{driverLabel ? (
{driverLabel}
) : (
)}
{batteryPercent == null ? '--%' : `${batteryPercent}%`}
) : ( <>
{rover.name || rover.id}
{driverLabel ? (
{driverLabel}
) : null}
{batteryPercent == null ? '--%' : `${batteryPercent}%`}
{showPreview ? (
) : null}
)}
); } function AutoFitText({ children, className = '', maxSize = 1000, minSize = 14 }) { const containerRef = useRef(null); const textRef = useRef(null); const [fontSize, setFontSize] = useState(maxSize); useLayoutEffect(() => { const container = containerRef.current; const textEl = textRef.current; if (!container || !textEl) return undefined; let raf = null; const fit = () => { const width = container.clientWidth; if (!width) { scheduleFit(); return; } let low = minSize; let high = maxSize; let best = minSize; while (low <= high) { const mid = Math.floor((low + high) / 2); textEl.style.fontSize = `${mid}px`; const fits = textEl.scrollWidth <= width; if (fits) { best = mid; low = mid + 1; } else { high = mid - 1; } } setFontSize(best); }; const scheduleFit = () => { if (raf) cancelAnimationFrame(raf); raf = requestAnimationFrame(fit); }; scheduleFit(); const ro = new ResizeObserver(scheduleFit); ro.observe(container); return () => { if (raf) cancelAnimationFrame(raf); ro.disconnect(); }; }, [children, maxSize, minSize]); return (
{children}
); } function MiniSummaryContent() { const { session } = useSession(); const spectatorReady = useSpectatorMode(); useDefaultNickname(); const inLockdown = session?.mode === 'lockdown'; const canSpectateVideo = Boolean(session?.isLocalNetwork); const frames = useTelemetryFrames(); const roster = session?.roster ?? []; const [index, setIndex] = useState(0); const activeDrivers = session?.activeDrivers || {}; const driverRoster = useMemo( () => roster.filter((rover) => activeDrivers[rover.id]), [roster, activeDrivers], ); const snapshotRoster = driverRoster.length ? driverRoster : roster; const snapshotFeeds = useRoverSnapshots( snapshotRoster.map((rover) => rover.id), { enabled: !inLockdown && !canSpectateVideo, version: session?.mode }, ); const videoEntries = useMemo( () => canSpectateVideo ? snapshotRoster.map((rover) => ({ type: 'rover', id: rover.id, key: rover.id })) : [], [canSpectateVideo, snapshotRoster], ); const videoSources = useVideoRequests(videoEntries, { enabled: !inLockdown && canSpectateVideo, version: session?.mode }); const audioEntries = useMemo( () => driverRoster.flatMap((rover) => { if (!rover?.id || !rover.media?.audioPublishUrl) return []; const id = String(rover.id); return [{ type: 'rover', id: `${id}-audio`, key: `${id}-audio` }]; }), [driverRoster], ); const audioSources = useVideoRequests(audioEntries, { enabled: !inLockdown, version: session?.mode }); const roverPool = useMemo(() => { if (!driverRoster.length) return []; if (!canSpectateVideo) { const withSnapshot = driverRoster.filter((rover) => snapshotFeeds[rover.id]?.objectUrl); return withSnapshot.length ? withSnapshot : driverRoster; } const withVideo = driverRoster.filter((rover) => { const sessionInfo = videoSources[rover.id]; return sessionInfo?.url && !sessionInfo?.error; }); return withVideo.length ? withVideo : driverRoster; }, [driverRoster, snapshotFeeds, videoSources, canSpectateVideo]); const rotationPool = useMemo(() => { return roverPool.map((rover) => ({ type: 'rover', rover })); }, [roverPool]); const rotationKey = useMemo( () => rotationPool .map((entry) => `r:${entry.rover.id}`) .join('|'), [rotationPool], ); useEffect(() => { setIndex(0); }, [rotationKey]); useEffect(() => { if (!rotationPool.length) return undefined; const timer = setInterval(() => { setIndex((prev) => (prev + 1) % rotationPool.length); }, ROTATE_MS); return () => clearInterval(timer); }, [rotationPool.length, rotationKey]); const activeEntry = rotationPool.length ? rotationPool[index % rotationPool.length] : null; const activeRover = activeEntry?.type === 'rover' ? activeEntry.rover : null; const activeSnapshot = !canSpectateVideo && activeRover ? snapshotFeeds[activeRover.id] || null : null; const activeVideo = canSpectateVideo && activeRover ? videoSources[activeRover.id] || null : null; const activeAudio = activeRover ? audioSources[`${activeRover.id}-audio`] || null : null; const activeFrame = activeRover ? frames[activeRover.id] || null : null; const driverLabel = activeRover ? formatDriverLabel({ roverId: activeRover.id, session }) : null; if (inLockdown) { return (

Mini spectator is disabled in lockdown.

It will automatically resume once lockdown ends.

); } if (!driverRoster.length) { return (
{roster.length ? ( roster.map((rover, idx) => ( )) ) : (
No rovers available.
)}
); } return (
{!spectatorReady ? (
Switching to spectator…
) : activeRover ? ( {canSpectateVideo ? (
{rotationPool.map((entry) => { const rover = entry.rover; const isActive = activeRover?.id === rover.id; return (
); })}
) : ( )}
) : (
{driverRoster.length ? 'No sources available.' : 'No active drivers.'}
)}
); } export default function MiniSummaryApp() { useEffect(() => { if (typeof window === 'undefined') return undefined; const timer = setTimeout(() => { const url = new URL(window.location.href); url.searchParams.set('refresh', Date.now().toString()); window.location.replace(url.toString()); }, HARD_REFRESH_MS); return () => clearTimeout(timer); }, []); return ( <> ); } function FitViewportFrame({ children }) { return (
{children}
); }