This commit is contained in:
legop3
2026-03-24 02:48:16 -04:00
parent e0167939d7
commit 3af21846c8
32 changed files with 1178 additions and 202 deletions
+120
View File
@@ -22,6 +22,7 @@ export default function AdminPanel() {
rebootRover,
rebootServer,
setAudioLevels,
setPrivateSafety,
llmControl,
adminLogs,
llmCommentaryState,
@@ -44,11 +45,13 @@ export default function AdminPanel() {
ttsGain: Number.isFinite(currentAudioLevels.ttsGain) ? currentAudioLevels.ttsGain : 1,
forwardGain: Number.isFinite(currentAudioLevels.forwardGain) ? currentAudioLevels.forwardGain : 1,
});
const [privateSafetyDrafts, setPrivateSafetyDrafts] = useState({});
const isAdmin =
session?.role === 'admin' ||
session?.role === 'lockdown' ||
session?.role === 'lockdown-admin';
const isLockdownAdmin = session?.role === 'lockdown' || session?.role === 'lockdown-admin';
const currentMode = session?.mode ?? 'open';
@@ -180,6 +183,41 @@ export default function AdminPanel() {
});
}, [currentAudioLevels.forwardGain, currentAudioLevels.hornGain, currentAudioLevels.ttsGain]);
useEffect(() => {
const next = {};
(roster || []).forEach((rover) => {
if (!rover?.private?.enabled) return;
next[rover.id] = {
speedLimitEnabled: Boolean(rover?.private?.safety?.speedLimitEnabled),
speedLimitMaxWheelSpeed: Number.isFinite(rover?.private?.safety?.speedLimitMaxWheelSpeed)
? rover.private.safety.speedLimitMaxWheelSpeed
: 250,
hardOvercurrentEnabled: Boolean(rover?.private?.safety?.hardOvercurrentEnabled),
overcurrentStopMs: Number.isFinite(rover?.private?.safety?.overcurrentStopMs)
? rover.private.safety.overcurrentStopMs
: 300,
hardBumpEnabled: Boolean(rover?.private?.safety?.hardBumpEnabled),
bumpBackoffSpeed: Number.isFinite(rover?.private?.safety?.bumpBackoffSpeed)
? rover.private.safety.bumpBackoffSpeed
: 250,
bumpBackoffMs: Number.isFinite(rover?.private?.safety?.bumpBackoffMs)
? rover.private.safety.bumpBackoffMs
: 350,
cliffEnabled: Boolean(rover?.private?.safety?.cliffEnabled),
cliffBackoffSpeed: Number.isFinite(rover?.private?.safety?.cliffBackoffSpeed)
? rover.private.safety.cliffBackoffSpeed
: 250,
cliffBackoffMs: Number.isFinite(rover?.private?.safety?.cliffBackoffMs)
? rover.private.safety.cliffBackoffMs
: 500,
triggerCooldownMs: Number.isFinite(rover?.private?.safety?.triggerCooldownMs)
? rover.private.safety.triggerCooldownMs
: 800,
};
});
setPrivateSafetyDrafts(next);
}, [roster]);
const lockMap = useMemo(() => {
const map = {};
roster.forEach((rover) => {
@@ -188,6 +226,23 @@ export default function AdminPanel() {
return map;
}, [roster, lockStates]);
const updatePrivateSafetyDraft = (roverId, patch = {}) => {
setPrivateSafetyDrafts((current) => ({
...(current || {}),
[roverId]: { ...(current?.[roverId] || {}), ...(patch || {}) },
}));
};
const handlePrivateSafetySave = async (roverId) => {
try {
const draft = privateSafetyDrafts?.[roverId];
if (!draft) return;
await setPrivateSafety(roverId, draft);
} catch (err) {
alert(err.message);
}
};
if (!isAdmin) return null;
return (
@@ -338,6 +393,71 @@ export default function AdminPanel() {
>
{rebootStates[rover.id] ? 'Rebooting...' : 'Reboot'}
</button>
{rover?.private?.enabled ? (
<div className="w-full rounded border border-slate-700/70 p-0.5 space-y-0.5 text-[0.7rem]">
<div className="text-slate-300">Private safety</div>
<label className="flex items-center gap-0.5">
<input
type="checkbox"
checked={Boolean(privateSafetyDrafts?.[rover.id]?.speedLimitEnabled)}
disabled={!isLockdownAdmin}
onChange={(event) =>
updatePrivateSafetyDraft(rover.id, { speedLimitEnabled: Boolean(event.target.checked) })}
/>
<span>Speed limit</span>
</label>
<input
type="number"
min="1"
max="500"
className="field-input text-xs w-20"
disabled={!isLockdownAdmin}
value={privateSafetyDrafts?.[rover.id]?.speedLimitMaxWheelSpeed ?? 250}
onChange={(event) =>
updatePrivateSafetyDraft(rover.id, {
speedLimitMaxWheelSpeed: Number(event.target.value) || 250,
})}
/>
<label className="flex items-center gap-0.5">
<input
type="checkbox"
checked={Boolean(privateSafetyDrafts?.[rover.id]?.hardOvercurrentEnabled)}
disabled={!isLockdownAdmin}
onChange={(event) =>
updatePrivateSafetyDraft(rover.id, { hardOvercurrentEnabled: Boolean(event.target.checked) })}
/>
<span>Hard overcurrent</span>
</label>
<label className="flex items-center gap-0.5">
<input
type="checkbox"
checked={Boolean(privateSafetyDrafts?.[rover.id]?.hardBumpEnabled)}
disabled={!isLockdownAdmin}
onChange={(event) =>
updatePrivateSafetyDraft(rover.id, { hardBumpEnabled: Boolean(event.target.checked) })}
/>
<span>Hard bump</span>
</label>
<label className="flex items-center gap-0.5">
<input
type="checkbox"
checked={Boolean(privateSafetyDrafts?.[rover.id]?.cliffEnabled)}
disabled={!isLockdownAdmin}
onChange={(event) =>
updatePrivateSafetyDraft(rover.id, { cliffEnabled: Boolean(event.target.checked) })}
/>
<span>Cliff safety</span>
</label>
<button
type="button"
disabled={!isLockdownAdmin}
onClick={() => handlePrivateSafetySave(rover.id)}
className="button-dark disabled:opacity-50"
>
Apply Safety
</button>
</div>
) : null}
</div>
)}
/>
+6 -1
View File
@@ -113,6 +113,7 @@ export default function RoverQueuesPanel({ title = 'Rovers' }) {
const isSelfNext = Boolean(selfId && nextId && nextId === selfId);
const showTimer = remainingSeconds != null && (isSelfCurrent || isSelfNext);
const locked = Boolean(rover.locked);
const isPrivateOpen = Boolean(rover?.private?.enabled && rover?.private?.open);
const lockLabel = rover.lockReason ? `locked: ${rover.lockReason}` : 'locked';
const buttonLabel = locked ? lockLabel : pending[roverId] ? '...' : 'request';
const canClickRow = canRequest && !locked && !pending[roverId];
@@ -122,7 +123,11 @@ export default function RoverQueuesPanel({ title = 'Rovers' }) {
className={classNames(
'surface flex flex-wrap items-start justify-between gap-0.5',
canClickRow && 'cursor-pointer',
locked && 'bg-red-900/40',
locked
? 'bg-red-900/40'
: isPrivateOpen
? 'bg-amber-700/35 border border-amber-200/30'
: null,
)}
onClick={() => {
if (!canClickRow) return;
+11 -3
View File
@@ -30,12 +30,19 @@ export default function RoverRoster({
<p className="text-sm text-slate-500">{emptyText}</p>
) : (
<ul className="space-y-0.5 text-sm">
{roster.map((rover) => (
{roster.map((rover) => {
const isPrivateOpen = Boolean(rover?.private?.enabled && rover?.private?.open);
const isLocked = Boolean(rover?.locked);
return (
<li
key={rover.id}
className={classNames(
'surface flex flex-wrap items-center justify-between gap-0.5',
rover.locked && 'bg-red-900/40',
isLocked
? 'bg-red-900/40'
: isPrivateOpen
? 'bg-amber-700/35 border border-amber-200/30'
: null,
)}
>
<div>
@@ -54,7 +61,8 @@ export default function RoverRoster({
</div>
{renderActions ? renderActions(rover) : null}
</li>
))}
);
})}
</ul>
)}
</div>
+3
View File
@@ -32,6 +32,7 @@ const SessionContext = createContext({
readyMicWhip: async () => {},
stopMicWhip: async () => {},
setAudioLevels: async () => {},
setPrivateSafety: async () => {},
llmControl: async () => {},
});
@@ -154,6 +155,8 @@ export function SessionProvider({ children }) {
readyMicWhip: (roverId) => emitWithAck('audio:micWhipReady', { roverId }),
stopMicWhip: (roverId) => emitWithAck('audio:micWhipStop', { roverId }),
setAudioLevels: (levels = {}) => emitWithAck('audioLevels:set', levels),
setPrivateSafety: (roverId, safety = {}) =>
emitWithAck('session:privateSafety:set', { roverId, safety }),
llmControl: (action, controls = {}) =>
emitWithAck('llm:control', { controls: { action, ...controls } }),
pushAlert: (alert) =>