This commit is contained in:
legop3
2025-11-15 22:02:44 -05:00
parent 7a7a194fde
commit d55726eb1f
11 changed files with 90 additions and 68 deletions
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
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -5,8 +5,8 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>webui</title>
<script type="module" crossorigin src="/assets/index-CXibTtk4.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BUE8ypH_.css">
<script type="module" crossorigin src="/assets/index-0rsVlryQ.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-f8aVcpCN.css">
</head>
<body>
<div id="root"></div>
+4 -4
View File
@@ -12,7 +12,7 @@ import { useVideoRequests } from './hooks/useVideoRequests.js';
function RoomCameraPanel() {
return (
<div className="min-h-[8rem] rounded-sm bg-[#1b1b1b] p-1 text-sm text-slate-200">
<div className="min-h-[8rem] rounded-sm bg-[#242a32] p-1 text-sm text-slate-200">
<p className="text-center text-xs text-slate-400">Room camera</p>
<p className="mt-1 text-center text-sm text-slate-200">Feed placeholder. Wire upcoming room cam here.</p>
</div>
@@ -52,7 +52,7 @@ function useLayoutMode() {
function LogPanel() {
const { logs } = useSession();
return (
<div className="rounded-sm bg-[#1b1b1b] p-1 text-xs text-slate-200">
<div className="rounded-sm bg-[#242a32] p-1 text-xs text-slate-200">
<div className="flex items-center justify-between text-[0.7rem] text-slate-400">
<span>Server logs</span>
<span>{logs.length}</span>
@@ -91,7 +91,7 @@ function DriverVideoPanel() {
}, [session?.roster, roverId]);
return (
<section className="rounded-sm bg-[#1b1b1b] p-1">
<section className="rounded-sm bg-[#242a32] p-1">
{roverId ? (
<div className="min-h-[55vh]">
<VideoTile
@@ -130,7 +130,7 @@ function AuthPanel() {
}
return (
<div className="rounded-sm bg-[#1b1b1b] p-1 text-sm text-slate-200">
<div className="rounded-sm bg-[#242a32] p-1 text-sm text-slate-200">
<p className="text-xs text-slate-400">Admin login</p>
<form className="mt-1 flex flex-col gap-1" onSubmit={handleLogin}>
<input
+1 -1
View File
@@ -57,7 +57,7 @@ export default function AdminPanel() {
if (!isAdmin) return null;
return (
<section className="rounded-sm bg-[#1b1b1b] p-1 text-xs text-slate-200">
<section className="rounded-sm bg-[#242a32] p-1 text-xs text-slate-200">
<div className="flex items-center justify-between gap-1">
<span>Admin controls</span>
<select
+49 -32
View File
@@ -19,42 +19,32 @@ export default function DrivePanel() {
const updated = frame?.receivedAt ? new Date(frame.receivedAt).toLocaleTimeString() : null;
return (
<section className="rounded-sm bg-[#1b1b1b] p-1 text-sm text-slate-100">
<div className="flex items-center justify-between text-xs text-slate-400">
<section className="rounded-sm bg-[#242a32] p-1 text-sm text-slate-100">
<div className="flex items-center justify-between text-xs text-slate-300">
<span>Drive control</span>
<span>{roverId ? `Rover ${roverId}` : 'unassigned'}</span>
</div>
<div className="mt-1 space-y-1">
<div>
<button
type="button"
onClick={runStartDockFull}
disabled={!roverId}
className="w-full rounded-sm bg-black/50 px-1 py-1 text-xs text-slate-200 disabled:opacity-30"
>
Enable driving mode
</button>
<p className="mt-1 text-xs text-slate-400">Runs Start Dock Full to prep sensors.</p>
<div className="mt-1 flex flex-wrap items-center gap-1 text-[0.65rem]">
<StatusPill label="Driving" active={drivingMode} />
{updated && <span className="text-slate-500">Sensors {updated}</span>}
</div>
</div>
<div>
<button
type="button"
onClick={seekDock}
disabled={!roverId}
className="w-full rounded-sm bg-black/50 px-1 py-1 text-xs text-slate-200 disabled:opacity-30"
>
Seek dock
</button>
<p className="mt-1 text-xs text-slate-400">Point the rover straight at the dock, about one foot away.</p>
<div className="mt-1 flex flex-wrap gap-1">
<StatusPill label={docked ? 'Docked' : 'Not docked'} active={docked} />
<StatusPill label={charging ? 'Charging' : 'Not charging'} active={charging} />
</div>
</div>
<ActionCard
title="Start Driving"
description="Press to enable driving mode, then start moving. The headlamps should illuminate."
statuses={[{ label: drivingMode ? 'Ready!' : 'Not Ready!', active: drivingMode }]}
gradient="from-emerald-600 to-emerald-400"
onClick={runStartDockFull}
disabled={!roverId}
footnote={updated ? `Sensor ping ${updated}` : null}
/>
<ActionCard
title="Dock and Charge"
description="Line the rover up about a foot from the dock, then trigger an automatic approach."
statuses={[
{ label: docked ? 'Docked!' : 'Not Docked!', active: docked },
{ label: charging ? 'Charging!' : 'Not Charging!', active: charging },
]}
gradient="from-indigo-600 to-purple-500"
onClick={seekDock}
disabled={!roverId}
/>
<SpeedRow left={speeds.left} right={speeds.right} />
<div>
<p className="text-xs text-slate-400">Manual OI</p>
@@ -88,6 +78,33 @@ export default function DrivePanel() {
);
}
function ActionCard({ title, description, statuses, gradient, onClick, disabled, footnote }) {
return (
<button
type="button"
onClick={onClick}
disabled={disabled}
className={`w-full rounded-lg bg-gradient-to-r ${gradient} px-2 py-2 text-left text-white disabled:opacity-40`}
>
<p className="text-base font-semibold">{title}</p>
<p className="text-sm text-emerald-50/80">{description}</p>
<div className="mt-2 flex flex-wrap gap-1">
{statuses.map((status) => (
<span
key={status.label}
className={`rounded-full px-3 py-0.5 text-xs font-semibold ${
status.active ? 'bg-lime-300 text-emerald-900' : 'bg-emerald-900 text-emerald-100'
}`}
>
{status.label}
</span>
))}
</div>
{footnote && <p className="mt-1 text-xs text-emerald-50/80">{footnote}</p>}
</button>
);
}
function SpeedRow({ left, right }) {
return (
<div className="grid grid-cols-2 gap-1 rounded-sm bg-black/40 p-1 text-[0.7rem]">
+2 -2
View File
@@ -27,7 +27,7 @@ function MobileJoystick() {
}, [disabled, driveWithVector]);
return (
<section className="rounded-sm bg-[#1b1b1b] p-1 text-sm text-slate-100">
<section className="rounded-sm bg-[#242a32] p-1 text-sm text-slate-100">
<div className="text-xs text-slate-400">Joystick</div>
<div className="mt-1 flex items-center justify-center">
<Joystick
@@ -79,7 +79,7 @@ function AuxMotorControls() {
];
return (
<section className="rounded-sm bg-[#1b1b1b] p-1 text-sm text-slate-100">
<section className="rounded-sm bg-[#242a32] p-1 text-sm text-slate-100">
<div className="text-xs text-slate-400">Aux motors</div>
<div className="mt-1 flex flex-wrap gap-1">
<button
+1 -1
View File
@@ -38,7 +38,7 @@ export default function TelemetryPanel() {
}
return (
<section className="rounded-sm bg-[#1b1b1b] p-1 text-sm text-slate-200">
<section className="rounded-sm bg-[#242a32] p-1 text-sm text-slate-200">
<div className="text-[0.7rem] text-slate-400">
<span>{connected ? 'online' : 'offline'}</span>
<span> · role {session?.role || 'unknown'}</span>
+19 -14
View File
@@ -1,24 +1,17 @@
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useSocket } from '../context/SocketContext.jsx';
export function useVideoRequests(roverIds = []) {
const socket = useSocket();
const [sources, setSources] = useState({});
const normalizedKey = Array.isArray(roverIds) ? roverIds.filter(Boolean).join('|') : '';
useEffect(() => {
const ids = Array.from(new Set(roverIds.filter(Boolean)));
if (!ids.length) {
setSources({});
return;
if (!normalizedKey) {
return undefined;
}
const ids = normalizedKey.split('|');
let cancelled = false;
setSources((prev) => {
const next = {};
ids.forEach((id) => {
if (prev[id]) next[id] = prev[id];
});
return next;
});
ids.forEach((roverId) => {
socket.emit('video:request', { roverId }, (resp = {}) => {
if (cancelled) return;
@@ -32,7 +25,19 @@ export function useVideoRequests(roverIds = []) {
return () => {
cancelled = true;
};
}, [socket, roverIds.join('|')]);
}, [socket, normalizedKey]);
return sources;
const filtered = useMemo(() => {
if (!normalizedKey) return {};
const ids = normalizedKey.split('|');
const next = {};
ids.forEach((id) => {
if (sources[id]) {
next[id] = sources[id];
}
});
return next;
}, [normalizedKey, sources]);
return filtered;
}