-address fixes and changes per feedback from Matt and Brad

This commit is contained in:
2025-11-05 16:30:27 +00:00
parent 861f2dd31d
commit d57ad1003a
15 changed files with 215 additions and 63 deletions

View File

@@ -4,7 +4,7 @@ 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 { useCameraMode, useCameraZoom } from "../../hooks/useCameraZoom";
import { parseRTSPUrl, reverseZoomMapping, zoomMapping } from "../../utils/utils";
type CameraSettingsProps = {
@@ -20,16 +20,21 @@ 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 { 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;
const handleModeClick = async (mode: string) => {
await cameraModeMutation.mutate({ camera: cameraControllerSide, mode });
};
useEffect(() => {
if (!query?.data) return;
onZoomLevelChange?.(apiZoom);
@@ -194,7 +199,30 @@ const CameraSettingFields = ({
))}
</div>
</div>
<div className="mt-3">
<div>
<CardHeader title="Mode" />
<div className="mx-auto grid grid-cols-2 place-items-center">
<button
type="button"
onClick={() => handleModeClick("day")}
className={`px-8 py-2 rounded-md border border-gray-300 ${
cameraMode === "day" ? "bg-gray-100 text-blue-600" : ""
}`}
>
Day
</button>
<button
type="button"
onClick={() => handleModeClick("night")}
className={`px-8 py-2 rounded-md border border-gray-300 ${
cameraMode === "night" ? "bg-gray-100 text-blue-600" : ""
}`}
>
Night
</button>
</div>
</div>
{/* <div className="mt-3">
{updateCameraConfigError ? (
<button className="bg-red-500 text-white rounded-lg p-2 mx-auto h-[100%] w-full" disabled>
Retry
@@ -205,11 +233,10 @@ const CameraSettingFields = ({
className="bg-blue-700 text-white rounded-lg p-2 mx-auto h-[100%] w-full"
disabled
>
{/* {isSubmitting ? "Saving" : "Save settings"} bg-[#26B170] */}
{"Disabled: Coming soon"}
{isSubmitting ? "Saving" : "Save settings"}
</button>
)}
</div>
</div> */}
</div>
</Form>
)}

View File

