mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
guh
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@
|
|||||||
<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-D4pzotAD.js"></script>
|
<script type="module" crossorigin src="/assets/index-BnJ59h72.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-D4wUVUN4.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-D4wUVUN4.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
+4
-138
@@ -1,23 +1,14 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useSession } from './context/SessionContext.jsx';
|
|
||||||
import { useTelemetryFrame } from './context/TelemetryContext.jsx';
|
|
||||||
import TelemetryPanel from './components/TelemetryPanel.jsx';
|
import TelemetryPanel from './components/TelemetryPanel.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 AlertFeed from './components/AlertFeed.jsx';
|
||||||
import AdminPanel from './components/AdminPanel.jsx';
|
import AdminPanel from './components/AdminPanel.jsx';
|
||||||
import MobileControls, { MobileJoystick, AuxMotorControls } from './components/MobileControls.jsx';
|
import MobileControls, { MobileJoystick, AuxMotorControls } from './components/MobileControls.jsx';
|
||||||
import { DriveControlProvider } from './context/DriveControlContext.jsx';
|
import { DriveControlProvider } from './context/DriveControlContext.jsx';
|
||||||
import { useVideoRequests } from './hooks/useVideoRequests.js';
|
import RoomCameraPanel from './components/RoomCameraPanel.jsx';
|
||||||
|
import LogPanel from './components/LogPanel.jsx';
|
||||||
function RoomCameraPanel() {
|
import AuthPanel from './components/AuthPanel.jsx';
|
||||||
return (
|
import DriverVideoPanel from './components/DriverVideoPanel.jsx';
|
||||||
<div className="min-h-[8rem] rounded-sm bg-[#242a32] p-2 text-base text-slate-100">
|
|
||||||
<p className="text-center text-sm text-slate-400">Room camera</p>
|
|
||||||
<p className="mt-1 text-center">Feed placeholder. Wire upcoming room cam here.</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function useLayoutMode() {
|
function useLayoutMode() {
|
||||||
const [mode, setMode] = useState(() => {
|
const [mode, setMode] = useState(() => {
|
||||||
@@ -49,131 +40,6 @@ function useLayoutMode() {
|
|||||||
return mode;
|
return mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
function LogPanel() {
|
|
||||||
const { logs } = useSession();
|
|
||||||
return (
|
|
||||||
<div className="rounded-sm bg-[#242a32] p-2 text-base text-slate-100">
|
|
||||||
<div className="flex items-center justify-between text-sm text-slate-400">
|
|
||||||
<span>Server logs</span>
|
|
||||||
<span>{logs.length}</span>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 h-48 overflow-y-auto rounded-sm bg-black/40 p-2 font-mono text-sm text-slate-300">
|
|
||||||
{logs.length === 0 ? (
|
|
||||||
<p>No logs yet.</p>
|
|
||||||
) : (
|
|
||||||
logs
|
|
||||||
.slice()
|
|
||||||
.reverse()
|
|
||||||
.map((entry) => (
|
|
||||||
<div key={entry.id} className="mb-1">
|
|
||||||
<span className="text-slate-500">{entry.timestamp}</span>{' '}
|
|
||||||
<span className="text-slate-300">[{entry.level}]</span>{' '}
|
|
||||||
{entry.label && <span className="text-slate-400">[{entry.label}]</span>}{' '}
|
|
||||||
<span>{entry.message}</span>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DriverVideoPanel() {
|
|
||||||
const { session } = useSession();
|
|
||||||
const roverId = session?.assignment?.roverId;
|
|
||||||
const sources = useVideoRequests(roverId ? [roverId] : []);
|
|
||||||
const info = roverId ? sources[roverId] : null;
|
|
||||||
const frame = useTelemetryFrame(roverId);
|
|
||||||
const batteryRecord =
|
|
||||||
roverId && session?.roster
|
|
||||||
? session.roster.find((item) => String(item.id) === String(roverId))
|
|
||||||
: null;
|
|
||||||
const batteryConfig = batteryRecord?.battery ?? null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="rounded-sm bg-[#242a32] p-1">
|
|
||||||
{roverId ? (
|
|
||||||
<div className="lg:min-h-[70vh]">
|
|
||||||
<VideoTile
|
|
||||||
sessionInfo={info}
|
|
||||||
label={roverId}
|
|
||||||
muted={false}
|
|
||||||
telemetryFrame={frame}
|
|
||||||
batteryConfig={batteryConfig}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className="text-sm text-slate-400">Assign a rover to view the video feed.</p>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AuthPanel() {
|
|
||||||
const { login, setRole } = useSession();
|
|
||||||
const [username, setUsername] = useState('');
|
|
||||||
const [password, setPassword] = useState('');
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
async function handleLogin(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
await login(username, password);
|
|
||||||
setUsername('');
|
|
||||||
setPassword('');
|
|
||||||
} catch (err) {
|
|
||||||
alert(err.message);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-sm bg-[#242a32] p-2 text-base text-slate-100">
|
|
||||||
<p className="text-sm text-slate-400">Admin login</p>
|
|
||||||
<form className="mt-2 flex flex-col gap-2" onSubmit={handleLogin}>
|
|
||||||
<input
|
|
||||||
className="rounded-sm bg-black/50 px-2 py-1 text-slate-100"
|
|
||||||
placeholder="Username"
|
|
||||||
value={username}
|
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
className="rounded-sm bg-black/50 px-2 py-1 text-slate-100"
|
|
||||||
placeholder="Password"
|
|
||||||
type="password"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={loading}
|
|
||||||
className="rounded-sm bg-slate-200 px-2 py-1 text-sm font-semibold text-black disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{loading ? 'Logging in…' : 'Login'}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<div className="mt-2 flex gap-2 text-sm">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setRole('user')}
|
|
||||||
className="flex-1 rounded-sm bg-black/40 px-2 py-1 text-slate-200"
|
|
||||||
>
|
|
||||||
Driver
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setRole('spectator')}
|
|
||||||
className="flex-1 rounded-sm bg-black/40 px-2 py-1 text-slate-200"
|
|
||||||
>
|
|
||||||
Spectator
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DesktopLayout() {
|
function DesktopLayout() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useSession } from '../context/SessionContext.jsx';
|
||||||
|
|
||||||
|
export default function AuthPanel() {
|
||||||
|
const { login, setRole } = useSession();
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
async function handleLogin(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await login(username, password);
|
||||||
|
setUsername('');
|
||||||
|
setPassword('');
|
||||||
|
} catch (err) {
|
||||||
|
alert(err.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-sm bg-[#242a32] p-2 text-base text-slate-100">
|
||||||
|
<p className="text-sm text-slate-400">Admin login</p>
|
||||||
|
<form className="mt-2 flex flex-col gap-2" onSubmit={handleLogin}>
|
||||||
|
<input
|
||||||
|
className="rounded-sm bg-black/50 px-2 py-1 text-slate-100"
|
||||||
|
placeholder="Username"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="rounded-sm bg-black/50 px-2 py-1 text-slate-100"
|
||||||
|
placeholder="Password"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="rounded-sm bg-slate-200 px-2 py-1 text-sm font-semibold text-black disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? 'Logging in…' : 'Login'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<div className="mt-2 flex gap-2 text-sm">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setRole('user')}
|
||||||
|
className="flex-1 rounded-sm bg-black/40 px-2 py-1 text-slate-200"
|
||||||
|
>
|
||||||
|
Driver
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setRole('spectator')}
|
||||||
|
className="flex-1 rounded-sm bg-black/40 px-2 py-1 text-slate-200"
|
||||||
|
>
|
||||||
|
Spectator
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { useSession } from '../context/SessionContext.jsx';
|
||||||
|
import { useTelemetryFrame } from '../context/TelemetryContext.jsx';
|
||||||
|
import { useVideoRequests } from '../hooks/useVideoRequests.js';
|
||||||
|
import VideoTile from './VideoTile.jsx';
|
||||||
|
|
||||||
|
export default function DriverVideoPanel() {
|
||||||
|
const { session } = useSession();
|
||||||
|
const roverId = session?.assignment?.roverId;
|
||||||
|
const sources = useVideoRequests(roverId ? [roverId] : []);
|
||||||
|
const info = roverId ? sources[roverId] : null;
|
||||||
|
const frame = useTelemetryFrame(roverId);
|
||||||
|
const batteryRecord =
|
||||||
|
roverId && session?.roster
|
||||||
|
? session.roster.find((item) => String(item.id) === String(roverId))
|
||||||
|
: null;
|
||||||
|
const batteryConfig = batteryRecord?.battery ?? null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="rounded-sm bg-[#242a32] p-1">
|
||||||
|
{roverId ? (
|
||||||
|
<div className="lg:min-h-[70vh]">
|
||||||
|
<VideoTile
|
||||||
|
sessionInfo={info}
|
||||||
|
label={roverId}
|
||||||
|
muted={false}
|
||||||
|
telemetryFrame={frame}
|
||||||
|
batteryConfig={batteryConfig}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-slate-400">Assign a rover to view the video feed.</p>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { useSession } from '../context/SessionContext.jsx';
|
||||||
|
|
||||||
|
export default function LogPanel() {
|
||||||
|
const { logs } = useSession();
|
||||||
|
return (
|
||||||
|
<div className="rounded-sm bg-[#242a32] p-2 text-base text-slate-100">
|
||||||
|
<div className="flex items-center justify-between text-sm text-slate-400">
|
||||||
|
<span>Server logs</span>
|
||||||
|
<span>{logs.length}</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 h-48 overflow-y-auto rounded-sm bg-black/40 p-2 font-mono text-sm text-slate-300">
|
||||||
|
{logs.length === 0 ? (
|
||||||
|
<p>No logs yet.</p>
|
||||||
|
) : (
|
||||||
|
logs
|
||||||
|
.slice()
|
||||||
|
.reverse()
|
||||||
|
.map((entry) => (
|
||||||
|
<div key={entry.id} className="mb-1">
|
||||||
|
<span className="text-slate-500">{entry.timestamp}</span>{' '}
|
||||||
|
<span className="text-slate-300">[{entry.level}]</span>{' '}
|
||||||
|
{entry.label && <span className="text-slate-400">[{entry.label}]</span>}{' '}
|
||||||
|
<span>{entry.message}</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export default function RoomCameraPanel() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-[8rem] rounded-sm bg-[#242a32] p-2 text-base text-slate-100">
|
||||||
|
<p className="text-center text-sm text-slate-400">Room camera</p>
|
||||||
|
<p className="mt-1 text-center">Feed placeholder. Wire upcoming room cam here.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user