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
+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) => {