pass 1 (broken)

This commit is contained in:
legop3
2026-05-02 15:39:38 -04:00
parent 53cf39e7fb
commit ba4b6cfc78
17 changed files with 247 additions and 182 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-title" content="Multi Roomba Rover" />
<title>Multi Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-BhTaDZYU.js"></script>
<script type="module" crossorigin src="/assets/index-CduNi74O.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CGvUXLso.css">
</head>
<body>
+3 -2
View File
@@ -93,6 +93,7 @@ function MobileFeatureTabs({
roomPanelId,
showTelemetry = true,
}) {
const [activeTab, setActiveTab] = useState('chat');
const session = useSessionSelector((state) => state.session);
const { state: controlState } = useControlSystem();
const { value: vipAudio } = useSettingsNamespace('vipAudio', { openMicEnabled: false, pttMode: 'live' });
@@ -116,7 +117,7 @@ function MobileFeatureTabs({
);
return (
<section className="panel text-base">
<Tabs defaultTab="chat">
<Tabs defaultTab="chat" currentTab={activeTab} onTabChange={setActiveTab}>
<TabList>
<Tab id="chat">Chat</Tab>
<Tab id="vip" highlight={vipClipPlaying ? 'green' : vipMicActive ? 'pink' : 'none'}>
@@ -141,7 +142,7 @@ function MobileFeatureTabs({
</div>
</TabPanel>
<TabPanel id="vip" keepMounted>
<VipPanel />
<VipPanel isActive={activeTab === 'vip'} />
</TabPanel>
<TabPanel id="roomcontrols">
<div className="space-y-0.5">
@@ -2,14 +2,14 @@
// Purpose: Defines the Global Objective Banner module and the local helpers/components used in this file.
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useEffect, useMemo, useRef, useState } from 'react';
import { useSession } from '../../context/SessionContext.jsx';
import { useSessionSelector } from '../../context/SessionContext.jsx';
const MOBILE_DISMISS_MS = 10000;
const MAX_FONT_PX = 28;
const MIN_FONT_PX = 14;
export default function GlobalObjectiveBanner({ layout = 'desktop', className = '', dismissable = true }) {
const { session } = useSession();
const session = useSessionSelector((state) => state.session);
const goalText = session?.globalObjective?.text ? String(session.globalObjective.text).trim() : '';
const isMobile = layout === 'mobile-portrait' || layout === 'mobile-landscape' || layout === 'mobile';
const [visible, setVisible] = useState(false);
+3 -4
View File
@@ -2,9 +2,11 @@
// Purpose: Defines the Log 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.
import { useSessionSelector } from '../../context/SessionContext.jsx';
import { useMemo } from 'react';
export default function LogPanel() {
const logs = useSessionSelector((state) => state.logs);
const rendered = useMemo(() => logs.slice().reverse(), [logs]);
return (
<div className="panel-section space-y-0.5 text-base">
<div className="flex items-center justify-between text-sm text-slate-400">
@@ -15,10 +17,7 @@ export default function LogPanel() {
{logs.length === 0 ? (
<p>No logs yet.</p>
) : (
logs
.slice()
.reverse()
.map((entry) => (
rendered.map((entry) => (
<div key={entry.id} className="surface">
<span className="text-amber-400">{entry.timestamp}</span>{' '}
<span className="text-lime-400">[{entry.level}]</span>{' '}
@@ -2,7 +2,7 @@
// Purpose: Defines the Raw User Pile 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.
import { useMemo } from 'react';
import { useSession } from '../../context/SessionContext.jsx';
import { useSessionSelector } from '../../context/SessionContext.jsx';
import NicknameForm from '../NicknameForm/index.jsx';
import SocialButtonsGrid from '../SocialButtonsGrid/index.jsx';
@@ -35,7 +35,7 @@ export default function RawUserPilePanel({
fillHeight = false,
compact = false,
}) {
const { session } = useSession();
const session = useSessionSelector((state) => state.session);
const canSetNickname = session?.role !== 'spectator';
const users = session?.users ?? [];
const selfId = session?.socketId || null;
+4 -2
View File
@@ -23,6 +23,7 @@ import VipPanel from '../VipPanel/index.jsx';
import { useSessionSelector } from '../../context/SessionContext.jsx';
import { useSettingsNamespace } from '../../settings/index.js';
import ButtonBoxPanel from '../ButtonBoxPanel/index.jsx';
import { useState } from 'react';
function TopDownMapPanel() {
const {
@@ -115,6 +116,7 @@ function DriveDockPanel() {
}
export default function RightPaneTabs({ layout, onOpenHelpOverlay }) {
const [activeTab, setActiveTab] = useState('telemetry');
const session = useSessionSelector((state) => state.session);
const { state: controlState } = useControlSystem();
const { value: vipAudio } = useSettingsNamespace('vipAudio', { openMicEnabled: false, pttMode: 'live' });
@@ -138,7 +140,7 @@ export default function RightPaneTabs({ layout, onOpenHelpOverlay }) {
);
return (
<section className="panel text-base">
<Tabs defaultTab="telemetry">
<Tabs defaultTab="telemetry" currentTab={activeTab} onTabChange={setActiveTab}>
<TabList>
<Tab id="telemetry">Controls</Tab>
<Tab id="vip" highlight={vipClipPlaying ? 'green' : vipMicActive ? 'pink' : 'none'}>
@@ -179,7 +181,7 @@ export default function RightPaneTabs({ layout, onOpenHelpOverlay }) {
</div>
</TabPanel>
<TabPanel id="vip" keepMounted>
<VipPanel />
<VipPanel isActive={activeTab === 'vip'} />
</TabPanel>
<TabPanel id="help">
<HelpPanel layout={layout} onOpenOverlay={onOpenHelpOverlay} />
@@ -2,7 +2,7 @@
// Purpose: Defines the Room Camera 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.
import { useEffect, useState } from 'react';
import { useSession } from '../../context/SessionContext.jsx';
import { useSessionSelector } from '../../context/SessionContext.jsx';
import { useSettingsNamespace } from '../../settings/index.js';
import { useRoomCameraSnapshots } from '../../hooks/useRoomCameraSnapshots.js';
import RoomCameraFeed from '../RoomCameraFeed/index.jsx';
@@ -32,7 +32,7 @@ export default function RoomCameraPanel({
hideHeader = false,
panelId = null,
}) {
const { session } = useSession();
const session = useSessionSelector((state) => state.session);
const cameras = session?.roomCameras || [];
const feedMap = useRoomCameraSnapshots(cameras.map((camera) => ({ id: camera.id })));
const { value: orientationSettings, save: saveOrientationSettings } = useSettingsNamespace('roomCameraPanels', {});
@@ -13,7 +13,7 @@ import {
cliffColor,
} from './visuals.jsx';
export default function TopDownMapContent({ sensors = {}, variant = 'full', size: overrideSize, overlay = false }) {
function TopDownMapContent({ sensors = {}, variant = 'full', size: overrideSize, overlay = false }) {
const size = overrideSize || (variant === 'mini' ? 190 : 260);
const center = size / 2;
const offsetY = size * 0.07;
@@ -147,3 +147,5 @@ export default function TopDownMapContent({ sensors = {}, variant = 'full', size
</div>
);
}
export default React.memo(TopDownMapContent);
@@ -5,7 +5,7 @@ import React from 'react';
import TopDownMap from '../TopDownMap/index.jsx';
import { roverNameChromeStyle } from '../../lib/roverColor.js';
export default function HudOverlay({
function HudOverlay({
sensors,
label,
roverColor = null,
@@ -174,3 +174,5 @@ export default function HudOverlay({
</div>
);
}
export default React.memo(HudOverlay);
@@ -3,7 +3,7 @@
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import React from 'react';
export default function LightBumpBars({ sensors }) {
function LightBumpBars({ sensors }) {
const values = [
sensors?.lightBumpLeftSignal,
sensors?.lightBumpFrontLeftSignal,
@@ -47,3 +47,5 @@ export default function LightBumpBars({ sensors }) {
</div>
);
}
export default React.memo(LightBumpBars);
+12 -4
View File
@@ -100,10 +100,18 @@ export default function VideoTile({
.map(([key]) => key);
const limiterCaps = overcurrentLimiter?.caps || null;
const limiterGroups = overcurrentLimiter?.overcurrent?.groups || null;
const debugAudio =
typeof window !== 'undefined' && new URLSearchParams(window.location.search).has('debugAudio');
const debugHud =
typeof window !== 'undefined' && new URLSearchParams(window.location.search).has('debugHud');
const debugFlags = useMemo(() => {
if (typeof window === 'undefined') {
return { debugAudio: false, debugHud: false };
}
const params = new URLSearchParams(window.location.search);
return {
debugAudio: params.has('debugAudio'),
debugHud: params.has('debugHud'),
};
}, []);
const debugAudio = debugFlags.debugAudio;
const debugHud = debugFlags.debugHud;
const limiterFill = useMemo(() => {
if (!limiterCaps) return null;
const driveCap = Number.isFinite(limiterCaps?.drive?.cap) ? limiterCaps.drive.cap : 1;
+6 -5
View File
@@ -2,7 +2,7 @@
// Purpose: Defines the Vip 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.
import { useMemo, useState } from 'react';
import { useSession } from '../../context/SessionContext.jsx';
import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
import { useSettingsNamespace } from '../../settings/index.js';
import { COOKIE_KEY_REGEX, flowWrapClass } from '../vip/constants.js';
import VipAudioUploadCard from '../vip/VipAudioUploadCard/index.jsx';
@@ -12,10 +12,10 @@ import VipPrivateRoverAccessCard from '../vip/VipPrivateRoverAccessCard.jsx';
import VipNeatoCard from '../vip/VipNeatoCard.jsx';
import VipLiftCard from '../vip/VipLiftCard.jsx';
export default function VipPanel() {
export default function VipPanel({ isActive = true }) {
const session = useSessionSelector((state) => state.session);
const neatoLidar = useSessionSelector((state) => state.neatoLidar);
const {
session,
neatoLidar,
identifySession,
requestVerification,
requestPrivateRoverAccess,
@@ -31,7 +31,7 @@ export default function VipPanel() {
neatoPowerCycle,
liftUp,
liftDown,
} = useSession();
} = useSessionActions();
const { value: identity, save: saveIdentity } = useSettingsNamespace('identity', { cookieUserId: '' });
const { value: profile } = useSettingsNamespace('profile', { nickname: '' });
@@ -80,6 +80,7 @@ export default function VipPanel() {
<VipNeatoCard
neato={session?.neato || null}
lidar={neatoLidar}
lidarActive={isActive}
onStart={neatoStart}
onSendHome={neatoSendHome}
onLocate={neatoLocate}
+8 -3
View File
@@ -1,7 +1,7 @@
// Vip Neato Card
// Purpose: Defines the Vip Neato Card module and the local helpers/components used in this file.
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
function normalizeState(value) {
return String(value || '').trim();
@@ -114,6 +114,7 @@ function buildLidarRenderPoints(points = []) {
export default function VipNeatoCard({
neato,
lidar,
lidarActive = true,
onStart,
onSendHome,
onLocate,
@@ -140,7 +141,10 @@ export default function VipNeatoCard({
const robotError = normalizeState(neato?.telemetry?.robotError) || '--';
const robotAlert = normalizeState(neato?.telemetry?.robotAlert) || '--';
const lidarPoints = Array.isArray(lidar?.points) ? lidar.points : [];
const lidarRenderPoints = buildLidarRenderPoints(lidarPoints);
const lidarRenderPoints = useMemo(
() => (lidarActive ? buildLidarRenderPoints(lidarPoints) : []),
[lidarActive, lidarPoints],
);
const lidarStatus = normalizeState(lidar?.status) || '--';
const lidarDebug = lidar?.debug && typeof lidar.debug === 'object' ? lidar.debug : null;
const lidarReason = normalizeState(lidarDebug?.reason) || '--';
@@ -180,13 +184,14 @@ export default function VipNeatoCard({
const primaryState = docked ? 'Docked' : uiStateLabel !== '--' ? uiStateLabel : 'Away from dock';
useEffect(() => {
if (!lidarActive) return undefined;
if (!lidar || !Array.isArray(lidar.points)) return undefined;
setLidarFlash(true);
const timer = setTimeout(() => {
setLidarFlash(false);
}, 120);
return () => clearTimeout(timer);
}, [lidar]);
}, [lidar, lidarActive]);
return (
<section className={`surface text-sm text-slate-200 ${wrapClass}`}>
+69 -12
View File
@@ -2,27 +2,75 @@
// Purpose: Maintains shared telemetry snapshots and rover status streams for UI consumers. Scope: Subscribes to telemetry events and exposes normalized read APIs to components.
/* eslint-disable react-refresh/only-export-components */
import { createContext, useContext, useMemo, useState, useEffect } from 'react';
import { createContext, useContext, useEffect, useMemo, useRef, useSyncExternalStore } from 'react';
import { useSocket } from './SocketContext.jsx';
const TelemetryContext = createContext({ frames: {} });
const EMPTY_FRAMES = Object.freeze({});
const EMPTY_FRAME = null;
const TelemetryContext = createContext(null);
export function TelemetryProvider({ children }) {
const socket = useSocket();
const [frames, setFrames] = useState({});
const framesRef = useRef({});
const roverSubscribersRef = useRef(new Map());
const allSubscribersRef = useRef(new Set());
const notifyRover = (roverId) => {
const listeners = roverSubscribersRef.current.get(roverId);
if (listeners) {
listeners.forEach((listener) => listener());
}
allSubscribersRef.current.forEach((listener) => listener());
};
const store = useMemo(
() => ({
getFrames: () => framesRef.current,
getFrame: (roverId) => {
if (!roverId) return EMPTY_FRAME;
return framesRef.current[roverId] ?? EMPTY_FRAME;
},
subscribeAll: (listener) => {
allSubscribersRef.current.add(listener);
return () => {
allSubscribersRef.current.delete(listener);
};
},
subscribeRover: (roverId, listener) => {
if (!roverId) return () => {};
let listeners = roverSubscribersRef.current.get(roverId);
if (!listeners) {
listeners = new Set();
roverSubscribersRef.current.set(roverId, listeners);
}
listeners.add(listener);
return () => {
const current = roverSubscribersRef.current.get(roverId);
if (!current) return;
current.delete(listener);
if (!current.size) {
roverSubscribersRef.current.delete(roverId);
}
};
},
}),
[],
);
useEffect(() => {
function handleSensorFrame({ roverId, sensors = {}, frame = {} }) {
if (!roverId) return;
setFrames((prev) => ({
...prev,
framesRef.current = {
...framesRef.current,
[roverId]: {
roverId,
sensors,
raw: frame?.data || null,
receivedAt: Date.now(),
},
}));
};
notifyRover(roverId);
}
socket.on('sensorFrame', handleSensorFrame);
@@ -31,16 +79,25 @@ export function TelemetryProvider({ children }) {
};
}, [socket]);
const value = useMemo(() => ({ frames }), [frames]);
return <TelemetryContext.Provider value={value}>{children}</TelemetryContext.Provider>;
return <TelemetryContext.Provider value={store}>{children}</TelemetryContext.Provider>;
}
export function useTelemetryFrames() {
return useContext(TelemetryContext).frames;
const store = useContext(TelemetryContext);
if (!store) {
throw new Error('useTelemetryFrames must be used within TelemetryProvider');
}
return useSyncExternalStore(store.subscribeAll, store.getFrames, () => EMPTY_FRAMES);
}
export function useTelemetryFrame(roverId) {
const frames = useTelemetryFrames();
if (!roverId) return null;
return frames[roverId] ?? null;
const store = useContext(TelemetryContext);
if (!store) {
throw new Error('useTelemetryFrame must be used within TelemetryProvider');
}
return useSyncExternalStore(
(listener) => store.subscribeRover(roverId, listener),
() => store.getFrame(roverId),
() => EMPTY_FRAME,
);
}
-14
View File
@@ -53,20 +53,6 @@ export function useDockIr(sensors, options = {}) {
}));
}, [sensors?.infraredCharacterLeft, sensors?.infraredCharacterRight, sensors?.infraredCharacterOmni]);
useEffect(() => {
// Debug: log when new codes appear
const { left, right, omni } = state;
const haveAny = left?.ts || right?.ts || omni?.ts;
if (!haveAny) return;
const stamp = new Date().toISOString();
// eslint-disable-next-line no-console
console.debug('[DockIR]', stamp, {
left: left?.code ?? 0,
omni: omni?.code ?? 0,
right: right?.code ?? 0,
});
}, [state.left?.code, state.right?.code, state.omni?.code]);
return useMemo(() => {
const now = Date.now();
const withWindow = (entry) => {