Skip to content

Commit c06f677

Browse files
committed
fix(client): complete simulation mode hardening for Vercel
- Simplify simulation detection logic with better HTTPS/local IP checks - Prevent all WebSocket and HTTP calls when in simulation mode - Fix infinite re-render loop in useRobotControl with useMemo - Update UI to show 'Offline' with red indicators in mock mode - Add debug logging for simulation mode activation - Skip ESP reset calls in simulation mode
1 parent 374e99b commit c06f677

File tree

9 files changed

+135
-69
lines changed

9 files changed

+135
-69
lines changed

client/src/components/Menu/sidenavbar.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { NavItem } from "./models/navigation-model";
1111
import { useConnectivity } from "@/context/connectivity-context";
1212
import { Chip } from "@mui/material";
1313
import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord';
14+
import { isSimulationMode } from "@/utils/simulation";
1415

1516
const iconsPages = [
1617
{ name: "menu.robot", linkTo: "/", icon: <SmartToyIcon /> },
@@ -33,7 +34,10 @@ export const SideNavBar = (): React.ReactElement => {
3334
};
3435

3536
const connectionInfo = getContextInfo();
36-
const isConnected = connectionInfo.status === 'connected';
37+
// Force disconnected style if in simulation mode, as simulation has its own alert
38+
const isSimulated = isSimulationMode();
39+
const isConnected = connectionInfo.status === 'connected' && !isSimulated;
40+
const displayLabel = isSimulated ? "Offline" : (connectionInfo.ip || "Offline");
3741

3842
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
3943
setAnchorEl(event.currentTarget);
@@ -81,7 +85,7 @@ export const SideNavBar = (): React.ReactElement => {
8185
>
8286
<Chip
8387
icon={<FiberManualRecordIcon style={{ fontSize: '0.8rem', color: isConnected ? '#4caf50' : '#f44336' }} />}
84-
label={connectionInfo.ip || "Offline"}
88+
label={displayLabel}
8589
sx={{
8690
backgroundColor: 'rgba(255, 255, 255, 0.05)',
8791
color: 'var(--text-color)',

client/src/pages/car/car-page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export const CarPage: React.FC = () => {
7474
<CarHeader
7575
connectedRobot={connectedRobot}
7676
connectedRemote={connectedRemote}
77+
isMock={isMock}
7778
ledState={dashboardState.robot.ledState || false}
7879
onToggleLed={toggleLED}
7980
onPing={() => sendWSMessage("ping")}

client/src/pages/car/components/car-header.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,22 @@ import { useTranslation } from "react-i18next";
66
interface HeaderProps {
77
connectedRobot: boolean;
88
connectedRemote: boolean;
9+
isMock: boolean;
910
ledState: boolean;
1011
onToggleLed: () => void;
1112
onPing: () => void;
1213
height: string;
1314
}
1415

15-
export const CarHeader: React.FC<HeaderProps> = ({ connectedRobot, connectedRemote, ledState, onToggleLed, onPing, height }) => {
16+
export const CarHeader: React.FC<HeaderProps> = ({
17+
connectedRobot,
18+
connectedRemote,
19+
isMock,
20+
ledState,
21+
onToggleLed,
22+
onPing,
23+
height
24+
}) => {
1625
const { t } = useTranslation();
1726
return (
1827
<Box
@@ -30,7 +39,7 @@ export const CarHeader: React.FC<HeaderProps> = ({ connectedRobot, connectedRemo
3039
<Typography variant="h4" className="tech-text neon-glow" sx={{ fontWeight: 800, color: 'var(--text-main)', lineHeight: 1 }}>
3140
{t('car.header.title')}<span style={{ color: 'var(--primary)' }}>CORE</span>
3241
</Typography>
33-
<ConnectInfo connectedRobot={connectedRobot} connectedRemote={connectedRemote} />
42+
<ConnectInfo connectedRobot={connectedRobot} connectedRemote={connectedRemote} isMock={isMock} />
3443
</Box>
3544

3645
<Box display="flex" gap={2}>
Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,45 @@
11
import { Box, Typography } from "@mui/material";
2-
import WifiIcon from '@mui/icons-material/Wifi';
3-
import SensorsOffIcon from '@mui/icons-material/SensorsOff';
42
import { useTranslation } from "react-i18next";
53

64
interface ConnectInfoProps {
75
connectedRobot: boolean;
86
connectedRemote: boolean;
7+
isMock?: boolean;
98
}
109

11-
export const ConnectInfo = ({ connectedRobot, connectedRemote }: ConnectInfoProps) => {
10+
export const ConnectInfo = ({ connectedRobot, connectedRemote, isMock }: ConnectInfoProps) => {
1211
const { t } = useTranslation();
1312

14-
const StatusBadge = ({ label, active }: { label: string, active: boolean }) => (
15-
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
16-
<Box
17-
className={active ? "status-pulse" : ""}
18-
sx={{
19-
width: 8,
20-
height: 8,
21-
borderRadius: "50%",
22-
backgroundColor: active ? 'var(--success)' : 'var(--error)',
23-
boxShadow: `0 0 8px ${active ? 'var(--success-glow)' : 'var(--error-glow)'}`,
24-
}}
25-
/>
26-
<Typography sx={{
27-
fontSize: '0.7rem',
28-
fontWeight: 700,
29-
color: active ? 'var(--text-main)' : 'var(--text-muted)'
30-
}}>
31-
{label}
32-
</Typography>
33-
</Box>
34-
);
13+
const StatusBadge = ({ label, active, simulated }: { label: string, active: boolean, simulated?: boolean }) => {
14+
// If simulated, it should look disconnected (Red/Offline) as per user request
15+
const isActuallyConnected = active && !simulated;
16+
17+
const color = isActuallyConnected ? 'var(--success)' : 'var(--error)';
18+
const glow = isActuallyConnected ? 'var(--success-glow)' : 'var(--error-glow)';
19+
const textLabel = isActuallyConnected ? label : "OFFLINE";
20+
21+
return (
22+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
23+
<Box
24+
className={isActuallyConnected ? "status-pulse" : ""}
25+
sx={{
26+
width: 8,
27+
height: 8,
28+
borderRadius: "50%",
29+
backgroundColor: color,
30+
boxShadow: `0 0 8px ${glow}`,
31+
}}
32+
/>
33+
<Typography sx={{
34+
fontSize: '0.7rem',
35+
fontWeight: 700,
36+
color: isActuallyConnected ? 'var(--text-main)' : 'var(--text-muted)'
37+
}}>
38+
{textLabel}
39+
</Typography>
40+
</Box>
41+
);
42+
};
3543

3644
return (
3745
<Box
@@ -46,9 +54,9 @@ export const ConnectInfo = ({ connectedRobot, connectedRemote }: ConnectInfoProp
4654
background: `rgba(0,0,0,0.2)`,
4755
}}
4856
>
49-
<StatusBadge label="ROBOT" active={connectedRobot} />
57+
<StatusBadge label="ROBOT" active={connectedRobot} simulated={isMock} />
5058
<Box sx={{ width: '1px', height: '12px', background: 'rgba(255,255,255,0.1)' }} />
51-
<StatusBadge label="REMOTE" active={connectedRemote} />
59+
<StatusBadge label="REMOTE" active={connectedRemote} simulated={isMock} />
5260
</Box>
5361
);
5462
};

client/src/pages/car/hooks/use-robot-control.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
1-
import { useState, useEffect, useCallback } from 'react';
1+
import { useState, useEffect, useCallback, useMemo } from 'react';
22
import { IDashboardState } from '@/pages/car/models/model';
33
import { robotService } from '@/services/robot.service';
44
import { directionWebRobot } from "@/config/api.config";
55
import { useRemoteControl } from '@/context/remote-control-context';
66
import { isSimulationMode } from '@/utils/simulation';
77

88
export const useRobotControl = () => {
9-
// Convert http/https to ws/wss
10-
const urlRobot = directionWebRobot.replace(/^http/, 'ws') + '/ws';
9+
// Convert http/https to ws/wss - memoized to prevent infinite loops
10+
const urlRobot = useMemo(() => directionWebRobot.replace(/^http/, 'ws') + '/ws', []);
1111

1212
// Use Global Remote Context
1313
const { remoteState, connectedRemote } = useRemoteControl();
1414

15-
console.log('[useRobotControl] URLs:', { urlRobot });
16-
1715
const [dashboardState, setDashboardState] = useState<IDashboardState>({
1816
robot: {
1917
ledState: false,
@@ -60,8 +58,8 @@ export const useRobotControl = () => {
6058
) => {
6159
// Mixed Content / HTTPS Detection
6260
if ((window.location.protocol === 'https:' && url.startsWith('ws:')) || isSimulationMode()) {
63-
console.warn(`[MockMode] Activation Request. Secure: ${window.location.protocol === 'https:'}, Simulation: ${isSimulationMode()}`);
64-
setIsMock(true);
61+
console.warn(`[MockMode] WebSocket Setup Blocked. Protocol: ${window.location.protocol}, Mock: ${isSimulationMode()}`);
62+
setConnectedRobot(true);
6563
return { cleanup: () => { } };
6664
}
6765

@@ -179,7 +177,7 @@ export const useRobotControl = () => {
179177
setWsRobot
180178
);
181179
return cleanup;
182-
}, [urlRobot, setupSocket]);
180+
}, [urlRobot, setupSocket, isMock]);
183181

184182
// Actions
185183
const handleColorChange = useCallback(async (newColor: string) => {

client/src/pages/drinks/hooks/use-drinks-page.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ export const useDrinksPage = () => {
5050

5151
// Reset ESP8266 state when entering the page
5252
useEffect(() => {
53+
if (isMock) {
54+
console.log("[useDrinksPage] Skipping ESP reset in simulation mode");
55+
return;
56+
}
5357
const resetEsp = async () => {
5458
try {
5559
await drinksService.sendControlCommand("cancel");
@@ -58,7 +62,7 @@ export const useDrinksPage = () => {
5862
}
5963
};
6064
resetEsp();
61-
}, []);
65+
}, [isMock]);
6266

6367
const handleTabChange = (tab: TabType) => {
6468
setActiveTab(tab);

client/src/pages/drinks/hooks/use-socket-sync.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useState, useEffect, useMemo } from "react";
22
import useWebSocket, { ReadyState } from 'react-use-websocket';
33
import { directionWebDrinks } from "@/config/api.config";
4+
import { isSimulationMode } from "@/utils/simulation";
45
import { Cocktail } from "@/pages/drinks/models/drinks-model";
56
import { useConnectivity } from "@/context/connectivity-context";
67

@@ -25,6 +26,13 @@ export const useSocketSync = ({
2526
const [socketUrl, setSocketUrl] = useState<string | null>(null);
2627

2728
useEffect(() => {
29+
// CRITICAL: Never attempt WebSocket connection in simulation mode
30+
if (isSimulationMode()) {
31+
console.log("[useSocketSync] Simulation mode detected, skipping WebSocket setup");
32+
setConnectionStatus('drinks', 'disconnected', 'Offline');
33+
return;
34+
}
35+
2836
if (!loading) {
2937
console.log("Starting WS Connection Sequence...");
3038
const timer = setTimeout(() => {
@@ -33,7 +41,6 @@ export const useSocketSync = ({
3341
setSocketUrl(url);
3442

3543
// Step 2: "Nudge" the ESP network stack to force event loop processing
36-
// We use a dedicated /drinks/ping endpoint to force the ESP event loop to cycle
3744
console.log("Step 2: Nudging ESP network stack (ping)...");
3845
[100, 400, 800].forEach(delay => {
3946
setTimeout(() => {
@@ -46,7 +53,7 @@ export const useSocketSync = ({
4653
} else {
4754
setSocketUrl(null);
4855
}
49-
}, [loading]);
56+
}, [loading, setConnectionStatus]);
5057

5158
const socketOptions = useMemo(() => ({
5259
shouldReconnect: () => true,

client/src/services/drinks.service.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
directionWebDrinks,
44
handleResponse,
55
} from "../config/api.config";
6-
import { isSimulationMode, activateReactiveSimulation } from "../utils/simulation";
6+
import { isSimulationMode, activateReactiveSimulation, withTimeout } from "../utils/simulation";
77
import { availableCocktails } from "../pages/drinks/data/cocktails.data";
88

99
const USE_MOCK = isSimulationMode();
@@ -19,7 +19,10 @@ export const drinksService = {
1919
return { success: true };
2020
}
2121
try {
22-
const response = await axios.get(`${directionWebDrinks}/drinks/navigation`, { params: { direction } });
22+
const response = await withTimeout(
23+
axios.get(`${directionWebDrinks}/drinks/navigation`, { params: { direction } }),
24+
2000
25+
);
2326
return handleResponse(response);
2427
} catch (error) {
2528
console.error("Drinks navigation error, triggering simulation fallback:", error);
@@ -39,7 +42,10 @@ export const drinksService = {
3942
}));
4043
}
4144
try {
42-
const response = await axios.get(`${directionWebDrinks}/drinks/cocktails`);
45+
const response = await withTimeout(
46+
axios.get(`${directionWebDrinks}/drinks/cocktails`),
47+
3000
48+
);
4349
return handleResponse(response);
4450
} catch (error) {
4551
console.error("Failed to fetch cocktails, triggering simulation mode:", error);
@@ -51,10 +57,13 @@ export const drinksService = {
5157
saveCocktail: async (name: string, ingredients: any[]): Promise<any> => {
5258
if (USE_MOCK) return { success: true };
5359
try {
54-
const response = await axios.post(`${directionWebDrinks}/drinks/save-cocktail`, {
55-
name,
56-
ingredients
57-
});
60+
const response = await withTimeout(
61+
axios.post(`${directionWebDrinks}/drinks/save-cocktail`, {
62+
name,
63+
ingredients
64+
}),
65+
5000
66+
);
5867
return handleResponse(response);
5968
} catch (error) {
6069
console.error("Failed to save cocktail, triggering simulation mode:", error);

0 commit comments

Comments
 (0)