deeper pass

This commit is contained in:
legop3
2026-05-02 17:12:38 -04:00
parent 2f570d92d7
commit 837701eee8
27 changed files with 296 additions and 273 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -11,7 +11,7 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" /> <meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
<title>Multi Roomba Rover</title> <title>Multi Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-BCMqnb9w.js"></script> <script type="module" crossorigin src="/assets/index-BVMv97rc.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CGvUXLso.css"> <link rel="stylesheet" crossorigin href="/assets/index-CGvUXLso.css">
</head> </head>
<body> <body>
+10 -7
View File
@@ -94,23 +94,26 @@ function MobileFeatureTabs({
showTelemetry = true, showTelemetry = true,
}) { }) {
const [activeTab, setActiveTab] = useState('chat'); const [activeTab, setActiveTab] = useState('chat');
const session = useSessionSelector((state) => state.session); const isVerified = useSessionSelector((state) => Boolean(state.session?.isVerified));
const ownRoverId = useSessionSelector((state) => String(state.session?.assignment?.roverId || '').trim());
const ownAudioForward = useSessionSelector((state) => {
const roverId = String(state.session?.assignment?.roverId || '').trim();
return roverId ? state.session?.audioForward?.[roverId] || null : null;
});
const { state: controlState } = useControlSystem(); const { state: controlState } = useControlSystem();
const { value: vipAudio } = useSettingsNamespace('vipAudio', { openMicEnabled: false, pttMode: 'live' }); const { value: vipAudio } = useSettingsNamespace('vipAudio', { openMicEnabled: false, pttMode: 'live' });
const vipDotClass = session?.isVerified ? 'bg-emerald-400' : 'bg-amber-400'; const vipDotClass = isVerified ? 'bg-emerald-400' : 'bg-amber-400';
const ownRoverId = String(session?.assignment?.roverId || '').trim();
const ownAudioForward = ownRoverId ? session?.audioForward?.[ownRoverId] : null;
const pttActive = Boolean(controlState?.mic?.pttActive); const pttActive = Boolean(controlState?.mic?.pttActive);
const openMicEnabled = Boolean(vipAudio?.openMicEnabled); const openMicEnabled = Boolean(vipAudio?.openMicEnabled);
const pttMode = vipAudio?.pttMode === 'clip' ? 'clip' : 'live'; const pttMode = vipAudio?.pttMode === 'clip' ? 'clip' : 'live';
const vipMicActive = Boolean( const vipMicActive = Boolean(
ownRoverId && ownRoverId &&
session?.isVerified && isVerified &&
(pttMode === 'clip' ? pttActive : (openMicEnabled || pttActive)), (pttMode === 'clip' ? pttActive : (openMicEnabled || pttActive)),
); );
const vipClipPlaying = Boolean( const vipClipPlaying = Boolean(
ownRoverId && ownRoverId &&
session?.isVerified && isVerified &&
pttMode === 'clip' && pttMode === 'clip' &&
ownAudioForward?.source === 'upload' && ownAudioForward?.source === 'upload' &&
ownAudioForward?.state === 'playing', ownAudioForward?.state === 'playing',
@@ -126,7 +129,7 @@ function MobileFeatureTabs({
<span <span
className={`inline-block h-1.5 w-1.5 rounded-full ${vipDotClass}`} className={`inline-block h-1.5 w-1.5 rounded-full ${vipDotClass}`}
aria-hidden="true" aria-hidden="true"
title={session?.isVerified ? 'Verified' : 'Not verified'} title={isVerified ? 'Verified' : 'Not verified'}
/> />
</span> </span>
</Tab> </Tab>
+3 -3
View File
@@ -20,7 +20,7 @@ function buildKey(alert) {
export default function AlertFeed({ scale = 1 }) { export default function AlertFeed({ scale = 1 }) {
const alerts = useSessionSelector((state) => state.alerts); const alerts = useSessionSelector((state) => state.alerts);
const session = useSessionSelector((state) => state.session); const buttonBoxButtons = useSessionSelector((state) => state.session?.buttonBox?.buttons ?? []);
const { pushAlert } = useSessionActions(); const { pushAlert } = useSessionActions();
const socket = useSocket(); const socket = useSocket();
const [now, setNow] = useState(() => Date.now()); const [now, setNow] = useState(() => Date.now());
@@ -29,7 +29,7 @@ export default function AlertFeed({ scale = 1 }) {
function onButtonIncrement(payload = {}) { function onButtonIncrement(payload = {}) {
const buttonId = Number(payload.buttonId); const buttonId = Number(payload.buttonId);
if (!Number.isFinite(buttonId) || buttonId < 1 || buttonId > 4) return; if (!Number.isFinite(buttonId) || buttonId < 1 || buttonId > 4) return;
const buttons = Array.isArray(session?.buttonBox?.buttons) ? session.buttonBox.buttons : []; const buttons = Array.isArray(buttonBoxButtons) ? buttonBoxButtons : [];
const button = buttons.find((entry) => Number(entry?.id) === buttonId) || {}; const button = buttons.find((entry) => Number(entry?.id) === buttonId) || {};
const count = Number.isFinite(payload.count) ? payload.count : Number(button.count) || 0; const count = Number.isFinite(payload.count) ? payload.count : Number(button.count) || 0;
const goal = Number.isFinite(button.goal) ? button.goal : 0; const goal = Number.isFinite(button.goal) ? button.goal : 0;
@@ -49,7 +49,7 @@ export default function AlertFeed({ scale = 1 }) {
return () => { return () => {
socket.off('buttonBox:increment', onButtonIncrement); socket.off('buttonBox:increment', onButtonIncrement);
}; };
}, [pushAlert, session?.buttonBox?.buttons, socket]); }, [pushAlert, buttonBoxButtons, socket]);
const latest = useMemo(() => alerts.slice(-12).map((alert) => ({ alert, key: buildKey(alert) })), [alerts]); const latest = useMemo(() => alerts.slice(-12).map((alert) => ({ alert, key: buildKey(alert) })), [alerts]);
@@ -2,7 +2,7 @@
// Purpose: Defines the Button Box Panel module and the local helpers/components used in this file. // Purpose: Defines the Button Box Panel module and the local helpers/components used in this file.
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit. // Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { useSession } from '../../context/SessionContext.jsx'; import { useSessionSelector } from '../../context/SessionContext.jsx';
import { useSocket } from '../../context/SocketContext.jsx'; import { useSocket } from '../../context/SocketContext.jsx';
import { useSettingsNamespace } from '../../settings/index.js'; import { useSettingsNamespace } from '../../settings/index.js';
import { AUDIO_SETTINGS_DEFAULTS } from '../../settings/namespaces.js'; import { AUDIO_SETTINGS_DEFAULTS } from '../../settings/namespaces.js';
@@ -18,7 +18,7 @@ const BUTTON_TONES = {
}; };
export default function ButtonBoxPanel() { export default function ButtonBoxPanel() {
const { session } = useSession(); const buttonBoxButtons = useSessionSelector((state) => state.session?.buttonBox?.buttons ?? []);
const socket = useSocket(); const socket = useSocket();
const { value: audioSettings } = useSettingsNamespace('audio', AUDIO_SETTINGS_DEFAULTS); const { value: audioSettings } = useSettingsNamespace('audio', AUDIO_SETTINGS_DEFAULTS);
const masterVolume = Number.isFinite(audioSettings?.masterVolume) const masterVolume = Number.isFinite(audioSettings?.masterVolume)
@@ -29,7 +29,7 @@ export default function ButtonBoxPanel() {
: AUDIO_SETTINGS_DEFAULTS.alertVolume; : AUDIO_SETTINGS_DEFAULTS.alertVolume;
const effectiveAlertVolume = Math.max(0, Math.min(1, masterVolume * alertVolume)); const effectiveAlertVolume = Math.max(0, Math.min(1, masterVolume * alertVolume));
const buttons = useMemo(() => { const buttons = useMemo(() => {
const list = Array.isArray(session?.buttonBox?.buttons) ? session.buttonBox.buttons : []; const list = Array.isArray(buttonBoxButtons) ? buttonBoxButtons : [];
if (list.length === 4) return list; if (list.length === 4) return list;
return [1, 2, 3, 4].map((id) => list.find((entry) => Number(entry?.id) === id) || { return [1, 2, 3, 4].map((id) => list.find((entry) => Number(entry?.id) === id) || {
id, id,
@@ -40,7 +40,7 @@ export default function ButtonBoxPanel() {
rewardNumber: null, rewardNumber: null,
lastRewardAt: null, lastRewardAt: null,
}); });
}, [session?.buttonBox?.buttons]); }, [buttonBoxButtons]);
const [incFlash, setIncFlash] = useState({}); const [incFlash, setIncFlash] = useState({});
const [rewardFlash, setRewardFlash] = useState({}); const [rewardFlash, setRewardFlash] = useState({});
+6 -5
View File
@@ -17,7 +17,9 @@ export default function ChatPanel({
fillHeight = false, fillHeight = false,
allowSpectatorInput = false, allowSpectatorInput = false,
}) { }) {
const session = useSessionSelector((state) => state.session); const role = useSessionSelector((state) => state.session?.role || null);
const currentRoverId = useSessionSelector((state) => state.session?.assignment?.roverId || null);
const roster = useSessionSelector((state) => state.session?.roster ?? []);
const { const {
messages, messages,
typing, typing,
@@ -38,13 +40,12 @@ export default function ChatPanel({
const [engine, setEngine] = useState(() => ttsSettings?.engine || 'flite'); const [engine, setEngine] = useState(() => ttsSettings?.engine || 'flite');
const [voice, setVoice] = useState(() => ttsSettings?.voice || 'rms'); const [voice, setVoice] = useState(() => ttsSettings?.voice || 'rms');
const [pitch, setPitch] = useState(() => (Number.isFinite(ttsSettings?.pitch) ? ttsSettings.pitch : 50)); const [pitch, setPitch] = useState(() => (Number.isFinite(ttsSettings?.pitch) ? ttsSettings.pitch : 50));
const canChat = session?.role !== 'spectator' || allowSpectatorInput; const canChat = role !== 'spectator' || allowSpectatorInput;
const listRef = useRef(null); const listRef = useRef(null);
const currentRoverId = session?.assignment?.roverId || null;
const rover = useMemo( const rover = useMemo(
() => session?.roster?.find((entry) => String(entry.id) === String(currentRoverId)) || null, () => roster.find((entry) => String(entry.id) === String(currentRoverId)) || null,
[currentRoverId, session?.roster], [currentRoverId, roster],
); );
const ttsSupported = Boolean(rover?.audio?.ttsEnabled); const ttsSupported = Boolean(rover?.audio?.ttsEnabled);
+18 -15
View File
@@ -10,7 +10,12 @@ import { useControlSystem } from '../../controls/index.js';
import VideoTile from '../VideoTile/index.jsx'; import VideoTile from '../VideoTile/index.jsx';
export default function DriverVideoPanel({layoutFormat = 'desktop'}) { export default function DriverVideoPanel({layoutFormat = 'desktop'}) {
const session = useSessionSelector((state) => state.session); const mode = useSessionSelector((state) => state.session?.mode || null);
const roverId = useSessionSelector((state) => state.session?.assignment?.roverId ?? null);
const roster = useSessionSelector((state) => state.session?.roster ?? []);
const turnQueues = useSessionSelector((state) => state.session?.turnQueues ?? {});
const socketId = useSessionSelector((state) => state.session?.socketId || null);
const activeDrivers = useSessionSelector((state) => state.session?.activeDrivers ?? {});
const { const {
state: { song, lastControlIntentAt }, state: { song, lastControlIntentAt },
overcurrentLimiter, overcurrentLimiter,
@@ -20,19 +25,17 @@ export default function DriverVideoPanel({layoutFormat = 'desktop'}) {
const [turnCueStartAt, setTurnCueStartAt] = useState(null); const [turnCueStartAt, setTurnCueStartAt] = useState(null);
const lastTurnRef = useRef({ active: false, roverId: null }); const lastTurnRef = useRef({ active: false, roverId: null });
useEffect(() => { useEffect(() => {
if (session?.mode !== 'turns') { if (mode !== 'turns') {
return undefined; return undefined;
} }
const timer = setInterval(() => setNow(Date.now()), 250); const timer = setInterval(() => setNow(Date.now()), 250);
return () => clearInterval(timer); return () => clearInterval(timer);
}, [session?.mode]); }, [mode]);
const roverId = session?.assignment?.roverId;
const rosterEntry = const rosterEntry =
roverId && session?.roster ? session.roster.find((item) => String(item.id) === String(roverId)) : null; roverId && roster ? roster.find((item) => String(item.id) === String(roverId)) : null;
const hasAudio = Boolean(rosterEntry?.media?.audioPublishUrl); const hasAudio = Boolean(rosterEntry?.media?.audioPublishUrl);
const turnInfo = roverId ? session?.turnQueues?.[roverId] : null; const turnInfo = roverId ? turnQueues?.[roverId] : null;
const socketId = session?.socketId || null; const activeDriverId = roverId ? activeDrivers?.[roverId] : null;
const activeDriverId = roverId ? session?.activeDrivers?.[roverId] : null;
const isActiveDriver = Boolean(socketId && activeDriverId === socketId); const isActiveDriver = Boolean(socketId && activeDriverId === socketId);
const nextDriverId = useMemo(() => { const nextDriverId = useMemo(() => {
const queue = turnInfo?.queue || []; const queue = turnInfo?.queue || [];
@@ -49,8 +52,8 @@ export default function DriverVideoPanel({layoutFormat = 'desktop'}) {
const msUntilTurn = deadline ? deadline - now : null; const msUntilTurn = deadline ? deadline - now : null;
const msUntilIdleSkip = idleDeadline ? idleDeadline - now : null; const msUntilIdleSkip = idleDeadline ? idleDeadline - now : null;
const isPreSwitchWindow = const isPreSwitchWindow =
session?.mode === 'turns' && isNextDriver && msUntilTurn != null && msUntilTurn <= 5000 && msUntilTurn > 0; mode === 'turns' && isNextDriver && msUntilTurn != null && msUntilTurn <= 5000 && msUntilTurn > 0;
const shouldShowVideo = session?.mode !== 'turns' || isActiveDriver || isPreSwitchWindow; const shouldShowVideo = mode !== 'turns' || isActiveDriver || isPreSwitchWindow;
const turnSeconds = const turnSeconds =
msUntilTurn != null && Number.isFinite(msUntilTurn) ? Math.max(0, Math.ceil(msUntilTurn / 1000)) : null; msUntilTurn != null && Number.isFinite(msUntilTurn) ? Math.max(0, Math.ceil(msUntilTurn / 1000)) : null;
const idleSkipSeconds = const idleSkipSeconds =
@@ -75,20 +78,20 @@ export default function DriverVideoPanel({layoutFormat = 'desktop'}) {
const audioInfo = roverId && hasAudio ? sources[`${roverId}-audio`] : null; const audioInfo = roverId && hasAudio ? sources[`${roverId}-audio`] : null;
const snapshotFeeds = useRoverSnapshots(roverId ? [roverId] : [], { const snapshotFeeds = useRoverSnapshots(roverId ? [roverId] : [], {
enabled: Boolean(roverId), enabled: Boolean(roverId),
version: session?.mode, version: mode,
}); });
const snapshotFeed = roverId ? snapshotFeeds[roverId] || null : null; const snapshotFeed = roverId ? snapshotFeeds[roverId] || null : null;
const frame = useTelemetryFrame(roverId); const frame = useTelemetryFrame(roverId);
const batteryRecord = const batteryRecord =
roverId && session?.roster roverId && roster
? session.roster.find((item) => String(item.id) === String(roverId)) ? roster.find((item) => String(item.id) === String(roverId))
: null; : null;
const batteryConfig = batteryRecord?.battery ?? null; const batteryConfig = batteryRecord?.battery ?? null;
const roverLabel = batteryRecord?.name || (roverId ? `Rover ${roverId}` : ''); const roverLabel = batteryRecord?.name || (roverId ? `Rover ${roverId}` : '');
useEffect(() => { useEffect(() => {
if (session?.mode !== 'turns') { if (mode !== 'turns') {
setTurnCueVisible(false); setTurnCueVisible(false);
setTurnCueStartAt(null); setTurnCueStartAt(null);
lastTurnRef.current = { active: false, roverId: null }; lastTurnRef.current = { active: false, roverId: null };
@@ -105,7 +108,7 @@ export default function DriverVideoPanel({layoutFormat = 'desktop'}) {
setTurnCueStartAt(null); setTurnCueStartAt(null);
} }
lastTurnRef.current = { active: isActiveDriver, roverId }; lastTurnRef.current = { active: isActiveDriver, roverId };
}, [isActiveDriver, roverId, session?.mode]); }, [isActiveDriver, roverId, mode]);
useEffect(() => { useEffect(() => {
if (!turnCueVisible || !turnCueStartAt) return; if (!turnCueVisible || !turnCueStartAt) return;
@@ -9,8 +9,10 @@ const MAX_FONT_PX = 28;
const MIN_FONT_PX = 14; const MIN_FONT_PX = 14;
export default function GlobalObjectiveBanner({ layout = 'desktop', className = '', dismissable = true }) { export default function GlobalObjectiveBanner({ layout = 'desktop', className = '', dismissable = true }) {
const session = useSessionSelector((state) => state.session); const goalText = useSessionSelector((state) => {
const goalText = session?.globalObjective?.text ? String(session.globalObjective.text).trim() : ''; const text = state.session?.globalObjective?.text;
return text ? String(text).trim() : '';
});
const isMobile = layout === 'mobile-portrait' || layout === 'mobile-landscape' || layout === 'mobile'; const isMobile = layout === 'mobile-portrait' || layout === 'mobile-landscape' || layout === 'mobile';
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [fontSize, setFontSize] = useState(MAX_FONT_PX); const [fontSize, setFontSize] = useState(MAX_FONT_PX);
@@ -243,10 +243,9 @@ export default function HomeAssistantControls() {
const { const {
state: { keymap }, state: { keymap },
} = useControlSystem(); } = useControlSystem();
const session = useSessionSelector((state) => state.session); const ha = useSessionSelector((state) => state.session?.homeAssistant || null);
const { homeAssistantToggle, homeAssistantSetLightColor, homeAssistantSetLightWhite } = const { homeAssistantToggle, homeAssistantSetLightColor, homeAssistantSetLightWhite } =
useSessionActions(); useSessionActions();
const ha = session?.homeAssistant;
const entities = useMemo(() => ha?.entities || [], [ha?.entities]); const entities = useMemo(() => ha?.entities || [], [ha?.entities]);
const lightPolicy = ha?.lightPolicy || null; const lightPolicy = ha?.lightPolicy || null;
const controlsLocked = Boolean(lightPolicy?.locked || lightPolicy?.lockedOn); const controlsLocked = Boolean(lightPolicy?.locked || lightPolicy?.lockedOn);
+11 -11
View File
@@ -28,19 +28,19 @@ function getModeDetails(mode = 'admin') {
} }
export default function ModeGateOverlay() { export default function ModeGateOverlay() {
const session = useSessionSelector((state) => state.session); const mode = useSessionSelector((state) => state.session?.mode || null);
const mode = session?.mode; const role = useSessionSelector((state) => state.session?.role || null);
const role = session?.role; const reason = useSessionSelector((state) => state.session?.adminReason?.text || '');
const reason = session?.adminReason?.text || ''; const reasonUpdatedAt = useSessionSelector((state) => state.session?.adminReason?.updatedAt || null);
const reasonUpdatedAt = session?.adminReason?.updatedAt || null; const timezone = useSessionSelector((state) => state.session?.timezone || 'UTC');
const timezone = session?.timezone || 'UTC'; const discordUrl = useSessionSelector((state) => {
const discordUrl = const socials = state.session?.socials || [];
session?.socials?.find((entry) => { const fromSocials = socials.find((entry) => {
const key = String(entry?.id || entry?.label || '').toLowerCase(); const key = String(entry?.id || entry?.label || '').toLowerCase();
return key === 'discord'; return key === 'discord';
})?.url || })?.url;
session?.discord?.invite || return fromSocials || state.session?.discord?.invite || null;
null; });
const restricted = RESTRICTED_MODES.has(mode); const restricted = RESTRICTED_MODES.has(mode);
const privileged = mode === 'lockdown' ? LOCKDOWN_ROLES.has(role) : PRIVILEGED_ROLES.has(role); const privileged = mode === 'lockdown' ? LOCKDOWN_ROLES.has(role) : PRIVILEGED_ROLES.has(role);
const [now, setNow] = useState(() => new Date()); const [now, setNow] = useState(() => new Date());
+4 -3
View File
@@ -2,16 +2,17 @@
// Purpose: Defines the Nickname Form module and the local helpers/components used in this file. // Purpose: Defines the Nickname Form module and the local helpers/components used in this file.
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit. // Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useSession } from '../../context/SessionContext.jsx'; import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
import { useSettingsNamespace } from '../../settings/index.js'; import { useSettingsNamespace } from '../../settings/index.js';
export default function NicknameForm({ compact = false }) { export default function NicknameForm({ compact = false }) {
const { session, setNickname } = useSession(); const role = useSessionSelector((state) => state.session?.role || null);
const { setNickname } = useSessionActions();
const { value, save } = useSettingsNamespace('profile', { nickname: '' }); const { value, save } = useSettingsNamespace('profile', { nickname: '' });
const [nicknameInput, setNicknameInput] = useState(value.nickname || ''); const [nicknameInput, setNicknameInput] = useState(value.nickname || '');
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const canSetNickname = session?.role !== 'spectator'; const canSetNickname = role !== 'spectator';
useEffect(() => { useEffect(() => {
setNicknameInput(value.nickname || ''); setNicknameInput(value.nickname || '');
@@ -35,10 +35,10 @@ export default function RawUserPilePanel({
fillHeight = false, fillHeight = false,
compact = false, compact = false,
}) { }) {
const session = useSessionSelector((state) => state.session); const role = useSessionSelector((state) => state.session?.role || null);
const canSetNickname = session?.role !== 'spectator'; const users = useSessionSelector((state) => state.session?.users ?? []);
const users = session?.users ?? []; const selfId = useSessionSelector((state) => state.session?.socketId || null);
const selfId = session?.socketId || null; const canSetNickname = role !== 'spectator';
const sorted = useMemo( const sorted = useMemo(
() => () =>
@@ -2,7 +2,7 @@
// Purpose: Defines the Replay Sources Panel module and the local helpers/components used in this file. // Purpose: Defines the Replay Sources Panel module and the local helpers/components used in this file.
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit. // Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useSession } from '../../context/SessionContext.jsx'; import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
import { useSettingsNamespace } from '../../settings/index.js'; import { useSettingsNamespace } from '../../settings/index.js';
import { roverNameChromeStyle } from '../../lib/roverColor.js'; import { roverNameChromeStyle } from '../../lib/roverColor.js';
@@ -22,8 +22,15 @@ function normalizeSources(list = []) {
} }
export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHeight = false }) { export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHeight = false }) {
const { session, triggerReplay } = useSession(); const replaySources = useSessionSelector((state) => state.session?.replaySources ?? []);
const sources = normalizeSources(session?.replaySources || []); const mode = useSessionSelector((state) => state.session?.mode || null);
const assignmentRoverId = useSessionSelector((state) => state.session?.assignment?.roverId ?? null);
const users = useSessionSelector((state) => state.session?.users ?? []);
const selfSocketId = useSessionSelector((state) => state.session?.socketId || null);
const roster = useSessionSelector((state) => state.session?.roster ?? []);
const replayState = useSessionSelector((state) => state.session?.replay || null);
const { triggerReplay } = useSessionActions();
const sources = normalizeSources(replaySources || []);
const { value: settings, save: saveSettings } = useSettingsNamespace('replaySources', {}); const { value: settings, save: saveSettings } = useSettingsNamespace('replaySources', {});
const [selected, setSelected] = useState([]); const [selected, setSelected] = useState([]);
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
@@ -32,29 +39,28 @@ export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHei
const [title, setTitle] = useState(''); const [title, setTitle] = useState('');
const [titleDirty, setTitleDirty] = useState(false); const [titleDirty, setTitleDirty] = useState(false);
const [includeSidebar, setIncludeSidebar] = useState(true); const [includeSidebar, setIncludeSidebar] = useState(true);
const replayState = session?.replay || null;
const [remainingMs, setRemainingMs] = useState(0); const [remainingMs, setRemainingMs] = useState(0);
const defaults = useMemo(() => { const defaults = useMemo(() => {
const roverId = session?.assignment?.roverId; const roverId = assignmentRoverId;
if (roverId) { if (roverId) {
return [`rover:${roverId}`]; return [`rover:${roverId}`];
} }
return []; return [];
}, [session?.assignment?.roverId]); }, [assignmentRoverId]);
const defaultTitle = useMemo(() => { const defaultTitle = useMemo(() => {
const self = Array.isArray(session?.users) const self = Array.isArray(users)
? session.users.find((entry) => entry?.socketId === session?.socketId) ? users.find((entry) => entry?.socketId === selfSocketId)
: null; : null;
const nickname = (self?.nickname || 'Someone').trim() || 'Someone'; const nickname = (self?.nickname || 'Someone').trim() || 'Someone';
const roverId = session?.assignment?.roverId || null; const roverId = assignmentRoverId || null;
const roverName = const roverName =
roverId && Array.isArray(session?.roster) roverId && Array.isArray(roster)
? session.roster.find((entry) => String(entry?.id) === String(roverId))?.name || roverId ? roster.find((entry) => String(entry?.id) === String(roverId))?.name || roverId
: 'a rover'; : 'a rover';
return `${nickname} driving ${roverName}`; return `${nickname} driving ${roverName}`;
}, [session?.users, session?.socketId, session?.assignment?.roverId, session?.roster]); }, [users, selfSocketId, assignmentRoverId, roster]);
useEffect(() => { useEffect(() => {
const saved = settings?.[panelId]; const saved = settings?.[panelId];
@@ -106,7 +112,7 @@ export default function ReplaySourcesPanel({ panelId = 'replay-sources', fillHei
return () => clearInterval(interval); return () => clearInterval(interval);
}, [replayState?.lastTriggeredAt, replayState?.cooldownMs, replayState?.remainingMs]); }, [replayState?.lastTriggeredAt, replayState?.cooldownMs, replayState?.remainingMs]);
const replayDisabled = busy || session?.mode === 'lockdown' || remainingMs > 0 || !selected.length; const replayDisabled = busy || mode === 'lockdown' || remainingMs > 0 || !selected.length;
const toggleKey = (key) => { const toggleKey = (key) => {
setSelected((prev) => { setSelected((prev) => {
+10 -7
View File
@@ -117,23 +117,26 @@ function DriveDockPanel() {
export default function RightPaneTabs({ layout, onOpenHelpOverlay }) { export default function RightPaneTabs({ layout, onOpenHelpOverlay }) {
const [activeTab, setActiveTab] = useState('telemetry'); const [activeTab, setActiveTab] = useState('telemetry');
const session = useSessionSelector((state) => state.session); const isVerified = useSessionSelector((state) => Boolean(state.session?.isVerified));
const ownRoverId = useSessionSelector((state) => String(state.session?.assignment?.roverId || '').trim());
const ownAudioForward = useSessionSelector((state) => {
const roverId = String(state.session?.assignment?.roverId || '').trim();
return roverId ? state.session?.audioForward?.[roverId] || null : null;
});
const { state: controlState } = useControlSystem(); const { state: controlState } = useControlSystem();
const { value: vipAudio } = useSettingsNamespace('vipAudio', { openMicEnabled: false, pttMode: 'live' }); const { value: vipAudio } = useSettingsNamespace('vipAudio', { openMicEnabled: false, pttMode: 'live' });
const vipDotClass = session?.isVerified ? 'bg-emerald-400' : 'bg-red-600'; const vipDotClass = isVerified ? 'bg-emerald-400' : 'bg-red-600';
const ownRoverId = String(session?.assignment?.roverId || '').trim();
const ownAudioForward = ownRoverId ? session?.audioForward?.[ownRoverId] : null;
const pttActive = Boolean(controlState?.mic?.pttActive); const pttActive = Boolean(controlState?.mic?.pttActive);
const openMicEnabled = Boolean(vipAudio?.openMicEnabled); const openMicEnabled = Boolean(vipAudio?.openMicEnabled);
const pttMode = vipAudio?.pttMode === 'clip' ? 'clip' : 'live'; const pttMode = vipAudio?.pttMode === 'clip' ? 'clip' : 'live';
const vipMicActive = Boolean( const vipMicActive = Boolean(
ownRoverId && ownRoverId &&
session?.isVerified && isVerified &&
(pttMode === 'clip' ? pttActive : (openMicEnabled || pttActive)), (pttMode === 'clip' ? pttActive : (openMicEnabled || pttActive)),
); );
const vipClipPlaying = Boolean( const vipClipPlaying = Boolean(
ownRoverId && ownRoverId &&
session?.isVerified && isVerified &&
pttMode === 'clip' && pttMode === 'clip' &&
ownAudioForward?.source === 'upload' && ownAudioForward?.source === 'upload' &&
ownAudioForward?.state === 'playing', ownAudioForward?.state === 'playing',
@@ -149,7 +152,7 @@ export default function RightPaneTabs({ layout, onOpenHelpOverlay }) {
<span <span
className={`inline-block h-3 w-3 rounded-full ${vipDotClass}`} className={`inline-block h-3 w-3 rounded-full ${vipDotClass}`}
aria-hidden="true" aria-hidden="true"
title={session?.isVerified ? 'Verified' : 'Not verified'} title={isVerified ? 'Verified' : 'Not verified'}
/> />
</span> </span>
</Tab> </Tab>
@@ -32,8 +32,7 @@ export default function RoomCameraPanel({
hideHeader = false, hideHeader = false,
panelId = null, panelId = null,
}) { }) {
const session = useSessionSelector((state) => state.session); const cameras = useSessionSelector((state) => state.session?.roomCameras || []);
const cameras = session?.roomCameras || [];
const feedMap = useRoomCameraSnapshots(cameras.map((camera) => ({ id: camera.id }))); const feedMap = useRoomCameraSnapshots(cameras.map((camera) => ({ id: camera.id })));
const { value: orientationSettings, save: saveOrientationSettings } = useSettingsNamespace('roomCameraPanels', {}); const { value: orientationSettings, save: saveOrientationSettings } = useSettingsNamespace('roomCameraPanels', {});
const [orientation, setOrientation] = useState(() => const [orientation, setOrientation] = useState(() =>
@@ -45,23 +45,20 @@ function formatLabel(user, selfId) {
} }
export default function RoverQueuesPanel({ title = 'Rovers' }) { export default function RoverQueuesPanel({ title = 'Rovers' }) {
const session = useSessionSelector((state) => state.session); const role = useSessionSelector((state) => state.session?.role || null);
const roster = useSessionSelector((state) => state.session?.roster ?? []);
const turnQueues = useSessionSelector((state) => state.session?.turnQueues ?? {});
const users = useSessionSelector((state) => state.session?.users ?? []);
const selfId = useSessionSelector((state) => state.session?.socketId || null);
const { requestControl } = useSessionActions(); const { requestControl } = useSessionActions();
const [pending, setPending] = useState({}); const [pending, setPending] = useState({});
const [now, setNow] = useState(() => Date.now()); const [now, setNow] = useState(() => Date.now());
const canRequest = useMemo(() => session?.role && session.role !== 'spectator', [session?.role]); const canRequest = useMemo(() => role && role !== 'spectator', [role]);
const adminCapable = useMemo( const adminCapable = useMemo(
() => () => role === 'admin' || role === 'lockdown' || role === 'lockdown-admin',
session?.role === 'admin' || [role],
session?.role === 'lockdown' ||
session?.role === 'lockdown-admin',
[session?.role],
); );
const roster = session?.roster ?? [];
const turnQueues = session?.turnQueues ?? {};
const users = session?.users ?? [];
const selfId = session?.socketId || null;
const hasDeadlines = useMemo( const hasDeadlines = useMemo(
() => Object.values(turnQueues || {}).some((info) => info?.deadline || info?.idleDeadline), () => Object.values(turnQueues || {}).some((info) => info?.deadline || info?.idleDeadline),
[turnQueues], [turnQueues],
@@ -1,20 +1,20 @@
// Social Buttons Grid // Social Buttons Grid
// Purpose: Defines the Social Buttons Grid module and the local helpers/components used in this file. // Purpose: Defines the Social Buttons Grid module and the local helpers/components used in this file.
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit. // Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useSession } from '../../context/SessionContext.jsx'; import { useSessionSelector } from '../../context/SessionContext.jsx';
import SocialButton from '../SocialButton/index.jsx'; import SocialButton from '../SocialButton/index.jsx';
function normalizeSocials(session) { function normalizeSocials({ socials, discordInvite, kofiLink }) {
const configured = Array.isArray(session?.socials) ? session.socials : null; const configured = Array.isArray(socials) ? socials : null;
if (configured && configured.length) { if (configured && configured.length) {
return configured; return configured;
} }
const fallback = []; const fallback = [];
if (session?.discord?.invite) { if (discordInvite) {
fallback.push({ id: 'discord', label: 'Discord', url: session.discord.invite }); fallback.push({ id: 'discord', label: 'Discord', url: discordInvite });
} }
if (session?.kofi?.link) { if (kofiLink) {
fallback.push({ id: 'kofi', label: 'Ko-fi', url: session.kofi.link }); fallback.push({ id: 'kofi', label: 'Ko-fi', url: kofiLink });
} }
return fallback; return fallback;
} }
@@ -28,8 +28,12 @@ function normalizeEntry(entry) {
} }
export default function SocialButtonsGrid({ className = '' }) { export default function SocialButtonsGrid({ className = '' }) {
const { session } = useSession(); const socialsInput = useSessionSelector((state) => ({
const socials = normalizeSocials(session) socials: state.session?.socials ?? [],
discordInvite: state.session?.discord?.invite || null,
kofiLink: state.session?.kofi?.link || null,
}));
const socials = normalizeSocials(socialsInput)
.map(normalizeEntry) .map(normalizeEntry)
.filter(Boolean) .filter(Boolean)
.slice(0, 4); .slice(0, 4);
+10 -6
View File
@@ -13,8 +13,13 @@ function formatMetric(value, fallback = '--') {
export default function TelemetryPanel() { export default function TelemetryPanel() {
const connected = useSessionSelector((state) => state.connected); const connected = useSessionSelector((state) => state.connected);
const session = useSessionSelector((state) => state.session); const roverId = useSessionSelector((state) => state.session?.assignment?.roverId ?? null);
const roverId = session?.assignment?.roverId; const activeDriverId = useSessionSelector((state) => {
const id = state.session?.assignment?.roverId;
return id ? state.session?.activeDrivers?.[id] || null : null;
});
const selfSocketId = useSessionSelector((state) => state.session?.socketId || null);
const users = useSessionSelector((state) => state.session?.users ?? []);
const frame = useTelemetryFrame(roverId); const frame = useTelemetryFrame(roverId);
const sensors = frame?.sensors || {}; const sensors = frame?.sensors || {};
const dockIr = useDockIr(sensors); const dockIr = useDockIr(sensors);
@@ -25,14 +30,13 @@ export default function TelemetryPanel() {
const capacity = sensors.batteryCapacityMah; const capacity = sensors.batteryCapacityMah;
const updated = frame?.receivedAt ? new Date(frame.receivedAt).toLocaleTimeString() : null; const updated = frame?.receivedAt ? new Date(frame.receivedAt).toLocaleTimeString() : null;
const rawSnippet = frame?.raw ? frame.raw : null; const rawSnippet = frame?.raw ? frame.raw : null;
const activeDriverId = roverId ? session?.activeDrivers?.[roverId] : null;
const driverLabel = useMemo(() => { const driverLabel = useMemo(() => {
if (!roverId) return 'n/a'; if (!roverId) return 'n/a';
if (!activeDriverId) return 'Available'; if (!activeDriverId) return 'Available';
if (activeDriverId === session?.socketId) return 'You'; if (activeDriverId === selfSocketId) return 'You';
const user = (session?.users || []).find((entry) => entry.socketId === activeDriverId); const user = users.find((entry) => entry.socketId === activeDriverId);
return user?.nickname || activeDriverId.slice(0, 6); return user?.nickname || activeDriverId.slice(0, 6);
}, [activeDriverId, roverId, session?.socketId, session?.users]); }, [activeDriverId, roverId, selfSocketId, users]);
return ( return (
<section className="panel-section space-y-0.5 text-base text-slate-100"> <section className="panel-section space-y-0.5 text-base text-slate-100">
@@ -1,7 +1,7 @@
// Turn Alert Listener // Turn Alert Listener
// Purpose: Defines the Turn Alert Listener module and the local helpers/components used in this file. // Purpose: Defines the Turn Alert Listener module and the local helpers/components used in this file.
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit. // Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useEffect, useMemo, useRef } from 'react'; import { useEffect, useRef } from 'react';
import turnSound from '../../assets/turn_alert.mp3'; import turnSound from '../../assets/turn_alert.mp3';
import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx'; import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
import { useSettingsNamespace } from '../../settings/index.js'; import { useSettingsNamespace } from '../../settings/index.js';
@@ -24,7 +24,9 @@ function useAudio(src, volume = 1) {
} }
export default function TurnAlertListener() { export default function TurnAlertListener() {
const session = useSessionSelector((state) => state.session); const socketId = useSessionSelector((state) => state.session?.socketId || null);
const assignments = useSessionSelector((state) => state.session?.turnQueues || {});
const roster = useSessionSelector((state) => state.session?.roster || []);
const { pushAlert } = useSessionActions(); const { pushAlert } = useSessionActions();
const { value: audioSettings } = useSettingsNamespace('audio', AUDIO_SETTINGS_DEFAULTS); const { value: audioSettings } = useSettingsNamespace('audio', AUDIO_SETTINGS_DEFAULTS);
const masterVolume = Number.isFinite(audioSettings?.masterVolume) ? audioSettings.masterVolume : AUDIO_SETTINGS_DEFAULTS.masterVolume; const masterVolume = Number.isFinite(audioSettings?.masterVolume) ? audioSettings.masterVolume : AUDIO_SETTINGS_DEFAULTS.masterVolume;
@@ -33,9 +35,6 @@ export default function TurnAlertListener() {
const playSound = useAudio(turnSound, effectiveAlertVolume); const playSound = useAudio(turnSound, effectiveAlertVolume);
const seenRoversRef = useRef(new Set()); const seenRoversRef = useRef(new Set());
const assignments = useMemo(() => session?.turnQueues || {}, [session?.turnQueues]);
const socketId = session?.socketId || null;
useEffect(() => { useEffect(() => {
if (!socketId) return; if (!socketId) return;
const newlyMine = []; const newlyMine = [];
@@ -53,7 +52,7 @@ export default function TurnAlertListener() {
}); });
if (newlyMine.length === 0) return; if (newlyMine.length === 0) return;
newlyMine.forEach((roverId) => { newlyMine.forEach((roverId) => {
const roverName = session?.roster?.find((r) => String(r.id) === String(roverId))?.name || roverId; const roverName = roster.find((r) => String(r.id) === String(roverId))?.name || roverId;
pushAlert({ pushAlert({
title: 'Your turn!', title: 'Your turn!',
message: `You now control ${roverName}.`, message: `You now control ${roverName}.`,
@@ -61,7 +60,7 @@ export default function TurnAlertListener() {
}); });
}); });
playSound(); playSound();
}, [assignments, playSound, pushAlert, session?.roster, socketId]); }, [assignments, playSound, pushAlert, roster, socketId]);
return null; return null;
} }
+9 -8
View File
@@ -2,7 +2,7 @@
// Purpose: Defines the User List Panel module and the local helpers/components used in this file. // Purpose: Defines the User List Panel module and the local helpers/components used in this file.
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit. // Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useEffect, useMemo, useState, useCallback } from 'react'; import { useEffect, useMemo, useState, useCallback } from 'react';
import { useSession } from '../../context/SessionContext.jsx'; import { useSessionSelector } from '../../context/SessionContext.jsx';
import NicknameForm from '../NicknameForm/index.jsx'; import NicknameForm from '../NicknameForm/index.jsx';
import SocialButtonsGrid from '../SocialButtonsGrid/index.jsx'; import SocialButtonsGrid from '../SocialButtonsGrid/index.jsx';
import { roverBadgeStyle, roverNameChromeStyle } from '../../lib/roverColor.js'; import { roverBadgeStyle, roverNameChromeStyle } from '../../lib/roverColor.js';
@@ -55,13 +55,14 @@ export default function UserListPanel({
compact = false, compact = false,
showBothTurnsAndUsers = false, showBothTurnsAndUsers = false,
}) { }) {
const { session } = useSession(); const role = useSessionSelector((state) => state.session?.role || null);
const canSetNickname = session?.role !== 'spectator'; const users = useSessionSelector((state) => state.session?.users ?? []);
const users = session?.users ?? []; const selfId = useSessionSelector((state) => state.session?.socketId || null);
const selfId = session?.socketId || null; const mode = useSessionSelector((state) => state.session?.mode || null);
const isTurnsMode = session?.mode === 'turns'; const turnQueues = useSessionSelector((state) => state.session?.turnQueues || {});
const turnQueues = session?.turnQueues || {}; const roster = useSessionSelector((state) => state.session?.roster || []);
const roster = session?.roster || []; const canSetNickname = role !== 'spectator';
const isTurnsMode = mode === 'turns';
const [turnView, setTurnView] = useState('queues'); const [turnView, setTurnView] = useState('queues');
useEffect(() => { useEffect(() => {
+10 -10
View File
@@ -73,9 +73,9 @@ export function ControlSystemProvider({ children }) {
typeof pageSettings?.driveMacroBackoffEnabled === 'boolean' typeof pageSettings?.driveMacroBackoffEnabled === 'boolean'
? pageSettings.driveMacroBackoffEnabled ? pageSettings.driveMacroBackoffEnabled
: true; : true;
const session = useSessionSelector((state) => state.session); const roverId = useSessionSelector((state) => state.session?.assignment?.roverId ?? null);
const homeAssistantEntities = useSessionSelector((state) => state.session?.homeAssistant?.entities ?? []);
const { homeAssistantSetState } = useSessionActions(); const { homeAssistantSetState } = useSessionActions();
const roverId = session?.assignment?.roverId ?? null;
const overcurrentLimiter = useOvercurrentLimiter(roverId); const overcurrentLimiter = useOvercurrentLimiter(roverId);
const driveTransform = useCallback( const driveTransform = useCallback(
(speeds) => applyDriveOvercurrentScale(speeds, overcurrentLimiter.scales, overcurrentLimiter.adminImmune), (speeds) => applyDriveOvercurrentScale(speeds, overcurrentLimiter.scales, overcurrentLimiter.adminImmune),
@@ -88,7 +88,7 @@ export function ControlSystemProvider({ children }) {
const pipeline = useCommandPipeline({ driveTransform, auxTransform }); const pipeline = useCommandPipeline({ driveTransform, auxTransform });
const turnOnAllLights = useCallback(() => { const turnOnAllLights = useCallback(() => {
const entities = session?.homeAssistant?.entities || []; const entities = homeAssistantEntities || [];
const targets = entities.filter( const targets = entities.filter(
(ent) => (ent) =>
(ent.type === 'light' || ent.type === 'switch') && (ent.type === 'light' || ent.type === 'switch') &&
@@ -105,7 +105,7 @@ export function ControlSystemProvider({ children }) {
}); });
// Give the loop a chance; clear pending after issuing commands. // Give the loop a chance; clear pending after issuing commands.
pendingLightsRef.current = false; pendingLightsRef.current = false;
}, [session?.homeAssistant?.entities, homeAssistantSetState]); }, [homeAssistantEntities, homeAssistantSetState]);
useEffect(() => { useEffect(() => {
dispatch({ type: 'control/set-rover', payload: pipeline.roverId }); dispatch({ type: 'control/set-rover', payload: pipeline.roverId });
@@ -114,18 +114,18 @@ export function ControlSystemProvider({ children }) {
useEffect(() => { useEffect(() => {
const prevMode = prevModeRef.current; const prevMode = prevModeRef.current;
prevModeRef.current = state.mode; prevModeRef.current = state.mode;
if (prevMode !== 'drive' && state.mode === 'drive' && session?.homeAssistant?.entities) { if (prevMode !== 'drive' && state.mode === 'drive' && homeAssistantEntities?.length) {
turnOnAllLights(); turnOnAllLights();
} else if (prevMode !== 'drive' && state.mode === 'drive') { } else if (prevMode !== 'drive' && state.mode === 'drive') {
pendingLightsRef.current = true; pendingLightsRef.current = true;
} }
}, [state.mode, session?.homeAssistant?.entities, turnOnAllLights]); }, [state.mode, homeAssistantEntities, turnOnAllLights]);
useEffect(() => { useEffect(() => {
if (pendingLightsRef.current && session?.homeAssistant?.entities) { if (pendingLightsRef.current && homeAssistantEntities?.length) {
turnOnAllLights(); turnOnAllLights();
} }
}, [session?.homeAssistant?.entities, turnOnAllLights]); }, [homeAssistantEntities, turnOnAllLights]);
useEffect(() => { useEffect(() => {
const mergedKeymap = { ...DEFAULT_KEYMAP, ...(controlSettings?.keymap || {}) }; const mergedKeymap = { ...DEFAULT_KEYMAP, ...(controlSettings?.keymap || {}) };
@@ -308,7 +308,7 @@ export function ControlSystemProvider({ children }) {
let macroToRun = macro; let macroToRun = macro;
if (macroId === 'drive-sequence') { if (macroId === 'drive-sequence') {
turnOnAllLights(); turnOnAllLights();
if (!session?.homeAssistant?.entities) { if (!homeAssistantEntities?.length) {
pendingLightsRef.current = true; pendingLightsRef.current = true;
} }
recordControlIntent(); recordControlIntent();
@@ -327,7 +327,7 @@ export function ControlSystemProvider({ children }) {
driveMacroBackoffEnabled, driveMacroBackoffEnabled,
pipeline, pipeline,
recordControlIntent, recordControlIntent,
session?.homeAssistant?.entities, homeAssistantEntities,
state.macros, state.macros,
turnOnAllLights, turnOnAllLights,
], ],
+5 -5
View File
@@ -16,13 +16,13 @@ import { bytesToBase64, clampRange, sleep } from './controlMath.js';
export function useCommandPipeline(options = {}) { export function useCommandPipeline(options = {}) {
const { driveTransform, auxTransform } = options; const { driveTransform, auxTransform } = options;
const socket = useSocket(); const socket = useSocket();
const session = useSessionSelector((state) => state.session); const roverId = useSessionSelector((state) => state.session?.assignment?.roverId ?? null);
const roverId = session?.assignment?.roverId; const roster = useSessionSelector((state) => state.session?.roster ?? []);
const rosterEntry = useMemo(() => { const rosterEntry = useMemo(() => {
if (!roverId || !Array.isArray(session?.roster)) return null; if (!roverId || !Array.isArray(roster)) return null;
return session.roster.find((entry) => String(entry.id) === String(roverId)) || null; return roster.find((entry) => String(entry.id) === String(roverId)) || null;
}, [roverId, session?.roster]); }, [roverId, roster]);
const servoConfig = useMemo(() => { const servoConfig = useMemo(() => {
if (!rosterEntry?.cameraServo || !rosterEntry.cameraServo.enabled) return null; if (!rosterEntry?.cameraServo || !rosterEntry.cameraServo.enabled) return null;
@@ -136,7 +136,7 @@ export default function KeyboardInputManager() {
sendSong, sendSong,
}, },
} = useControlSystem(); } = useControlSystem();
const session = useSessionSelector((state) => state.session); const homeAssistant = useSessionSelector((state) => state.session?.homeAssistant || null);
const { homeAssistantSetState } = useSessionActions(); const { homeAssistantSetState } = useSessionActions();
const { focusChat, blurChat, isChatFocused } = useChat(); const { focusChat, blurChat, isChatFocused } = useChat();
const { value: inputSettings } = useSettingsNamespace('inputs', INPUT_SETTINGS_DEFAULTS); const { value: inputSettings } = useSettingsNamespace('inputs', INPUT_SETTINGS_DEFAULTS);
@@ -318,7 +318,7 @@ export default function KeyboardInputManager() {
const triggerHomeAssistantCycle = useCallback( const triggerHomeAssistantCycle = useCallback(
(targetState) => { (targetState) => {
const ha = session?.homeAssistant; const ha = homeAssistant;
if (!ha?.enabled || !ha?.connected) return; if (!ha?.enabled || !ha?.connected) return;
if (ha?.lightPolicy?.locked || ha?.lightPolicy?.lockedOn) return; if (ha?.lightPolicy?.locked || ha?.lightPolicy?.lockedOn) return;
const entities = ha.entities || []; const entities = ha.entities || [];
@@ -346,7 +346,7 @@ export default function KeyboardInputManager() {
} }
} }
}, },
[homeAssistantSetState, session?.homeAssistant], [homeAssistantSetState, homeAssistant],
); );
useEffect(() => { useEffect(() => {
+2 -5
View File
@@ -29,7 +29,7 @@ function clampUnit(value) {
} }
export function useOvercurrentLimiter(roverId, options = {}) { export function useOvercurrentLimiter(roverId, options = {}) {
const session = useSessionSelector((state) => state.session); const role = useSessionSelector((state) => state.session?.role || null);
const frame = useTelemetryFrame(roverId); const frame = useTelemetryFrame(roverId);
const sensors = frame?.sensors || {}; const sensors = frame?.sensors || {};
const overcurrentFlags = sensors?.wheelOvercurrents || {}; const overcurrentFlags = sensors?.wheelOvercurrents || {};
@@ -131,10 +131,7 @@ export function useOvercurrentLimiter(roverId, options = {}) {
return { motors, groups }; return { motors, groups };
}, [overcurrentFlags]); }, [overcurrentFlags]);
const adminImmune = const adminImmune = role === 'admin' || role === 'lockdown' || role === 'lockdown-admin';
session?.role === 'admin' ||
session?.role === 'lockdown' ||
session?.role === 'lockdown-admin';
return useMemo( return useMemo(
() => ({ () => ({
+4 -2
View File
@@ -16,6 +16,8 @@ export function useRoomCameraSnapshots(sourceList = [], options = {}) {
const idsRef = useRef([]); const idsRef = useRef([]);
const [connectionNonce, setConnectionNonce] = useState(0); const [connectionNonce, setConnectionNonce] = useState(0);
const statsRef = useRef(new Map()); const statsRef = useRef(new Map());
const debugSnapshots =
typeof window !== 'undefined' && new URLSearchParams(window.location.search).has('debugSnapshots');
useEffect(() => { useEffect(() => {
if (!socket) return undefined; if (!socket) return undefined;
@@ -61,7 +63,7 @@ export function useRoomCameraSnapshots(sourceList = [], options = {}) {
totalBytes: prevStats.totalBytes + sizeBytes, totalBytes: prevStats.totalBytes + sizeBytes,
lastLogAt: prevStats.lastLogAt, lastLogAt: prevStats.lastLogAt,
}; };
if (!nextStats.lastLogAt || now - nextStats.lastLogAt >= 10000) { if (debugSnapshots && (!nextStats.lastLogAt || now - nextStats.lastLogAt >= 10000)) {
const avgBytes = nextStats.count ? nextStats.totalBytes / nextStats.count : 0; const avgBytes = nextStats.count ? nextStats.totalBytes / nextStats.count : 0;
console.log( console.log(
'[roomCamera]', '[roomCamera]',
@@ -120,7 +122,7 @@ export function useRoomCameraSnapshots(sourceList = [], options = {}) {
objectUrls.current.forEach((url) => URL.revokeObjectURL(url)); objectUrls.current.forEach((url) => URL.revokeObjectURL(url));
objectUrls.current.clear(); objectUrls.current.clear();
}; };
}, [socket, idsKey, enabled, connectionNonce]); }, [socket, idsKey, enabled, connectionNonce, debugSnapshots]);
return feeds; return feeds;
} }
+4 -2
View File
@@ -19,6 +19,8 @@ export function useRoverSnapshots(sourceList = [], options = {}) {
const idsRef = useRef([]); const idsRef = useRef([]);
const [connectionNonce, setConnectionNonce] = useState(0); const [connectionNonce, setConnectionNonce] = useState(0);
const statsRef = useRef(new Map()); const statsRef = useRef(new Map());
const debugSnapshots =
typeof window !== 'undefined' && new URLSearchParams(window.location.search).has('debugSnapshots');
useEffect(() => { useEffect(() => {
if (!socket) return undefined; if (!socket) return undefined;
@@ -64,7 +66,7 @@ export function useRoverSnapshots(sourceList = [], options = {}) {
totalBytes: prevStats.totalBytes + sizeBytes, totalBytes: prevStats.totalBytes + sizeBytes,
lastLogAt: prevStats.lastLogAt, lastLogAt: prevStats.lastLogAt,
}; };
if (!nextStats.lastLogAt || now - nextStats.lastLogAt >= 10000) { if (debugSnapshots && (!nextStats.lastLogAt || now - nextStats.lastLogAt >= 10000)) {
const avgBytes = nextStats.count ? nextStats.totalBytes / nextStats.count : 0; const avgBytes = nextStats.count ? nextStats.totalBytes / nextStats.count : 0;
console.log( console.log(
'[roverSnapshot]', '[roverSnapshot]',
@@ -123,7 +125,7 @@ export function useRoverSnapshots(sourceList = [], options = {}) {
objectUrls.current.forEach((url) => URL.revokeObjectURL(url)); objectUrls.current.forEach((url) => URL.revokeObjectURL(url));
objectUrls.current.clear(); objectUrls.current.clear();
}; };
}, [socket, idsKey, enabled, connectionNonce]); }, [socket, idsKey, enabled, connectionNonce, debugSnapshots]);
return feeds; return feeds;
} }