| name | console-frontend-review |
| description | Reviews React/TypeScript code for the depot console web application with focus on real-time rover teleoperation, state management, WebSocket communication, and 3D visualization. Use when reviewing console frontend changes, debugging teleop UI issues, optimizing rendering performance, validating WebSocket protocols, checking React Three Fiber implementations, or evaluating state management patterns. Covers Zustand store architecture, binary protocol encoding, input handling, page visibility safety, memory management, and 360-degree video streaming. |
| allowed-tools | Read, Grep, Glob, Bash(npm:test), Bash(npm:build), Bash(npm:lint) |
Depot Console Frontend Code Review Skill
This skill provides comprehensive code review for the depot console React/TypeScript web application used for fleet operations and rover teleoperation.
Overview
The depot console is a React 19 web application providing real-time control and monitoring of BVR rovers. It features 3D visualization, WebSocket-based teleop, and 360° video streaming.
Technology Stack:
- Framework: React 19 with Vite
- Language: TypeScript (strict mode)
- State: Zustand (single store)
- Styling: Tailwind CSS v4
- 3D: React Three Fiber + drei
- UI Components: Radix UI primitives
- Routing: React Router v7
- Build: Vite with ESM
Architecture:
depot/console/
├── src/
│ ├── main.tsx # Entry point
│ ├── App.tsx # Router setup
│ ├── store.ts # Zustand global state (single source of truth)
│ ├── components/ # React components
│ │ ├── scene/ # React Three Fiber 3D components
│ │ ├── teleop/ # Teleoperation UI panels
│ │ └── ui/ # Radix UI + CVA primitives
│ ├── views/ # Page-level components
│ ├── hooks/ # Custom React hooks
│ │ ├── useRoverConnection.ts # WebSocket teleop
│ │ ├── useVideoStream.ts # 360 video stream
│ │ ├── useGamepad.ts # Gamepad input polling
│ │ ├── useKeyboard.ts # Keyboard input handling
│ │ └── useDiscovery.ts # Rover discovery service
│ └── lib/
│ ├── types.ts # TypeScript type definitions
│ ├── protocol.ts # Binary protocol codec
│ └── utils.ts # Utility functions
Critical Files:
- Store:
depot/console/src/store.ts (~400 lines)
- Types:
depot/console/src/lib/types.ts (~300 lines)
- Protocol:
depot/console/src/lib/protocol.ts (~200 lines)
- Rover connection:
depot/console/src/hooks/useRoverConnection.ts (~250 lines)
- Video stream:
depot/console/src/hooks/useVideoStream.ts (~150 lines)
- 3D scene:
depot/console/src/components/scene/Scene.tsx (~200 lines)
State Management Review (Zustand)
Store Architecture
Location: depot/console/src/store.ts
Key Points to Review:
Store Domains:
-
Fleet Management:
rovers: RoverInfo[] - List of discovered rovers
selectedRoverId: string | null - Currently selected rover
selectRover(id) - Select rover and update addresses
-
Connection State:
roverAddress: string - WebSocket teleop address (ws://localhost:4850)
videoAddress: string - WebSocket video address (ws://localhost:4851)
connected: boolean - Connection status
latencyMs: number - Round-trip latency
-
Telemetry (Real-time Rover State):
mode: Mode - Operational mode (Idle, Teleop, etc.)
pose: Pose - Position (x, y, theta)
velocity: Twist - Current velocity
power: PowerStatus - Battery voltage, current
temperatures: TempStatus - Motor and controller temps
-
Input:
input: GamepadInput - Normalized gamepad/keyboard input
inputSource: InputSource - "gamepad" | "keyboard" | "none"
-
Camera:
cameraMode: CameraMode - ThirdPerson, FirstPerson, FreeLook
cameraSettings - FOV, distance, offset
-
Video:
videoFrame: string | null - Blob URL for current frame
videoConnected: boolean
videoFps: number
Example Pattern:
export const useConsoleStore = create<ConsoleState>((set) => ({
rovers: [],
selectedRoverId: null,
selectRover: (id: string) =>
set((state) => {
const rover = state.rovers.find((r) => r.id === id);
return {
selectedRoverId: id,
roverAddress: rover ? `ws://${rover.hostname}:4850` : state.roverAddress,
videoAddress: rover ? `ws://${rover.hostname}:4851` : state.videoAddress,
};
}),
mode: Mode.Disabled,
pose: { x: 0, y: 0, theta: 0 },
updateTelemetry: (telemetry: Partial<Telemetry>) =>
set((state) => ({
...state,
...telemetry,
})),
connected: false,
setConnected: (connected: boolean) => set({ connected }),
}));
Red Flags:
- Direct state mutation (
state.rovers.push(...))
- Missing immutability in updates
- No TypeScript types for state shape
- Large monolithic state (should be split into domains)
- Computed values stored in state (should be derived)
See: Zustand Best Practices
State Consumption in Components
Key Points to Review:
Example Pattern:
function TelemetryPanel() {
const { mode, pose, velocity } = useConsoleStore((state) => ({
mode: state.mode,
pose: state.pose,
velocity: state.velocity,
}));
return (
<div>
<div>Mode: {ModeLabels[mode]}</div>
<div>Position: ({pose.x.toFixed(2)}, {pose.y.toFixed(2)})</div>
<div>Velocity: {velocity.linear.toFixed(2)} m/s</div>
</div>
);
}
const state = useConsoleStore();
TypeScript Patterns Review
Type Definitions
Location: depot/console/src/lib/types.ts
Key Points to Review:
Example Pattern:
export const Mode = {
Disabled: 0,
Idle: 1,
Teleop: 2,
Autonomous: 3,
EStop: 4,
Fault: 5,
} as const;
export type Mode = (typeof Mode)[keyof typeof Mode];
export const ModeLabels: Record<Mode, string> = {
[Mode.Disabled]: "Disabled",
[Mode.Idle]: "Idle",
[Mode.Teleop]: "Teleop",
[Mode.Autonomous]: "Autonomous",
[Mode.EStop]: "E-Stop",
[Mode.Fault]: "Fault",
};
export interface Telemetry {
mode: Mode;
pose: Pose;
velocity: Twist;
power: PowerStatus;
temperatures: TempStatus;
}
export type InputSource = "gamepad" | "keyboard" | "none";
Red Flags:
- String enums instead of numeric (breaks binary protocol)
type used for objects (use interface)
- Missing null checks
any types
- Duplicate type definitions
Component Props
Key Points to Review:
Example Pattern:
interface TelemetryPanelProps {
className?: string;
showAdvanced?: boolean;
}
export function TelemetryPanel({ className, showAdvanced = false }: TelemetryPanelProps) {
}
WebSocket Communication Review
Binary Protocol Implementation
Location: depot/console/src/lib/protocol.ts
Message Types:
- Commands (Console → Rover):
0x01-0x0F
- Telemetry (Rover → Console):
0x10-0x1F
- Video (Rover → Console):
0x20-0x2F
Key Points to Review:
Command Encoding:
export function encodeTwist(twist: Twist): ArrayBuffer {
const buffer = new ArrayBuffer(25);
const view = new DataView(buffer);
view.setUint8(0, MSG_TWIST);
view.setFloat64(1, twist.linear, true);
view.setFloat64(9, twist.angular, true);
view.setUint8(17, twist.boost ? 1 : 0);
return buffer;
}
const json = JSON.stringify({ type: "twist", ...twist });
Telemetry Decoding:
export function decodeTelemetry(data: ArrayBuffer): Telemetry {
if (data.byteLength < 90) {
throw new Error(`Telemetry frame too short: ${data.byteLength} bytes`);
}
const view = new DataView(data);
const type = view.getUint8(0);
if (type !== MSG_TELEMETRY) {
throw new Error(`Invalid message type: ${type}`);
}
return {
mode: view.getUint8(1),
pose: {
x: view.getFloat64(2, true),
y: view.getFloat64(10, true),
theta: view.getFloat32(18, true),
},
velocity: {
linear: view.getFloat32(22, true),
angular: view.getFloat32(26, true),
boost: view.getUint8(30) !== 0,
},
};
}
See: websocket-protocols.md for complete protocol reference.
WebSocket Connection Management
Location: depot/console/src/hooks/useRoverConnection.ts
Key Points to Review:
Example Pattern:
export function useRoverConnection() {
const [ws, setWs] = useState<WebSocket | null>(null);
const address = useConsoleStore((state) => state.roverAddress);
useEffect(() => {
const socket = new WebSocket(address);
socket.binaryType = "arraybuffer";
socket.onopen = () => {
console.log("Connected to rover");
useConsoleStore.getState().setConnected(true);
};
socket.onmessage = (event: MessageEvent) => {
const telemetry = decodeTelemetry(event.data);
useConsoleStore.getState().updateTelemetry(telemetry);
};
socket.onclose = () => {
console.log("Disconnected from rover");
useConsoleStore.getState().setConnected(false);
setTimeout(() => setWs(null), 3000);
};
socket.onerror = (error) => {
console.error("WebSocket error:", error);
};
setWs(socket);
return () => {
socket.close();
};
}, [address]);
return { ws };
}
Red Flags:
- No
binaryType = "arraybuffer" (defaults to Blob, slower)
- Missing cleanup (memory leak)
- No reconnection logic
- Errors thrown instead of logged
- No timeout handling
Command Transmission
Key Points to Review:
Example Pattern:
useEffect(() => {
if (!ws || !connected) return;
const interval = setInterval(() => {
const input = useConsoleStore.getState().input;
const twist = { linear: input.linear, angular: input.angular, boost: input.boost };
const buffer = encodeTwist(twist);
ws.send(buffer);
}, 10);
return () => clearInterval(interval);
}, [ws, connected]);
useEffect(() => {
if (!ws || !connected) return;
const interval = setInterval(() => {
const buffer = encodeHeartbeat();
ws.send(buffer);
}, 100);
return () => clearInterval(interval);
}, [ws, connected]);
Safety Features Review
Page Visibility Tracking
Purpose: Stop sending motor commands when tab loses focus (user switches tabs).
Key Points to Review:
Example Pattern:
useEffect(() => {
const handleVisibilityChange = () => {
if (document.hidden) {
useConsoleStore.getState().setInput({
linear: 0,
angular: 0,
boost: false,
});
useConsoleStore.getState().setInputSource("none");
console.warn("Tab hidden, stopping commands");
}
};
document.addEventListener("visibilitychange", handleVisibilityChange);
return () => document.removeEventListener("visibilitychange", handleVisibilityChange);
}, []);
Red Flags:
- No visibility tracking (rover continues moving when tab hidden)
- Commands sent regardless of focus state
E-Stop Button
Key Points to Review:
Example Pattern:
function EStopButton() {
const { ws } = useRoverConnection();
const handleEStop = () => {
if (!ws) return;
const buffer = encodeEStop();
ws.send(buffer);
useConsoleStore.getState().addToast({
title: "E-Stop Activated",
variant: "destructive",
});
};
return (
<Button
variant="destructive"
size="lg"
onClick={handleEStop}
className="fixed top-4 right-4 z-50"
>
<AlertTriangle className="mr-2" />
E-STOP
</Button>
);
}
Input Handling Review
Gamepad Input
Location: depot/console/src/hooks/useGamepad.ts
Key Points to Review:
Example Pattern:
export function useGamepad() {
const [input, setInput] = useState<GamepadInput>({ linear: 0, angular: 0, boost: false });
useEffect(() => {
let frameId: number;
const poll = () => {
const gamepads = navigator.getGamepads();
const gamepad = gamepads[0];
if (gamepad) {
const DEADZONE = 0.1;
let linear = -gamepad.axes[1];
if (Math.abs(linear) < DEADZONE) linear = 0;
let angular = gamepad.axes[2];
if (Math.abs(angular) < DEADZONE) angular = 0;
const boost = gamepad.buttons[0].pressed;
setInput({ linear, angular, boost });
useConsoleStore.getState().setInputSource("gamepad");
}
frameId = requestAnimationFrame(poll);
};
frameId = requestAnimationFrame(poll);
return () => cancelAnimationFrame(frameId);
}, []);
return input;
}
Red Flags:
- Event-based (gamepad API doesn't support events reliably)
- No dead zone (jittery input)
- Axes not normalized
- Missing cleanup
Keyboard Input
Location: depot/console/src/hooks/useKeyboard.ts
Key Points to Review:
Example Pattern:
export function useKeyboard() {
const [keys, setKeys] = useState<Set<string>>(new Set());
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.target instanceof HTMLInputElement) return;
setKeys((prev) => new Set(prev).add(e.code));
};
const handleKeyUp = (e: KeyboardEvent) => {
setKeys((prev) => {
const next = new Set(prev);
next.delete(e.code);
return next;
});
};
document.addEventListener("keydown", handleKeyDown);
document.addEventListener("keyup", handleKeyUp);
return () => {
document.removeEventListener("keydown", handleKeyDown);
document.removeEventListener("keyup", handleKeyUp);
};
}, []);
const input = useMemo(() => {
let linear = 0;
let angular = 0;
if (keys.has("KeyW")) linear += 1;
if (keys.has("KeyS")) linear -= 1;
if (keys.has("KeyA")) angular += 1;
if (keys.has("KeyD")) angular -= 1;
return { linear, angular, boost: keys.has("ShiftLeft") };
}, [keys]);
if (input.linear !== 0 || input.angular !== 0) {
useConsoleStore.getState().setInputSource("keyboard");
}
return input;
}
3D Visualization Review (React Three Fiber)
Scene Setup
Location: depot/console/src/components/scene/Scene.tsx
Key Points to Review:
Example Pattern:
export function Scene() {
return (
<Canvas shadows camera={{ fov: 60, position: [0, 5, 10] }}>
<ambientLight intensity={0.5} />
<directionalLight position={[10, 10, 5]} castShadow />
<RoverModel />
<Ground />
<EquirectangularSky />
<CameraController />
</Canvas>
);
}
Rover Model Animation
Key Points to Review:
Example Pattern:
function RoverModel() {
const pose = useConsoleStore((state) => state.pose);
const ref = useRef<THREE.Group>(null);
useFrame((state, delta) => {
if (!ref.current) return;
ref.current.position.x = THREE.MathUtils.lerp(ref.current.position.x, pose.x, delta * 10);
ref.current.position.z = THREE.MathUtils.lerp(ref.current.position.z, -pose.y, delta * 10);
const targetRot = -pose.theta;
const currentRot = ref.current.rotation.y;
const diff = ((targetRot - currentRot + Math.PI) % (2 * Math.PI)) - Math.PI;
ref.current.rotation.y += diff * delta * 10;
});
return (
<group ref={ref}>
{/* Rover geometry */}
</group>
);
}
Red Flags:
- Direct assignment (no interpolation, jumpy motion)
- No wraparound handling for angles
- Fixed delta (not frame-rate independent)
Memory Management
Key Points to Review:
Example Pattern:
useEffect(() => {
if (!videoFrame) return;
const texture = new THREE.TextureLoader().load(videoFrame);
return () => {
texture.dispose();
URL.revokeObjectURL(videoFrame);
};
}, [videoFrame]);
See: performance.md for optimization strategies.
Component Patterns Review
File Naming
Convention: PascalCase for components, camelCase for hooks/utils.
Key Points to Review:
Component Structure
Key Points to Review:
Example Pattern:
interface TelemetryPanelProps {
className?: string;
}
export function TelemetryPanel({ className }: TelemetryPanelProps) {
const { mode, pose, velocity } = useConsoleStore((state) => ({
mode: state.mode,
pose: state.pose,
velocity: state.velocity,
}));
const [expanded, setExpanded] = useState(false);
useEffect(() => {
}, []);
const handleToggle = () => setExpanded(!expanded);
return (
<Card className={cn("p-4", className)}>
<h2>Telemetry</h2>
<div>Mode: {ModeLabels[mode]}</div>
<div>Position: ({pose.x.toFixed(2)}, {pose.y.toFixed(2)})</div>
<Button onClick={handleToggle}>Toggle</Button>
</Card>
);
}
Testing and Linting
ESLint Configuration
Key Points to Review:
Run linting:
npm run lint
Type Checking
Key Points to Review:
Build Verification
Key Points to Review:
References and Additional Resources
For more detailed information, see:
Quick Review Commands
npm run lint
npm run build
npm run dev
npm run test