Merged in bugfix/minor-issues-5 (pull request #21)

Bugfix/minor issues 5
This commit is contained in:
2025-10-15 12:56:03 +00:00
23 changed files with 254 additions and 344 deletions

3
.prettierrc Normal file
View File

@@ -0,0 +1,3 @@
{
"printWidth": 120
}

View File

@@ -1,19 +0,0 @@
TODO:
Hotlist upload (Question for Dion about API) and hits popping up in sighting stack.
NPED API working and catagories popping up in sighting stack. Images added to public folder.
Make the friendly name of each camera permeate throughout.
Make favicon MAV logo.
Swipe down to get to session page.
I have made an error I don't know how to fix in SightingFeedProvider.tsx
There is a bug in /front-camera-settings where the navigation arrow doesn't have a transparent background. I don't know why it is only that one and I can't find out why. Very strange.
The selected sighting in the sighting stack seems a tad buggy. Sometimes multiple get selected.
Can the selected sighting be shown in full detail. How this will look is still up for debate. Either as a pop up card as in AiQ Flexi, or in the OVerview card??
How do you know if the time has sync? Make UTC red if not sync.
Can the relative aspect ratio in SightingOverview.tsx be the ratio of image pixel size of the image to best take advantage of the space?
obscure details on dashboard to a toggle
FYI:
Session, WiFi and Modem stuff isn't implimented in the backend. Those are just placeholders for now.

View File

@@ -1,10 +1,5 @@
import { Formik, Field, Form } from "formik";
import type {
CameraConfig,
CameraSettingErrorValues,
CameraSettingValues,
ZoomInOptions,
} from "../../types/types";
import type { CameraConfig, CameraSettingErrorValues, CameraSettingValues, ZoomInOptions } from "../../types/types";
import { useEffect, useMemo, useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faEye, faEyeSlash } from "@fortawesome/free-regular-svg-icons";
@@ -28,8 +23,7 @@ const CameraSettingFields = ({
updateCameraConfigError,
}: CameraSettingsProps) => {
const [showPwd, setShowPwd] = useState(false);
const cameraControllerSide =
initialData?.id === "CameraA" ? "CameraControllerA" : "CameraControllerB";
const cameraControllerSide = initialData?.id === "CameraA" ? "CameraControllerA" : "CameraControllerB";
const { mutation, query } = useCameraZoom({ camera: cameraControllerSide });
const zoomOptions = [1, 2, 4, 8];
@@ -109,9 +103,7 @@ const CameraSettingFields = ({
<div className="flex flex-col space-y-2 relative">
<label htmlFor="friendlyName">Name</label>
{touched.friendlyName && errors.friendlyName && (
<small className="absolute right-0 top-0 text-red-500">
{errors.friendlyName}
</small>
<small className="absolute right-0 top-0 text-red-500">{errors.friendlyName}</small>
)}
<Field
id="friendlyName"
@@ -126,9 +118,7 @@ const CameraSettingFields = ({
<div className="flex flex-col space-y-2 relative">
<label htmlFor="cameraAddress">Camera Address</label>
{touched.cameraAddress && errors.cameraAddress && (
<small className="absolute right-0 top-0 text-red-500">
{errors.cameraAddress}
</small>
<small className="absolute right-0 top-0 text-red-500">{errors.cameraAddress}</small>
)}
<Field
id="cameraAddress"
@@ -143,9 +133,7 @@ const CameraSettingFields = ({
<div className="flex flex-col space-y-2 relative">
<label htmlFor="userName">User Name</label>
{touched.userName && errors.userName && (
<small className="absolute right-0 top-0 text-red-500">
{errors.userName}
</small>
<small className="absolute right-0 top-0 text-red-500">{errors.userName}</small>
)}
<Field
id="userName"
@@ -161,9 +149,7 @@ const CameraSettingFields = ({
<div className="flex flex-col space-y-2 relative">
<label htmlFor="password">Password</label>
{touched.password && errors.password && (
<small className="absolute right-0 top-0 text-red-500">
{errors.password}
</small>
<small className="absolute right-0 top-0 text-red-500">{errors.password}</small>
)}
<div className="flex gap-2 items-center relative mb-4">
<Field
@@ -209,10 +195,7 @@ const CameraSettingFields = ({
</div>
<div className="mt-3">
{updateCameraConfigError ? (
<button
className="bg-red-500 text-white rounded-lg p-2 mx-auto h-[100%] w-full"
disabled
>
<button className="bg-red-500 text-white rounded-lg p-2 mx-auto h-[100%] w-full" disabled>
Retry
</button>
) : (

View File

@@ -15,8 +15,7 @@ const CameraSettings = ({
zoomLevel?: number;
onZoomLevelChange?: (level: number) => void;
}) => {
const { data, updateCameraConfig, updateCameraConfigError } =
useFetchCameraConfig(side);
const { data, updateCameraConfig, updateCameraConfigError } = useFetchCameraConfig(side);
return (
<Card className="overflow-hidden min-h-[40vh] md:min-h-[60vh] max-h-[80vh] lg:w-[40%] p-4">

View File

@@ -43,9 +43,7 @@ const AlertItem = ({ item }: AlertItemProps) => {
path: "alertHistory",
});
const oldArray = res?.result;
const updatedArray = oldArray?.filter(
(item: SightingType) => item?.ref !== deletedItem?.ref
);
const updatedArray = oldArray?.filter((item: SightingType) => item?.ref !== deletedItem?.ref);
mutation.mutate({
operation: "INSERT",
@@ -58,45 +56,14 @@ const AlertItem = ({ item }: AlertItemProps) => {
<div className="flex flex-col w-full">
<div className="border border-gray-600 rounded-lg items-center py-1">
<InfoBar obj={item} />
<div
className=" flex flex-row p-4 w-full mx-auto justify-between"
onClick={handleClick}
>
{isHotListHit && (
<img
src={HotListImg}
alt="hotlistHit"
className="h-20 object-contain rounded-md"
/>
)}
{isNPEDHitA && (
<img
src={NPED_CAT_A}
alt="NPEDHITicon"
className="h-20 object-contain rounded-md"
/>
)}
{isNPEDHitB && (
<img
src={NPED_CAT_B}
alt="NPEDHITicon"
className="h-20 object-contain rounded-md"
/>
)}
{isNPEDHitC && (
<img
src={NPED_CAT_C}
alt="NPEDHITicon"
className="h-20 object-contain rounded-md"
/>
)}
<div className="flex flex-col">
<small>MAKE: {item.make}</small>
<small>MODEL: {item.model}</small>
<small>COLOUR: {item.color}</small>
<div className="flex flex-row p-4 w-full mx-auto justify-between" onClick={handleClick}>
{isHotListHit && <img src={HotListImg} alt="hotlistHit" className="h-20 object-contain rounded-md" />}
{isNPEDHitA && <img src={NPED_CAT_A} alt="NPEDHITicon" className="h-20 object-contain rounded-md" />}
{isNPEDHitB && <img src={NPED_CAT_B} alt="NPEDHITicon" className="h-20 object-contain rounded-md" />}
{isNPEDHitC && <img src={NPED_CAT_C} alt="NPEDHITicon" className="h-20 object-contain rounded-md" />}
<div className={`border p-1 hidden md:block`}>
<img src={item?.plateUrlColour} height={48} width={200} alt="colour patch" />
</div>
<NumberPlate vrm={item.vrm} motion={motionAway} />
</div>
<SightingModal

View File

@@ -120,24 +120,27 @@ const ChannelFields = () => {
</FormGroup>
<FormGroup>
<label htmlFor="password">Password</label>
<Field
name={"password"}
type={showPwd ? "text" : "password"}
id="password"
placeholder="Back office password"
className={`p-1.5 border ${
errors.password && touched.password
? "border-red-500"
: "border-gray-400 "
} rounded-lg w-full md:w-60`}
/>
<FontAwesomeIcon
type="button"
className="absolute right-5 end-0"
onClick={() => setShowPwd((s) => !s)}
icon={showPwd ? faEyeSlash : faEye}
/>
<div className="flex gap-2 items-center relative mb-4">
<Field
name={"password"}
type={showPwd ? "text" : "password"}
id="password"
placeholder="Back office password"
className={`p-1.5 border ${
errors.password && touched.password
? "border-red-500"
: "border-gray-400 "
} rounded-lg w-full md:w-60`}
/>
<FontAwesomeIcon
type="button"
className="absolute right-5 end-0"
onClick={() => setShowPwd((s) => !s)}
icon={showPwd ? faEyeSlash : faEye}
/>
</div>
</FormGroup>
<FormGroup>
<label htmlFor="connectTimeoutSeconds">
Connect Timeout Seconds

View File

@@ -73,24 +73,26 @@ const NPEDFields = () => {
</FormGroup>
<FormGroup>
<label htmlFor="password">Password</label>
{touched.password && errors.password && (
<small className="absolute right-0 -top-5 text-red-500">
{errors.password}
</small>
)}
<Field
name="password"
type={showPwd ? "text" : "password"}
id="password"
placeholder="NPED Password"
className="p-1.5 border border-gray-400 rounded-lg"
/>
<FontAwesomeIcon
type="button"
className="absolute right-5 end-0"
onClick={() => setShowPwd((s) => !s)}
icon={showPwd ? faEyeSlash : faEye}
/>
<div className="flex gap-2 items-center relative mb-4">
<Field
name="password"
type={showPwd ? "text" : "password"}
id="password"
placeholder="NPED Password"
className="p-2 border border-gray-400 rounded-lg w-full"
/>
{touched.password && errors.password && (
<small className="absolute right-0 -top-5 text-red-500">
{errors.password}
</small>
)}
<FontAwesomeIcon
type="button"
className="absolute right-5 end-0"
onClick={() => setShowPwd((s) => !s)}
icon={showPwd ? faEyeSlash : faEye}
/>
</div>
</FormGroup>
<FormGroup>
<label htmlFor="clientId">Client ID</label>

View File

@@ -4,9 +4,12 @@ import type { ModemSettingsType } from "../../../types/types";
import { useWifiAndModem } from "../../../hooks/useCameraWifiandModem";
import { useEffect, useState } from "react";
import ModemToggle from "./ModemToggle";
import { faEyeSlash, faEye } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
const ModemSettings = () => {
const [showSettings, setShowSettings] = useState(false);
const [showPwd, setShowPwd] = useState(false);
const { modemQuery, modemMutation } = useWifiAndModem();
const apn = modemQuery?.data?.propAPN?.value;
@@ -102,13 +105,21 @@ const ModemSettings = () => {
>
Password
</label>
<Field
placeholder="Enter Password"
name="password"
id="password"
type="text"
className="p-1.5 border border-gray-400 rounded-lg"
/>
<div className="flex gap-2 items-center relative mb-4">
<Field
id="password"
name="password"
type={showPwd ? "text" : "password"}
className="p-2 border border-gray-400 rounded-lg w-full"
placeholder="Enter Password"
/>
<FontAwesomeIcon
type="button"
className="absolute right-5 end-0"
onClick={() => setShowPwd((s) => !s)}
icon={showPwd ? faEyeSlash : faEye}
/>
</div>
</FormGroup>
<FormGroup>
<label

View File

@@ -66,19 +66,21 @@ const WiFiSettingsForm = () => {
>
Password
</label>
<Field
id="password"
name="password"
type={showPwd ? "text" : "password"}
className="p-1.5 border border-gray-400 rounded-lg"
placeholder="Enter Password"
/>
<FontAwesomeIcon
type="button"
className="absolute right-5 end-0"
onClick={() => setShowPwd((s) => !s)}
icon={showPwd ? faEyeSlash : faEye}
/>
<div className="flex gap-2 items-center relative mb-4">
<Field
id="password"
name="password"
type={showPwd ? "text" : "password"}
className="p-2 border border-gray-400 rounded-lg w-full"
placeholder="Enter Password"
/>
<FontAwesomeIcon
type="button"
className="absolute right-5 end-0"
onClick={() => setShowPwd((s) => !s)}
icon={showPwd ? faEyeSlash : faEye}
/>
</div>
</FormGroup>
<FormGroup>
<label

View File

@@ -6,7 +6,7 @@ type FormGroupProps = {
const FormGroup = ({ children }: FormGroupProps) => {
return (
<div className="flex flex-col md:flex-row items-center justify-between relative">
<div className="flex flex-col md:flex-row md:items-center justify-between relative">
{children}
</div>
);

View File

@@ -10,7 +10,7 @@ import HotListImg from "/Hotlist_Hit.svg";
import NPED_CAT_A from "/NPED_Cat_A.svg";
import NPED_CAT_B from "/NPED_Cat_B.svg";
import NPED_CAT_C from "/NPED_Cat_C.svg";
import { checkIsHotListHit, getNPEDCategory } from "../../utils/utils";
import { checkIsHotListHit, getHotlistName, getNPEDCategory } from "../../utils/utils";
type SightingModalProps = {
isSightingModalOpen: boolean;
@@ -19,15 +19,12 @@ type SightingModalProps = {
onDelete?: (deletedItem: SightingType | null) => void;
};
const SightingModal = ({
isSightingModalOpen,
handleClose,
sighting,
onDelete,
}: SightingModalProps) => {
const SightingModal = ({ isSightingModalOpen, handleClose, sighting, onDelete }: SightingModalProps) => {
const { dispatch } = useAlertHitContext();
const { query, mutation } = useCameraBlackboard();
const hotlistName = getHotlistName(sighting?.metadata?.hotlistMatches);
const handleAcknowledgeButton = () => {
try {
if (!sighting) {
@@ -78,9 +75,7 @@ const SightingModal = ({
<ModalComponent isModalOpen={isSightingModalOpen} close={handleClose}>
<div className="max-w-screen-lg mx-auto py-4 px-2">
<div className="border-b border-gray-600 mb-4">
<h2 className="text-lg md:text-xl font-semibold">
Sighting Details
</h2>
<h2 className="text-lg md:text-xl font-semibold">Sighting Details</h2>
</div>
<div className="mt-3 flex-col-reverse gap-3 md:flex-row md:justify-center flex md:hidden">
{onDelete ? (
@@ -121,41 +116,19 @@ const SightingModal = ({
<div className="flex flex-col md:flex-row gap-3 items-center mb-2 justify-between">
<div className="flex flex-col md:flex-row gap-3 items-center">
<NumberPlate vrm={sighting?.vrm} motion={motionAway} />
<img
src={sighting?.plateUrlColour}
alt="plate patch"
className="h-16 object-contain rounded-md"
/>
<img src={sighting?.plateUrlColour} alt="plate patch" className="h-16 object-contain rounded-md" />
{hotlistName && (
<div>
<p className="text-gray-300">Hotlist</p>
<p className="font-medium text-2xl break-all">{hotlistName ? hotlistName[0] : "-"}</p>
</div>
)}
</div>
{isHotListHit && (
<img
src={HotListImg}
alt="hotlistHit"
className="h-20 object-contain rounded-md"
/>
)}
{isNPEDHitA && (
<img
src={NPED_CAT_A}
alt="hotlistHit"
className="h-20 object-contain rounded-md"
/>
)}
{isNPEDHitB && (
<img
src={NPED_CAT_B}
alt="hotlistHit"
className="h-20 object-contain rounded-md"
/>
)}
{isNPEDHitC && (
<img
src={NPED_CAT_C}
alt="hotlistHit"
className="h-20 object-contain rounded-md"
/>
)}
{isHotListHit && <img src={HotListImg} alt="hotlistHit" className="h-20 object-contain rounded-md" />}
{isNPEDHitA && <img src={NPED_CAT_A} alt="hotlistHit" className="h-20 object-contain rounded-md" />}
{isNPEDHitB && <img src={NPED_CAT_B} alt="hotlistHit" className="h-20 object-contain rounded-md" />}
{isNPEDHitC && <img src={NPED_CAT_C} alt="hotlistHit" className="h-20 object-contain rounded-md" />}
</div>
<div className="flex flex-col lg:flex-row items-center gap-3">
<img
@@ -164,52 +137,37 @@ const SightingModal = ({
className="w-full h-56 sm:h-72 md:h-96 rounded-lg object-cover border border-gray-700"
/>
<aside className="w-full lg:w-80 bg-gray-800/70 text-white rounded-xl py-4 px-2 border h-[70%] border-gray-700">
<h3 className="text-base md:text-lg font-semibold pb-2 border-b border-gray-700">
Vehicle Info
</h3>
<h3 className="text-base md:text-lg font-semibold pb-2 border-b border-gray-700">Vehicle Info</h3>
<dl className="mt-3 gap-x-4 gap-y-2 text-sm">
<div>
<dt className="text-gray-300">VRM</dt>
<dd className="font-medium text-2xl break-all">
{sighting?.vrm ?? "-"}
</dd>
<dd className="font-medium text-2xl break-all">{sighting?.vrm ?? "-"}</dd>
</div>
<div>
<dt className="text-gray-300">Motion</dt>
<dd className="font-medium text-2xl">
{sighting?.motion ?? "-"}
</dd>
<dd className="font-medium text-2xl">{sighting?.motion ?? "-"}</dd>
</div>
<div>
<dt className="text-gray-300">Seen Count</dt>
<dd className="font-medium text-2xl">
{sighting?.seenCount ?? "-"}
</dd>
<dd className="font-medium text-2xl">{sighting?.seenCount ?? "-"}</dd>
</div>
<div>
<dt className="text-gray-300">Make</dt>
<dd className="font-medium text-2xl">
{sighting?.make ?? "-"}
</dd>
<dd className="font-medium text-2xl">{sighting?.make ?? "-"}</dd>
</div>
<div>
<dt className="text-gray-300">Model</dt>
<dd className="font-medium text-2xl">
{sighting?.model ?? "-"}
</dd>
<dd className="font-medium text-2xl">{sighting?.model ?? "-"}</dd>
</div>
<div className="sm:col-span-2">
<dt className="text-gray-300">Colour</dt>
<dd className="font-medium text-2xl">
{sighting?.color ?? "-"}
</dd>
<dd className="font-medium text-2xl">{sighting?.color ?? "-"}</dd>
</div>
<div>
<dt className="text-gray-300">Time</dt>
<dd className="font-medium text-xl">
{sighting?.timeStamp ?? "-"}
</dd>
<dd className="font-medium text-xl">{sighting?.timeStamp ?? "-"}</dd>
</div>
</dl>
</aside>

View File

@@ -32,8 +32,8 @@ export default function Header() {
};
return (
<div className="relative bg-[#253445] border-b border-gray-500 items-center mx-auto px-2 sm:px-6 lg:px-8 p-4 flex flex-col md:flex-row justify-between mb-7 space-y-6 md:space-y-0">
<div className="w-30">
<div className="relative bg-[#253445] border-b border-gray-500 items-center mx-auto sm:px-3 lg:px-4 py-4 flex flex-col md:flex-row justify-between mb-7 space-y-6 md:space-y-0">
<div className="w-28">
<Link to={"/"}>
<img src={Logo} alt="Logo" width={150} height={150} />
</Link>

View File

@@ -11,13 +11,14 @@ const NavigationArrow = ({ side, settingsPage }: NavigationArrowProps) => {
const navigate = useNavigate();
const navigationDest = (side: string | undefined) => {
console.log(side);
if (settingsPage) {
navigate("/");
return;
}
if (side === "Front") {
navigate("/front-camera-settings");
navigate("/camera-settings");
} else if (side === "Rear") {
navigate("/Rear-Camera-settings");
}
@@ -28,13 +29,15 @@ const NavigationArrow = ({ side, settingsPage }: NavigationArrowProps) => {
<>
{side === "CameraA" ? (
<FontAwesomeIcon
size="2xl"
icon={faArrowRight}
className="absolute top-[50%] right-[2%] backdrop-blur-lg hover:cursor-pointer animate-bounce z-30"
onClick={() => navigationDest(side)}
onClick={() => navigationDest("Front")}
/>
) : (
<FontAwesomeIcon
icon={faArrowLeft}
size="2xl"
className="absolute top-[50%] left-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce z-30"
onClick={() => navigationDest(side)}
/>
@@ -46,14 +49,16 @@ const NavigationArrow = ({ side, settingsPage }: NavigationArrowProps) => {
<>
<FontAwesomeIcon
icon={faArrowLeft}
className="absolute top-[50%] left-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce z-30"
onClick={() => navigationDest(side)}
size="2xl"
className="absolute top-[50%] left-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce z-100 "
onClick={() => navigationDest("Front")}
/>
<FontAwesomeIcon
icon={faArrowRight}
className="absolute top-[50%] right-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce z-30"
onClick={() => navigationDest(side)}
size="2xl"
className="absolute top-[50%] right-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce z-100"
onClick={() => navigationDest("Rear")}
/>
</>
);

View File

@@ -4,6 +4,7 @@ import type { SoundAction, SoundState } from "../types/types";
type SoundContextType = {
state: SoundState;
dispatch: Dispatch<SoundAction>;
audioArmed: boolean;
};
export const SoundContext = createContext<SoundContextType | undefined>(

View File

@@ -1,4 +1,11 @@
import { useEffect, useMemo, useReducer, type ReactNode } from "react";
import {
useEffect,
useMemo,
useReducer,
useRef,
useState,
type ReactNode,
} from "react";
import { SoundContext } from "../SoundContext";
import { initialState, reducer } from "../reducers/SoundContextReducer";
import { useCameraBlackboard } from "../../hooks/useCameraBlackboard";
@@ -8,6 +15,9 @@ type SoundContextProviderProps = {
};
const SoundContextProvider = ({ children }: SoundContextProviderProps) => {
const audioReady = useRef(false);
const [audioArmed, setAudioArmed] = useState(false);
const audioCtxRef = useRef<AudioContext | null>(null);
const [state, dispatch] = useReducer(reducer, initialState);
const { mutation } = useCameraBlackboard();
@@ -23,7 +33,40 @@ const SoundContextProvider = ({ children }: SoundContextProviderProps) => {
fetchSound();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const value = useMemo(() => ({ state, dispatch }), [state, dispatch]);
useEffect(() => {
const unlock = async () => {
if (!audioCtxRef.current) audioCtxRef.current = new AudioContext();
if (audioCtxRef.current.state !== "running") {
try {
await audioCtxRef.current.resume();
} catch {
/* empty */
}
}
const armed = audioCtxRef.current.state === "running";
audioReady.current = audioCtxRef.current.state === "running";
setAudioArmed(armed);
if (audioReady.current) {
window.removeEventListener("pointerdown", unlock);
window.removeEventListener("keydown", unlock);
window.removeEventListener("touchstart", unlock);
}
};
window.addEventListener("pointerdown", unlock, { once: false });
window.addEventListener("keydown", unlock, { once: false });
window.addEventListener("touchstart", unlock, { once: false });
return () => {
window.removeEventListener("pointerdown", unlock);
window.removeEventListener("keydown", unlock);
window.removeEventListener("touchstart", unlock);
};
}, []);
const value = useMemo(
() => ({ state, dispatch, audioArmed }),
[state, audioArmed]
);
return (
<SoundContext.Provider value={value}>{children}</SoundContext.Provider>
);

View File

@@ -50,8 +50,7 @@ export const useCameraBlackboard = () => {
});
useEffect(() => {
if (query.isError)
toast.error(query.error.message, { id: "viewBlackboardData" });
if (query.isError) toast.error(query.error.message, { id: "viewBlackboardData" });
}, [query?.error?.message, query.isError]);
return { query, mutation };

View File

@@ -14,11 +14,7 @@ const fetchCameraSideConfig = async ({ queryKey }: { queryKey: string[] }) => {
return response.json();
};
const updateCamerasideConfig = async (data: {
id: string | number;
friendlyName: string;
cameraAddress: string;
}) => {
const updateCamerasideConfig = async (data: { id: string | number; friendlyName: string; cameraAddress: string }) => {
const updateUrl = `${base_url}/update-config?id=${data.id}`;
const updateConfigPayload = {
@@ -30,7 +26,7 @@ const updateCamerasideConfig = async (data: {
},
],
};
console.log(updateConfigPayload);
const response = await fetch(updateUrl, {
method: "POST",
body: JSON.stringify(updateConfigPayload),

View File

@@ -34,9 +34,7 @@ const updateDispatcherConfig = async (data: BearerTypeFieldType) => {
};
const getBackOfficeConfig = async () => {
const response = await fetch(
`${CAM_BASE}/api/fetch-config?id=Dispatcher-json`
);
const response = await fetch(`${CAM_BASE}/api/fetch-config?id=Dispatcher-json`);
if (!response.ok) throw new Error("Cannot get Back Office configuration");
return response.json();
};
@@ -67,13 +65,10 @@ const updateBackOfficeConfig = async (data: InitialValuesForm) => {
},
],
};
const response = await fetch(
`${CAM_BASE}/api/update-config?id=Dispatcher-json`,
{
method: "POST",
body: JSON.stringify(updateConfigPayload),
}
);
const response = await fetch(`${CAM_BASE}/api/update-config?id=Dispatcher-json`, {
method: "POST",
body: JSON.stringify(updateConfigPayload),
});
if (!response.ok) throw new Error("Cannot update Back Office configuration");
return response.json();
};

View File

@@ -5,12 +5,9 @@ import { useEffect } from "react";
import { toast } from "sonner";
const getWiFiSettings = async () => {
const response = await fetch(
`${CAM_BASE}/api/fetch-config?id=ModemAndWifiManager-wifi`,
{
signal: AbortSignal.timeout(500),
}
);
const response = await fetch(`${CAM_BASE}/api/fetch-config?id=ModemAndWifiManager-wifi`, {
signal: AbortSignal.timeout(500),
});
if (!response.ok) {
throw new Error("Cannot fetch Wifi settings");
}
@@ -18,14 +15,11 @@ const getWiFiSettings = async () => {
};
const updateWifiSettings = async (wifiConfig: WifiConfig) => {
const response = await fetch(
`${CAM_BASE}/api/update-config?id=ModemAndWifiManager-wifi`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(wifiConfig),
}
);
const response = await fetch(`${CAM_BASE}/api/update-config?id=ModemAndWifiManager-wifi`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(wifiConfig),
});
if (!response.ok) {
throw new Error("Cannot update wifi settings");
}
@@ -33,12 +27,9 @@ const updateWifiSettings = async (wifiConfig: WifiConfig) => {
};
const getModemSettings = async () => {
const response = await fetch(
`${CAM_BASE}/api/fetch-config?id=ModemAndWifiManager-modem`,
{
signal: AbortSignal.timeout(500),
}
);
const response = await fetch(`${CAM_BASE}/api/fetch-config?id=ModemAndWifiManager-modem`, {
signal: AbortSignal.timeout(500),
});
if (!response.ok) {
throw new Error("Cannot fetch modem settings");
}
@@ -46,14 +37,11 @@ const getModemSettings = async () => {
};
const updateModemSettings = async (modemConfig: ModemConfig) => {
const response = await fetch(
`${CAM_BASE}/api/update-config?id=ModemAndWifiManager-modem`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(modemConfig),
}
);
const response = await fetch(`${CAM_BASE}/api/update-config?id=ModemAndWifiManager-modem`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(modemConfig),
});
if (!response.ok) {
throw new Error("cannot update modem settings");
}
@@ -100,13 +88,11 @@ export const useWifiAndModem = () => {
});
useEffect(() => {
if (wifiQuery.isError)
toast.error("Cannot get WiFi settings", { id: "wiFiSettings" });
if (wifiQuery.isError) toast.error("Cannot get WiFi settings", { id: "wiFiSettings" });
}, [wifiQuery?.error?.message, wifiQuery.isError]);
useEffect(() => {
if (modemQuery.isError)
toast.error("Cannot get Modem settings", { id: "modemSettings" });
if (modemQuery.isError) toast.error("Cannot get Modem settings", { id: "modemSettings" });
}, [modemQuery?.error?.message, modemQuery.isError]);
return {
wifiQuery,

View File

@@ -1,15 +1,12 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { Query, useQuery } from "@tanstack/react-query";
import type { SightingType } from "../types/types";
import { useSoundOnChange } from "react-sounds";
import { useSoundContext } from "../context/SoundContext";
import { getSoundFileURL } from "../utils/utils";
import switchSound from "../assets/sounds/ui/switch.mp3";
async function fetchSighting(
url: string | undefined,
ref: number
): Promise<SightingType> {
async function fetchSighting(url: string | undefined, ref: number): Promise<SightingType> {
const res = await fetch(`${url}${ref}`, {
signal: AbortSignal.timeout(5000),
});
@@ -18,86 +15,77 @@ async function fetchSighting(
}
export function useSightingFeed(url: string | undefined) {
const { state } = useSoundContext();
const { state, audioArmed } = useSoundContext();
const [sightings, setSightings] = useState<SightingType[]>([]);
const [selectedRef, setSelectedRef] = useState<number | null>(null);
const [sessionStarted, setSessionStarted] = useState(false);
const [selectedSighting, setSelectedSighting] = useState<SightingType | null>(null);
const mostRecent = sightings[0] ?? null;
const latestRef = mostRecent?.ref ?? null;
const [selectedSighting, setSelectedSighting] = useState<SightingType | null>(
null
);
const first = useRef(true);
const lastSoundAt = useRef(0);
const COOLDOWN_MS = 1500;
const currentRef = useRef<number>(-1);
const lastValidTimestamp = useRef<number>(Date.now());
const trigger = useMemo(() => {
if (latestRef == null) return null;
if (latestRef == null || !audioArmed) return null;
if (first.current) {
first.current = false;
return Symbol("skip");
}
const now = Date.now();
if (now - lastSoundAt.current < COOLDOWN_MS) return Symbol("skip");
lastSoundAt.current = now;
return latestRef;
}, [latestRef]);
}, [audioArmed, latestRef]);
const soundSrc = useMemo(() => {
return getSoundFileURL(state?.sightingSound) ?? switchSound;
}, [state.sightingSound]);
//use latestref instead of trigger to revert back
useSoundOnChange(soundSrc, trigger, {
volume: 1,
});
function refetchInterval(query: Query<SightingType, Error, SightingType, (string | undefined)[]>) {
if (!query) return;
const data = query.state.data as SightingType | undefined;
const now = Date.now();
const currentRef = useRef<number>(-1);
const lastValidTimestamp = useRef<number>(Date.now());
if (data && data.ref !== -1) {
lastValidTimestamp.current = now;
return 100;
}
if (now - lastValidTimestamp.current > 60_000) {
currentRef.current = -1;
lastValidTimestamp.current = now;
}
return 400;
}
const query = useQuery({
queryKey: ["sighting-feed", url],
enabled: !!url,
queryFn: () => fetchSighting(url, currentRef.current),
refetchInterval: (q) => {
const data = q.state.data as SightingType | undefined;
const now = Date.now();
if (data && data.ref !== -1) {
lastValidTimestamp.current = now;
return 100;
}
if (now - lastValidTimestamp.current > 60_000) {
currentRef.current = -1;
lastValidTimestamp.current = now;
}
return 400;
},
refetchInterval: (q) => refetchInterval(q),
refetchIntervalInBackground: true,
refetchOnWindowFocus: false,
retry: false,
staleTime: 0,
});
//use latestref instead of trigger to revert back
useSoundOnChange(soundSrc, trigger, {
volume: 1,
initial: false,
});
useEffect(() => {
const data = query.data;
if (!data) return;
if (!data || data.ref === -1) return;
const now = Date.now();
if (data.ref === -1) {
// setSightings((prev) => {
// if (prev[0]?.ref === data.ref) return prev;
// const dedupPrev = prev.filter((s) => s.ref !== data.ref);
// return [data, ...dedupPrev].slice(0, 7);
// });
return;
}
// if (Notification.permission === "granted") {
// new Notification("New Sighting!", {
// body: `Ref: ${data.ref}`,
// icon: "/MAV-blue.svg",
// });
// }
currentRef.current = data.ref;
lastValidTimestamp.current = now;
@@ -110,11 +98,6 @@ export function useSightingFeed(url: string | undefined) {
setSelectedRef(data.ref);
}, [query.data]);
useEffect(() => {
if (query.error) {
// console.error("Sighting feed error:", query.error);
}
}, [query.error]);
return {
sightings,
selectedRef,

View File

@@ -5,7 +5,6 @@ import { CAM_BASE } from "../utils/config";
const Dashboard = () => {
const base_url = `${CAM_BASE}/SightingList/sightingSummary?mostRecentRef=`;
return (
<SightingFeedProvider url={base_url}>
<div className="mx-auto flex flex-col lg:flex-row gap-2 px-1 sm:px-2 lg:px-0 w-full min-h-screen">

View File

@@ -14,12 +14,7 @@ const FrontCamera = () => {
zoomLevel={zoomLevel}
onZoomLevelChange={setZoomLevel}
/>
<CameraSettings
title="Camera A Settings"
side="CameraA"
zoomLevel={zoomLevel}
onZoomLevelChange={setZoomLevel}
/>
<CameraSettings title="Camera A Settings" side="CameraA" zoomLevel={zoomLevel} onZoomLevelChange={setZoomLevel} />
<Toaster />
</div>
);

View File

@@ -1,7 +1,7 @@
import switchSound from "../assets/sounds/ui/switch.mp3";
import popup from "../assets/sounds/ui/popup_open.mp3";
import notification from "../assets/sounds/ui/notification.mp3";
import type { SightingType } from "../types/types";
import type { HotlistMatches, SightingType } from "../types/types";
export function getSoundFileURL(name: string) {
const sounds: Record<string, string> = {
@@ -79,8 +79,7 @@ export const formatNumberPlate = (plate: string) => {
return formattedPlate;
};
export const BLANK_IMG =
"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
export const BLANK_IMG = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
export function capitalize(s?: string) {
return s ? s.charAt(0).toUpperCase() + s.slice(1) : "";
@@ -120,12 +119,7 @@ export function drawRects(
rects.forEach((r) => {
const [x, y, rw, rh] = r;
ctx.beginPath();
ctx.rect(
Math.round(x * w),
Math.round(y * h),
Math.round(rw * w),
Math.round(rh * h)
);
ctx.rect(Math.round(x * w), Math.round(y * h), Math.round(rw * w), Math.round(rh * h));
ctx.stroke();
});
}
@@ -133,13 +127,18 @@ export function drawRects(
export const checkIsHotListHit = (sigthing: SightingType | null) => {
if (!sigthing) return;
if (sigthing?.metadata?.hotlistMatches) {
const isHotListHit = Object.values(
sigthing?.metadata?.hotlistMatches
).includes(true);
const isHotListHit = Object.values(sigthing?.metadata?.hotlistMatches).includes(true);
return isHotListHit;
}
};
export function getHotlistName(obj: HotlistMatches | undefined) {
if (!obj || Object.values(obj).includes(false)) return;
const keys = Object.keys(obj);
return keys;
}
export const getNPEDCategory = (r?: SightingType | null) =>
r?.metadata?.npedJSON?.["NPED CATEGORY"] as "A" | "B" | "C" | undefined;