import { useEffect, useMemo, useState } from 'react'; import { useSession } from '../context/SessionContext.jsx'; import RoverRoster from './RoverRoster.jsx'; const MODES = [ { key: 'open', label: 'Open' }, { key: 'turns', label: 'Turns' }, { key: 'admin', label: 'Admin' }, { key: 'lockdown', label: 'Lockdown' }, ]; export default function AdminPanel() { const { session, lockRover, setMode, requestControl, setCommunityGoal } = useSession(); const roster = useMemo(() => session?.roster ?? [], [session?.roster]); const [lockStates, setLockStates] = useState({}); const health = session?.health || null; const currentGoal = session?.communityGoal?.text || ''; const goalUpdatedAt = session?.communityGoal?.updatedAt || null; const [goalDraft, setGoalDraft] = useState(currentGoal); const socketIps = useMemo(() => { const users = session?.users ?? []; return users .map((user) => ({ socketId: user.socketId, label: user.nickname || user.socketId?.slice(0, 6) || 'unknown', ip: user.ip || null, })) .filter((entry) => entry.ip); }, [session?.users]); 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 handleGoalSave = async () => { try { await setCommunityGoal(goalDraft); } catch (err) { alert(err.message); } }; const handleGoalClear = async () => { try { await setCommunityGoal(null); } catch (err) { alert(err.message); } }; useEffect(() => { setGoalDraft(currentGoal); }, [currentGoal]); const lockMap = useMemo(() => { const map = {}; roster.forEach((rover) => { map[rover.id] = lockStates[rover.id] ?? rover.locked; }); return map; }, [roster, lockStates]); if (!isAdmin) return null; return (
Admin controls
Community goal {goalUpdatedAt ? ( Updated {new Date(goalUpdatedAt).toLocaleString()} ) : null}
setGoalDraft(event.target.value)} placeholder="Set a community goal" className="field-input text-sm" />
(
)} />
); } 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'}
))}
); } function WebsocketIpPanel({ entries }) { const total = entries?.length || 0; const uniqueCount = useMemo(() => new Set((entries || []).map((entry) => entry.ip)).size, [entries]); return (
Websocket IPs {uniqueCount} unique / {total} sockets
{total === 0 ? (

No IPs reported yet.

) : ( entries.map((entry) => (
{entry.label} {entry.ip}
)) )}
); }