added sounds, updated nped config and tweaks + code quality improvements

This commit is contained in:
2025-09-23 13:03:54 +01:00
parent eab7e79d01
commit c2074f86a2
27 changed files with 224 additions and 139 deletions

2
.env
View File

@@ -2,7 +2,7 @@ VITE_BASEURL=http://192.168.75.11/
VITE_CAM_BASE=http://100.113.222.39
VITE_FOLKESTONE_BASE=http://100.116.253.81
VITE_TESTURL=http://100.82.205.44/SightingListRear/sightingSummary?mostRecentRef=-1
VITE_OUTSIDE_BASEURL=http://100.82.205.44/api
VITE_OUTSIDE_BASEURL=http://100.82.205.44
VITE_FOLKESTONE_URL=http://100.116.253.81/mergedHistory/sightingSummary?mostRecentRef=
VITE_MAV_URL=http://192.168.75.11/SightingListFront/sightingSummary?mostRecentRef=

View File

@@ -7,23 +7,26 @@ import SystemSettings from "./pages/SystemSettings";
import Session from "./pages/Session";
import { NPEDUserProvider } from "./context/providers/NPEDUserContextProvider";
import { AlertHitProvider } from "./context/providers/AlertHitProvider";
import { SoundProvider } from "react-sounds";
function App() {
return (
<NPEDUserProvider>
<AlertHitProvider>
<Routes>
<Route path="/" element={<Container />}>
<Route index element={<Dashboard />} />
<Route path="front-camera-settings" element={<FrontCamera />} />
<Route path="rear-camera-settings" element={<RearCamera />} />
<Route path="system-settings" element={<SystemSettings />} />
<Route path="session-settings" element={<Session />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Route>
</Routes>
</AlertHitProvider>
</NPEDUserProvider>
<SoundProvider initialEnabled={true}>
<NPEDUserProvider>
<AlertHitProvider>
<Routes>
<Route path="/" element={<Container />}>
<Route index element={<Dashboard />} />
<Route path="front-camera-settings" element={<FrontCamera />} />
<Route path="rear-camera-settings" element={<RearCamera />} />
<Route path="system-settings" element={<SystemSettings />} />
<Route path="session-settings" element={<Session />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Route>
</Routes>
</AlertHitProvider>
</NPEDUserProvider>
</SoundProvider>
);
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -21,13 +21,13 @@ const CameraSettingFields = ({
const initialValues = useMemo<CameraSettingValues>(
() => ({
friendlyName: initialData?.propLEDDriverControlURI?.value ?? "",
cameraAddress: "",
friendlyName: initialData?.id ?? "",
cameraAddress: initialData?.propURI?.value ?? "",
userName: "",
password: "",
id: initialData?.id,
}),
[initialData?.id, initialData?.propLEDDriverControlURI?.value]
[initialData?.id, initialData?.propURI?.value]
);
const validateValues = (values: CameraSettingValues) => {

View File

@@ -6,6 +6,7 @@ import { useSwipeable } from "react-swipeable";
import { useNavigate } from "react-router";
import { useOverviewVideo } from "../../hooks/useOverviewVideo";
import SightingOverview from "../SightingOverview/SightingOverview";
import { useSightingFeedContext } from "../../context/SightingFeedContext";
type CardProps = React.HTMLAttributes<HTMLDivElement>;
@@ -17,6 +18,7 @@ const FrontCameraOverviewCard = ({ className }: CardProps) => {
trackMouse: true,
});
const { mostRecent } = useSightingFeedContext();
return (
<Card
@@ -26,7 +28,11 @@ const FrontCameraOverviewCard = ({ className }: CardProps) => {
)}
>
<div className="flex flex-col space-y-3 h-full" {...handlers}>
<CardHeader title="Front Overview" icon={faCamera} />
<CardHeader
title="Front Overview"
icon={faCamera}
sighting={mostRecent}
/>
<SightingOverview />
{/* <SnapshotContainer side="TargetDetectionFront" /> */}
</div>

View File

@@ -6,6 +6,7 @@ import { useNavigate } from "react-router";
import CardHeader from "../UI/CardHeader";
import { faCamera } from "@fortawesome/free-regular-svg-icons";
import SightingOverview from "../SightingOverview/SightingOverview";
import { useSightingFeedContext } from "../../context/SightingFeedContext";
type CardProps = React.HTMLAttributes<HTMLDivElement>;
@@ -15,7 +16,7 @@ const RearCameraOverviewCard = ({ className }: CardProps) => {
onSwipedLeft: () => navigate("/rear-camera-settings"),
trackMouse: true,
});
const { mostRecent } = useSightingFeedContext();
return (
<Card
className={clsx(
@@ -24,7 +25,11 @@ const RearCameraOverviewCard = ({ className }: CardProps) => {
)}
>
<div className="flex flex-col space-y-3 h-full" {...handlers}>
<CardHeader title="Rear Overview" icon={faCamera} />
<CardHeader
title="Rear Overview"
icon={faCamera}
sighting={mostRecent}
/>
<SightingOverview />
</div>
</Card>

View File

@@ -1,10 +1,12 @@
import { useSound } from "react-sounds";
import Card from "../UI/Card";
import CardHeader from "../UI/CardHeader";
const SessionCard = () => {
function onStart(): void {
throw new Error("Function not implemented.");
}
const { play } = useSound("notification/notification");
// function onStart(): void {
// throw new Error("Function not implemented.");
// }
return (
<Card>
@@ -12,7 +14,9 @@ const SessionCard = () => {
<div className="flex flex-col gap-4">
<button
className="bg-[#26B170] text-white px-4 py-2 rounded hover:bg-green-700 transition w-full max-w-md"
onClick={onStart}
onClick={() => {
play();
}}
>
Start Session
</button>

View File

@@ -1,4 +1,5 @@
import type { SystemValues } from "../../../types/types";
import { CAM_BASE } from "../../../utils/config";
export async function handleSystemSave(values: SystemValues) {
const payload = {
@@ -16,7 +17,7 @@ export async function handleSystemSave(values: SystemValues) {
};
try {
const response = await fetch("http://192.168.75.11/api/update-config", {
const response = await fetch(`${CAM_BASE}/api/update-config`, {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -39,7 +40,7 @@ export async function handleSystemSave(values: SystemValues) {
}
export async function handleSystemRecall() {
const url = "http://192.168.75.11/api/fetch-config?id=GLOBAL--Device";
const url = `${CAM_BASE}/api/fetch-config?id=GLOBAL--Device`;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 7000);

View File

@@ -8,7 +8,6 @@ import { useSystemConfig } from "../../../hooks/useSystemConfig";
const SystemConfigFields = () => {
const { saveSystemSettings, systemSettingsData } = useSystemConfig();
const initialvalues: SystemValues = {
deviceName: systemSettingsData?.deviceName ?? "",
timeZone: systemSettingsData?.timeZone ?? "",

View File

@@ -13,6 +13,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 popup from "../../assets/sounds/ui/popup_open.mp3";
import { useSound } from "react-sounds";
function useNow(tickMs = 1000) {
@@ -38,7 +39,7 @@ export default function SightingHistoryWidget({
title,
}: SightingHistoryProps) {
useNow(1000);
const { play } = useSound("notification/notification");
const { play } = useSound(popup);
const {
sightings,
setSelectedSighting,
@@ -72,14 +73,13 @@ export default function SightingHistoryWidget({
const isNPEDHitC = obj?.metadata?.npedJSON?.["NPED CATEGORY"] === "C";
if (isNPEDHitA || isNPEDHitB || isNPEDHitC) {
play();
dispatch({
type: "ADD",
payload: obj,
});
}
});
}, [rows, dispatch, play]);
}, [dispatch, rows]);
useEffect(() => {
if (hasAutoOpenedRef.current) return;

View File

@@ -1,5 +1,4 @@
import type { SightingType } from "../../types/types";
import { useState } from "react";
type SightingWidgetDetailsProps = {
effectiveSelected: SightingType | null;
@@ -8,85 +7,38 @@ type SightingWidgetDetailsProps = {
const SightingWidgetDetails = ({
effectiveSelected,
}: SightingWidgetDetailsProps) => {
const [advancedDetailsEnabled, setAdvancedDetailsEnabled] = useState(false);
const handleDetailsClick = () =>
setAdvancedDetailsEnabled(!advancedDetailsEnabled);
return (
<>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-sm">
<div>
VRM:{" "}
<span className="opacity-90">{effectiveSelected?.vrm ?? ""}</span>
</div>
<div>
Make:{" "}
<span className="opacity-90">{effectiveSelected?.make ?? ""}</span>
</div>
<div>
Model:{" "}
<span className="opacity-90">{effectiveSelected?.model ?? "—"}</span>
</div>
<div>
Colour:{" "}
<span className="opacity-90">{effectiveSelected?.color ?? ""}</span>
</div>
<div className="col-span-4">
Timestamp:{" "}
<span className="opacity-90">
{effectiveSelected?.timeStamp ?? "—"}
</span>
</div>
{advancedDetailsEnabled && (
<>
<div>
Country:{" "}
<span className="opacity-90">
{effectiveSelected?.countryCode ?? "—"}
</span>
</div>
<div>
Seen:{" "}
<span className="opacity-90">
{effectiveSelected?.seenCount ?? "—"}
</span>
</div>
<div>
Category:{" "}
<span className="opacity-90">
{effectiveSelected?.category ?? "—"}
</span>
</div>
<div>
Char Ht:{" "}
<span className="opacity-90">
{effectiveSelected?.charHeight ?? "—"}
</span>
</div>
<div>
Plate Size:{" "}
<span className="opacity-90">
{effectiveSelected?.plateSize ?? "—"}
</span>
</div>
<div>
Overview Size:{" "}
<span className="opacity-90">
{effectiveSelected?.overviewSize ?? "—"}
</span>
</div>
</>
{effectiveSelected?.vrm && (
<div>
VRM:{" "}
<span className="opacity-90">{effectiveSelected?.vrm ?? "—"}</span>
</div>
)}
{effectiveSelected?.make !== "" && (
<div>
Make:{" "}
<span className="opacity-90">{effectiveSelected?.make ?? ""}</span>
</div>
)}
{effectiveSelected?.model.trim() !== "" && (
<div>
Model:{" "}
<span className="opacity-90">
{effectiveSelected?.model ?? "—"}
</span>
</div>
)}
{effectiveSelected?.color !== "" && (
<div>
Colour:{" "}
<span className="opacity-90">
{effectiveSelected?.color ?? "—"}
</span>
</div>
)}
</div>
<div className="col-span-half">
<p
onClick={handleDetailsClick}
className="underline text-blue-300 hover:cursor-pointer"
>
Sighting Details
</p>
</div>
</>
);

View File

@@ -1,18 +1,27 @@
import type { IconProp } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import clsx from "clsx";
import NumberPlate from "../PlateStack/NumberPlate";
import type { SightingType } from "../../types/types";
type CameraOverviewHeaderProps = {
title: string;
icon?: IconProp;
img?: string;
sighting?: SightingType | null;
};
const CardHeader = ({ title, icon, img }: CameraOverviewHeaderProps) => {
const CardHeader = ({
title,
icon,
img,
sighting,
}: CameraOverviewHeaderProps) => {
// console.log(sighting?.debug.toLowerCase());
return (
<div
className={clsx(
"w-full border-b border-gray-600 flex flex-row items-center space-x-2 md:mb-6 relative"
"w-full border-b border-gray-600 flex flex-row items-center space-x-2 md:mb-6 relative justify-between"
)}
>
<div className="flex items-center space-x-2">
@@ -22,6 +31,7 @@ const CardHeader = ({ title, icon, img }: CameraOverviewHeaderProps) => {
{img && (
<img src={img} alt="Logo" width={100} height={50} className="ml-auto" />
)}
{sighting?.vrm && <NumberPlate vrm={sighting.vrm} motion={false} />}
</div>
);
};

View File

@@ -10,6 +10,7 @@ import {
} from "@fortawesome/free-solid-svg-icons";
import type { VersionFieldType } from "../../types/types";
import { useEffect, useState } from "react";
import SoundBtn from "./SoundBtn";
async function fetchVersions(
signal?: AbortSignal
@@ -120,6 +121,7 @@ export default function Header() {
size="2x"
/>
</Link>
<SoundBtn />
<Link to={"/system-settings"}>
<FontAwesomeIcon className="text-white" icon={faGear} size="2x" />
</Link>

View File

@@ -0,0 +1,22 @@
import { faVolumeHigh, faVolumeXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useSoundEnabled } from "react-sounds";
const SoundBtn = () => {
const [enabled, setEnabled] = useSoundEnabled();
const handleClick = () => {
setEnabled(!enabled);
};
return (
<button onClick={handleClick}>
<FontAwesomeIcon
icon={enabled ? faVolumeHigh : faVolumeXmark}
size="2x"
/>
</button>
);
};
export default SoundBtn;

View File

@@ -14,6 +14,7 @@ type SightingFeedContextType = {
isSightingModalOpen: boolean;
isError: boolean;
isLoading: boolean;
data: SightingType | undefined;
};
export const SightingFeedContext = createContext<

View File

@@ -17,13 +17,13 @@ export const SightingFeedProvider = ({
sightings,
selectedRef,
setSelectedRef,
data,
isLoading,
isError,
setSelectedSighting,
selectedSighting,
mostRecent,
} = useSightingFeed(url);
} = useSightingFeed(url, side);
const [isSightingModalOpen, setSightingModalOpen] = useState(false);
@@ -41,6 +41,7 @@ export const SightingFeedProvider = ({
isError,
isLoading,
side,
data,
}}
>
{children}

View File

@@ -1,7 +1,9 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import { toast } from "sonner";
import { CAM_BASE } from "../utils/config";
const base_url = import.meta.env.VITE_OUTSIDE_BASEURL;
const base_url = `${CAM_BASE}/api`;
console.log(base_url);
const fetchCameraSideConfig = async ({ queryKey }: { queryKey: string[] }) => {
const [, cameraSide] = queryKey;

View File

@@ -5,7 +5,6 @@ import { CAM_BASE } from "../utils/config";
const apiUrl = CAM_BASE;
async function fetchSnapshot(cameraSide: string) {
console.log(`${apiUrl}/${cameraSide}-preview`);
const response = await fetch(`${apiUrl}/${cameraSide}-preview`);
if (!response.ok) {
throw new Error("Cannot reach endpoint");

View File

@@ -2,10 +2,10 @@ import { useMutation, useQuery } from "@tanstack/react-query";
import type { NPEDFieldType } from "../types/types";
import { useNPEDContext } from "../context/NPEDUserContext";
import { useEffect } from "react";
import { CAM_BASE } from "../utils/config";
const base_url = import.meta.env.VITE_OUTSIDE_BASEURL;
async function fetchNPEDDetails() {
const fetchUrl = `${base_url}/fetch-config?id=NPED`;
const fetchUrl = `${CAM_BASE}/api/fetch-config?id=NPED`;
const response = await fetch(fetchUrl);
if (!response.ok) throw new Error("Cannot reach fetch-config endpoint");
@@ -14,8 +14,8 @@ async function fetchNPEDDetails() {
async function signIn(loginDetails: NPEDFieldType) {
const { frontId, rearId, username, password, clientId } = loginDetails;
const NPEDLoginURLFront = `${base_url}/update-config?id=${frontId}`;
const NPEDLoginURLRear = `${base_url}/update-config?id=${rearId}`;
const NPEDLoginURLFront = `${CAM_BASE}/api/update-config?id=${frontId}`;
const NPEDLoginURLRear = `${CAM_BASE}/api/update-config?id=${rearId}`;
const frontCameraPayload = {
id: frontId,
fields: [
@@ -66,7 +66,7 @@ async function signOut() {
{ property: "propClientID", value: "" },
],
};
const NPEDLoginURLFront = `${base_url}/update-config?id=NPED`;
const NPEDLoginURLFront = `${CAM_BASE}/api/update-config?id=NPED`;
const response = await fetch(NPEDLoginURLFront, {
method: "POST",
body: JSON.stringify(nullPayload),

View File

@@ -1,6 +1,8 @@
import { useEffect, useRef, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import type { SightingType } from "../types/types";
import { useSoundOnChange } from "react-sounds";
import click from "../assets/sounds/ui/computer-mouse-click.mp3";
async function fetchSighting(url: string, ref: number): Promise<SightingType> {
const res = await fetch(`${url}${ref}`);
@@ -8,14 +10,19 @@ async function fetchSighting(url: string, ref: number): Promise<SightingType> {
return res.json();
}
export function useSightingFeed(url: string) {
export function useSightingFeed(url: string, side: string) {
const [sightings, setSightings] = useState<SightingType[]>([]);
const [selectedRef, setSelectedRef] = useState<number | null>(null);
const mostRecent = sightings[0] ?? null;
const latestRef = mostRecent?.ref ?? null;
const [selectedSighting, setSelectedSighting] = useState<SightingType | null>(
null
);
useSoundOnChange(click, latestRef, {
volume: side === "Rear" ? 0 : 1,
});
const currentRef = useRef<number>(-1);
const lastValidTimestamp = useRef<number>(Date.now());
@@ -61,6 +68,13 @@ export function useSightingFeed(url: string) {
return;
}
// if (Notification.permission === "granted") {
// new Notification("New Sighting!", {
// body: `Ref: ${data.ref}`,
// icon: "/MAV-blue.svg",
// });
// }
currentRef.current = data.ref;
lastValidTimestamp.current = now;
@@ -75,7 +89,6 @@ export function useSightingFeed(url: string) {
useEffect(() => {
if (query.error) {
// you can add logging/telemetry here
// console.error("Sighting feed error:", query.error);
}
}, [query.error]);

64
src/hooks/useSound.ts Normal file
View File

@@ -0,0 +1,64 @@
// useBeep.ts
import { useEffect, useRef } from "react";
import { useSoundEnabled } from "react-sounds"; // so it respects your SoundBtn toggle
/**
* Plays a sound whenever `latestRef` changes.
*
* @param src Path to the sound file
* @param latestRef The primitive value to watch (e.g. sighting.ref)
* @param opts volume: 0..1, enabledOverride: force enable/disable, minGapMs: throttle interval
*/
export function useBeep(
src: string,
latestRef: number | null,
opts?: { volume?: number; enabledOverride?: boolean; minGapMs?: number }
) {
const audioRef = useRef<HTMLAudioElement>(undefined);
const prevRef = useRef<number | null>(null);
const lastPlay = useRef(0);
const [enabled] = useSoundEnabled();
const minGap = opts?.minGapMs ?? 250; // dont play more than 4 times/sec
// Create the audio element once
useEffect(() => {
const a = new Audio(src);
a.preload = "auto";
if (opts?.volume !== undefined) a.volume = opts.volume;
audioRef.current = a;
return () => {
a.pause();
};
}, [src, opts?.volume]);
// Watch for ref changes
useEffect(() => {
if (latestRef == null) return;
const canPlay =
(opts?.enabledOverride ?? enabled) &&
document.visibilityState === "visible";
if (!canPlay) {
prevRef.current = latestRef; // consume the change
return;
}
if (prevRef.current !== null && latestRef !== prevRef.current) {
const now = Date.now();
if (now - lastPlay.current >= minGap) {
const a = audioRef.current;
if (a) {
try {
a.currentTime = 0; // restart from beginning
void a.play(); // fire and forget
lastPlay.current = now;
} catch (err) {
console.warn("Audio play failed:", err);
}
}
}
}
prevRef.current = latestRef;
}, [latestRef, enabled, opts?.enabledOverride, minGap]);
}

View File

@@ -3,6 +3,7 @@ import RearCameraOverviewCard from "../components/RearCameraOverview/RearCameraO
import SightingHistoryWidget from "../components/SightingsWidget/SightingWidget";
import { SightingFeedProvider } from "../context/providers/SightingFeedProvider";
import { CAM_BASE } from "../utils/config";
const Dashboard = () => {
const dev_REAR_URL = `${CAM_BASE}/SightingListRear/sightingSummary?mostRecentRef=`;
const dev_FRONT_URL = `${CAM_BASE}/SightingListFront/sightingSummary?mostRecentRef=`;

View File

@@ -12,15 +12,14 @@ const FrontCamera = () => {
});
return (
<div
className="mx-auto grid grid-cols-1 sm:grid-cols-1 lg:grid-cols-2 gap-4 px-2 sm:px-4 lg:px-0 w-full"
{...handlers}
>
<OverviewVideoContainer
title={"Front Camera"}
side="CameraFront"
settingsPage={true}
/>
<div className="mx-auto grid grid-cols-1 sm:grid-cols-1 lg:grid-cols-2 gap-4 px-2 sm:px-4 lg:px-0 w-full">
<div {...handlers}>
<OverviewVideoContainer
title={"Front Camera"}
side="CameraFront"
settingsPage={true}
/>
</div>
<CameraSettings title="Front Camera Settings" side="CameraFront" />
<Toaster />
</div>

View File

@@ -12,16 +12,16 @@ const RearCamera = () => {
});
return (
<div
className="mx-auto grid grid-cols-1 sm:grid-cols-1 lg:grid-cols-2 gap-4 px-2 sm:px-4 lg:px-0 w-full order-first"
{...handlers}
>
<div className="mx-auto grid grid-cols-1 sm:grid-cols-1 lg:grid-cols-2 gap-4 px-2 sm:px-4 lg:px-0 w-full order-first">
<CameraSettings title="Rear Camera Settings" side={"CameraRear"} />
<OverviewVideoContainer
title={"Rear Camera"}
side={"CameraRear"}
settingsPage={true}
/>
<div {...handlers}>
<OverviewVideoContainer
title={"Rear Camera"}
side={"CameraRear"}
settingsPage={true}
/>
</div>
<Toaster />
</div>
);

View File

@@ -1,4 +1,5 @@
const rawCamBase = import.meta.env.VITE_CAM_BASE;
// const rawCamBase = import.meta.env.VITE_CAM_BASE;
const rawCamBase = import.meta.env.VITE_OUTSIDE_BASEURL;
export const CAM_BASE =
rawCamBase && rawCamBase.trim().length > 0