diff --git a/src/App.tsx b/src/App.tsx index 980e322..5aaa5a7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -19,8 +19,8 @@ function App() { }> } /> - } /> - } /> + } /> + } /> } /> } /> } /> diff --git a/src/components/CameraOverview/SnapshotContainer.tsx b/src/components/CameraOverview/SnapshotContainer.tsx index 87cb48f..2b3e1e1 100644 --- a/src/components/CameraOverview/SnapshotContainer.tsx +++ b/src/components/CameraOverview/SnapshotContainer.tsx @@ -1,11 +1,8 @@ import { useGetOverviewSnapshot } from "../../hooks/useGetOverviewSnapshot"; -import type { ZoomInOptions } from "../../types/types"; import NavigationArrow from "../UI/NavigationArrow"; -import { useCameraZoom } from "../../hooks/useCameraZoom"; -import { useEffect } from "react"; + import Loading from "../UI/Loading"; import ErrorState from "../UI/ErrorState"; - type SnapshotContainerProps = { side: string; settingsPage?: boolean; @@ -13,52 +10,20 @@ type SnapshotContainerProps = { onZoomLevelChange?: (level: number) => void; }; -export const SnapshotContainer = ({ - side, - settingsPage, - zoomLevel, - onZoomLevelChange, -}: SnapshotContainerProps) => { +export const SnapshotContainer = ({ side, settingsPage }: SnapshotContainerProps) => { const { canvasRef, isError, isPending } = useGetOverviewSnapshot(side); - const cameraControllerSide = - side === "CameraA" ? "CameraControllerA" : "CameraControllerB"; - const { mutation } = useCameraZoom({ camera: cameraControllerSide }); - - const handleZoomClick = () => { - const baseLevel = zoomLevel ?? 1; - const newLevel = baseLevel >= 8 ? 1 : baseLevel * 2; - - if (onZoomLevelChange) onZoomLevelChange(newLevel); - - if (!zoomLevel) return; - }; - - useEffect(() => { - if (zoomLevel) { - const zoomInOptions: ZoomInOptions = { - camera: cameraControllerSide, - multiplier: zoomLevel, - }; - mutation.mutate(zoomInOptions); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [zoomLevel]); return (
-
+
{isError && } {isPending && ( -
+
)} - +
); diff --git a/src/components/CameraSettings/CameraSettingFields.tsx b/src/components/CameraSettings/CameraSettingFields.tsx index 1cb3786..c7040bf 100644 --- a/src/components/CameraSettings/CameraSettingFields.tsx +++ b/src/components/CameraSettings/CameraSettingFields.tsx @@ -4,14 +4,14 @@ import { useEffect, useMemo, useState } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faEye, faEyeSlash } from "@fortawesome/free-regular-svg-icons"; import CardHeader from "../UI/CardHeader"; -import { useCameraZoom } from "../../hooks/useCameraZoom"; -import { parseRTSPUrl } from "../../utils/utils"; +import { useCameraMode, useCameraZoom } from "../../hooks/useCameraZoom"; +import { parseRTSPUrl, reverseZoomMapping, zoomMapping } from "../../utils/utils"; type CameraSettingsProps = { initialData: CameraConfig; updateCameraConfig: (values: CameraSettingValues) => Promise | void; zoomLevel?: number; - onZoomLevelChange?: (level: number) => void; + onZoomLevelChange?: (level: number | undefined) => void; updateCameraConfigError: null | Error; }; @@ -20,38 +20,22 @@ const CameraSettingFields = ({ updateCameraConfig, zoomLevel, onZoomLevelChange, - updateCameraConfigError, }: CameraSettingsProps) => { const [showPwd, setShowPwd] = useState(false); + const cameraControllerSide = initialData?.id === "CameraA" ? "CameraControllerA" : "CameraControllerB"; const { mutation, query } = useCameraZoom({ camera: cameraControllerSide }); - const zoomOptions = [1, 2, 4, 8]; - + const { cameraModeQuery, cameraModeMutation } = useCameraMode({ camera: cameraControllerSide }); + const zoomOptions = [1, 2, 4]; + const magnification = query?.data?.propMagnification?.value; + const apiZoom = reverseZoomMapping(magnification); const parsed = parseRTSPUrl(initialData?.propURI?.value); + const cameraMode = cameraModeQuery?.data?.propDayNightMode?.value; useEffect(() => { if (!query?.data) return; - const apiZoom = getZoomLevel(query.data); onZoomLevelChange?.(apiZoom); - }, [query?.data, onZoomLevelChange]); - - const getZoomLevel = (levelstring: string | undefined) => { - switch (levelstring) { - case "1x": - return 1; - - case "2x": - return 2; - - case "4x": - return 4; - - case "8x": - return 8; - default: - return 1; - } - }; + }, [query?.data, onZoomLevelChange, apiZoom]); const initialValues = useMemo( () => ({ @@ -60,11 +44,11 @@ const CameraSettingFields = ({ userName: parsed?.username ?? "", password: parsed?.password ?? "", id: initialData?.id, - - zoom: zoomLevel, + mode: cameraMode ?? "day", + zoom: apiZoom, }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [initialData?.id, initialData?.propURI?.value, zoomLevel] + + [initialData?.id, initialData?.propURI?.value, parsed?.username, parsed?.password, cameraMode, apiZoom] ); const validateValues = (values: CameraSettingValues) => { @@ -80,15 +64,18 @@ const CameraSettingFields = ({ const handleRadioButtonChange = async (levelNumber: number) => { if (!onZoomLevelChange || !zoomLevel) return; + const text = zoomMapping(levelNumber); onZoomLevelChange(levelNumber); const zoomInOptions: ZoomInOptions = { camera: cameraControllerSide, multiplier: levelNumber, + multiplierText: text, }; mutation.mutate(zoomInOptions); }; + const selectedZoom = zoomLevel ?? 1; return ( - {({ errors, touched }) => ( -
+ {({ errors, touched, values, setFieldValue, isSubmitting }) => ( +
{touched.friendlyName && errors.friendlyName && ( @@ -111,7 +98,6 @@ const CameraSettingFields = ({ type="text" className="p-2 border border-gray-400 rounded-lg" placeholder="Enter camera name" - disabled />
@@ -126,7 +112,6 @@ const CameraSettingFields = ({ type="text" className="p-2 border border-gray-400 rounded-lg" placeholder="RTSP://..." - disabled />
@@ -142,7 +127,6 @@ const CameraSettingFields = ({ className="p-2 border border-gray-400 rounded-lg" placeholder="Enter user name" autoComplete="username" - disabled />
@@ -158,7 +142,6 @@ const CameraSettingFields = ({ type={showPwd ? "text" : "password"} className="p-2 border border-gray-400 rounded-lg w-full " placeholder="Enter password" - disabled />
-
+
{zoomOptions.map((zoom) => (
- x{zoom} + {zoomMapping(zoom)} + +
+ ))} +
+
+
+ +
+ {["day", "night"].map((el) => ( +
+ { + setFieldValue("mode", el); + await cameraModeMutation.mutateAsync({ camera: cameraControllerSide, mode: el }); + }} + /> +
))}
- {updateCameraConfigError ? ( - - ) : ( - - )} + }
diff --git a/src/components/CameraSettings/CameraSettings.tsx b/src/components/CameraSettings/CameraSettings.tsx index c491f6c..22156fc 100644 --- a/src/components/CameraSettings/CameraSettings.tsx +++ b/src/components/CameraSettings/CameraSettings.tsx @@ -13,15 +13,14 @@ const CameraSettings = ({ title: string; side: string; zoomLevel?: number; - onZoomLevelChange?: (level: number) => void; + onZoomLevelChange?: (level: number | undefined) => void; }) => { const { data, updateCameraConfig, updateCameraConfigError } = useFetchCameraConfig(side); return ( - +
- { { const navigate = useNavigate(); const handlers = useSwipeable({ - onSwipedRight: () => navigate("/camera-settings"), - onSwipedLeft: () => navigate("/rear-camera-settings"), + onSwipedRight: () => navigate("/a-camera-settings"), + onSwipedLeft: () => navigate("/b-camera-settings"), trackMouse: true, }); diff --git a/src/components/FrontCameraSettings/OverviewVideoContainer.tsx b/src/components/FrontCameraSettings/OverviewVideoContainer.tsx index 379f95b..44bb0a4 100644 --- a/src/components/FrontCameraSettings/OverviewVideoContainer.tsx +++ b/src/components/FrontCameraSettings/OverviewVideoContainer.tsx @@ -20,17 +20,17 @@ const OverviewVideoContainer = ({ const location = useLocation(); const handlers = useSwipeable({ onSwipedLeft: () => { - if (location.pathname === "/rear-camera-settings") return; + if (location.pathname === "/b-camera-settings") return; navigate("/"); }, onSwipedRight: () => { - if (location.pathname === "/camera-settings") return; + if (location.pathname === "/a-camera-settings") return; navigate("/"); }, trackMouse: true, }); return ( - +
{ return (
@@ -50,9 +48,7 @@ const NumberPlate = ({ motion, vrm, size }: NumberPlateProps) => {
-

- {vrm && formatNumberPlate(vrm)} -

+

{vrm && formatNumberPlate(vrm)}

); diff --git a/src/components/RearCameraOverview/RearCameraOverviewCard.tsx b/src/components/RearCameraOverview/RearCameraOverviewCard.tsx index 883d096..9fa608e 100644 --- a/src/components/RearCameraOverview/RearCameraOverviewCard.tsx +++ b/src/components/RearCameraOverview/RearCameraOverviewCard.tsx @@ -13,7 +13,7 @@ type CardProps = React.HTMLAttributes; const RearCameraOverviewCard = ({ className }: CardProps) => { const navigate = useNavigate(); const handlers = useSwipeable({ - onSwipedLeft: () => navigate("/rear-camera-settings"), + onSwipedLeft: () => navigate("/b-camera-settings"), trackMouse: true, }); const { mostRecent } = useSightingFeedContext(); diff --git a/src/components/SettingForms/BearerType/BearerTypeCard.tsx b/src/components/SettingForms/BearerType/BearerTypeCard.tsx index 2e42a60..b5eaa02 100644 --- a/src/components/SettingForms/BearerType/BearerTypeCard.tsx +++ b/src/components/SettingForms/BearerType/BearerTypeCard.tsx @@ -4,7 +4,7 @@ import BearerTypeFields from "./BearerTypeFields"; const BearerTypeCard = () => { return ( - + diff --git a/src/components/SettingForms/Channel1-JSON/ChannelCard.tsx b/src/components/SettingForms/Channel1-JSON/ChannelCard.tsx index 3311e09..5bc6454 100644 --- a/src/components/SettingForms/Channel1-JSON/ChannelCard.tsx +++ b/src/components/SettingForms/Channel1-JSON/ChannelCard.tsx @@ -9,14 +9,17 @@ import { useEffect, useMemo } from "react"; type ChannelCardProps = { touched: FormikTouched; isSubmitting: boolean; + isBof2ConstantsLoading: boolean; + isDispatcherLoading: boolean; }; -const ChannelCard = ({ touched, isSubmitting }: ChannelCardProps) => { +const ChannelCard = ({ touched, isSubmitting, isBof2ConstantsLoading, isDispatcherLoading }: ChannelCardProps) => { const { values, setFieldValue } = useFormikContext(); const { backOfficeQuery } = useCameraBackOfficeOutput(values?.format); + const isBackOfficeQueryLoading = backOfficeQuery?.isFetching; const mapped = useMemo(() => { - const d = backOfficeQuery.data; + const d = backOfficeQuery?.data; return { backOfficeURL: d?.propBackofficeURL?.value ?? "", username: d?.propUsername?.value ?? "", @@ -24,24 +27,28 @@ const ChannelCard = ({ touched, isSubmitting }: ChannelCardProps) => { connectTimeoutSeconds: Number(d?.propConnectTimeoutSeconds?.value), readTimeoutSeconds: Number(d?.propReadTimeoutSeconds?.value), }; - }, [backOfficeQuery.data]); + }, [backOfficeQuery?.data]); useEffect(() => { - if (!backOfficeQuery.isSuccess) return; + if (!backOfficeQuery?.isSuccess) return; for (const [key, value] of Object.entries(mapped)) { setFieldValue(key, value); } }, [backOfficeQuery.isSuccess, mapped, setFieldValue]); return ( - + - + {!isBof2ConstantsLoading && !isDispatcherLoading && !isBackOfficeQueryLoading ? ( + + ) : ( + <>Loading... + )} ); }; diff --git a/src/components/SettingForms/Channel1-JSON/ChannelFields.tsx b/src/components/SettingForms/Channel1-JSON/ChannelFields.tsx index 24cde7c..36b505f 100644 --- a/src/components/SettingForms/Channel1-JSON/ChannelFields.tsx +++ b/src/components/SettingForms/Channel1-JSON/ChannelFields.tsx @@ -58,9 +58,7 @@ const ChannelFields = ({ touched, isSubmitting, format }: ChannelFieldsProps) => type="text" id="backoffice" placeholder="https://www.backoffice.com" - className={`p-1.5 border ${ - errors.backOfficeURL && touched.backOfficeURL ? "border-red-500" : "border-gray-400 " - } rounded-lg w-full md:w-60`} + className={`p-1.5 border border-gray-400 rounded-lg w-full md:w-60`} /> @@ -70,9 +68,7 @@ const ChannelFields = ({ touched, isSubmitting, format }: ChannelFieldsProps) => type="text" id="username" placeholder="Back office username" - className={`p-1.5 border ${ - errors.username && touched.username ? "border-red-500" : "border-gray-400 " - } rounded-lg w-full md:w-60`} + className={`p-1.5 border border-gray-400 rounded-lg w-full md:w-60`} /> @@ -119,64 +115,119 @@ const ChannelFields = ({ touched, isSubmitting, format }: ChannelFieldsProps) => } rounded-lg w-full md:w-60`} /> - + {/* Overview quality and scale */} + + + + + + + + + {/* propOverviewImageScaleFactor cropSizeFactor */} + + + + + + + + + {format?.toLowerCase() === "bof2" && ( <> -
-

{values.format} Constants

+
+
+

{values.format} Constants

+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+
+

{values.format} Lane ID Config

+
+ + + + + + + +
- - - - - - - - - - - - - - - - - - - - - - - - - - )}
diff --git a/src/components/SettingForms/SettingForms/SettingForms.tsx b/src/components/SettingForms/SettingForms/SettingForms.tsx index f89b538..42b89fd 100644 --- a/src/components/SettingForms/SettingForms/SettingForms.tsx +++ b/src/components/SettingForms/SettingForms/SettingForms.tsx @@ -7,27 +7,42 @@ import type { InitialValuesForm, InitialValuesFormErrors, OptionalBOF2Constants, + OptionalBOF2LaneIDs, } from "../../../types/types"; import { useQueryClient } from "@tanstack/react-query"; import { useUpdateBackOfficeConfig } from "../../../hooks/useBackOfficeConfig"; import { useFormVaidate } from "../../../hooks/useFormValidate"; +import { useSightingAmend } from "../../../hooks/useSightingAmend"; +import StoreCard from "../Store/StoreCard"; const SettingForms = () => { const qc = useQueryClient(); - const { dispatcherQuery, dispatcherMutation, backOfficeDispatcherMutation } = useCameraOutput(); + const { dispatcherQuery, dispatcherMutation, backOfficeDispatcherMutation, bof2LandMutation, laneIdQuery } = + useCameraOutput(); const { backOfficeMutation } = useUpdateBackOfficeConfig(); const { bof2ConstantsQuery } = useGetDispatcherConfig(); const { validateMutation } = useFormVaidate(); + const { sightingAmendQuery, sightingAmendMutation } = useSightingAmend(); const format = dispatcherQuery?.data?.propFormat?.value; const enabled = dispatcherQuery?.data?.propEnabled?.value; + const sightingQuality = sightingAmendQuery?.data?.propOverviewQuality?.value; + const cropSizeFactor = sightingAmendQuery?.data?.propOverviewImageScaleFactor?.value; + + const laneID = laneIdQuery?.data?.id; + const LID1 = laneIdQuery?.data?.propLaneID1?.value; + const LID2 = laneIdQuery?.data?.propLaneID2?.value; + const FFID = bof2ConstantsQuery?.data?.propFeedIdentifier?.value; const SCID = bof2ConstantsQuery?.data?.propSourceIdentifier?.value; const GPSFormat = bof2ConstantsQuery?.data?.propGpsFormat?.value; const timestampSource = bof2ConstantsQuery?.data?.propTimeZoneType?.value; - const initialValues: BearerTypeFieldType & InitialValuesForm & OptionalBOF2Constants = { + const isDispatcherLoading = dispatcherQuery?.isFetching; + const isBof2ConstantsLoading = bof2ConstantsQuery?.isFetching; + + const initialValues: BearerTypeFieldType & InitialValuesForm & OptionalBOF2Constants & OptionalBOF2LaneIDs = { format: format ?? "JSON", enabled: enabled === "true", backOfficeURL: "", @@ -35,27 +50,24 @@ const SettingForms = () => { password: "", connectTimeoutSeconds: Number(5), readTimeoutSeconds: Number(15), + overviewQuality: sightingQuality ?? "HIGH", + cropSizeFactor: cropSizeFactor ?? "3/4", // Bof2 - optional constants FFID: FFID ?? "", SCID: SCID ?? "", timestampSource: timestampSource ?? "", GPSFormat: GPSFormat ?? "", + + //BOF2 - optional Lane IDs + laneId: laneID ?? "", + LID1: LID1 ?? "", + LID2: LID2 ?? "", }; const validateValues = (values: InitialValuesForm): InitialValuesFormErrors => { const errors: InitialValuesFormErrors = {}; - const url = values.backOfficeURL?.trim(); - const username = values.username?.trim(); - const password = values.password?.trim(); - if (!url) { - errors.backOfficeURL = "Required"; - } - - if (!username) errors.username = "Required"; - if (!password) errors.password = "Required"; - const read = Number(values.readTimeoutSeconds); if (!Number.isFinite(read)) { errors.readTimeoutSeconds = "Must be a number"; @@ -72,10 +84,11 @@ const SettingForms = () => { return errors; }; - const handleSubmit = async (values: BearerTypeFieldType & InitialValuesForm & OptionalBOF2Constants) => { - // if (formErrors && Object.entries(formErrors).length > 0) { - // return; - // } + const handleSubmit = async ( + values: BearerTypeFieldType & InitialValuesForm & OptionalBOF2Constants & OptionalBOF2LaneIDs + ) => { + const validResponse = await validateMutation.mutateAsync(values); + const dispatcherData = { format: values.format, enabled: values.enabled, @@ -85,23 +98,32 @@ const SettingForms = () => { if (result?.id) { qc.invalidateQueries({ queryKey: ["dispatcher"] }); qc.invalidateQueries({ queryKey: ["backoffice", values.format] }); - const validResponse = await validateMutation.mutateAsync(values); + if (validResponse?.reason === "OK") { await backOfficeMutation.mutateAsync(values); + await sightingAmendMutation.mutateAsync(values); + + if (values.format.toLowerCase() === "bof2") { + const bof2ConstantsData: OptionalBOF2Constants = { + FFID: values.FFID, + SCID: values.SCID, + timestampSource: values.timestampSource, + GPSFormat: values.GPSFormat, + }; + + const bof2LaneData: OptionalBOF2LaneIDs = { + laneId: laneIdQuery?.data?.id, + LID1: values.LID1, + LID2: values.LID2, + }; + await bof2LandMutation.mutateAsync(bof2LaneData); + await backOfficeDispatcherMutation.mutateAsync(bof2ConstantsData); + } } else { + console.log("error"); return; } } - - if (values.format.toLowerCase() === "bof2") { - const bof2ConstantsData: OptionalBOF2Constants = { - FFID: values.FFID, - SCID: values.SCID, - timestampSource: values.timestampSource, - GPSFormat: values.GPSFormat, - }; - await backOfficeDispatcherMutation.mutateAsync(bof2ConstantsData); - } }; return ( @@ -109,8 +131,17 @@ const SettingForms = () => { {({ isSubmitting, touched }) => (
- - +
+ + +
+ +
)} diff --git a/src/components/SettingForms/Store/StoreCard.tsx b/src/components/SettingForms/Store/StoreCard.tsx new file mode 100644 index 0000000..ef91bb5 --- /dev/null +++ b/src/components/SettingForms/Store/StoreCard.tsx @@ -0,0 +1,14 @@ +import Card from "../../UI/Card"; +import CardHeader from "../../UI/CardHeader"; +import StoreFields from "./StoreFields"; + +const StoreCard = () => { + return ( + + + + + ); +}; + +export default StoreCard; diff --git a/src/components/SettingForms/Store/StoreFields.tsx b/src/components/SettingForms/Store/StoreFields.tsx new file mode 100644 index 0000000..3426032 --- /dev/null +++ b/src/components/SettingForms/Store/StoreFields.tsx @@ -0,0 +1,29 @@ +import { useStoreDispatch } from "../../../hooks/useStoreDispatch"; +import VehicleSessionItem from "../../UI/VehicleSessionItem"; + +const StoreFields = () => { + const { storeQuery } = useStoreDispatch(); + + const totalPending = storeQuery?.data?.totalPending; + const totalActive = storeQuery?.data?.totalActive; + const totalSent = storeQuery?.data?.totalSent; + const totalReceived = storeQuery?.data?.totalReceived; + const totalLost = storeQuery?.data?.totalLost; + + if (storeQuery.isLoading) return
Loading store data...
; + if (storeQuery.error) return
Error: {storeQuery.error.message}
; + + return ( +
+
    + + + + + +
+
+ ); +}; + +export default StoreFields; diff --git a/src/components/SettingForms/System/SettingSaveRecall.tsx b/src/components/SettingForms/System/SettingSaveRecall.tsx index 8d9b3e7..9a240ed 100644 --- a/src/components/SettingForms/System/SettingSaveRecall.tsx +++ b/src/components/SettingForms/System/SettingSaveRecall.tsx @@ -2,6 +2,8 @@ import { toast } from "sonner"; import type { SystemValues } from "../../../types/types"; import { CAM_BASE } from "../../../utils/config"; +const camBase = import.meta.env.MODE !== "development" ? CAM_BASE : ""; + export async function handleSystemSave(values: SystemValues) { const payload = { // Build JSON @@ -18,7 +20,7 @@ export async function handleSystemSave(values: SystemValues) { }; try { - const response = await fetch(`${CAM_BASE}/api/update-config`, { + const response = await fetch(`${camBase}/api/update-config`, { method: "POST", headers: { "Content-Type": "application/json", @@ -29,11 +31,7 @@ export async function handleSystemSave(values: SystemValues) { if (!response.ok) { const text = await response.text().catch(() => ""); - throw new Error( - `HTTP ${response.status} ${response.statusText}${ - text ? ` - ${text}` : "" - }` - ); + throw new Error(`HTTP ${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`); } } catch (err) { if (err instanceof Error) { @@ -47,10 +45,10 @@ export async function handleSystemSave(values: SystemValues) { } export async function handleSystemRecall() { - const url = `${CAM_BASE}/api/fetch-config?id=GLOBAL--Device`; + const url = `${camBase}/api/fetch-config?id=GLOBAL--Device`; const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 7000); + const timeoutId = setTimeout(() => controller.abort(), 70000); try { const response = await fetch(url, { @@ -61,11 +59,7 @@ export async function handleSystemRecall() { if (!response.ok) { const text = await response.text().catch(() => ""); - throw new Error( - `HTTP ${response.status} ${response.statusText}${ - text ? ` - ${text}` : "" - }` - ); + throw new Error(`HTTP ${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`); } const data = await response.json(); @@ -76,9 +70,7 @@ export async function handleSystemRecall() { const sntpIntervalRaw = data?.propSNTPIntervalMinutes?.value; let sntpInterval = - typeof sntpIntervalRaw === "number" - ? sntpIntervalRaw - : Number.parseInt(String(sntpIntervalRaw).trim(), 10); + typeof sntpIntervalRaw === "number" ? sntpIntervalRaw : Number.parseInt(String(sntpIntervalRaw).trim(), 10); if (!Number.isFinite(sntpInterval)) { sntpInterval = 60; diff --git a/src/components/SettingForms/System/SystemConfigFields.tsx b/src/components/SettingForms/System/SystemConfigFields.tsx index 21a280e..7d8951f 100644 --- a/src/components/SettingForms/System/SystemConfigFields.tsx +++ b/src/components/SettingForms/System/SystemConfigFields.tsx @@ -4,21 +4,29 @@ import { useReboots } from "../../../hooks/useReboots"; import { timezones } from "./timezones"; import SystemFileUpload from "./SystemFileUpload"; import type { SystemValues, SystemValuesErrors } from "../../../types/types"; -import { useSystemConfig } from "../../../hooks/useSystemConfig"; +import { useDNSSettings, useSystemConfig } from "../../../hooks/useSystemConfig"; const SystemConfigFields = () => { const { saveSystemSettings, systemSettingsData, saveSystemSettingsLoading } = useSystemConfig(); - const { softRebootMutation, hardRebootMutation } = useReboots(); + const { hardRebootMutation } = useReboots(); + const { dnsQuery, dnsMutation } = useDNSSettings(); + const dnsPrimary = dnsQuery?.data?.propNameServerPrimary?.value; + const dnsSecondary = dnsQuery?.data?.propNameServerSecondary?.value; const initialvalues: SystemValues = { deviceName: systemSettingsData?.deviceName ?? "", timeZone: systemSettingsData?.timeZone ?? "", sntpServer: systemSettingsData?.sntpServer ?? "", sntpInterval: systemSettingsData?.sntpInterval ?? 60, + serverPrimary: dnsPrimary ?? "", + serverSecondary: dnsSecondary ?? "", softwareUpdate: null, }; - const handleSubmit = (values: SystemValues) => saveSystemSettings(values); + const handleSubmit = async (values: SystemValues) => { + saveSystemSettings(values); + await dnsMutation.mutateAsync(values); + }; const validateValues = (values: SystemValues) => { const errors: SystemValuesErrors = {}; @@ -30,9 +38,9 @@ const SystemConfigFields = () => { return errors; }; - const handleSoftReboot = async () => { - await softRebootMutation.mutate(); - }; + // const handleSoftReboot = async () => { + // await softRebootMutation.mutate(); + // }; const handleHardReboot = async () => { await hardRebootMutation.mutate(); @@ -102,6 +110,7 @@ const SystemConfigFields = () => { autoComplete="off" /> + + + + + + + + + + +
- + */} - - )} - - )} + + + +
+ + setShowPwd((s) => !s)} + icon={showPwd ? faEyeSlash : faEye} + /> +
+
+ + + + + + + + + + )} + + + )} + ); }; diff --git a/src/components/SightingsWidget/SightingWidget.tsx b/src/components/SightingsWidget/SightingWidget.tsx index 11827b8..7b1059b 100644 --- a/src/components/SightingsWidget/SightingWidget.tsx +++ b/src/components/SightingsWidget/SightingWidget.tsx @@ -59,11 +59,10 @@ export default function SightingHistoryWidget({ className, title }: SightingHist isLoading, } = useSightingFeedContext(); - const { dispatch } = useAlertHitContext(); + const { dispatch, state: alertState } = useAlertHitContext(); const { state: integrationState, dispatch: integrationDispatch } = useIntegrationsContext(); const sessionStarted = integrationState.sessionStarted; const sessionPaused = integrationState.sessionPaused; - const processedRefs = useRef>(new Set()); const hasAutoOpenedRef = useRef(false); @@ -72,6 +71,11 @@ export default function SightingHistoryWidget({ className, title }: SightingHist const enqueue = useCallback((sighting: SightingType, kind: HitKind) => { const id = sighting.vrm ?? sighting.ref; if (processedRefs.current.has(id)) return; + + const inList = alertState?.alertList?.find((sighting) => sighting.vrm === id); + if (inList) { + return; + } processedRefs.current.add(id); setModalQueue((q) => [...q, { id, sighting, kind }]); @@ -154,7 +158,6 @@ export default function SightingHistoryWidget({ className, title }: SightingHist if (firstNPED) { enqueue(firstNPED, "NPED"); - npedRef.current = true; } @@ -173,6 +176,7 @@ export default function SightingHistoryWidget({ className, title }: SightingHist else hotlistsound(); setSelectedSighting(next.sighting); + setSightingModalOpen(true); } }, [isSightingModalOpen, npedSound, hotlistsound, setSelectedSighting, setSightingModalOpen, modalQueue]); diff --git a/src/components/UI/NavigationArrow.tsx b/src/components/UI/NavigationArrow.tsx index e133a88..6913c6f 100644 --- a/src/components/UI/NavigationArrow.tsx +++ b/src/components/UI/NavigationArrow.tsx @@ -17,9 +17,9 @@ const NavigationArrow = ({ side, settingsPage }: NavigationArrowProps) => { } if (side === "Front") { - navigate("/camera-settings"); + navigate("/a-camera-settings"); } else if (side === "Rear") { - navigate("/Rear-Camera-settings"); + navigate("/b-Camera-settings"); } }; @@ -30,15 +30,15 @@ const NavigationArrow = ({ side, settingsPage }: NavigationArrowProps) => { navigationDest("Front")} + className="absolute top-[50%] right-[2%] backdrop-blur-lg hover:cursor-pointer animate-bounce z-30 rounded-md arrow-outline" + onClick={() => navigationDest("a")} /> ) : ( navigationDest(side)} + className="absolute top-[50%] left-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce z-30 rounded-md arrow-outline" + onClick={() => navigationDest("b")} /> )} @@ -49,14 +49,14 @@ const NavigationArrow = ({ side, settingsPage }: NavigationArrowProps) => { navigationDest("Front")} /> navigationDest("Rear")} /> diff --git a/src/hooks/useBackOfficeConfig.ts b/src/hooks/useBackOfficeConfig.ts index 63f936b..f7f8d43 100644 --- a/src/hooks/useBackOfficeConfig.ts +++ b/src/hooks/useBackOfficeConfig.ts @@ -36,7 +36,7 @@ const updateBackOfficeConfig = async (data: InitialValuesForm) => { }, ], }; - const response = await fetch(`${CAM_BASE}/api/update-config?id=Dispatcher`, { + const response = await fetch(`${CAM_BASE}/api/update-config`, { method: "POST", body: JSON.stringify(updateConfigPayload), }); diff --git a/src/hooks/useCameraBlackboard.ts b/src/hooks/useCameraBlackboard.ts index 6c31236..0608ee8 100644 --- a/src/hooks/useCameraBlackboard.ts +++ b/src/hooks/useCameraBlackboard.ts @@ -8,7 +8,7 @@ const camBase = import.meta.env.MODE !== "development" ? CAM_BASE : ""; const getAllBlackboardData = async () => { const response = await fetch(`${camBase}/api/blackboard`, { - signal: AbortSignal.timeout(500), + signal: AbortSignal.timeout(300000), }); if (!response.ok) { throw new Error("Failed to fetch blackboard data"); diff --git a/src/hooks/useCameraConfig.ts b/src/hooks/useCameraConfig.ts index 4396ad3..cd78a43 100644 --- a/src/hooks/useCameraConfig.ts +++ b/src/hooks/useCameraConfig.ts @@ -8,7 +8,7 @@ const fetchCameraSideConfig = async ({ queryKey }: { queryKey: string[] }) => { const [, cameraSide] = queryKey; const fetchUrl = `${base_url}/fetch-config?id=${cameraSide}`; const response = await fetch(fetchUrl, { - signal: AbortSignal.timeout(500), + signal: AbortSignal.timeout(300000), }); if (!response.ok) throw new Error("cannot react cameraSide "); return response.json(); @@ -31,7 +31,7 @@ const updateCamerasideConfig = async (data: { id: string | number; friendlyName: method: "POST", body: JSON.stringify(updateConfigPayload), }); - if (!response.ok) throw new Error("Feature unavailable: Coming soon"); + if (!response.ok) throw new Error("Please make sure fields are filled in correctly"); }; export const useFetchCameraConfig = (cameraSide: string) => { diff --git a/src/hooks/useCameraOutput.ts b/src/hooks/useCameraOutput.ts index 96c9412..4bdb9ca 100644 --- a/src/hooks/useCameraOutput.ts +++ b/src/hooks/useCameraOutput.ts @@ -2,7 +2,7 @@ import { useMutation, useQuery } from "@tanstack/react-query"; import { CAM_BASE } from "../utils/config"; import { useEffect } from "react"; import { toast } from "sonner"; -import type { BearerTypeFieldType, OptionalBOF2Constants } from "../types/types"; +import type { BearerTypeFieldType, OptionalBOF2Constants, OptionalBOF2LaneIDs } from "../types/types"; const getDispatcherConfig = async () => { const response = await fetch(`${CAM_BASE}/api/fetch-config?id=Dispatcher`); @@ -24,7 +24,7 @@ const updateDispatcherConfig = async (data: BearerTypeFieldType) => { }, ], }; - const response = await fetch(`${CAM_BASE}/api/update-config?id=Dispatcher`, { + const response = await fetch(`${CAM_BASE}/api/update-config`, { method: "POST", body: JSON.stringify(updateConfigPayload), }); @@ -54,7 +54,7 @@ const updateBackOfficeDispatcher = async (data: OptionalBOF2Constants) => { }, ], }; - const response = await fetch(`${CAM_BASE}/api/update-config?id=Dispatcher-bof2-constants`, { + const response = await fetch(`${CAM_BASE}/api/update-config`, { method: "POST", body: JSON.stringify(bof2ContantsPayload), }); @@ -63,11 +63,40 @@ const updateBackOfficeDispatcher = async (data: OptionalBOF2Constants) => { }; const getBof2DispatcherData = async () => { - const response = await fetch(`http://100.118.196.113:8080/api/fetch-config?id=Dispatcher-bof2-constants`); + const response = await fetch(`${CAM_BASE}/api/fetch-config?id=Dispatcher-bof2-constants`); if (!response.ok) throw new Error("Cannot get BOF2 dispatcher config"); return response.json(); }; +const updateBOF2LaneId = async (data: OptionalBOF2LaneIDs) => { + const bof2LaneIds = { + id: data?.laneId, + fields: [ + { + property: "propLaneID1", + value: data?.LID1, + }, + { + property: "propLaneID2", + value: data?.LID2, + }, + ], + }; + + const response = await fetch(`${CAM_BASE}/api/update-config`, { + method: "post", + body: JSON.stringify(bof2LaneIds), + }); + if (!response.ok) throw new Error("cannot send to lane IDs"); + return response.json(); +}; + +const getBOF2LaneId = async () => { + const response = await fetch(`${CAM_BASE}/api/fetch-config?id=SightingAmmendA-lane-ids`); + if (!response.ok) throw new Error("Canot get Lane Ids"); + return response.json(); +}; + export const useCameraOutput = () => { const dispatcherQuery = useQuery({ queryKey: ["dispatcher"], @@ -95,6 +124,16 @@ export const useCameraOutput = () => { }, }); + const bof2LandMutation = useMutation({ + mutationKey: ["updateBOF2LaneId"], + mutationFn: updateBOF2LaneId, + }); + + const laneIdQuery = useQuery({ + queryKey: ["getBOF2LaneId"], + queryFn: getBOF2LaneId, + }); + useEffect(() => { if (dispatcherQuery.isError) toast.error(dispatcherQuery.error.message); }, [dispatcherQuery?.error?.message, dispatcherQuery.isError]); @@ -103,6 +142,8 @@ export const useCameraOutput = () => { dispatcherQuery, dispatcherMutation, backOfficeDispatcherMutation, + bof2LandMutation, + laneIdQuery, }; }; diff --git a/src/hooks/useCameraWifiandModem.ts b/src/hooks/useCameraWifiandModem.ts index 6e5edce..13a15de 100644 --- a/src/hooks/useCameraWifiandModem.ts +++ b/src/hooks/useCameraWifiandModem.ts @@ -3,10 +3,11 @@ import { CAM_BASE } from "../utils/config"; import type { ModemConfig, WifiConfig } from "../types/types"; import { useEffect } from "react"; import { toast } from "sonner"; +const camBase = import.meta.env.MODE !== "development" ? CAM_BASE : ""; const getWiFiSettings = async () => { - const response = await fetch(`${CAM_BASE}/api/fetch-config?id=ModemAndWifiManager-wifi`, { - signal: AbortSignal.timeout(500), + const response = await fetch(`${camBase}/api/fetch-config?id=ModemAndWifiManager-wifi`, { + signal: AbortSignal.timeout(600000), }); if (!response.ok) { throw new Error("Cannot fetch Wifi settings"); @@ -15,7 +16,7 @@ const getWiFiSettings = async () => { }; const updateWifiSettings = async (wifiConfig: WifiConfig) => { - const response = await fetch(`${CAM_BASE}/api/update-config?id=ModemAndWifiManager-wifi`, { + const response = await fetch(`${camBase}/api/update-config`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(wifiConfig), @@ -27,8 +28,8 @@ 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(`${camBase}/api/fetch-config?id=ModemAndWifiManager-modem`, { + signal: AbortSignal.timeout(600000), }); if (!response.ok) { throw new Error("Cannot fetch modem settings"); @@ -37,7 +38,7 @@ const getModemSettings = async () => { }; const updateModemSettings = async (modemConfig: ModemConfig) => { - const response = await fetch(`${CAM_BASE}/api/update-config?id=ModemAndWifiManager-modem`, { + const response = await fetch(`${camBase}/api/update-config`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(modemConfig), diff --git a/src/hooks/useCameraZoom.ts b/src/hooks/useCameraZoom.ts index fb02523..e7e174a 100644 --- a/src/hooks/useCameraZoom.ts +++ b/src/hooks/useCameraZoom.ts @@ -1,18 +1,39 @@ -import { - useMutation, - useQuery, - type QueryFunctionContext, -} from "@tanstack/react-query"; +import { useMutation, useQuery, type QueryFunctionContext } from "@tanstack/react-query"; import { CAM_BASE } from "../utils/config"; import type { zoomConfig, ZoomInOptions } from "../types/types"; import { toast } from "sonner"; import { useEffect } from "react"; +const getCameraMode = async (options: { camera: string }) => { + const response = await fetch(`${CAM_BASE}/api/fetch-config?id=Ip${options.camera}`); + if (!response.ok) throw new Error("Cannot get camera mode"); + return response.json(); +}; + +const updateCameraMode = async (options: { camera: string; mode: string }) => { + console.log(options); + const dayNightPayload = { + id: options.camera, + fields: [ + { + property: "propDayNightMode", + value: options.mode, + }, + ], + }; + const response = await fetch(`${CAM_BASE}/Ip${options.camera}-command?dayNightMode=${options.mode}`, { + method: "post", + body: JSON.stringify(dayNightPayload), + }); + if (!response.ok) throw new Error("cannot update camera mode"); + return response.json(); +}; + async function zoomIn(options: ZoomInOptions) { const response = await fetch( - `${CAM_BASE}/Ip${options.camera}-command?magnification=${options.multiplier}x`, + `${CAM_BASE}/Ip${options.camera}-command?magnification=${options.multiplierText?.toLowerCase()}`, { - signal: AbortSignal.timeout(500), + signal: AbortSignal.timeout(300000), } ); if (!response.ok) { @@ -22,28 +43,21 @@ async function zoomIn(options: ZoomInOptions) { return response.json(); } -async function fetchZoomInConfig({ - queryKey, -}: QueryFunctionContext<[string, zoomConfig]>) { +async function fetchZoomInConfig({ queryKey }: QueryFunctionContext<[string, zoomConfig]>) { const [, { camera }] = queryKey; - const response = await fetch(`${CAM_BASE}/Ip${camera}-inspect`, { - signal: AbortSignal.timeout(500), + const response = await fetch(`${CAM_BASE}/api/fetch-config?id=Ip${camera}`, { + signal: AbortSignal.timeout(300000), }); if (!response.ok) { throw new Error("Cannot get camera zoom settings"); } - return response.text(); + return response.json(); } //change to string export const useCameraZoom = (options: zoomConfig) => { const mutation = useMutation({ mutationKey: ["zoomIn"], mutationFn: (options: ZoomInOptions) => zoomIn(options), - onError: (err) => { - toast.error(`Failed to update zoom settings: ${err.message}`, { - id: "zoom", - }); - }, }); const query = useQuery({ @@ -52,8 +66,25 @@ export const useCameraZoom = (options: zoomConfig) => { }); useEffect(() => { - if (query.isError) toast.error(query.error.message, { id: "hardReboot" }); + if (query.isError) toast.error(query.error.message, { id: "zoom" }); }, [query?.error?.message, query.isError]); return { mutation, query }; }; + +export const useCameraMode = (option: { camera: string }) => { + const cameraModeQuery = useQuery({ + queryKey: ["getCameraMode"], + queryFn: () => getCameraMode(option), + }); + + const cameraModeMutation = useMutation({ + mutationKey: ["updateCameraMode"], + mutationFn: updateCameraMode, + }); + + return { + cameraModeQuery, + cameraModeMutation, + }; +}; diff --git a/src/hooks/useGetOverviewSnapshot copy.ts b/src/hooks/useGetOverviewSnapshot copy.ts new file mode 100644 index 0000000..6bed2e3 --- /dev/null +++ b/src/hooks/useGetOverviewSnapshot copy.ts @@ -0,0 +1,153 @@ +import { useRef, useCallback, useEffect } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { CAM_BASE } from "../utils/config"; + +const apiUrl = CAM_BASE; + +async function fetchSnapshot(cameraSide: string): Promise { + const response = await fetch(`${apiUrl}/${cameraSide}-preview`, { + signal: AbortSignal.timeout(300000), + cache: "no-store", + }); + if (!response.ok) { + throw new Error(`Cannot reach endpoint (${response.status})`); + } + return response.blob(); +} + +/** Draw an ImageBitmap to canvas with aspect-fill (like object-fit: cover) */ +function drawBitmapToCanvas(canvas: HTMLCanvasElement, bitmap: ImageBitmap) { + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + const dpr = window.devicePixelRatio || 1; + const cssWidth = canvas.clientWidth; + const cssHeight = canvas.clientHeight; + + const width = Math.floor(cssWidth * dpr); + const height = Math.floor(cssHeight * dpr); + + if (canvas.width !== width || canvas.height !== height) { + canvas.width = width; + canvas.height = height; + } + + ctx.clearRect(0, 0, width, height); + + const srcW = bitmap.width; + const srcH = bitmap.height; + const srcAspect = srcW / srcH; + const dstAspect = width / height; + + let drawWidth = width; + let drawHeight = height; + + // aspect-fit calculation (no cropping) + if (srcAspect > dstAspect) { + // image is wider → fit to canvas width + drawWidth = width; + drawHeight = width / srcAspect; + } else { + // image is taller → fit to canvas height + drawHeight = height; + drawWidth = height * srcAspect; + } + + // center image (adds black borders if aspect ratios differ) + const dx = (width - drawWidth) / 50; + const dy = (height - drawHeight) / 2; + + ctx.imageSmoothingEnabled = true; + ctx.imageSmoothingQuality = "high"; + ctx.drawImage(bitmap, 0, 0, srcW, srcH, dx, dy, drawWidth, drawHeight); +} + +export function useGetOverviewSnapshot(side: string) { + const canvasRef = useRef(null); + const latestBitmapRef = useRef(null); + + // Redraw helper; always draws the current bitmap if available + const draw = useCallback(() => { + const canvas = canvasRef.current; + const bmp = latestBitmapRef.current; + if (!canvas || !bmp) return; + drawBitmapToCanvas(canvas, bmp); + }, []); + + const { + data: snapshotBlob, + isError, + error, + isPending, + } = useQuery({ + queryKey: ["overviewSnapshot", side], + queryFn: () => fetchSnapshot(side), + // Poll ~4 fps when visible; pause when tab hidden + refetchInterval: () => (document.visibilityState === "visible" ? 250 : false), + refetchOnWindowFocus: false, + // Avoid keeping lots of blobs around in cache + gcTime: 0, // v5 name (cacheTime in v4) + staleTime: 0, + retry: false, // or a small number if you prefer retries + }); + + // Convert Blob -> ImageBitmap and draw + useEffect(() => { + let cancelled = false; + if (!snapshotBlob) return; + + (async () => { + try { + const bitmap = await createImageBitmap(snapshotBlob); + if (cancelled) { + bitmap.close(); + return; + } + + // Dispose previous bitmap to free memory + if (latestBitmapRef.current) { + latestBitmapRef.current.close(); + } + latestBitmapRef.current = bitmap; + + // Draw now (and again on next resize) + draw(); + } catch { + // noop — fetch handler surfaces the main error path + } + })(); + + return () => { + cancelled = true; + }; + }, [snapshotBlob, draw]); + + // Redraw on resize & DPR changes + useEffect(() => { + const onResize = () => draw(); + const onDPR = () => draw(); + window.addEventListener("resize", onResize); + // Listen for DPR changes (some browsers support this) + const mql = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`); + mql.addEventListener?.("change", onDPR); + return () => { + window.removeEventListener("resize", onResize); + mql.removeEventListener?.("change", onDPR); + }; + }, [draw]); + + // Cleanup on unmount + useEffect(() => { + return () => { + if (latestBitmapRef.current) { + latestBitmapRef.current.close(); + latestBitmapRef.current = null; + } + }; + }, []); + + // Optional: normalize error type + const typedError = error instanceof Error ? error : undefined; + + return { canvasRef, isError, error: typedError, isPending }; +} diff --git a/src/hooks/useGetOverviewSnapshot.ts b/src/hooks/useGetOverviewSnapshot.ts index 294e8ab..3b1f513 100644 --- a/src/hooks/useGetOverviewSnapshot.ts +++ b/src/hooks/useGetOverviewSnapshot.ts @@ -3,34 +3,75 @@ import { useQuery } from "@tanstack/react-query"; import { CAM_BASE } from "../utils/config"; const apiUrl = CAM_BASE; -// const fetch_url = `http://100.82.205.44/Colour-preview`; -async function fetchSnapshot(cameraSide: string) { + +async function fetchSnapshot(cameraSide: string): Promise { const response = await fetch(`${apiUrl}/${cameraSide}-preview`, { - signal: AbortSignal.timeout(500), + signal: AbortSignal.timeout(300000), + cache: "no-store", }); if (!response.ok) { - throw new Error("Cannot reach endpoint"); + throw new Error(`Cannot reach endpoint (${response.status})`); + } + return response.blob(); +} + +/** Draw an ImageBitmap to canvas with aspect-fill (like object-fit: cover) */ +function drawBitmapToCanvas(canvas: HTMLCanvasElement, bitmap: ImageBitmap) { + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + const dpr = window.devicePixelRatio || 1; + const cssWidth = canvas.clientWidth; + const cssHeight = canvas.clientHeight; + + const width = Math.floor(cssWidth * dpr); + const height = Math.floor(cssHeight * dpr); + + if (canvas.width !== width || canvas.height !== height) { + canvas.width = width; + canvas.height = height; } - return await response.blob(); + ctx.clearRect(0, 0, width, height); + + const srcW = bitmap.width; + const srcH = bitmap.height; + const srcAspect = srcW / srcH; + const dstAspect = width / height; + + let drawWidth = width; + let drawHeight = height; + + // aspect-fit calculation (no cropping) + if (srcAspect > dstAspect) { + // image is wider → fit to canvas width + drawWidth = width; + drawHeight = width / srcAspect; + } else { + // image is taller → fit to canvas height + drawHeight = height; + drawWidth = height * srcAspect; + } + + // center image (adds black borders if aspect ratios differ) + const dx = (width - drawWidth) / 50; + const dy = (height - drawHeight) / 2; + + ctx.imageSmoothingEnabled = true; + ctx.imageSmoothingQuality = "high"; + ctx.drawImage(bitmap, 0, 0, srcW, srcH, dx, dy, drawWidth, drawHeight); } export function useGetOverviewSnapshot(side: string) { - const latestUrlRef = useRef(null); const canvasRef = useRef(null); - const imageRef = useRef(null); + const latestBitmapRef = useRef(null); - const drawImage = useCallback(() => { + // Redraw helper; always draws the current bitmap if available + const draw = useCallback(() => { const canvas = canvasRef.current; - const ctx = canvas?.getContext("2d"); - const img = imageRef.current; - - if (!canvas || !ctx || !img) return; - - canvas.width = canvas.clientWidth; - canvas.height = canvas.clientHeight; - - ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + const bmp = latestBitmapRef.current; + if (!canvas || !bmp) return; + drawBitmapToCanvas(canvas, bmp); }, []); const { @@ -39,43 +80,82 @@ export function useGetOverviewSnapshot(side: string) { error, isPending, } = useQuery({ - queryKey: ["overviewSnapshot"], + queryKey: ["overviewSnapshot", side], queryFn: () => fetchSnapshot(side), + // Poll ~4 fps when visible; pause when tab hidden + refetchInterval: () => (document.visibilityState === "visible" ? 250 : false), refetchOnWindowFocus: false, - refetchInterval: 250, + // Avoid keeping lots of blobs around in cache + gcTime: 0, // v5 name (cacheTime in v4) + staleTime: 0, + retry: false, // or a small number if you prefer retries }); + // Convert Blob -> ImageBitmap and draw useEffect(() => { + let cancelled = false; if (!snapshotBlob) return; - const imgUrl = URL.createObjectURL(snapshotBlob); - const img = new Image(); - imageRef.current = img; + (async () => { + try { + const bitmap = await createImageBitmap(snapshotBlob); + if (cancelled) { + bitmap.close(); + return; + } - img.onload = () => { - drawImage(); - }; - img.src = imgUrl; + // Dispose previous bitmap to free memory + if (latestBitmapRef.current) { + latestBitmapRef.current.close(); + } + latestBitmapRef.current = bitmap; - if (latestUrlRef.current) { - URL.revokeObjectURL(latestUrlRef.current); - } - latestUrlRef.current = imgUrl; + // Draw now (and again on next resize) + draw(); + } catch { + // noop — fetch handler surfaces the main error path + } + })(); return () => { - if (latestUrlRef.current) { - URL.revokeObjectURL(latestUrlRef.current); - latestUrlRef.current = null; - } + cancelled = true; }; - }, [snapshotBlob, drawImage]); + }, [snapshotBlob, draw]); + + // Redraw on resize & DPR changes + useEffect(() => { + const onResize = () => draw(); + const onDPR = () => draw(); + window.addEventListener("resize", onResize); + // Listen for DPR changes (some browsers support this) + const mql = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`); + mql.addEventListener?.("change", onDPR); + return () => { + window.removeEventListener("resize", onResize); + mql.removeEventListener?.("change", onDPR); + }; + }, [draw]); useEffect(() => { - window.addEventListener("resize", drawImage); - return () => { - window.removeEventListener("resize", drawImage); - }; - }, [drawImage]); + const el = canvasRef.current?.parentElement; // the box + if (!el) return; + const ro = new ResizeObserver(() => draw()); // your draw() calls aspect-fit logic + ro.observe(el); + return () => ro.disconnect(); + }, [draw]); - return { canvasRef, isError, error, isPending }; + // Cleanup on unmount + useEffect(() => { + return () => { + if (latestBitmapRef.current) { + latestBitmapRef.current.close(); + latestBitmapRef.current = null; + } + }; + }, []); + + // Optional: normalize error type + const typedError = error instanceof Error ? error : undefined; + + return { canvasRef, isError, error: typedError, isPending }; } diff --git a/src/hooks/useNPEDAuth.ts b/src/hooks/useNPEDAuth.ts index 31c7ea7..1b4c855 100644 --- a/src/hooks/useNPEDAuth.ts +++ b/src/hooks/useNPEDAuth.ts @@ -8,7 +8,7 @@ import { toast } from "sonner"; async function fetchNPEDDetails() { const fetchUrl = `${CAM_BASE}/api/fetch-config?id=NPED`; const response = await fetch(fetchUrl, { - signal: AbortSignal.timeout(500), + signal: AbortSignal.timeout(300000), }); if (!response.ok) throw new Error("Cannot reach fetch-config endpoint"); diff --git a/src/hooks/useSightingAmend.ts b/src/hooks/useSightingAmend.ts new file mode 100644 index 0000000..4115efa --- /dev/null +++ b/src/hooks/useSightingAmend.ts @@ -0,0 +1,48 @@ +import { useMutation, useQuery } from "@tanstack/react-query"; +import { CAM_BASE } from "../utils/config"; +import type { InitialValuesForm } from "../types/types"; + +const getSightingAmend = async () => { + const response = await fetch(`${CAM_BASE}/api/fetch-config?id=SightingAmmendA`); + if (!response.ok) throw new Error("Cannot reach sighting amend endpoint"); + return response.json(); +}; + +const updateSightingAmend = async (data: InitialValuesForm) => { + const updateSightingAmendPayload = { + id: "SightingAmmendA", + fields: [ + { + property: "propOverviewQuality", + value: data.overviewQuality, + }, + { + property: "propOverviewImageScaleFactor", + value: data.cropSizeFactor, + }, + ], + }; + const response = await fetch(`${CAM_BASE}/api/update-config`, { + method: "Post", + body: JSON.stringify(updateSightingAmendPayload), + }); + if (!response.ok) throw new Error("cannot update camera control"); + return response.json(); +}; + +export const useSightingAmend = () => { + const sightingAmendQuery = useQuery({ + queryKey: ["getSightingAmend"], + queryFn: getSightingAmend, + }); + + const sightingAmendMutation = useMutation({ + mutationKey: ["updateSightingAmend"], + mutationFn: updateSightingAmend, + }); + + return { + sightingAmendQuery, + sightingAmendMutation, + }; +}; diff --git a/src/hooks/useSightingFeed.ts b/src/hooks/useSightingFeed.ts index 520b4c0..38eca48 100644 --- a/src/hooks/useSightingFeed.ts +++ b/src/hooks/useSightingFeed.ts @@ -12,7 +12,7 @@ import { useCachedSoundSrc } from "./usecachedSoundSrc"; async function fetchSighting(url: string | undefined, ref: number): Promise { const res = await fetch(`${url}${ref}`, { - signal: AbortSignal.timeout(5000), + signal: AbortSignal.timeout(300000), }); if (!res.ok) throw new Error(String(res.status)); return res.json(); @@ -48,7 +48,7 @@ export function useSightingFeed(url: string | undefined) { return 100; } - if (now - lastValidTimestamp.current > 60_000) { + if (now - lastValidTimestamp.current > 600_000) { currentRef.current = -1; lastValidTimestamp.current = now; } diff --git a/src/hooks/useStoreDispatch.ts b/src/hooks/useStoreDispatch.ts new file mode 100644 index 0000000..b3c181c --- /dev/null +++ b/src/hooks/useStoreDispatch.ts @@ -0,0 +1,19 @@ +import { useQuery } from "@tanstack/react-query"; +import { CAM_BASE } from "../utils/config"; + +const getStoreData = async () => { + const response = await fetch(`${CAM_BASE}/Store/diagnostics-json`); + if (!response.ok) throw new Error("Cannot get store data"); + + return response.json(); +}; + +export const useStoreDispatch = () => { + const storeQuery = useQuery({ + queryKey: ["getStoreData"], + queryFn: getStoreData, + refetchInterval: 1000, + refetchOnWindowFocus: true, + }); + return { storeQuery }; +}; diff --git a/src/hooks/useSystemConfig.ts b/src/hooks/useSystemConfig.ts index 9fd4b3c..989251f 100644 --- a/src/hooks/useSystemConfig.ts +++ b/src/hooks/useSystemConfig.ts @@ -1,11 +1,41 @@ import { useMutation, useQuery } from "@tanstack/react-query"; import { sendBlobFileUpload } from "../components/SettingForms/System/Upload"; import { toast } from "sonner"; -import { - handleSystemSave, - handleSystemRecall, -} from "../components/SettingForms/System/SettingSaveRecall"; +import { handleSystemSave, handleSystemRecall } from "../components/SettingForms/System/SettingSaveRecall"; import { useEffect } from "react"; +import { CAM_BASE } from "../utils/config"; +import type { DNSSettingsType } from "../types/types"; + +const camBase = import.meta.env.MODE !== "development" ? CAM_BASE : ""; + +const getDNSSettings = async () => { + const response = await fetch(`${camBase}/api/fetch-config?id=GLOBAL--NetworkConfig`); + if (!response.ok) throw new Error("Cannot get DNS Settings"); + return response.json(); +}; + +const updateDNSSettings = async (data: DNSSettingsType) => { + const dnsSettingsPayload = { + id: "GLOBAL--NetworkConfig", + fields: [ + { + property: "propNameServerPrimary", + value: data?.serverPrimary, + }, + { + property: "propNameServerSecondary", + value: data?.serverSecondary, + }, + ], + }; + const response = await fetch(`${camBase}/api/update-config`, { + method: "post", + body: JSON.stringify(dnsSettingsPayload), + }); + if (!response.ok) throw new Error("cannot send to DNS endpoint"); + + return response.json(); +}; export const useSystemConfig = () => { const uploadSettingsMutation = useMutation({ @@ -51,3 +81,20 @@ export const useSystemConfig = () => { saveSystemSettingsLoading: saveSystemSettings.isPending, }; }; + +export const useDNSSettings = () => { + const dnsQuery = useQuery({ + queryKey: ["getDNSSettings"], + queryFn: getDNSSettings, + }); + + const dnsMutation = useMutation({ + mutationKey: ["updateDNSSettings"], + mutationFn: updateDNSSettings, + }); + + return { + dnsQuery, + dnsMutation, + }; +}; diff --git a/src/index.css b/src/index.css index 326a490..92bfa28 100644 --- a/src/index.css +++ b/src/index.css @@ -31,3 +31,9 @@ body { } } } + +.arrow-outline path { + stroke: black; /* outline color */ + stroke-width: 20px; /* thickness of outline (tweak this) */ + stroke-linejoin: round; +} diff --git a/src/pages/FrontCamera.tsx b/src/pages/FrontCamera.tsx index 2e63620..b3ce215 100644 --- a/src/pages/FrontCamera.tsx +++ b/src/pages/FrontCamera.tsx @@ -4,7 +4,7 @@ import OverviewVideoContainer from "../components/FrontCameraSettings/OverviewVi import { Toaster } from "sonner"; const FrontCamera = () => { - const [zoomLevel, setZoomLevel] = useState(1); + const [zoomLevel, setZoomLevel] = useState(1); return (
{ - const [zoomLevel, setZoomLevel] = useState(1); + const [zoomLevel, setZoomLevel] = useState(1); return ( -
+
>; @@ -58,6 +59,8 @@ export type InitialValuesForm = { password: string; connectTimeoutSeconds: number; readTimeoutSeconds: number; + overviewQuality?: string; + cropSizeFactor?: string; }; export type InitialValuesFormErrors = { @@ -69,10 +72,17 @@ export type InitialValuesFormErrors = { }; export type OptionalBOF2Constants = { - FFID?: ""; - SCID?: ""; - timestampSource?: ""; - GPSFormat?: ""; + FFID?: string; + SCID?: string; + timestampSource?: string; + GPSFormat?: string; +}; + +export type OptionalBOF2LaneIDs = { + laneId?: string; + LID1?: string; + LID2?: string; + LID3?: string; }; export type NPEDFieldType = { @@ -157,6 +167,13 @@ export type SystemValues = { sntpInterval: number; timeZone: string; softwareUpdate?: File | null; + serverPrimary?: string; + serverSecondary?: string; +}; + +export type DNSSettingsType = { + serverPrimary?: string; + serverSecondary?: string; }; export type SystemValuesErrors = { @@ -374,6 +391,7 @@ export type ModemConfig = { export type ZoomInOptions = { camera: string; multiplier: number; + multiplierText?: string; }; export type zoomConfig = { @@ -407,5 +425,6 @@ export type NPEDSTATE = { export type NPEDACTION = { type: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any payload: any; }; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 4f869e5..ed80a28 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -158,3 +158,29 @@ export function getHotlistName(obj: HotlistMatches | undefined) { export const getNPEDCategory = (r?: SightingType | null) => r?.metadata?.npedJSON?.["NPED CATEGORY"] as "A" | "B" | "C" | "D" | undefined; + +export const zoomMapping = (zoomLevel: number | undefined) => { + switch (zoomLevel) { + case 1: + return "Near"; + case 2: + return "Mid"; + case 4: + return "Far"; + default: + break; + } +}; + +export const reverseZoomMapping = (magnification: string) => { + switch (magnification) { + case "near": + return 1; + case "mid": + return 2; + case "far": + return 4; + default: + break; + } +};