mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 17:40:46 -04:00
guh
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,8 +5,8 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>webui</title>
|
<title>webui</title>
|
||||||
<script type="module" crossorigin src="/assets/index-n1qWThah.js"></script>
|
<script type="module" crossorigin src="/assets/index-_yFT6hTb.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-Cyy-WeOF.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-CIpY4YLn.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { useSession } from './context/SessionContext.jsx';
|
|||||||
import TelemetryPanel from './components/TelemetryPanel.jsx';
|
import TelemetryPanel from './components/TelemetryPanel.jsx';
|
||||||
import VideoTile from './components/VideoTile.jsx';
|
import VideoTile from './components/VideoTile.jsx';
|
||||||
import DrivePanel from './components/DrivePanel.jsx';
|
import DrivePanel from './components/DrivePanel.jsx';
|
||||||
|
import AlertFeed from './components/AlertFeed.jsx';
|
||||||
|
import AdminPanel from './components/AdminPanel.jsx';
|
||||||
import { useVideoRequests } from './hooks/useVideoRequests.js';
|
import { useVideoRequests } from './hooks/useVideoRequests.js';
|
||||||
|
|
||||||
function StatusBadge({ connected, role, mode }) {
|
function StatusBadge({ connected, role, mode }) {
|
||||||
@@ -268,6 +270,8 @@ function App() {
|
|||||||
<TelemetryPanel />
|
<TelemetryPanel />
|
||||||
<AuthPanel />
|
<AuthPanel />
|
||||||
<SessionInspector />
|
<SessionInspector />
|
||||||
|
<AdminPanel />
|
||||||
|
<AlertFeed />
|
||||||
</section>
|
</section>
|
||||||
<LogPanel />
|
<LogPanel />
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { useSession } from '../context/SessionContext.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 } = useSession();
|
||||||
|
const roster = session?.roster ?? [];
|
||||||
|
const [lockStates, setLockStates] = useState({});
|
||||||
|
|
||||||
|
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 lockMap = useMemo(() => {
|
||||||
|
const map = {};
|
||||||
|
roster.forEach((rover) => {
|
||||||
|
map[rover.id] = lockStates[rover.id] ?? rover.locked;
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}, [roster, lockStates]);
|
||||||
|
|
||||||
|
if (!isAdmin) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="rounded-2xl border border-amber-600/40 bg-amber-500/5 p-4 text-sm text-amber-100">
|
||||||
|
<header className="mb-3 flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<h2 className="text-lg font-semibold text-white">Admin Controls</h2>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<label className="text-xs uppercase tracking-[0.3em] text-amber-300">Server mode</label>
|
||||||
|
<select
|
||||||
|
value={currentMode}
|
||||||
|
onChange={handleModeChange}
|
||||||
|
className="rounded-md border border-amber-600/40 bg-slate-950/60 px-2 py-1 text-sm text-white"
|
||||||
|
>
|
||||||
|
{MODES.map((mode) => (
|
||||||
|
<option key={mode.key} value={mode.key}>
|
||||||
|
{mode.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{roster.map((rover) => (
|
||||||
|
<div
|
||||||
|
key={rover.id}
|
||||||
|
className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-amber-600/30 bg-slate-950/30 px-3 py-2"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p className="text-white">{rover.name}</p>
|
||||||
|
<p className="text-xs text-amber-200">Locked: {lockMap[rover.id] ? 'yes' : 'no'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleLockToggle(rover.id, !lockMap[rover.id])}
|
||||||
|
className="rounded-md border border-amber-500 px-3 py-1 text-xs font-semibold text-amber-100"
|
||||||
|
>
|
||||||
|
{lockMap[rover.id] ? 'Unlock' : 'Lock'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleForceControl(rover.id)}
|
||||||
|
className="rounded-md border border-amber-500 px-3 py-1 text-xs font-semibold text-amber-100"
|
||||||
|
>
|
||||||
|
Force Control
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useSession } from '../context/SessionContext.jsx';
|
||||||
|
|
||||||
|
function AlertCard({ alert }) {
|
||||||
|
const color =
|
||||||
|
alert.color === 'error'
|
||||||
|
? 'bg-red-500/10 border-red-500/40 text-red-100'
|
||||||
|
: alert.color === 'warn'
|
||||||
|
? 'bg-amber-500/10 border-amber-500/40 text-amber-100'
|
||||||
|
: 'bg-emerald-500/10 border-emerald-500/30 text-emerald-100';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`rounded-2xl border px-4 py-3 text-sm ${color}`}>
|
||||||
|
<p className="text-xs uppercase tracking-[0.3em] opacity-60">{alert.title || 'Alert'}</p>
|
||||||
|
<p className="mt-1 text-base font-semibold">{alert.message}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AlertFeed() {
|
||||||
|
const { alerts } = useSession();
|
||||||
|
const recent = useMemo(() => alerts.slice(-5).reverse(), [alerts]);
|
||||||
|
if (recent.length === 0) return null;
|
||||||
|
return (
|
||||||
|
<section className="rounded-2xl border border-slate-800 bg-slate-900/60 p-4">
|
||||||
|
<h2 className="text-lg font-semibold text-white">Recent Alerts</h2>
|
||||||
|
<div className="mt-4 space-y-3">{recent.map((alert) => <AlertCard key={alert.id || alert.message} alert={alert} />)}</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ export function SessionProvider({ children }) {
|
|||||||
const emitWithAck = useAckEmitter(socket);
|
const emitWithAck = useAckEmitter(socket);
|
||||||
const [session, setSession] = useState(null);
|
const [session, setSession] = useState(null);
|
||||||
const [logs, setLogs] = useState([]);
|
const [logs, setLogs] = useState([]);
|
||||||
|
const [alerts, setAlerts] = useState([]);
|
||||||
const [connected, setConnected] = useState(socket.connected);
|
const [connected, setConnected] = useState(socket.connected);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -59,10 +60,14 @@ export function SessionProvider({ children }) {
|
|||||||
socket.on('session:sync', handleSession);
|
socket.on('session:sync', handleSession);
|
||||||
socket.on('log:init', handleLogInit);
|
socket.on('log:init', handleLogInit);
|
||||||
socket.on('log:entry', handleLogEntry);
|
socket.on('log:entry', handleLogEntry);
|
||||||
|
socket.on('alert:new', (payload = {}) => {
|
||||||
|
setAlerts((prev) => [...prev.slice(-49), payload]);
|
||||||
|
});
|
||||||
return () => {
|
return () => {
|
||||||
socket.off('session:sync', handleSession);
|
socket.off('session:sync', handleSession);
|
||||||
socket.off('log:init', handleLogInit);
|
socket.off('log:init', handleLogInit);
|
||||||
socket.off('log:entry', handleLogEntry);
|
socket.off('log:entry', handleLogEntry);
|
||||||
|
socket.off('alert:new');
|
||||||
};
|
};
|
||||||
}, [socket]);
|
}, [socket]);
|
||||||
|
|
||||||
@@ -74,6 +79,8 @@ export function SessionProvider({ children }) {
|
|||||||
emitWithAck('session:requestControl', { roverId, ...options }),
|
emitWithAck('session:requestControl', { roverId, ...options }),
|
||||||
releaseControl: (roverId) => emitWithAck('session:releaseControl', { roverId }),
|
releaseControl: (roverId) => emitWithAck('session:releaseControl', { roverId }),
|
||||||
subscribeAll: () => emitWithAck('session:subscribeAll'),
|
subscribeAll: () => emitWithAck('session:subscribeAll'),
|
||||||
|
lockRover: (roverId, locked) => emitWithAck('session:lockRover', { roverId, locked }),
|
||||||
|
setMode: (mode) => emitWithAck('setMode', { mode }),
|
||||||
}),
|
}),
|
||||||
[emitWithAck],
|
[emitWithAck],
|
||||||
);
|
);
|
||||||
@@ -83,9 +90,10 @@ export function SessionProvider({ children }) {
|
|||||||
connected,
|
connected,
|
||||||
session,
|
session,
|
||||||
logs,
|
logs,
|
||||||
|
alerts,
|
||||||
...actions,
|
...actions,
|
||||||
}),
|
}),
|
||||||
[actions, connected, logs, session],
|
[actions, alerts, connected, logs, session],
|
||||||
);
|
);
|
||||||
|
|
||||||
return <SessionContext.Provider value={value}>{children}</SessionContext.Provider>;
|
return <SessionContext.Provider value={value}>{children}</SessionContext.Provider>;
|
||||||
|
|||||||
Reference in New Issue
Block a user