mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -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-0rsVlryQ.js"></script>
|
<script type="module" crossorigin src="/assets/index-yT6hjp7B.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-f8aVcpCN.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-CrWVatgk.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
+19
-19
@@ -12,9 +12,9 @@ import { useVideoRequests } from './hooks/useVideoRequests.js';
|
|||||||
|
|
||||||
function RoomCameraPanel() {
|
function RoomCameraPanel() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-[8rem] rounded-sm bg-[#242a32] p-1 text-sm text-slate-200">
|
<div className="min-h-[8rem] rounded-sm bg-[#242a32] p-2 text-base text-slate-100">
|
||||||
<p className="text-center text-xs text-slate-400">Room camera</p>
|
<p className="text-center text-sm 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>
|
<p className="mt-1 text-center">Feed placeholder. Wire upcoming room cam here.</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -52,12 +52,12 @@ function useLayoutMode() {
|
|||||||
function LogPanel() {
|
function LogPanel() {
|
||||||
const { logs } = useSession();
|
const { logs } = useSession();
|
||||||
return (
|
return (
|
||||||
<div className="rounded-sm bg-[#242a32] p-1 text-xs text-slate-200">
|
<div className="rounded-sm bg-[#242a32] p-2 text-base text-slate-100">
|
||||||
<div className="flex items-center justify-between text-[0.7rem] text-slate-400">
|
<div className="flex items-center justify-between text-sm text-slate-400">
|
||||||
<span>Server logs</span>
|
<span>Server logs</span>
|
||||||
<span>{logs.length}</span>
|
<span>{logs.length}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 h-48 overflow-y-auto rounded-sm bg-black/40 p-1 font-mono text-[0.65rem] text-slate-300">
|
<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 ? (
|
{logs.length === 0 ? (
|
||||||
<p>No logs yet.</p>
|
<p>No logs yet.</p>
|
||||||
) : (
|
) : (
|
||||||
@@ -93,7 +93,7 @@ function DriverVideoPanel() {
|
|||||||
return (
|
return (
|
||||||
<section className="rounded-sm bg-[#242a32] p-1">
|
<section className="rounded-sm bg-[#242a32] p-1">
|
||||||
{roverId ? (
|
{roverId ? (
|
||||||
<div className="min-h-[55vh]">
|
<div className="min-h-[70vh]">
|
||||||
<VideoTile
|
<VideoTile
|
||||||
sessionInfo={info}
|
sessionInfo={info}
|
||||||
label={roverId}
|
label={roverId}
|
||||||
@@ -130,17 +130,17 @@ function AuthPanel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-sm bg-[#242a32] p-1 text-sm text-slate-200">
|
<div className="rounded-sm bg-[#242a32] p-2 text-base text-slate-100">
|
||||||
<p className="text-xs text-slate-400">Admin login</p>
|
<p className="text-sm text-slate-400">Admin login</p>
|
||||||
<form className="mt-1 flex flex-col gap-1" onSubmit={handleLogin}>
|
<form className="mt-2 flex flex-col gap-2" onSubmit={handleLogin}>
|
||||||
<input
|
<input
|
||||||
className="rounded-sm bg-black/50 px-1 py-1 text-slate-100"
|
className="rounded-sm bg-black/50 px-2 py-1 text-slate-100"
|
||||||
placeholder="Username"
|
placeholder="Username"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
className="rounded-sm bg-black/50 px-1 py-1 text-slate-100"
|
className="rounded-sm bg-black/50 px-2 py-1 text-slate-100"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
@@ -149,23 +149,23 @@ function AuthPanel() {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="rounded-sm bg-slate-200 px-1 py-1 text-xs font-semibold text-black disabled:opacity-50"
|
className="rounded-sm bg-slate-200 px-2 py-1 text-sm font-semibold text-black disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{loading ? 'Logging in…' : 'Login'}
|
{loading ? 'Logging in…' : 'Login'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<div className="mt-1 flex gap-1 text-xs">
|
<div className="mt-2 flex gap-2 text-sm">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setRole('user')}
|
onClick={() => setRole('user')}
|
||||||
className="flex-1 rounded-sm bg-black/40 px-1 py-1 text-slate-200"
|
className="flex-1 rounded-sm bg-black/40 px-2 py-1 text-slate-200"
|
||||||
>
|
>
|
||||||
Driver
|
Driver
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setRole('spectator')}
|
onClick={() => setRole('spectator')}
|
||||||
className="flex-1 rounded-sm bg-black/40 px-1 py-1 text-slate-200"
|
className="flex-1 rounded-sm bg-black/40 px-2 py-1 text-slate-200"
|
||||||
>
|
>
|
||||||
Spectator
|
Spectator
|
||||||
</button>
|
</button>
|
||||||
@@ -177,7 +177,7 @@ function AuthPanel() {
|
|||||||
function DesktopLayout() {
|
function DesktopLayout() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<section className="grid grid-cols-[minmax(0,0.9fr)_minmax(0,1.6fr)_minmax(0,0.9fr)] gap-1">
|
<section className="grid grid-cols-[minmax(0,0.6fr)_minmax(0,2.2fr)_minmax(0,0.6fr)] gap-1">
|
||||||
<TelemetryPanel />
|
<TelemetryPanel />
|
||||||
<DriverVideoPanel />
|
<DriverVideoPanel />
|
||||||
<DrivePanel />
|
<DrivePanel />
|
||||||
@@ -212,7 +212,7 @@ function MobilePortraitLayout() {
|
|||||||
function MobileLandscapeLayout() {
|
function MobileLandscapeLayout() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<section className="grid grid-cols-[minmax(0,1fr)_minmax(0,1.4fr)_minmax(0,1fr)] gap-1">
|
<section className="grid grid-cols-[minmax(0,0.7fr)_minmax(0,2.1fr)_minmax(0,0.7fr)] gap-1">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<DrivePanel />
|
<DrivePanel />
|
||||||
<AuxMotorControls />
|
<AuxMotorControls />
|
||||||
@@ -244,7 +244,7 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-black text-slate-50">
|
<div className="min-h-screen bg-black text-slate-50">
|
||||||
<DriveControlProvider>
|
<DriveControlProvider>
|
||||||
<main className="flex w-full flex-col gap-1 px-1 py-1">{renderedLayout}</main>
|
<main className="flex w-full flex-col gap-1 px-1 py-1 text-base">{renderedLayout}</main>
|
||||||
<AlertFeed />
|
<AlertFeed />
|
||||||
</DriveControlProvider>
|
</DriveControlProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -57,13 +57,13 @@ export default function AdminPanel() {
|
|||||||
if (!isAdmin) return null;
|
if (!isAdmin) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="rounded-sm bg-[#242a32] p-1 text-xs text-slate-200">
|
<section className="rounded-sm bg-[#242a32] p-2 text-base text-slate-100">
|
||||||
<div className="flex items-center justify-between gap-1">
|
<div className="flex items-center justify-between gap-2 text-sm">
|
||||||
<span>Admin controls</span>
|
<span>Admin controls</span>
|
||||||
<select
|
<select
|
||||||
value={currentMode}
|
value={currentMode}
|
||||||
onChange={handleModeChange}
|
onChange={handleModeChange}
|
||||||
className="rounded-sm bg-black/40 px-1 py-0.5 text-[0.7rem] text-slate-200"
|
className="rounded-sm bg-black/40 px-2 py-1 text-sm text-slate-100"
|
||||||
>
|
>
|
||||||
{MODES.map((mode) => (
|
{MODES.map((mode) => (
|
||||||
<option key={mode.key} value={mode.key}>
|
<option key={mode.key} value={mode.key}>
|
||||||
@@ -73,25 +73,25 @@ export default function AdminPanel() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-1 space-y-1">
|
<div className="mt-2 space-y-2 text-sm">
|
||||||
{roster.map((rover) => (
|
{roster.map((rover) => (
|
||||||
<div key={rover.id} className="flex flex-wrap items-center justify-between gap-1 rounded-sm bg-black/30 px-1 py-1">
|
<div key={rover.id} className="flex flex-wrap items-center justify-between gap-2 rounded-sm bg-black/30 px-2 py-2">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-100">{rover.name}</p>
|
<p className="text-slate-100">{rover.name}</p>
|
||||||
<p className="text-[0.6rem] text-slate-500">{lockMap[rover.id] ? 'locked' : 'unlocked'}</p>
|
<p className="text-xs text-slate-400">{lockMap[rover.id] ? 'locked' : 'unlocked'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-1 text-[0.6rem]">
|
<div className="flex flex-wrap gap-1 text-xs">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleLockToggle(rover.id, !lockMap[rover.id])}
|
onClick={() => handleLockToggle(rover.id, !lockMap[rover.id])}
|
||||||
className="rounded-sm bg-black/50 px-1 py-0.5 text-slate-100"
|
className="rounded-sm bg-black/50 px-2 py-1 text-slate-100"
|
||||||
>
|
>
|
||||||
{lockMap[rover.id] ? 'Unlock' : 'Lock'}
|
{lockMap[rover.id] ? 'Unlock' : 'Lock'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleForceControl(rover.id)}
|
onClick={() => handleForceControl(rover.id)}
|
||||||
className="rounded-sm bg-black/50 px-1 py-0.5 text-slate-100"
|
className="rounded-sm bg-black/50 px-2 py-1 text-slate-100"
|
||||||
>
|
>
|
||||||
Force
|
Force
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -19,17 +19,17 @@ export default function DrivePanel() {
|
|||||||
const updated = frame?.receivedAt ? new Date(frame.receivedAt).toLocaleTimeString() : null;
|
const updated = frame?.receivedAt ? new Date(frame.receivedAt).toLocaleTimeString() : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="rounded-sm bg-[#242a32] p-1 text-sm text-slate-100">
|
<section className="rounded-sm bg-[#242a32] p-2 text-base text-slate-100">
|
||||||
<div className="flex items-center justify-between text-xs text-slate-300">
|
<div className="flex items-center justify-between text-sm text-slate-300">
|
||||||
<span>Drive control</span>
|
<span>Drive control</span>
|
||||||
<span>{roverId ? `Rover ${roverId}` : 'unassigned'}</span>
|
<span>{roverId ? `Rover ${roverId}` : 'unassigned'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 space-y-1">
|
<div className="mt-2 space-y-2">
|
||||||
<ActionCard
|
<ActionCard
|
||||||
title="Start Driving"
|
title="Start Driving"
|
||||||
description="Press to enable driving mode, then start moving. The headlamps should illuminate."
|
description="Press to enable driving mode, then start moving. The headlamps should illuminate."
|
||||||
statuses={[{ label: drivingMode ? 'Ready!' : 'Not Ready!', active: drivingMode }]}
|
statuses={[{ label: drivingMode ? 'Ready!' : 'Not Ready!', active: drivingMode }]}
|
||||||
gradient="from-emerald-600 to-emerald-400"
|
tone="emerald"
|
||||||
onClick={runStartDockFull}
|
onClick={runStartDockFull}
|
||||||
disabled={!roverId}
|
disabled={!roverId}
|
||||||
footnote={updated ? `Sensor ping ${updated}` : null}
|
footnote={updated ? `Sensor ping ${updated}` : null}
|
||||||
@@ -41,13 +41,13 @@ export default function DrivePanel() {
|
|||||||
{ label: docked ? 'Docked!' : 'Not Docked!', active: docked },
|
{ label: docked ? 'Docked!' : 'Not Docked!', active: docked },
|
||||||
{ label: charging ? 'Charging!' : 'Not Charging!', active: charging },
|
{ label: charging ? 'Charging!' : 'Not Charging!', active: charging },
|
||||||
]}
|
]}
|
||||||
gradient="from-indigo-600 to-purple-500"
|
tone="indigo"
|
||||||
onClick={seekDock}
|
onClick={seekDock}
|
||||||
disabled={!roverId}
|
disabled={!roverId}
|
||||||
/>
|
/>
|
||||||
<SpeedRow left={speeds.left} right={speeds.right} />
|
<SpeedRow left={speeds.left} right={speeds.right} />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs text-slate-400">Manual OI</p>
|
<p className="text-sm text-slate-300">Manual OI</p>
|
||||||
<div className="mt-1 flex flex-wrap gap-1">
|
<div className="mt-1 flex flex-wrap gap-1">
|
||||||
{manualOiButtons.map((btn) => (
|
{manualOiButtons.map((btn) => (
|
||||||
<button
|
<button
|
||||||
@@ -55,7 +55,7 @@ export default function DrivePanel() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => sendOiCommand(btn.key)}
|
onClick={() => sendOiCommand(btn.key)}
|
||||||
disabled={!roverId}
|
disabled={!roverId}
|
||||||
className="rounded-sm bg-black/40 px-1 py-0.5 text-[0.65rem] text-slate-200 disabled:opacity-30"
|
className="rounded-sm bg-black/40 px-2 py-0.5 text-sm text-slate-200 disabled:opacity-30"
|
||||||
>
|
>
|
||||||
{btn.label}
|
{btn.label}
|
||||||
</button>
|
</button>
|
||||||
@@ -67,27 +67,28 @@ export default function DrivePanel() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={stopMotors}
|
onClick={stopMotors}
|
||||||
disabled={!roverId}
|
disabled={!roverId}
|
||||||
className="flex-1 rounded-sm bg-red-600/60 px-1 py-1 text-xs text-white disabled:opacity-40"
|
className="flex-1 rounded-sm bg-red-600/70 px-2 py-1 text-sm text-white disabled:opacity-40"
|
||||||
>
|
>
|
||||||
Stop motors
|
Stop motors
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-[0.65rem] text-slate-500">Sensor streaming auto-starts after each OI change.</p>
|
<p className="mt-2 text-sm text-slate-400">Sensor streaming auto-starts after each OI change.</p>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ActionCard({ title, description, statuses, gradient, onClick, disabled, footnote }) {
|
function ActionCard({ title, description, statuses, tone, onClick, disabled, footnote }) {
|
||||||
|
const baseColor = tone === 'indigo' ? 'bg-indigo-600' : 'bg-emerald-600';
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={`w-full rounded-lg bg-gradient-to-r ${gradient} px-2 py-2 text-left text-white disabled:opacity-40`}
|
className={`w-full rounded-lg ${baseColor} px-3 py-2 text-left text-white disabled:opacity-40`}
|
||||||
>
|
>
|
||||||
<p className="text-base font-semibold">{title}</p>
|
<p className="text-base font-semibold">{title}</p>
|
||||||
<p className="text-sm text-emerald-50/80">{description}</p>
|
<p className="text-sm text-white/90">{description}</p>
|
||||||
<div className="mt-2 flex flex-wrap gap-1">
|
<div className="mt-2 flex flex-wrap gap-1">
|
||||||
{statuses.map((status) => (
|
{statuses.map((status) => (
|
||||||
<span
|
<span
|
||||||
@@ -107,13 +108,13 @@ function ActionCard({ title, description, statuses, gradient, onClick, disabled,
|
|||||||
|
|
||||||
function SpeedRow({ left, right }) {
|
function SpeedRow({ left, right }) {
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-2 gap-1 rounded-sm bg-black/40 p-1 text-[0.7rem]">
|
<div className="grid grid-cols-2 gap-1 rounded-sm bg-black/40 p-2 text-base">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[0.6rem] text-slate-500">Left</p>
|
<p className="text-sm text-slate-300">Left</p>
|
||||||
<p className="font-semibold text-slate-100">{left}</p>
|
<p className="font-semibold text-slate-100">{left}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[0.6rem] text-slate-500">Right</p>
|
<p className="text-sm text-slate-300">Right</p>
|
||||||
<p className="font-semibold text-slate-100">{right}</p>
|
<p className="font-semibold text-slate-100">{right}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -123,7 +124,7 @@ function SpeedRow({ left, right }) {
|
|||||||
function StatusPill({ label, active }) {
|
function StatusPill({ label, active }) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={`rounded-sm px-1 py-0.5 text-[0.6rem] ${
|
className={`rounded-sm px-2 py-0.5 text-xs ${
|
||||||
active ? 'bg-emerald-500/30 text-emerald-100' : 'bg-black/40 text-slate-500'
|
active ? 'bg-emerald-500/30 text-emerald-100' : 'bg-black/40 text-slate-500'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ export default function TelemetryPanel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="rounded-sm bg-[#242a32] p-1 text-sm text-slate-200">
|
<section className="rounded-sm bg-[#242a32] p-2 text-base text-slate-100">
|
||||||
<div className="text-[0.7rem] text-slate-400">
|
<div className="text-sm text-slate-400">
|
||||||
<span>{connected ? 'online' : 'offline'}</span>
|
<span>{connected ? 'online' : 'offline'}</span>
|
||||||
<span> · role {session?.role || 'unknown'}</span>
|
<span> · role {session?.role || 'unknown'}</span>
|
||||||
<span> · mode {session?.mode || '--'}</span>
|
<span> · mode {session?.mode || '--'}</span>
|
||||||
@@ -50,7 +50,7 @@ export default function TelemetryPanel() {
|
|||||||
) : !frame ? (
|
) : !frame ? (
|
||||||
<p className="mt-1 text-[0.75rem] text-slate-400">Waiting for sensor frames…</p>
|
<p className="mt-1 text-[0.75rem] text-slate-400">Waiting for sensor frames…</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-1 space-y-1 text-[0.75rem]">
|
<div className="mt-2 space-y-1 text-base">
|
||||||
<Metric label="Charge" value={formatMetric(charge != null && capacity != null ? `${charge}/${capacity} mAh` : null)} />
|
<Metric label="Charge" value={formatMetric(charge != null && capacity != null ? `${charge}/${capacity} mAh` : null)} />
|
||||||
<Metric label="Charging" value={formatMetric(sensors.chargingState?.label)} />
|
<Metric label="Charging" value={formatMetric(sensors.chargingState?.label)} />
|
||||||
<Metric label="OI mode" value={formatMetric(sensors.oiMode?.label)} />
|
<Metric label="OI mode" value={formatMetric(sensors.oiMode?.label)} />
|
||||||
@@ -59,21 +59,21 @@ export default function TelemetryPanel() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{rawSnippet && (
|
{rawSnippet && (
|
||||||
<pre className="mt-1 overflow-hidden rounded-sm bg-black/60 p-1 text-[0.6rem] text-lime-300">
|
<pre className="mt-2 overflow-hidden rounded-sm bg-black/60 p-2 text-sm text-lime-300">
|
||||||
{rawSnippet}
|
{rawSnippet}
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
)}
|
||||||
<div className="mt-1">
|
<div className="mt-2">
|
||||||
<p className="text-xs text-slate-400">Rovers</p>
|
<p className="text-sm text-slate-400">Rovers</p>
|
||||||
{roster.length === 0 ? (
|
{roster.length === 0 ? (
|
||||||
<p className="text-xs text-slate-500">No roster data.</p>
|
<p className="text-sm text-slate-500">No roster data.</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="mt-1 space-y-1 text-xs">
|
<ul className="mt-2 space-y-1 text-sm">
|
||||||
{roster.map((rover) => (
|
{roster.map((rover) => (
|
||||||
<li key={rover.id} className="flex items-center justify-between gap-1">
|
<li key={rover.id} className="flex items-center justify-between gap-1">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-slate-200">{rover.name}</p>
|
<p className="text-slate-200">{rover.name}</p>
|
||||||
<p className="text-[0.6rem] text-slate-500">
|
<p className="text-xs text-slate-500">
|
||||||
{rover.locked ? 'locked' : 'free'} · {rover.lastSeen ? 'seen' : 'unknown'}
|
{rover.locked ? 'locked' : 'free'} · {rover.lastSeen ? 'seen' : 'unknown'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -82,7 +82,7 @@ export default function TelemetryPanel() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleRequest(rover.id)}
|
onClick={() => handleRequest(rover.id)}
|
||||||
disabled={pending[rover.id]}
|
disabled={pending[rover.id]}
|
||||||
className="rounded-sm bg-black/40 px-1 py-0.5 text-[0.6rem] text-slate-200 disabled:opacity-40"
|
className="rounded-sm bg-black/40 px-2 py-0.5 text-xs text-slate-200 disabled:opacity-40"
|
||||||
>
|
>
|
||||||
{pending[rover.id] ? '...' : 'request'}
|
{pending[rover.id] ? '...' : 'request'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -145,9 +145,12 @@ export default function VideoTile({ sessionInfo, label, forceMute = false, telem
|
|||||||
function BatteryBar({ charge, capacity, config, label, status }) {
|
function BatteryBar({ charge, capacity, config, label, status }) {
|
||||||
if (charge == null || !config?.full || config.warn == null) {
|
if (charge == null || !config?.full || config.warn == null) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between text-xs text-slate-400">
|
<div className="rounded-sm bg-[#1e1e1e] px-2 py-1 text-sm text-slate-200">
|
||||||
<span>{label}</span>
|
<div className="flex items-center justify-between text-xs text-slate-400">
|
||||||
<span>{status}</span>
|
<span>{label}</span>
|
||||||
|
<span>{status}</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-xs text-slate-400">Battery telemetry unavailable</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -161,16 +164,16 @@ function BatteryBar({ charge, capacity, config, label, status }) {
|
|||||||
const barClass = depleted ? 'bg-red-500 animate-pulse' : urgent ? 'bg-amber-400' : 'bg-emerald-500';
|
const barClass = depleted ? 'bg-red-500 animate-pulse' : urgent ? 'bg-amber-400' : 'bg-emerald-500';
|
||||||
const capText = capacity ? `${charge}/${capacity}` : `${charge}`;
|
const capText = capacity ? `${charge}/${capacity}` : `${charge}`;
|
||||||
return (
|
return (
|
||||||
<div className="space-y-1 rounded-sm bg-[#111] p-1 text-xs text-slate-200">
|
<div className="space-y-2 rounded-sm bg-[#1e1e1e] px-2 py-2 text-sm text-slate-200">
|
||||||
<div className="flex items-center justify-between text-[0.65rem] text-slate-400">
|
<div className="flex items-center justify-between text-xs text-slate-400">
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
<span>{status}</span>
|
<span>{status}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between text-[0.7rem]">
|
<div className="flex items-center justify-between text-sm">
|
||||||
<span>Battery</span>
|
<span>Battery</span>
|
||||||
<span>{capText} mAh</span>
|
<span>{capText} mAh</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-2 w-full rounded-full bg-slate-800">
|
<div className="h-4 w-full rounded-full bg-slate-800">
|
||||||
<div className={`h-full rounded-full transition-[width] ${barClass}`} style={{ width: `${percentDisplay}%` }} />
|
<div className={`h-full rounded-full transition-[width] ${barClass}`} style={{ width: `${percentDisplay}%` }} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user