storing changes, starting history list

This commit is contained in:
2025-09-15 10:27:31 +01:00
parent 7588326cbe
commit c414342515
12 changed files with 129 additions and 107 deletions

View File

@@ -33,8 +33,6 @@ export async function handleSystemSave(values: SystemValues) {
}` }`
); );
} }
alert("System Settings Saved Successfully!");
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }

View File

@@ -1,57 +1,8 @@
import React from "react";
import Card from "../../UI/Card"; import Card from "../../UI/Card";
import CardHeader from "../../UI/CardHeader"; import CardHeader from "../../UI/CardHeader";
import { sendBlobFileUpload } from "./Upload";
import SystemConfigFields from "./SystemConfigFields.tsx"; import SystemConfigFields from "./SystemConfigFields.tsx";
const SystemCard = () => { const SystemCard = () => {
const [selectedFile, setSelectedFile] = React.useState<File | null>(null);
const [error, setError] = React.useState("");
// useEffect(() => {
// (async () => {
// const result = await handleSystemRecall(); // returns { deviceName, sntpServer, sntpInterval, timeZone } | null
// if (result) {
// const {
// deviceName: dn,
// sntpServer: ss,
// sntpInterval: si,
// timeZone: tz,
// } = result;
// setDeviceName(dn ?? "");
// setSntpServer(ss ?? "");
// setSntpInterval(Number.isFinite(si) ? si : 60);
// setTimeZone(tz ?? "UTC (UTC-00)");
// }
// })();
// }, []);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); // prevent full page reload
if (!selectedFile) {
setError("Please select a file before uploading.");
return;
}
setError("");
const result = await sendBlobFileUpload(selectedFile, {
timeoutMs: 30000,
fieldName: "upload",
});
// The helper returns a string (either success body or formatted error)
// You can decide how to distinguish. Here, we show it optimistically and let the text speak.
if (
result.startsWith("Server returned") ||
result.startsWith("Timeout") ||
result.startsWith("HTTP error") ||
result.startsWith("Unexpected error")
) {
setError(result);
}
};
return ( return (
<Card> <Card>
<CardHeader title={"System Config"} /> <CardHeader title={"System Config"} />

View File

@@ -1,12 +1,14 @@
import { Formik, Field, Form } from "formik"; import { Formik, Field, Form } from "formik";
import FormGroup from "../components/FormGroup"; import FormGroup from "../components/FormGroup";
import { handleSoftReboot, handleHardReboot } from "./Reboots"; import { handleSoftReboot, handleHardReboot } from "./Reboots";
import { handleSystemSave } from "./SettingSaveRecall";
import { timezones } from "./timezones"; import { timezones } from "./timezones";
import SystemFileUpload from "./SystemFileUpload"; import SystemFileUpload from "./SystemFileUpload";
import type { SystemValues, SystemValuesErrors } from "../../../types/types"; import type { SystemValues, SystemValuesErrors } from "../../../types/types";
import { useSystemConfig } from "../../../hooks/useSystemConfig";
const SystemConfigFields = () => { const SystemConfigFields = () => {
const { saveSystemSettings, getSystemSettingsData } = useSystemConfig();
console.log(getSystemSettingsData);
const initialvalues: SystemValues = { const initialvalues: SystemValues = {
deviceName: "", deviceName: "",
timeZone: "", timeZone: "",
@@ -15,7 +17,7 @@ const SystemConfigFields = () => {
softwareUpdate: null, softwareUpdate: null,
}; };
const handleSubmit = (values: SystemValues) => handleSystemSave(values); const handleSubmit = (values: SystemValues) => saveSystemSettings(values);
const validateValues = (values: SystemValues) => { const validateValues = (values: SystemValues) => {
const errors: SystemValuesErrors = {}; const errors: SystemValuesErrors = {};
const interval = Number(values.sntpInterval); const interval = Number(values.sntpInterval);

View File

@@ -1,7 +1,7 @@
import { useFormikContext } from "formik"; import { useFormikContext } from "formik";
import FormGroup from "../components/FormGroup"; import FormGroup from "../components/FormGroup";
import { toast } from "sonner"; import { toast } from "sonner";
import { useSystemConfig } from "../../../hooks/useSystemConfig";
type SystemFileUploadProps = { type SystemFileUploadProps = {
name: string; name: string;
@@ -10,7 +10,16 @@ type SystemFileUploadProps = {
const SystemFileUpload = ({ name, selectedFile }: SystemFileUploadProps) => { const SystemFileUpload = ({ name, selectedFile }: SystemFileUploadProps) => {
const { setFieldValue } = useFormikContext(); const { setFieldValue } = useFormikContext();
const handleFileUploadClick = () => console.log(selectedFile); const { uploadSettings } = useSystemConfig();
const handleFileUploadClick = () => {
if (!selectedFile) return;
uploadSettings(selectedFile, {
timeoutMs: 30000,
fieldName: "upload",
});
};
return ( return (
<div className="py-8 w-full"> <div className="py-8 w-full">
<FormGroup> <FormGroup>

View File

@@ -14,7 +14,7 @@ export async function sendBlobFileUpload(
const form = new FormData(); const form = new FormData();
form.append(fieldName, file, fileName); form.append(fieldName, file, fileName);
const resp = await fetch('http://192.168.75.11/upload/software-update/2', { const resp = await fetch("http://192.168.75.11/upload/hotlist-upload/2", {
method: "POST", method: "POST",
body: form, body: form,
signal: controller.signal, signal: controller.signal,
@@ -24,6 +24,9 @@ export async function sendBlobFileUpload(
const bodyText = await resp.text(); const bodyText = await resp.text();
if (!resp.ok) { if (!resp.ok) {
// throw new Error(
// `Server returned ${resp.status}: ${resp.statusText}. Details: ${bodyText}`
// );
return `Server returned ${resp.status}: ${resp.statusText}. Details: ${bodyText}`; return `Server returned ${resp.status}: ${resp.statusText}. Details: ${bodyText}`;
} }
return bodyText; return bodyText;
@@ -35,7 +38,9 @@ export async function sendBlobFileUpload(
if (err instanceof TypeError) { if (err instanceof TypeError) {
return `HTTP error uploading to /upload/software-update/2: ${err.message}`; return `HTTP error uploading to /upload/software-update/2: ${err.message}`;
} }
return `Unexpected error uploading to /upload/software-update/2: ${err?.message ?? String(err)} ${err?.cause ?? ""}`; return `Unexpected error uploading to /upload/software-update/2: ${
err?.message ?? String(err)
} ${err?.cause ?? ""}`;
} finally { } finally {
clearTimeout(timeout); clearTimeout(timeout);
} }

View File

@@ -14,6 +14,7 @@ const SightingModal = ({
sighting, sighting,
}: SightingModalProps) => { }: SightingModalProps) => {
const motionAway = (sighting?.motion ?? "").toUpperCase() === "AWAY"; const motionAway = (sighting?.motion ?? "").toUpperCase() === "AWAY";
console.log(sighting);
return ( return (
<ModalComponent isModalOpen={isSightingModalOpen} close={handleClose}> <ModalComponent isModalOpen={isSightingModalOpen} close={handleClose}>
<div> <div>

View File

@@ -17,18 +17,21 @@ function useNow(tickMs = 1000) {
return undefined; return undefined;
} }
export type SightingHistoryProps = { type SightingHistoryProps = {
baseUrl: string; baseUrl?: string;
entries?: number; // number of rows to show entries?: number; // number of rows to show
pollMs?: number; // poll frequency pollMs?: number; // poll frequency
autoSelectLatest?: boolean; autoSelectLatest?: boolean;
title: string;
className: React.HTMLAttributes<HTMLDivElement> | string;
}; };
type SightingHistoryWidgetProps = React.HTMLAttributes<HTMLDivElement>; // /type SightingHistoryWidgetProps = React.HTMLAttributes<HTMLDivElement>;
export default function SightingHistoryWidget({ export default function SightingHistoryWidget({
className, className,
}: SightingHistoryWidgetProps) { title,
}: SightingHistoryProps) {
useNow(1000); useNow(1000);
const { const {
@@ -40,7 +43,7 @@ export default function SightingHistoryWidget({
} = useSightingFeedContext(); } = useSightingFeedContext();
const onRowClick = useCallback( const onRowClick = useCallback(
(sighting: SightingType) => { (sighting: SightingType | SightingWidgetType) => {
if (!sighting) return; if (!sighting) return;
setSightingModalOpen(!isSightingModalOpen); setSightingModalOpen(!isSightingModalOpen);
setSelectedSighting(sighting); setSelectedSighting(sighting);
@@ -57,7 +60,7 @@ export default function SightingHistoryWidget({
return ( return (
<> <>
<Card className={clsx("overflow-y-auto h-100", className)}> <Card className={clsx("overflow-y-auto h-100", className)}>
<CardHeader title="Front Camera Sightings" /> <CardHeader title={title} />
<div className="flex flex-col gap-3 "> <div className="flex flex-col gap-3 ">
{/* Rows */} {/* Rows */}
<div className="flex flex-col"> <div className="flex flex-col">
@@ -73,7 +76,9 @@ export default function SightingHistoryWidget({
onClick={() => onRowClick(obj)} onClick={() => onRowClick(obj)}
> >
{/* Info bar */} {/* Info bar */}
<div className="flex items-center gap-3 text-xs bg-neutral-900 px-2 py-1 rounded"> <div className="flex items-center gap-3 text-xs bg-neutral-900 px-2 py-1 rounded justify-between">
<div className="flex items-center gap-3 text-xs">
{" "}
<div className="min-w-14"> <div className="min-w-14">
CH: {obj ? obj.charHeight : "—"} CH: {obj ? obj.charHeight : "—"}
</div> </div>
@@ -88,6 +93,17 @@ export default function SightingHistoryWidget({
</div> </div>
</div> </div>
<div className="min-w-14 opacity-80 ">
{isNPEDHit ? (
<span className="text-red-500 font-semibold">
NPED HIT
</span>
) : (
""
)}
</div>
</div>
{/* Patch row */} {/* Patch row */}
<div <div
className={`flex items-center gap-3 mt-2 className={`flex items-center gap-3 mt-2

View File

@@ -9,7 +9,9 @@ type SightingFeedContextType = {
mostRecent: SightingWidgetType | null; mostRecent: SightingWidgetType | null;
side: string; side: string;
selectedSighting: SightingType | null; selectedSighting: SightingType | null;
setSelectedSighting: (sighting: SightingType | null) => void; setSelectedSighting: (
sighting: SightingType | SightingWidgetType | null
) => void;
setSightingModalOpen: (isSightingModalOpen: boolean) => void; setSightingModalOpen: (isSightingModalOpen: boolean) => void;
isSightingModalOpen: boolean; isSightingModalOpen: boolean;
}; };

View File

@@ -0,0 +1,32 @@
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";
export const useSystemConfig = () => {
const uploadSettingsMutation = useMutation({
mutationKey: ["uploadSettings"],
mutationFn: sendBlobFileUpload,
onError: () => console.log("upload failed"),
onSuccess: (test) => console.log(test),
});
const saveSystemSettings = useMutation({
mutationKey: ["systemSaveSettings"],
mutationFn: handleSystemSave,
onSuccess: () => toast("System Settings Saved Successfully!"),
});
const getSystemSettings = useQuery({
queryKey: ["getSystemSettings"],
queryFn: handleSystemRecall,
});
return {
uploadSettings: uploadSettingsMutation.mutate,
saveSystemSettings: saveSystemSettings.mutate,
getSystemSettingsData: getSystemSettings.data,
};
};

View File

@@ -1,7 +1,6 @@
import FrontCameraOverviewCard from "../components/FrontCameraOverview/FrontCameraOverviewCard"; import FrontCameraOverviewCard from "../components/FrontCameraOverview/FrontCameraOverviewCard";
import RearCameraOverviewCard from "../components/RearCameraOverview/RearCameraOverviewCard"; import RearCameraOverviewCard from "../components/RearCameraOverview/RearCameraOverviewCard";
import SightingHistoryWidget from "../components/SightingsWidget/SightingWidget"; import SightingHistoryWidget from "../components/SightingsWidget/SightingWidget";
import { SightingFeedProvider } from "../context/providers/SightingFeedProvider"; import { SightingFeedProvider } from "../context/providers/SightingFeedProvider";
const Dashboard = () => { const Dashboard = () => {
@@ -9,24 +8,30 @@ const Dashboard = () => {
<div className="mx-auto grid grid-cols-1 sm:grid-cols-1 lg:grid-cols-2 gap-2 px-1 sm:px-2 lg:px-0 w-full"> <div className="mx-auto grid grid-cols-1 sm:grid-cols-1 lg:grid-cols-2 gap-2 px-1 sm:px-2 lg:px-0 w-full">
<SightingFeedProvider <SightingFeedProvider
url={ url={
"http://100.116.253.81/mergedHistory/sightingSummary?mostRecentRef=" // "http://100.116.253.81/mergedHistory/sightingSummary?mostRecentRef="
// "http://192.168.75.11/SightingListFront/sightingSummary?mostRecentRef=" "http://192.168.75.11/SightingListFront/sightingSummary?mostRecentRef="
} }
side="Front" side="Front"
> >
<FrontCameraOverviewCard className="order-1" /> <FrontCameraOverviewCard className="order-1" />
<SightingHistoryWidget className="order-5" /> <SightingHistoryWidget
className="order-3"
title="Front Camera Sightings"
/>
</SightingFeedProvider> </SightingFeedProvider>
<SightingFeedProvider <SightingFeedProvider
url={ url={
"http://100.116.253.81/mergedHistory/sightingSummary?mostRecentRef=" // "http://100.116.253.81/mergedHistory/sightingSummary?mostRecentRef="
// "http://192.168.75.11/SightingListRear/sightingSummary?mostRecentRef=" "http://192.168.75.11/SightingListRear/sightingSummary?mostRecentRef="
} }
side="Rear" side="Rear"
> >
<RearCameraOverviewCard className="order-2" /> <RearCameraOverviewCard className="order-2" />
<SightingHistoryWidget className="order-4" /> <SightingHistoryWidget
className="order-4"
title="Rear Camera Sightings"
/>
</SightingFeedProvider> </SightingFeedProvider>
</div> </div>
); );

View File

@@ -11,7 +11,7 @@ import { useNPEDAuth } from "../hooks/useNPEDAuth";
const SystemSettings = () => { const SystemSettings = () => {
const { user } = useNPEDAuth(); const { user } = useNPEDAuth();
console.log(user);
return ( return (
<div className="m-4"> <div className="m-4">
<Tabs selectedTabClassName="bg-gray-300 text-gray-900 font-semibold border-none"> <Tabs selectedTabClassName="bg-gray-300 text-gray-900 font-semibold border-none">

View File

@@ -71,35 +71,36 @@ export type HotlistUploadType = {
}; };
export type SightingWidgetType = { export type SightingWidgetType = {
debug: string;
SaFID: string;
ref: number; // unique, increasing ref: number; // unique, increasing
idx?: number; // client-side slot index idx?: number; // client-side slot index
vrm: string; vrm: string;
vrmSecondary?: string; // empty string means missing vrmSecondary: string; // empty string means missing
countryCode?: string; countryCode: string;
timeStamp?: string; // formatted string timeStamp: string; // formatted string
timeStampMillis: number; // epoch millis timeStampMillis: number; // epoch millis
motion: string; // e.g., "AWAY" or "TOWARDS" motion: string; // e.g., "AWAY" or "TOWARDS"
seenCount: number; seenCount: string;
charHeight: string | number; charHeight: string;
overviewUrl: string; overviewUrl: string;
detailsUrl?: string; detailsUrl: string;
make?: string; make: string;
model?: string; model: string;
color?: string; color: string;
category?: string; category: string;
plateSize?: string | number; plateSize: string | number;
overviewSize?: string | number; overviewSize: string | number;
locationName?: string; locationName: string;
laneID?: string | number; laneID: string | number;
radarSpeed?: string | number; radarSpeed: string | number;
trackSpeed?: string | number; trackSpeed: string | number;
srcCam?: 0 | 1; srcCam: 0 | 1;
plateUrlInfrared?: string; plateUrlInfrared: string;
plateUrlColour?: string; plateUrlColour: string;
overviewPlateRect?: [number, number, number, number]; // [x,y,w,h] normalized 0..1 overviewPlateRect: [number, number, number, number]; // [x,y,w,h] normalized 0..1
plateTrack?: [number, number, number, number][]; plateTrack: [number, number, number, number][];
metadata?: Metadata; metadata: Metadata;
// list of rects normalized 0..1
}; };
export type VersionFieldType = { export type VersionFieldType = {