import { useEffect, useMemo, useState } from 'react'; import { useCommandPipeline } from '../controls/commandPipeline.js'; import { useSession } from '../context/SessionContext.jsx'; import { useSocket } from '../context/SocketContext.jsx'; import RoverRoster from './RoverRoster.jsx'; const MODES = [ { key: 'open', label: 'Open' }, { key: 'turns', label: 'Turns' }, { key: 'admin', label: 'Admin' }, { key: 'lockdown', label: 'Lockdown' }, ]; const IR_SHOT_CODE = 200; export default function AdminPanel() { const { session, lockRover, setMode, requestControl } = useSession(); const socket = useSocket(); const pipeline = useCommandPipeline(); const roster = useMemo(() => session?.roster ?? [], [session?.roster]); const [lockStates, setLockStates] = useState({}); const health = session?.health || null; const [transport, setTransport] = useState(null); const isAdmin = session?.role === 'admin' || session?.role === 'lockdown' || session?.role === 'lockdown-admin'; const currentMode = session?.mode ?? 'open'; const handleLockToggle = async (roverId, locked) => { try { await lockRover(roverId, locked); setLockStates((prev) => ({ ...prev, [roverId]: locked })); } catch (err) { alert(err.message); } }; const handleModeChange = async (event) => { const mode = event.target.value; try { await setMode(mode); } catch (err) { alert(err.message); } }; const handleForceControl = async (roverId) => { try { await requestControl(roverId, { force: true }); } catch (err) { alert(err.message); } }; const handleIrShot = (roverId) => { pipeline.emitCommand( { type: 'ir', data: { ir: { code: IR_SHOT_CODE } }, }, null, roverId, ); }; const lockMap = useMemo(() => { const map = {}; roster.forEach((rover) => { map[rover.id] = lockStates[rover.id] ?? rover.locked; }); return map; }, [roster, lockStates]); useEffect(() => { if (!socket) return undefined; const updateTransport = () => { const name = socket.io?.engine?.transport?.name || null; setTransport(name); }; updateTransport(); socket.on('connect', updateTransport); socket.on('disconnect', () => setTransport(null)); socket.io?.engine?.on('upgrade', updateTransport); return () => { socket.off('connect', updateTransport); socket.off('disconnect'); socket.io?.engine?.off('upgrade', updateTransport); }; }, [socket]); return (
{isAdmin ? (
Admin controls
{transport ? `Conn: ${transport}` : 'Conn: —'}
) : (
Rover controls Limited access
)} (
{isAdmin ? ( <> ) : null}
)} /> {isAdmin ? : null}
); } function ReplaySnapshotHealth({ health }) { if (!health) return null; const replay = health.replay || { sources: [], readyCount: 0, totalCount: 0 }; const snapshots = health.snapshots || { rovers: [], rooms: [] }; const replaySummary = `${replay.readyCount}/${replay.totalCount} sources ready`; const roverStale = snapshots.rovers.filter((entry) => entry.stale).length; const roomStale = snapshots.rooms.filter((entry) => entry.stale).length; return (
Health
Replay segments {replaySummary}
Rover snapshots {snapshots.rovers.length - roverStale}/{snapshots.rovers.length} ok
Room cameras {snapshots.rooms.length - roomStale}/{snapshots.rooms.length} ok
{replay.sources.map((source) => (
{source.label} {source.recentCount}/{source.neededCount}
))}
{snapshots.rovers.map((entry) => (
{entry.name} {entry.stale ? 'stale' : 'ok'}
))}
{snapshots.rooms.map((entry) => (
{entry.name} {entry.error ? 'error' : entry.stale ? 'stale' : 'ok'}
))}
); }