@@ -18,7 +18,7 @@ const CameraSettings = ({
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">
<Card className="overflow-x-auto min-h-[40vh] md:min-h-[60vh] max-h-[83vh] lg:w-[40%] p-4">
<div className="relative flex flex-col space-y-3">
<CardHeader title={title} icon={faWrench} />

View File

@@ -115,7 +115,35 @@ const ChannelFields = ({ touched, isSubmitting, format }: ChannelFieldsProps) =>
} rounded-lg w-full md:w-60`}
/>
</FormGroup>
{/* Overview quality and scale */}
<FormGroup>
<label htmlFor="overviewQuality">Overview quality and scale</label>
<Field
name={"overviewQuality"}
as="select"
id="overviewQuality"
className="p-2 border border-gray-400 rounded-lg text-white bg-[#253445] w-full md:w-60"
>
<option value={"HIGH"}>High</option>
<option value={"MEDIUM"}>Medium</option>
<option value={"LOW"}>Low</option>
</Field>
</FormGroup>
{/* propOverviewImageScaleFactor cropSizeFactor */}
<FormGroup>
<label htmlFor="cropSizeFactor">Crop Size Factor</label>
<Field
name={"cropSizeFactor"}
as="select"
id="cropSizeFactor"
className="p-2 border border-gray-400 rounded-lg text-white bg-[#253445] w-full md:w-60"
>
<option value={"FULL"}>Full</option>
<option value={"3/4"}>3/4</option>
<option value={"1/2"}>1/2</option>
<option value={"1/4"}>1/4</option>
</Field>
</FormGroup>
{format?.toLowerCase() === "bof2" && (
<>
<div className="space-y-3">
@@ -176,7 +204,7 @@ const ChannelFields = ({ touched, isSubmitting, format }: ChannelFieldsProps) =>
<h2 className="font-bold">{values.format} Lane ID Config</h2>
</div>
<FormGroup>
<label htmlFor="LID1">Lane ID 1</label>
<label htmlFor="LID1">Lane ID 1 (Camera A)</label>
<Field
name={"LID1"}
type="text"
@@ -188,7 +216,7 @@ const ChannelFields = ({ touched, isSubmitting, format }: ChannelFieldsProps) =>
/>
</FormGroup>
<FormGroup>
<label htmlFor="LID2">Lane ID 2</label>
<label htmlFor="LID2">Lane ID 2 (Camera B)</label>
<Field
name={"LID2"}
type="text"

View File

@@ -12,6 +12,7 @@ import type {
import { useQueryClient } from "@tanstack/react-query";
import { useUpdateBackOfficeConfig } from "../../../hooks/useBackOfficeConfig";
import { useFormVaidate } from "../../../hooks/useFormValidate";
import { useSightingAmend } from "../../../hooks/useSightingAmend";
const SettingForms = () => {
const qc = useQueryClient();
@@ -20,10 +21,14 @@ const SettingForms = () => {
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 LID1 = laneIdQuery?.data?.propLaneID1?.value;
const LID2 = laneIdQuery?.data?.propLaneID2?.value;
@@ -43,6 +48,8 @@ const SettingForms = () => {
password: "",
connectTimeoutSeconds: Number(5),
readTimeoutSeconds: Number(15),
overviewQuality: sightingQuality ?? "HIGH",
cropSizeFactor: cropSizeFactor ?? "3/4",
// Bof2 - optional constants
FFID: FFID ?? "",
@@ -91,6 +98,7 @@ const SettingForms = () => {
if (validResponse?.reason === "OK") {
await backOfficeMutation.mutateAsync(values);
await sightingAmendMutation.mutateAsync(values);
if (values.format.toLowerCase() === "bof2") {
const bof2ConstantsData: OptionalBOF2Constants = {

View File

@@ -8,9 +8,8 @@ import { useDNSSettings, useSystemConfig } from "../../../hooks/useSystemConfig"
const SystemConfigFields = () => {
const { saveSystemSettings, systemSettingsData, saveSystemSettingsLoading } = useSystemConfig();
const { softRebootMutation, hardRebootMutation } = useReboots();
const { hardRebootMutation } = useReboots();
const { dnsQuery, dnsMutation } = useDNSSettings();
console.log(dnsQuery?.data);
const dnsPrimary = dnsQuery?.data?.propNameServerPrimary?.value;
const dnsSecondary = dnsQuery?.data?.propNameServerSecondary?.value;
@@ -39,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();
@@ -111,34 +110,7 @@ const SystemConfigFields = () => {
autoComplete="off"
/>
</FormGroup>
<FormGroup>
<label htmlFor="serverPrimary" className="font-medium whitespace-nowrap md:w-1/2 text-left">
Server Primary
</label>
<Field
id="serverPrimary"
name="serverPrimary"
type="text"
className="p-2 border border-gray-400 rounded-lg w-full max-w-xs"
placeholder="Enter DNS primary address"
autoComplete="off"
/>
</FormGroup>
<FormGroup>
<label htmlFor="serverSecondary" className="font-medium whitespace-nowrap md:w-1/2 text-left">
Server Secondary
</label>
<Field
id="serverSecondary"
name="serverSecondary"
type="text"
className="p-2 border border-gray-400 rounded-lg w-full max-w-xs"
placeholder="Enter DNS secondary address"
autoComplete="off"
/>
</FormGroup>
<FormGroup>
<label htmlFor="sntpInterval" className="font-medium whitespace-nowrap md:w-1/2 text-left">
SNTP Interval minutes
@@ -155,6 +127,34 @@ const SystemConfigFields = () => {
className="p-2 border border-gray-400 rounded-lg w-full max-w-xs"
/>
</FormGroup>
<FormGroup>
<label htmlFor="serverPrimary" className="font-medium whitespace-nowrap md:w-1/2 text-left">
Primary DNS Server
</label>
<Field
id="serverPrimary"
name="serverPrimary"
type="text"
className="p-2 border border-gray-400 rounded-lg w-full max-w-xs"
placeholder="Enter DNS primary address"
autoComplete="off"
/>
</FormGroup>
<FormGroup>
<label htmlFor="serverSecondary" className="font-medium whitespace-nowrap md:w-1/2 text-left">
Secondary DNS Server
</label>
<Field
id="serverSecondary"
name="serverSecondary"
type="text"
className="p-2 border border-gray-400 rounded-lg w-full max-w-xs"
placeholder="Enter DNS secondary address"
autoComplete="off"
/>
</FormGroup>
<button
type="submit"
className="w-1/4 text-white bg-green-700 hover:bg-green-800 font-small rounded-lg text-sm px-2 py-2.5"
@@ -167,13 +167,13 @@ const SystemConfigFields = () => {
<p>Reboot</p>
</div>
<button
{/* <button
type="button"
className="bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700 transition w-full md:w-[50%]"
onClick={handleSoftReboot}
>
{softRebootMutation.isPending || isSubmitting ? "Rebooting..." : "Software Reboot"}
</button>
</button> */}
<button
type="button"
className="bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700 transition w-full md:w-[50%]"

View File

@@ -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<Set<number | string>>(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]);

View File

@@ -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");

View File

@@ -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();

View File

@@ -7,7 +7,7 @@ const camBase = import.meta.env.MODE !== "development" ? CAM_BASE : "";
const getWiFiSettings = async () => {
const response = await fetch(`${camBase}/api/fetch-config?id=ModemAndWifiManager-wifi`, {
signal: AbortSignal.timeout(500),
signal: AbortSignal.timeout(300000),
});
if (!response.ok) {
throw new Error("Cannot fetch Wifi settings");
@@ -29,7 +29,7 @@ const updateWifiSettings = async (wifiConfig: WifiConfig) => {
const getModemSettings = async () => {
const response = await fetch(`${camBase}/api/fetch-config?id=ModemAndWifiManager-modem`, {
signal: AbortSignal.timeout(500),
signal: AbortSignal.timeout(300000),
});
if (!response.ok) {
throw new Error("Cannot fetch modem settings");

View File

@@ -4,12 +4,35 @@ 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 }) => {
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) {
console.log(options);
const response = await fetch(
`${CAM_BASE}/Ip${options.camera}-command?magnification=${options.multiplierText?.toLowerCase()}`,
{
signal: AbortSignal.timeout(500),
signal: AbortSignal.timeout(300000),
}
);
if (!response.ok) {
@@ -22,7 +45,7 @@ async function zoomIn(options: ZoomInOptions) {
async function fetchZoomInConfig({ queryKey }: QueryFunctionContext<[string, zoomConfig]>) {
const [, { camera }] = queryKey;
const response = await fetch(`${CAM_BASE}/api/fetch-config?id=Ip${camera}`, {
signal: AbortSignal.timeout(500),
signal: AbortSignal.timeout(300000),
});
if (!response.ok) {
throw new Error("Cannot get camera zoom settings");
@@ -34,11 +57,6 @@ 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,3 +70,20 @@ export const useCameraZoom = (options: zoomConfig) => {
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,
};
};

View File

@@ -6,7 +6,7 @@ const apiUrl = CAM_BASE;
// const fetch_url = `http://100.82.205.44/Colour-preview`;
async function fetchSnapshot(cameraSide: string) {
const response = await fetch(`${apiUrl}/${cameraSide}-preview`, {
signal: AbortSignal.timeout(500),
signal: AbortSignal.timeout(300000),
});
if (!response.ok) {
throw new Error("Cannot reach endpoint");

View File

@@ -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");

View File

@@ -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?id=SightingAmmendA`, {
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,
};
};

View File

@@ -12,7 +12,7 @@ import { useCachedSoundSrc } from "./usecachedSoundSrc";
async function fetchSighting(url: string | undefined, ref: number): Promise<SightingType> {
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;
}

View File

@@ -58,6 +58,8 @@ export type InitialValuesForm = {
password: string;
connectTimeoutSeconds: number;
readTimeoutSeconds: number;
overviewQuality?: string;
cropSizeFactor?: string;
};
export type InitialValuesFormErrors = {