Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion docs/CLIENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@ const [currentAppState, setCurrentAppState] = useState<IAppState>({
appUnits: APPUNITS.METRIC,
carLatency: 0,
connectionType: CONNECTIONTYPES.DEMO,
darkMode: false,
displayLoading: true,
error: false,
favourites: [],
Expand Down
4 changes: 2 additions & 2 deletions packages/client/src/components/atoms/BatteryIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { usePacket } from "@/contexts/PacketContext";
import { usePacketStore } from "@/stores/usePacket";

function BatteryIconComponent() {
const { currentPacket } = usePacket();
const { currentPacket } = usePacketStore();
const batteryLevel = currentPacket.Battery.PackStateOfCharge;

return (
Expand Down
4 changes: 2 additions & 2 deletions packages/client/src/components/atoms/SpeedAtom.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React from "react";

import { usePacket } from "@/contexts/PacketContext";
import useUnitsHandler from "@/hooks/PIS/useUnitsHandler";
import { UnitType } from "@/objects/PIS/PIS.interface";
import { usePacketStore } from "@/stores/usePacket";
import { calculateVehicleVelocity } from "@shared/helios-types";

function SpeedAtom() {
const { currentPacket } = usePacket();
const { currentPacket } = usePacketStore();

const speedValue = React.useMemo(
() =>
Expand Down
4 changes: 2 additions & 2 deletions packages/client/src/components/atoms/ThrottleIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { usePacket } from "@/contexts/PacketContext";
import { usePacketStore } from "@/stores/usePacket";

function ThrottleIcon() {
const { currentPacket } = usePacket();
const { currentPacket } = usePacketStore();
const gasPos = currentPacket.B3.Acceleration;
const regenPos = currentPacket.B3.RegenBraking;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useCallback } from "react";

import { useAppState } from "@/contexts/AppStateContext";
import usePIS from "@/hooks/PIS/usePIS";
import { useFavouriteLookupTable } from "@/hooks/favouriteLookupTable";
import type I_PIS from "@/objects/PIS/PIS.interface";
import { useAppState } from "@/stores/useAppState";

function BottomInformationContainer() {
const { currentAppState, setCurrentAppState } = useAppState();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useEffect, useState } from "react";
import type { PlotParams } from "react-plotly.js";

import useWindowDimensions from "@/hooks/PIS/useWindowDimensions";
import { useAppState } from "@/stores/useAppState";

import Plotly from "./Plotly";

Expand Down
6 changes: 3 additions & 3 deletions packages/client/src/components/containers/MapContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { type JSX, useEffect, useState } from "react";

import Map from "@/components/molecules/MapMolecules/Map";
import MapText from "@/components/molecules/MapMolecules/MapText";
import { useAppState } from "@/contexts/AppStateContext";
import { usePacket } from "@/contexts/PacketContext";
import { useAppState } from "@/stores/useAppState";
import { usePacketStore } from "@/stores/usePacket";
import { Coords } from "@shared/helios-types";

import { GEO_DATA } from "../molecules/MapMolecules/MapSetup";
Expand All @@ -18,7 +18,7 @@ const startingLocation: Coords = {
};
function MapContainer(): JSX.Element {
const { currentAppState } = useAppState();
const { currentPacket } = usePacket();
const { currentPacket } = usePacketStore();

const isDemo = currentAppState.connectionType === "DEMO";
const [carLocation, setCarLocation] = useState(
Expand Down
119 changes: 119 additions & 0 deletions packages/client/src/components/global/AppStateEffectsManager.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"use client";

import { useCallback, useEffect } from "react";

import { CONNECTIONTYPES, useAppState } from "@/stores/useAppState";

export default function AppStateEffects() {
const { currentAppState, setCurrentAppState } = useAppState();

useEffect(() => {
if (currentAppState.connectionType === CONNECTIONTYPES.DEMO) {
if (currentAppState.socketConnected) {
setCurrentAppState((prev) => ({
...prev,
connectionType: CONNECTIONTYPES.NETWORK,
loading: false,
}));
}
if (currentAppState.radioConnected) {
setCurrentAppState((prev) => ({
...prev,
connectionType: CONNECTIONTYPES.RADIO,
loading: false,
}));
}
}
if (currentAppState.connectionType === CONNECTIONTYPES.NETWORK) {
setCurrentAppState((prev) => ({
...prev,
loading: !currentAppState.socketConnected,
}));
}
if (currentAppState.connectionType === CONNECTIONTYPES.RADIO) {
setCurrentAppState((prev) => ({
...prev,
loading: !currentAppState.radioConnected,
}));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
currentAppState.socketConnected,
currentAppState.radioConnected,
setCurrentAppState,
]);

useEffect(() => {
setTimeout(() => {
setCurrentAppState((prev) => ({
...prev,
loading: false,
}));
}, 5000);
}, [setCurrentAppState]);
Comment on lines +46 to +53
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Unconditionally clearing loading masks real connection failures.

This effect always flips loading to false after five seconds, even if socketConnected/radioConnected are still false. As soon as a connection stalls, the spinner disappears forever and currentAppState.loading reports a success that never happened. Guard the timeout so it only clears loading after a successful connection (or cancel it when connectivity is still down), and ensure you clear the timer on unmount.

🤖 Prompt for AI Agents
In packages/client/src/components/global/AppStateEffectsManager.tsx around lines
46 to 53, the effect unconditionally clears loading after 5s which masks real
connection failures; change it to start the timeout only when a successful
connection exists (e.g. socketConnected || radioConnected), include
socketConnected and radioConnected in the dependency array, store the timer id
in a ref/local variable and clearTimeout in the effect cleanup so the timer is
canceled on unmount or when connectivity changes, and ensure you only call
setCurrentAppState to set loading: false if the connectivity check still passes
when the timer fires.


const fetchSettingsFromLocalStorage = useCallback(() => {
const savedSettings = localStorage.getItem("settings");
const favourites = localStorage.getItem("favourites");

if (savedSettings) {
const parsedSettings = JSON.parse(savedSettings);

const parsedFavourites = favourites
? (JSON.parse(favourites) as string[])
: [
"Motor Temp",
"Battery Cell Voltage",
"Vehicle Velocity",
"Pack Voltage",
"Pack Current",
"Battery Average Voltage",
];

const hasPlaybackDateTime = !!parsedSettings.playbackDateTime;

const parsedPlaybackDateTime = hasPlaybackDateTime
? {
date: parsedSettings.playbackDateTime!.date
? new Date(parsedSettings.playbackDateTime!.date)
: null,
endTime: parsedSettings.playbackDateTime!.endTime
? new Date(parsedSettings.playbackDateTime!.endTime)
: null,
startTime: parsedSettings.playbackDateTime!.startTime
? new Date(parsedSettings.playbackDateTime!.startTime)
: null,
}
: {
date: null,
endTime: null,
startTime: null,
};

setCurrentAppState((prev) => ({
...prev,
appUnits: parsedSettings.appUnits ?? prev.appUnits,
connectionType: parsedSettings.connectionType ?? prev.connectionType,
favourites: parsedFavourites,
lapCoords: parsedSettings.lapCoords ?? prev.lapCoords,
playbackDateTime: parsedPlaybackDateTime,
}));
}
}, [setCurrentAppState]);

const saveSettingsToLocalStorage = useCallback(() => {
localStorage.setItem("settings", JSON.stringify(currentAppState));
}, [currentAppState]);

useEffect(() => {
fetchSettingsFromLocalStorage();
}, [fetchSettingsFromLocalStorage]);

useEffect(() => {
if (!currentAppState.loading) {
saveSettingsToLocalStorage();
}
}, [currentAppState.loading, saveSettingsToLocalStorage]);

return null; // This component only handles side effects
}
15 changes: 15 additions & 0 deletions packages/client/src/components/global/EffectsProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import AppStateEffectsManager from "@/components/global/AppStateEffectsManager";
import { LapListenerManager } from "@/components/global/LapDataListenerManager";
import { PacketListenerManager } from "@/components/global/PacketListenerManager";
import SocketManager from "@/components/global/SocketManager";

export function EffectsProvider() {
return (
<>
<LapListenerManager />
<PacketListenerManager />
<AppStateEffectsManager />
<SocketManager />
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { useEffect } from "react";

import { socketIO } from "@/components/global/SocketManager";
import { CONNECTIONTYPES, useAppState } from "@/stores/useAppState";
import { formatLapData, useLapDataStore } from "@/stores/useLapData";
import { notifications } from "@mantine/notifications";
import type { ILapData } from "@shared/helios-types";

export function LapListenerManager(): React.ReactElement | null {
const { currentAppState } = useAppState();
const { addLapData, clearLapData, fetchLapData } = useLapDataStore();

// Fetch initial lap data when manager mounts
useEffect(() => {
fetchLapData();
}, [fetchLapData]);

// Handle connection type changes
useEffect(() => {
// Playback mode: no listeners, clear data
if (currentAppState.playbackSwitch) {
clearLapData();
return;
}

// Network mode: attach socket listeners
if (currentAppState.connectionType === CONNECTIONTYPES.NETWORK) {
const handleLapData = (lapPacket: ILapData) => {
const formattedData = formatLapData(lapPacket);
addLapData(formattedData);
};

const handleLapComplete = () => {
notifications.show({
color: "green",
message: "A lap has been completed!",
title: "Lap Completion",
});
};

socketIO.on("lapData", handleLapData);
socketIO.on("lapComplete", handleLapComplete);

return () => {
socketIO.off("lapData", handleLapData);
socketIO.off("lapComplete", handleLapComplete);
};
}

// Demo mode: clear existing lap data
else if (currentAppState.connectionType === CONNECTIONTYPES.DEMO) {
clearLapData();
}

// Radio mode: TODO
else if (currentAppState.connectionType === CONNECTIONTYPES.RADIO) {
// TODO: handle radio
}
}, [
currentAppState.connectionType,
currentAppState.playbackSwitch,
addLapData,
clearLapData,
]);

return null;
}
2 changes: 1 addition & 1 deletion packages/client/src/components/global/Loading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { memo, useMemo } from "react";
import { twMerge } from "tailwind-merge";

import { LOADINGSTAGES } from "@/components/global/LoadingWrapper";
import { useAppState } from "@/contexts/AppStateContext";
import { useAppState } from "@/stores/useAppState";

const Loading = (props: { currentLoadingState: LOADINGSTAGES }) => {
const { currentAppState } = useAppState();
Expand Down
Loading