console-frontend-review
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:
- Single store created with
create<ConsoleState>() - State organized into logical domains (fleet, connection, telemetry, input, camera, video)
- Immutable updates (no direct state mutation)
- Partial updates via
set()with object spread - No Redux-style actions (direct setter methods)
Store Domains:
-
Fleet Management:
rovers: RoverInfo[]- List of discovered roversselectedRoverId: string | null- Currently selected roverselectRover(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 statuslatencyMs: 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 velocitypower: PowerStatus- Battery voltage, currenttemperatures: TempStatus- Motor and controller temps
-
Input:
input: GamepadInput- Normalized gamepad/keyboard inputinputSource: InputSource- "gamepad" | "keyboard" | "none"
-
Camera:
cameraMode: CameraMode- ThirdPerson, FirstPerson, FreeLookcameraSettings- FOV, distance, offset
-
Video:
videoFrame: string | null- Blob URL for current framevideoConnected: booleanvideoFps: number
Example Pattern:
// Good: Zustand store with domain slices
export const useConsoleStore = create<ConsoleState>((set) => ({
// Fleet state
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,
};
}),
// Telemetry state
mode: Mode.Disabled,
pose: { x: 0, y: 0, theta: 0 },
updateTelemetry: (telemetry: Partial<Telemetry>) =>
set((state) => ({
...state,
...telemetry, // Partial merge
})),
// Connection state
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)
State Consumption in Components
Key Points to Review:
- Use
useConsoleStore()hook to access state - Selector functions for performance (only re-render on relevant changes)
- No prop drilling (state accessed directly from store)
Example Pattern:
// Good: Selector for specific state slice
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>
);
}
// Bad: Selecting entire state (re-renders on any state change)
const state = useConsoleStore(); // ❌ Don't do this
TypeScript Patterns Review
Type Definitions
Location: depot/console/src/lib/types.ts
Key Points to Review:
- Enums defined with
as constpattern - Type aliases derived from enum keys
- Interfaces for object shapes (not types)
- No
anytypes (useunknownor proper types) - Strict null checks enabled
Example Pattern:
// Good: Const enum with type alias
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];
// Good: Label map for UI display
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",
};
// Good: Interface for objects
export interface Telemetry {
mode: Mode;
pose: Pose;
velocity: Twist;
power: PowerStatus;
temperatures: TempStatus;
}
// Good: Discriminated union for input source
export type InputSource = "gamepad" | "keyboard" | "none";
Red Flags:
- String enums instead of numeric (breaks binary protocol)
typeused for objects (useinterface)- Missing null checks
anytypes- Duplicate type definitions
Component Props
Key Points to Review:
- Props interfaces defined with
interfacekeyword - Optional props use
?operator - Destructured in function parameters
- Children typed with
React.ReactNode
Example Pattern:
// Good: Props interface
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:
- Binary encoding uses
DataViewwith little-endian - Message type byte at offset 0
- Payload follows type byte
- Bounds checking before reading
- No string encoding in critical path (use binary for performance)
Command Encoding:
// Good: Binary command encoding
export function encodeTwist(twist: Twist): ArrayBuffer {
const buffer = new ArrayBuffer(25); // 1 + 3*8 bytes
const view = new DataView(buffer);
view.setUint8(0, MSG_TWIST); // Message type
view.setFloat64(1, twist.linear, true); // Little-endian f64
view.setFloat64(9, twist.angular, true);
view.setUint8(17, twist.boost ? 1 : 0);
return buffer;
}
// Bad: JSON encoding (too slow for 100Hz)
const json = JSON.stringify({ type: "twist", ...twist }); // ❌ Inefficient
Telemetry Decoding:
// Good: Binary telemetry decoding with bounds check
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,
},
// ... more fields
};
}
See: websocket-protocols.md for complete protocol reference.
WebSocket Connection Management
Location: depot/console/src/hooks/useRoverConnection.ts
Key Points to Review:
- WebSocket created with
new WebSocket(url) - Event listeners:
onopen,onmessage,onclose,onerror - Binary type set to
"arraybuffer" - Auto-reconnect with exponential backoff
- Cleanup on unmount (close socket)
- Error handling doesn't crash app
Example Pattern:
// Good: WebSocket with cleanup
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"; // Critical for binary protocol
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);
// Reconnect after 3s
setTimeout(() => setWs(null), 3000);
};
socket.onerror = (error) => {
console.error("WebSocket error:", error);
};
setWs(socket);
// Cleanup on unmount
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:
- Commands sent at appropriate rate (100Hz for twist)
- Heartbeat sent periodically (10Hz)
- No commands sent when disconnected
- Binary encoding used (not JSON)
Example Pattern:
// Good: Command sending at 100Hz
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); // 100Hz = 10ms period
return () => clearInterval(interval);
}, [ws, connected]);
// Good: Heartbeat at 10Hz
useEffect(() => {
if (!ws || !connected) return;
const interval = setInterval(() => {
const buffer = encodeHeartbeat();
ws.send(buffer);
}, 100); // 10Hz = 100ms period
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:
-
document.visibilityStatemonitored - Commands stopped when
hidden - Input cleared when tab not visible
- Warning shown to user
Example Pattern:
// Good: Page visibility tracking
useEffect(() => {
const handleVisibilityChange = () => {
if (document.hidden) {
// Stop all commands
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:
- Prominent e-stop button in UI
- Sends e-stop command immediately on click
- Visual feedback (overlay, color change)
- Keyboard shortcut (e.g., Space bar)
Example Pattern:
// Good: E-Stop button
function EStopButton() {
const { ws } = useRoverConnection();
const handleEStop = () => {
if (!ws) return;
const buffer = encodeEStop();
ws.send(buffer);
// Visual feedback
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:
- Polling via
requestAnimationFrame(not event-based) - Dead zone applied (e.g., 0.1 threshold)
- Axes normalized to [-1, 1]
- Button state tracked
- Cleanup on unmount
Example Pattern:
// Good: Gamepad polling with deadzone
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;
// Left stick Y (inverted): linear
let linear = -gamepad.axes[1];
if (Math.abs(linear) < DEADZONE) linear = 0;
// Right stick X: angular
let angular = gamepad.axes[2];
if (Math.abs(angular) < DEADZONE) angular = 0;
// Button 0 (A): boost
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:
- Global listeners on
documentorwindow - Key state tracked in
Setor object - Input normalized to [-1, 1]
- Cleanup removes listeners
- Doesn't interfere with text inputs
Example Pattern:
// Good: Keyboard input with state tracking
export function useKeyboard() {
const [keys, setKeys] = useState<Set<string>>(new Set());
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Don't capture if typing in input
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);
};
}, []);
// Convert keys to input
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:
-
<Canvas>wraps all Three.js components - Camera FOV reasonable (60-75°)
- Lighting includes ambient + directional
- Shadows enabled if needed
- Performance monitoring (
<Perf>in dev)
Example Pattern:
// Good: Canvas setup with lighting
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:
- Position interpolated with
lerp(smooth motion) - Angle interpolation handles wraparound (0° ↔ 360°)
- Delta time used for frame-rate independence
- Model updates on telemetry change
Example Pattern:
// Good: Smooth position interpolation
function RoverModel() {
const pose = useConsoleStore((state) => state.pose);
const ref = useRef<THREE.Group>(null);
useFrame((state, delta) => {
if (!ref.current) return;
// Lerp position
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);
// Lerp rotation (handle wraparound)
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:
- Textures disposed on unmount
- Geometries disposed when not needed
- Materials disposed when not needed
- Blob URLs revoked with
URL.revokeObjectURL()
Example Pattern:
// Good: Texture cleanup
useEffect(() => {
if (!videoFrame) return;
const texture = new THREE.TextureLoader().load(videoFrame);
return () => {
texture.dispose(); // Free GPU memory
URL.revokeObjectURL(videoFrame); // Free blob URL
};
}, [videoFrame]);
See: performance.md for optimization strategies.
Component Patterns Review
File Naming
Convention: PascalCase for components, camelCase for hooks/utils.
Key Points to Review:
- Components:
TelemetryPanel.tsx,RoverModel.tsx - Hooks:
useGamepad.ts,useRoverConnection.ts - Utils:
utils.ts,protocol.ts - All TypeScript (
.tsor.tsx)
Component Structure
Key Points to Review:
- Functional components (not class components)
- Props destructured in parameters
- Hooks at top of function (before any conditional)
- Event handlers defined inside component
- Return JSX with semantic HTML
Example Pattern:
// Good: Component structure
interface TelemetryPanelProps {
className?: string;
}
export function TelemetryPanel({ className }: TelemetryPanelProps) {
// 1. Zustand store access
const { mode, pose, velocity } = useConsoleStore((state) => ({
mode: state.mode,
pose: state.pose,
velocity: state.velocity,
}));
// 2. Local state
const [expanded, setExpanded] = useState(false);
// 3. Effects
useEffect(() => {
// Side effects
}, []);
// 4. Event handlers
const handleToggle = () => setExpanded(!expanded);
// 5. Render
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:
- TypeScript ESLint rules enabled
- React hooks rules enforced
- No
anytypes allowed - Unused vars detected
- Import order enforced
Run linting:
npm run lint
Type Checking
Key Points to Review:
- No TypeScript errors:
npm run build - Strict mode enabled in
tsconfig.json - All imports have types
Build Verification
Key Points to Review:
- Vite build succeeds:
npm run build - Bundle size reasonable (<1MB for main chunk)
- No console errors in production build
References and Additional Resources
For more detailed information, see:
- websocket-protocols.md - Binary protocol encoding/decoding
- performance.md - React rendering optimization and memory management
- CLAUDE.md - Project-wide conventions
Quick Review Commands
# Lint code
npm run lint
# Type check and build
npm run build
# Dev server with hot-reload
npm run dev
# Run tests (if configured)
npm run test