mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
the
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
Reference in New Issue
Block a user