Merge branch 'Bradley'
This commit is contained in:
@@ -4,6 +4,7 @@ import { Route, Routes } from "react-router";
|
||||
import FrontCamera from "./pages/FrontCamera";
|
||||
import RearCamera from "./pages/RearCamera";
|
||||
import SystemSettings from "./pages/SystemSettings";
|
||||
import Session from "./pages/Session";
|
||||
import { NPEDUserProvider } from "./context/providers/NPEDUserContextProvider";
|
||||
|
||||
function App() {
|
||||
@@ -15,6 +16,7 @@ function App() {
|
||||
<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>
|
||||
</Routes>
|
||||
</NPEDUserProvider>
|
||||
|
||||
@@ -71,6 +71,7 @@ const CameraSettingFields = () => {
|
||||
type="text"
|
||||
className="p-2 border border-gray-400 rounded-lg"
|
||||
placeholder="RTSP://..."
|
||||
autoComplete="street-address"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ const FrontCameraOverviewCard = ({ className }: CardProps) => {
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col space-y-3 h-full" {...handlers}>
|
||||
<CardHeader title="Front Overiew" icon={faCamera} />
|
||||
<CardHeader title="Front Overview" icon={faCamera} />
|
||||
<SightingOverview />
|
||||
{/* <SnapshotContainer side="TargetDetectionFront" /> */}
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,7 @@ const RearCameraOverviewCard = ({ className }: CardProps) => {
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col space-y-3 h-full" {...handlers}>
|
||||
<CardHeader title="Rear Overiew" icon={faCamera} />
|
||||
<CardHeader title="Rear Overview" icon={faCamera} />
|
||||
<SightingOverview />
|
||||
{/* <SnapshotContainer side="TargetDetectionRear" /> */}
|
||||
</div>
|
||||
|
||||
35
src/components/SessionForm/HitSearchCard.tsx
Normal file
35
src/components/SessionForm/HitSearchCard.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import Card from "../UI/Card";
|
||||
import CardHeader from "../UI/CardHeader";
|
||||
import FormGroup from "../SettingForms/components/FormGroup";
|
||||
|
||||
const SessionCard = () => {
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader title={"Hit Search"} />
|
||||
<div className="flex flex-col gap-4">
|
||||
<FormGroup>
|
||||
<label htmlFor="VRM" className="font-medium whitespace-nowrap md:w-1/2 text-left">VRM (Min 2 letters)</label>
|
||||
<div className="flex-1 flex justify-end md:w-2/3">
|
||||
<input
|
||||
id="VRMSelect"
|
||||
name="VRMSelect"
|
||||
type="text"
|
||||
className="p-2 border border-gray-400 rounded-lg w-full max-w-xs"
|
||||
placeholder="Enter VRM"
|
||||
//onChange={e => setSntpServer(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</FormGroup>
|
||||
<button
|
||||
className="bg-[#26B170] text-white px-4 py-2 rounded hover:bg-green-700 transition w-full max-w-md"
|
||||
//onClick={() => handleModemSave(apn, username, password, authType)}
|
||||
>
|
||||
Search Hit list
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SessionCard;
|
||||
27
src/components/SessionForm/SessionCard.tsx
Normal file
27
src/components/SessionForm/SessionCard.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import Card from "../UI/Card";
|
||||
import CardHeader from "../UI/CardHeader";
|
||||
|
||||
const SessionCard = () => {
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader title={"Session"} />
|
||||
<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={() => handleModemSave(apn, username, password, authType)}
|
||||
>
|
||||
Start Session
|
||||
</button>
|
||||
<h2 className="text-white mb-2">Number of cars: </h2>
|
||||
<h2 className="text-white mb-2">Cars without Tax: </h2>
|
||||
<h2 className="text-white mb-2">Cars without MOT: </h2>
|
||||
<h2 className="text-white mb-2">Cars with NPED Cat A: </h2>
|
||||
<h2 className="text-white mb-2">Cars with NPED Cat B: </h2>
|
||||
<h2 className="text-white mb-2">Cars with NPED Cat C: </h2>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SessionCard;
|
||||
@@ -5,7 +5,7 @@ import NPEDFields from "./NPEDFields";
|
||||
const NPEDCard = () => {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader title={"NPED Config"} />
|
||||
<CardHeader title={"NPED Config"} img={"/NPED.jpg"} />
|
||||
<NPEDFields />
|
||||
</Card>
|
||||
);
|
||||
|
||||
15
src/components/SettingForms/System/Reboots.tsx
Normal file
15
src/components/SettingForms/System/Reboots.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
export async function handleSoftReboot() {
|
||||
const response = await fetch(
|
||||
`http://192.168.75.11/api/restart-flexiai`
|
||||
);
|
||||
if (!response.ok) throw new Error("Failed to Software Reboot");
|
||||
else alert("Software reboot triggered!");
|
||||
}
|
||||
|
||||
export async function handleHardReboot() {
|
||||
const response = await fetch(
|
||||
`http://192.168.75.11/api/restart-hardware`
|
||||
);
|
||||
if (!response.ok) throw new Error("Failed to Hardware Reboot");
|
||||
else alert("Hardware reboot triggered!");
|
||||
}
|
||||
74
src/components/SettingForms/System/SettingSaveRecall.tsx
Normal file
74
src/components/SettingForms/System/SettingSaveRecall.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
export async function handleSystemSave(deviceName: string, sntpServer: string, sntpInterval: number, timeZone: string) {
|
||||
const payload = { // Build JSON
|
||||
id: "GLOBAL--Device",
|
||||
fields: [
|
||||
{ property: "propDeviceName", value: deviceName },
|
||||
{ property: "propSNTPServer", value: sntpServer },
|
||||
{ property: "propSNTPIntervalMinutes", value: Number(sntpInterval) },
|
||||
{ property: "propLocalTimeZone", value: timeZone }
|
||||
]
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch("http://192.168.75.11/api/update-config", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => "");
|
||||
throw new Error(`HTTP ${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`);
|
||||
}
|
||||
|
||||
alert("System Settings Saved Successfully!");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleSystemRecall() {
|
||||
const url = "http://192.168.75.11/api/fetch-config?id=GLOBAL--Device";
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 7000);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: { "Accept": "application/json" },
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => "");
|
||||
throw new Error(`HTTP ${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const deviceName = data?.propDeviceName?.value ?? null;
|
||||
const sntpServer = data?.propSNTPServer?.value ?? null;
|
||||
const timeZone = data?.propLocalTimeZone?.value ?? null;
|
||||
|
||||
let sntpIntervalRaw = data?.propSNTPIntervalMinutes?.value;
|
||||
let sntpInterval =
|
||||
typeof sntpIntervalRaw === "number"
|
||||
? sntpIntervalRaw
|
||||
: Number.parseInt(String(sntpIntervalRaw).trim(), 10);
|
||||
|
||||
if (!Number.isFinite(sntpInterval)) {
|
||||
sntpInterval = 60;
|
||||
}
|
||||
|
||||
return { deviceName, sntpServer, sntpInterval, timeZone };
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return null;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
219
src/components/SettingForms/System/SystemCard.tsx
Normal file
219
src/components/SettingForms/System/SystemCard.tsx
Normal file
@@ -0,0 +1,219 @@
|
||||
import React from "react";
|
||||
import { useEffect } from "react"
|
||||
import Card from "../../UI/Card";
|
||||
import CardHeader from "../../UI/CardHeader";
|
||||
import FormGroup from "../components/FormGroup";
|
||||
import { sendBlobFileUpload } from "./Upload";
|
||||
import { handleSoftReboot, handleHardReboot } from "./Reboots.tsx";
|
||||
import { handleSystemRecall, handleSystemSave } from "./SettingSaveRecall.tsx";
|
||||
|
||||
const SystemCard = () => {
|
||||
const [deviceName, setDeviceName] = React.useState("");
|
||||
const [timeZone, setTimeZone] = React.useState("Europe/London (UTC+00:00");
|
||||
const [sntpServer, setSntpServer] = React.useState("1.uk.pool.ntp.org");
|
||||
const [sntpInterval, setSntpInterval] = React.useState(60);
|
||||
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 handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0] ?? null;
|
||||
setSelectedFile(file);
|
||||
if (!file) {
|
||||
setError("No file selected.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.size > 8 * 1024 * 1024) {
|
||||
setError("File is too large (max 8MB).");
|
||||
setSelectedFile(null);
|
||||
return
|
||||
};
|
||||
setError("");
|
||||
}
|
||||
|
||||
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 (
|
||||
<Card className="flex flex-col items-center justify-center">
|
||||
<CardHeader title={"System Config"} />
|
||||
<div className="flex flex-col gap-4 w-full items-left max-w-md">
|
||||
<FormGroup>
|
||||
<label htmlFor="deviceName" className="font-medium whitespace-nowrap md:w-1/2 text-left">Device Name</label>
|
||||
<div className="flex-1 flex justify-end md:w-2/3">
|
||||
<input
|
||||
id="deviceName"
|
||||
name="deviceName"
|
||||
type="text"
|
||||
className="p-2 border border-gray-400 rounded-lg w-full max-w-xs"
|
||||
placeholder="Enter device name"
|
||||
value={deviceName}
|
||||
onChange={e => setDeviceName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<label htmlFor="timeZone" className="font-medium whitespace-nowrap md:w-1/2 text-left">Local Time Zone</label>
|
||||
<div className="flex-1 flex justify-end md:w-2/3">
|
||||
<select
|
||||
id="timeZone"
|
||||
name="timeZone"
|
||||
className="p-2 border border-gray-400 rounded-lg text-white bg-[#253445] w-full max-w-xs"
|
||||
value={timeZone}
|
||||
onChange={e => setTimeZone(e.target.value)}
|
||||
>
|
||||
<option value="">Select Time Zone</option>
|
||||
<option value="Europe/London (UTC+00)">UTC (UTC+00)</option>
|
||||
<option value="Africa/Cairo (UTC+02)">Africa/Cairo (UTC+02)</option>
|
||||
<option value="Africa/Johannesburg (UTC+02)">Africa/Johannesburg (UTC+02)</option>
|
||||
<option value="Africa/Lagos (UTC+01)">Africa/Lagos (UTC+01)</option>
|
||||
<option value="Africa/Monrousing (UTC+00)">Africa/Monrousing (UTC+00)</option>
|
||||
<option value="America/Anchorage (UTC-09)">America/Anchorage (UTC-09)</option>
|
||||
<option value="America/Chicago (UTC-06)">America/Chicago (UTC-06)</option>
|
||||
<option value="America/Denver (UTC-07)">America/Denver (UTC-07)</option>
|
||||
<option value="America/Edmonton (UTC-07)">America/Edmonton (UTC-07)</option>
|
||||
<option value="America/Jamaica (UTC-05)">America/Jamaica (UTC-05)</option>
|
||||
<option value="America/Los Angeles (UTC-08)">America/Los Angeles (UTC-08)</option>
|
||||
<option value="America/Mexico City (UTC-06)">America/Mexico City (UTC-06)</option>
|
||||
<option value="America/Montreal (UTC-05)">America/Montreal (UTC-05)</option>
|
||||
<option value="America/New York (UTC-05)">America/New York (UTC-05)</option>
|
||||
<option value="America/Phoenix (UTC-07)">America/Phoenix (UTC-07)</option>
|
||||
<option value="America/Puerto Rico (UTC-04)">America/Puerto Rico (UTC-04)</option>
|
||||
<option value="America/Sao Paulo (UTC-03)">America/Sao Paulo (UTC-03)</option>
|
||||
<option value="America/Toronto (UTC-05)">America/Toronto (UTC-05)</option>
|
||||
<option value="America/Vancouver (UTC-08)">America/Vancouver (UTC-08)</option>
|
||||
<option value="Asia/Hong Kong (UTC+08)">Asia/Hong Kong (UTC+08)</option>
|
||||
<option value="Asia/Jerusalem (UTC+02)">Asia/Jerusalem (UTC+02)</option>
|
||||
<option value="Asia/Manila (UTC+08)">Asia/Manila (UTC+08)</option>
|
||||
<option value="Asia/Seoul (UTC+09)">Asia/Seoul (UTC+09)</option>
|
||||
<option value="Asia/Tokyo (UTC+09)">Asia/Tokyo (UTC+09)</option>
|
||||
<option value="Atlantic/Reykjavik (UTC+00)">Atlantic/Reykjavik (UTC+00)</option>
|
||||
<option value="Australia/Perth (UTC+08)">Australia/Perth (UTC+08)</option>
|
||||
<option value="Australia/Sydney (UTC+10)">Australia/Sydney (UTC+10)</option>
|
||||
<option value="Europe/Athens (UTC+02)">Europe/Athens (UTC+02)</option>
|
||||
<option value="Europe/Berlin (UTC+01)">Europe/Berlin (UTC+01)</option>
|
||||
<option value="Europe/Brussels (UTC+01)">Europe/Brussels (UTC+01)</option>
|
||||
<option value="Europe/Copenhagen (UTC+01)">Europe/Copenhagen (UTC+01)</option>
|
||||
<option value="Europe/London (UTC+00)">Europe/London (UTC+00)</option>
|
||||
<option value="Europe/Madrid (UTC+01)">Europe/Madrid (UTC+01)</option>
|
||||
<option value="Europe/Moscow (UTC+04)">Europe/Moscow (UTC+04)</option>
|
||||
<option value="Europe/Paris (UTC+01)">Europe/Paris (UTC+01)</option>
|
||||
<option value="Europe/Prague (UTC+01)">Europe/Prague (UTC+01)</option>
|
||||
<option value="Europe/Rome (UTC+01)">Europe/Rome (UTC+01)</option>
|
||||
<option value="Europe/Warsaw (UTC+01)">Europe/Warsaw (UTC+01)</option>
|
||||
<option value="Pacific/Guam (UTC+10)">Pacific/Guam (UTC+10)</option>
|
||||
<option value="Pacific/Honolulu (UTC-10)">Pacific/Honolulu (UTC-10)</option>
|
||||
</select>
|
||||
</div>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<label htmlFor="sntpServer" className="font-medium whitespace-nowrap md:w-1/2 text-left">SNTP Server</label>
|
||||
<div className="flex-1 flex justify-end md:w-2/3">
|
||||
<input
|
||||
id="sntpServer"
|
||||
name="sntpServer"
|
||||
type="text"
|
||||
className="p-2 border border-gray-400 rounded-lg w-full max-w-xs"
|
||||
placeholder="Enter SNTP server address"
|
||||
value={sntpServer}
|
||||
onChange={e => setSntpServer(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<label htmlFor="sntpInterval" className="font-medium whitespace-nowrap md:w-1/2 text-left">SNTP Interval minutes</label>
|
||||
<div className="flex-1 flex justify-end md:w-2/3">
|
||||
<input
|
||||
id="sntpInterval"
|
||||
name="sntpInterval"
|
||||
type="number"
|
||||
min={1}
|
||||
className="p-2 border border-gray-400 rounded-lg w-full max-w-xs"
|
||||
value={sntpInterval}
|
||||
onChange={e => setSntpInterval(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
</FormGroup>
|
||||
<button
|
||||
className="bg-[#26B170] text-white px-4 py-2 rounded hover:bg-green-700 transition w-full max-w-md"
|
||||
onClick={() => handleSystemSave(deviceName, sntpServer, sntpInterval, timeZone)}
|
||||
>
|
||||
Save System Settings
|
||||
</button>
|
||||
<div className="py-8 w-full">
|
||||
<form onSubmit={handleSubmit} className="flex flex-col items-center gap-2 w-full">
|
||||
<FormGroup>
|
||||
<div className="flex-1 flex justify-end md:w-2/3">
|
||||
<input
|
||||
type="file"
|
||||
name="softwareUpdate"
|
||||
id="softwareUpdate"
|
||||
className="file:px-10 file:border file:border-gray-500 file:rounded-lg file:bg-blue-800 file:mr-5 w-full max-w-xs"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
</div>
|
||||
</FormGroup>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full max-w-md text-white bg-[#26B170] hover:bg-green-700 font-small rounded-lg text-sm px-2 py-2.5 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled={!selectedFile}
|
||||
>
|
||||
Upload Software Update
|
||||
</button>
|
||||
{error && <p className="text-red-500 text-sm">{error}</p>}
|
||||
</form>
|
||||
</div>
|
||||
<button
|
||||
className="bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700 transition w-full max-w-md"
|
||||
onClick={handleSoftReboot}
|
||||
>
|
||||
Software Reboot
|
||||
</button>
|
||||
<button
|
||||
className="bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700 transition w-full max-w-md"
|
||||
onClick={handleHardReboot}
|
||||
>
|
||||
Hardware Reboot
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemCard;
|
||||
42
src/components/SettingForms/System/Upload.tsx
Normal file
42
src/components/SettingForms/System/Upload.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
// CORS (server missing Access-Control-Allow-* headers)??
|
||||
export async function sendBlobFileUpload(
|
||||
file: File,
|
||||
opts?: { timeoutMs?: number; fieldName?: string; overrideFileName?: string }
|
||||
): Promise<string> {
|
||||
const timeoutMs = opts?.timeoutMs ?? 30000;
|
||||
const fieldName = opts?.fieldName ?? "upload";
|
||||
const fileName = opts?.overrideFileName ?? file.name;
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
const form = new FormData();
|
||||
form.append(fieldName, file, fileName);
|
||||
|
||||
const resp = await fetch('http://192.168.75.11/upload/software-update/2', {
|
||||
method: "POST",
|
||||
body: form,
|
||||
signal: controller.signal,
|
||||
redirect: "follow",
|
||||
});
|
||||
|
||||
const bodyText = await resp.text();
|
||||
|
||||
if (!resp.ok) {
|
||||
return `Server returned ${resp.status}: ${resp.statusText}. Details: ${bodyText}`;
|
||||
}
|
||||
return bodyText;
|
||||
} catch (err: any) {
|
||||
if (err?.name === "AbortError") {
|
||||
return `Timeout uploading to /upload/software-update/2.`;
|
||||
}
|
||||
// In browsers, fetch throws TypeError on network-level failures
|
||||
if (err instanceof TypeError) {
|
||||
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 ?? ""}`;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
77
src/components/SettingForms/WiFi&Modem/ModemCard.tsx
Normal file
77
src/components/SettingForms/WiFi&Modem/ModemCard.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import Card from "../../UI/Card";
|
||||
import CardHeader from "../../UI/CardHeader";
|
||||
import { useState } from "react";
|
||||
import FormGroup from "../components/FormGroup";
|
||||
|
||||
const ModemCard = () => {
|
||||
const [apn, setApn] = useState("");
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [authType, setAuthType] = useState("PAP");
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader title={"Modem"} />
|
||||
<div className="flex flex-col gap-4">
|
||||
<FormGroup>
|
||||
<label htmlFor="apn" className="font-medium whitespace-nowrap md:w-2/3">APN</label>
|
||||
<input
|
||||
id="apn"
|
||||
name="apn"
|
||||
type="text"
|
||||
className="p-2 border border-gray-400 rounded-lg flex-1 md:w-2/3"
|
||||
placeholder="Enter APN"
|
||||
value={apn}
|
||||
onChange={e => setApn(e.target.value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<label htmlFor="modemUsername" className="font-medium whitespace-nowrap md:w-2/3">Username</label>
|
||||
<input
|
||||
id="modemUsername"
|
||||
name="modemUsername"
|
||||
type="text"
|
||||
className="p-2 border border-gray-400 rounded-lg flex-1 md:w-2/3"
|
||||
placeholder="Enter Username"
|
||||
value={username}
|
||||
onChange={e => setUsername(e.target.value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<label htmlFor="modemPassword" className="font-medium whitespace-nowrap md:w-2/3">Password</label>
|
||||
<input
|
||||
id="modemPassword"
|
||||
name="modemPassword"
|
||||
type="password"
|
||||
className="p-2 border border-gray-400 rounded-lg flex-1 md:w-2/3"
|
||||
placeholder="Enter Password"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<label htmlFor="authType" className="font-medium whitespace-nowrap md:w-2/3">Authentication Type</label>
|
||||
<select
|
||||
id="authType"
|
||||
name="authType"
|
||||
className="p-2 border border-gray-400 rounded-lg text-white bg-[#253445] flex-1 md:w-2/3"
|
||||
value={authType}
|
||||
onChange={e => setAuthType(e.target.value)}
|
||||
>
|
||||
<option value="PAP">PAP</option>
|
||||
<option value="CHAP">CHAP</option>
|
||||
<option value="None">None</option>
|
||||
</select>
|
||||
</FormGroup>
|
||||
<button
|
||||
className="bg-[#26B170] text-white px-4 py-2 rounded hover:bg-green-700 transition w-full max-w-md"
|
||||
//onClick={() => handleModemSave(apn, username, password, authType)}
|
||||
>
|
||||
Save Modem Settings
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModemCard;
|
||||
65
src/components/SettingForms/WiFi&Modem/WiFiCard.tsx
Normal file
65
src/components/SettingForms/WiFi&Modem/WiFiCard.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import Card from "../../UI/Card";
|
||||
import CardHeader from "../../UI/CardHeader";
|
||||
import { useState } from "react";
|
||||
import FormGroup from "../components/FormGroup";
|
||||
|
||||
const WiFiCard = () => {
|
||||
const [ssid, setSsid] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [encryption, setEncryption] = useState("WPA2");
|
||||
|
||||
return (
|
||||
<Card className="mb-4">
|
||||
<CardHeader title={"WiFi"} />
|
||||
<div className="flex flex-col gap-4">
|
||||
<FormGroup>
|
||||
<label htmlFor="ssid" className="font-medium whitespace-nowrap md:w-2/3">SSID</label>
|
||||
<input
|
||||
id="ssid"
|
||||
name="ssid"
|
||||
type="text"
|
||||
className="p-2 border border-gray-400 rounded-lg flex-1 md:w-2/3"
|
||||
placeholder="Enter SSID"
|
||||
value={ssid}
|
||||
onChange={e => setSsid(e.target.value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<label htmlFor="password" className="font-medium whitespace-nowrap md:w-2/3">Password</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
className="p-2 border border-gray-400 rounded-lg flex-1 md:w-2/3"
|
||||
placeholder="Enter Password"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<label htmlFor="encryption" className="font-medium whitespace-nowrap md:w-2/3">WPA/Encryption Type</label>
|
||||
<select
|
||||
id="encryption"
|
||||
name="encryption"
|
||||
className="p-2 border border-gray-400 rounded-lg text-white bg-[#253445] flex-1 md:w-2/3"
|
||||
value={encryption}
|
||||
onChange={e => setEncryption(e.target.value)}
|
||||
>
|
||||
<option value="WPA2">WPA2</option>
|
||||
<option value="WPA3">WPA3</option>
|
||||
<option value="WEP">WEP</option>
|
||||
<option value="None">None</option>
|
||||
</select>
|
||||
</FormGroup>
|
||||
<button
|
||||
className="bg-[#26B170] text-white px-4 py-2 rounded hover:bg-green-700 transition w-full max-w-md"
|
||||
//onClick={() => handleWiFiSave(ssid, password, encryption)}
|
||||
>
|
||||
Save WiFi Settings
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default WiFiCard;
|
||||
@@ -35,7 +35,7 @@ const SightingOverview = () => {
|
||||
<div className="mt-2 grid gap-3">
|
||||
<div className="inline-block w-[90%] mx-auto" {...handlers}>
|
||||
<NavigationArrow side={side} />
|
||||
<div className="relative aspect-[5/4]">
|
||||
<div className="relative aspect-[1280/800]">
|
||||
<img
|
||||
ref={imgRef}
|
||||
onLoad={() => {
|
||||
|
||||
@@ -9,6 +9,14 @@ const SightingWidgetDetails = ({
|
||||
}: SightingWidgetDetailsProps) => {
|
||||
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>
|
||||
Timestamp:{" "}
|
||||
<span className="opacity-90">{effectiveSelected?.timeStamp ?? "—"}</span>
|
||||
</div>
|
||||
<div>
|
||||
Make:{" "}
|
||||
<span className="opacity-90">{effectiveSelected?.make ?? "—"}</span>
|
||||
@@ -17,6 +25,16 @@ const SightingWidgetDetails = ({
|
||||
Model:{" "}
|
||||
<span className="opacity-90">{effectiveSelected?.model ?? "—"}</span>
|
||||
</div>
|
||||
<div>
|
||||
Country:{" "}
|
||||
<span className="opacity-90">{effectiveSelected?.countryCode ?? "—"}</span>
|
||||
</div>
|
||||
<div>
|
||||
Seen:{" "}
|
||||
<span className="opacity-90">
|
||||
{effectiveSelected?.seenCount ?? "—"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
Colour:{" "}
|
||||
<span className="opacity-90">{effectiveSelected?.color ?? "—"}</span>
|
||||
@@ -43,40 +61,8 @@ const SightingWidgetDetails = ({
|
||||
{effectiveSelected?.overviewSize ?? "—"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
Motion:{" "}
|
||||
<span className="opacity-90">{effectiveSelected?.motion ?? "—"}</span>
|
||||
</div>
|
||||
<div>
|
||||
Seen:{" "}
|
||||
<span className="opacity-90">
|
||||
{effectiveSelected?.seenCount ?? "—"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
Location:{" "}
|
||||
<span className="opacity-90">
|
||||
{effectiveSelected?.locationName ?? "—"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
Lane:{" "}
|
||||
<span className="opacity-90">{effectiveSelected?.laneID ?? "—"}</span>
|
||||
</div>
|
||||
<div>
|
||||
Radar:{" "}
|
||||
<span className="opacity-90">
|
||||
{effectiveSelected?.radarSpeed ?? "—"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
Track:{" "}
|
||||
<span className="opacity-90">
|
||||
{effectiveSelected?.trackSpeed ?? "—"}
|
||||
</span>
|
||||
</div>
|
||||
{effectiveSelected?.detailsUrl ? (
|
||||
<div className="col-span-full">
|
||||
<div className="col-span-half">
|
||||
<a
|
||||
href={effectiveSelected.detailsUrl}
|
||||
target="_blank"
|
||||
|
||||
@@ -10,7 +10,7 @@ const Card = ({ children, className }: CardProps) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"bg-[#253445] rounded-lg mt-6 mx-4 px-4 py-4 shadow-2xl overflow-y-auto md:row-span-1",
|
||||
"bg-[#253445] rounded-lg mt-4 mx-2 px-4 py-4 shadow-2xl overflow-y-auto md:row-span-1",
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
import type { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import clsx from "clsx";
|
||||
|
||||
type CameraOverviewHeaderProps = {
|
||||
title: string;
|
||||
icon?: IconProp;
|
||||
img?: string;
|
||||
};
|
||||
|
||||
const CardHeader = ({ title, icon }: CameraOverviewHeaderProps) => {
|
||||
const CardHeader = ({ title, icon, img }: CameraOverviewHeaderProps) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"w-full border-b border-gray-600 flex flex-row items-center space-x-2 md:mb-6 relative"
|
||||
)}
|
||||
>
|
||||
{icon && <FontAwesomeIcon icon={icon} className="size-4" />}
|
||||
<h2 className="text-xl">{title}</h2>
|
||||
<div className="flex items-center space-x-2">
|
||||
{icon && <FontAwesomeIcon icon={icon} className="size-4" />}
|
||||
<h2 className="text-xl">{title}</h2>
|
||||
</div>
|
||||
{img && (
|
||||
<img src={img} alt="Logo" width={100} height={50} className="ml-auto" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,21 +1,82 @@
|
||||
import * as React from "react";
|
||||
import { Link } from "react-router";
|
||||
import Logo from "/MAV.svg";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faSliders } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faGear, faListCheck } from "@fortawesome/free-solid-svg-icons";
|
||||
import type { VersionFieldType } from "../../types/types";
|
||||
|
||||
async function fetchVersions(signal?: AbortSignal): Promise<VersionFieldType> {
|
||||
const res = await fetch("http://192.168.75.11/api/versions", { signal });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
const pad = (n: number) => String(n).padStart(2, "0");
|
||||
const normalizeToMs = (ts: number) => (ts < 1e12 ? ts * 1000 : ts); // seconds → ms if needed
|
||||
|
||||
function formatFromMs(ms: number, tz: "local" | "utc" = "local") {
|
||||
const d = new Date(ms);
|
||||
const h = tz === "utc" ? d.getUTCHours() : d.getHours();
|
||||
const m = tz === "utc" ? d.getUTCMinutes() : d.getMinutes();
|
||||
const s = tz === "utc" ? d.getUTCSeconds() : d.getSeconds();
|
||||
const day = tz === "utc" ? d.getUTCDate() : d.getDate();
|
||||
const month = (tz === "utc" ? d.getUTCMonth() : d.getMonth()) + 1;
|
||||
const year = tz === "utc" ? d.getUTCFullYear() : d.getFullYear();
|
||||
return `${pad(h)}:${pad(m)}:${pad(s)} ${pad(day)}-${pad(month)}-${year}`;
|
||||
}
|
||||
|
||||
export default function Header() {
|
||||
const [offsetMs, setOffsetMs] = React.useState<number | null>(null);
|
||||
const [nowMs, setNowMs] = React.useState<number>(Date.now());
|
||||
|
||||
React.useEffect(() => {
|
||||
const ac = new AbortController();
|
||||
fetchVersions(ac.signal)
|
||||
.then((data) => {
|
||||
const serverMs = normalizeToMs(data.timeStamp);
|
||||
setOffsetMs(serverMs - Date.now());
|
||||
})
|
||||
return () => ac.abort();
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
let timer: number;
|
||||
const schedule = () => {
|
||||
const now = Date.now();
|
||||
setNowMs(now);
|
||||
const delay = 1000 - (now % 1000);
|
||||
timer = window.setTimeout(schedule, delay);
|
||||
};
|
||||
schedule();
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
const serverNowMs = offsetMs == null ? nowMs : nowMs + offsetMs;
|
||||
const localStr = formatFromMs(serverNowMs, "local");
|
||||
const utcStr = formatFromMs(serverNowMs, "utc");
|
||||
|
||||
const Header = () => {
|
||||
return (
|
||||
<div className="relative bg-[#253445] border-b border-gray-500 items-center mx-auto px-2 sm:px-6 lg:px-8 p-4 flex flex-row justify-between">
|
||||
{/* Left: Logo */}
|
||||
<div className="w-30">
|
||||
<Link to={"/"}>
|
||||
<img src={Logo} alt="Logo" width={100} height={100} />
|
||||
<img src={Logo} alt="Logo" width={150} height={150} />
|
||||
</Link>
|
||||
</div>
|
||||
{/* Right: Texts stacked + icons */}
|
||||
<div className="flex items-center space-x-12">
|
||||
<div className="flex flex-col leading-tight text-white text-sm tabular-nums">
|
||||
<h2>Local: {localStr}</h2>
|
||||
<h2>UTC: {utcStr}</h2>
|
||||
</div>
|
||||
|
||||
<Link to={"/session-settings"}>
|
||||
<FontAwesomeIcon className="text-white" icon={faListCheck} />
|
||||
</Link>
|
||||
<Link to={"/system-settings"}>
|
||||
<FontAwesomeIcon className="text-white" icon={faGear} />
|
||||
</Link>
|
||||
</div>
|
||||
<Link to={"/system-settings"}>
|
||||
<FontAwesomeIcon icon={faSliders} />
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ const NavigationArrow = ({ side, settingsPage }: NavigationArrowProps) => {
|
||||
if (settingsPage) {
|
||||
return (
|
||||
<>
|
||||
{side === "TargetDetectionFront" ? (
|
||||
{side === "CameraFront" ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowRight}
|
||||
className="absolute top-[50%] right-[2%] backdrop-blur-md hover:cursor-pointer animate-bounce"
|
||||
|
||||
@@ -41,7 +41,7 @@ export function useGetOverviewSnapshot(cameraSide: string) {
|
||||
queryKey: ["overviewSnapshot", cameraSide],
|
||||
queryFn: () => fetchSnapshot(cameraSide),
|
||||
refetchOnWindowFocus: false,
|
||||
// refetchInterval: 1000,
|
||||
refetchInterval: 250,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useCallback, useEffect, useRef } from "react";
|
||||
async function fetchSighting() {
|
||||
const response = await fetch(
|
||||
// `http://100.82.205.44/api`
|
||||
`http://100.116.253.81/mergedHistory/sightingSummary?mostRecentRef=-1`
|
||||
`http://192.168.75.11/mergedHistory/sightingSummary?mostRecentRef=-1`
|
||||
);
|
||||
if (!response.ok) throw new Error("Failed to fetch sighting");
|
||||
return response.json();
|
||||
@@ -31,7 +31,7 @@ export function useLatestSighting() {
|
||||
const { data } = useQuery({
|
||||
queryKey: ["latestSighting"],
|
||||
queryFn: fetchSighting,
|
||||
refetchInterval: 500,
|
||||
refetchInterval: 100,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,86 +1,70 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { SightingWidgetType } from "../types/types";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
// const url = `http://100.82.205.44/SightingListFront/sightingSummary?mostRecentRef=-1`;
|
||||
|
||||
async function fetchSighting(url: string, ref: number, signal?: AbortSignal) {
|
||||
const dynamicUrl = `${url}${ref}`;
|
||||
|
||||
const res = await fetch(dynamicUrl, { signal });
|
||||
async function fetchSighting(url: string, ref: number): Promise<SightingWidgetType> {
|
||||
const res = await fetch(`${url}${ref}`);
|
||||
if (!res.ok) throw new Error(String(res.status));
|
||||
return (await res.json()) as SightingWidgetType;
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
export function useSightingFeed(url: string) {
|
||||
const [sightings, setSightings] = useState<SightingWidgetType[]>(
|
||||
() => Array(7).fill(null) as unknown as SightingWidgetType[]
|
||||
);
|
||||
const [noSighting, setNoSighting] = useState(false);
|
||||
const [sightings, setSightings] = useState<SightingWidgetType[]>([]);
|
||||
const [selectedRef, setSelectedRef] = useState<number | null>(null);
|
||||
const [mostRecent, setMostRecent] = useState<SightingWidgetType | null>(null);
|
||||
|
||||
const mostRecentRef = useRef<number>(-1);
|
||||
|
||||
const lastSeenRef = useRef<number | null>(null);
|
||||
|
||||
const { data, isPending } = useQuery({
|
||||
queryKey: ["sighting"],
|
||||
queryFn: ({ signal }) => fetchSighting(url, mostRecentRef.current, signal),
|
||||
refetchInterval: 2000,
|
||||
refetchIntervalInBackground: true,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 0,
|
||||
notifyOnChangeProps: ["data"],
|
||||
});
|
||||
const currentRef = useRef<number>(-1);
|
||||
const pollingTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const lastValidTimestamp = useRef<number>(Date.now());
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
const poll = async () => {
|
||||
try {
|
||||
const data = await fetchSighting(url, currentRef.current);
|
||||
|
||||
if (data.ref === -1) {
|
||||
setNoSighting(true);
|
||||
} else {
|
||||
setNoSighting(false);
|
||||
}
|
||||
if (data.ref === lastSeenRef.current) return; // duplicate payload → do nothing
|
||||
lastSeenRef.current = data.ref;
|
||||
const now = Date.now();
|
||||
|
||||
setSightings((prev) => {
|
||||
const existing = prev.find((p) => p?.ref === data.ref);
|
||||
const next = existing
|
||||
? prev
|
||||
: [data, ...prev.filter(Boolean)].slice(0, 7);
|
||||
if (data.ref === -1) {
|
||||
if (now - lastValidTimestamp.current > 60000) {
|
||||
console.warn("No valid sighting in over a minute. Restarting...");
|
||||
currentRef.current = -1;
|
||||
lastValidTimestamp.current = now;
|
||||
}
|
||||
|
||||
const stillHasSelection =
|
||||
selectedRef != null && next.some((s) => s?.ref === selectedRef);
|
||||
if (!stillHasSelection) {
|
||||
setSelectedRef(data.ref);
|
||||
pollingTimeout.current = setTimeout(poll, 400);
|
||||
} else {
|
||||
currentRef.current = data.ref;
|
||||
lastValidTimestamp.current = now;
|
||||
|
||||
setSightings(prev => {
|
||||
const updated = [data, ...prev].slice(0, 7);
|
||||
return updated;
|
||||
});
|
||||
|
||||
setMostRecent(data);
|
||||
setSelectedRef(data.ref);
|
||||
|
||||
pollingTimeout.current = setTimeout(poll, 100);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Polling error:", err);
|
||||
pollingTimeout.current = setTimeout(poll, 100);
|
||||
}
|
||||
};
|
||||
|
||||
return next;
|
||||
});
|
||||
setMostRecent(sightings[0]);
|
||||
mostRecentRef.current = data.ref ?? -1;
|
||||
}, [data, selectedRef, sightings]);
|
||||
poll();
|
||||
|
||||
const selected = useMemo(
|
||||
() =>
|
||||
selectedRef == null
|
||||
? null
|
||||
: sightings.find((s) => s?.ref === selectedRef) ?? null,
|
||||
[sightings, selectedRef]
|
||||
);
|
||||
return () => {
|
||||
if (pollingTimeout.current) clearTimeout(pollingTimeout.current);
|
||||
};
|
||||
}, [url]);
|
||||
|
||||
const effectiveSelected = selected ?? mostRecent ?? null;
|
||||
const selected = sightings.find(s => s?.ref === selectedRef) ?? mostRecent;
|
||||
|
||||
return {
|
||||
sightings,
|
||||
selectedRef,
|
||||
setSelectedRef,
|
||||
mostRecent,
|
||||
effectiveSelected,
|
||||
mostRecentRef,
|
||||
isPending,
|
||||
noSighting,
|
||||
effectiveSelected: selected,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,11 +6,10 @@ import { SightingFeedProvider } from "../context/providers/SightingFeedProvider"
|
||||
|
||||
const Dashboard = () => {
|
||||
return (
|
||||
<div className="mx-auto grid grid-cols-1 sm:grid-cols-1 lg:grid-cols-2 gap-2 px-2 sm:px-4 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
|
||||
url={
|
||||
"http://100.82.205.44/SightingListFront/sightingSummary?mostRecentRef="
|
||||
// "http://100.116.253.81/mergedHistory/sightingSummary?mostRecentRef="
|
||||
"http://192.168.75.11/SightingListFront/sightingSummary?mostRecentRef="
|
||||
}
|
||||
side="Front"
|
||||
>
|
||||
@@ -19,7 +18,7 @@ const Dashboard = () => {
|
||||
</SightingFeedProvider>
|
||||
|
||||
<SightingFeedProvider
|
||||
url="http://100.82.205.44/SightingListRear/sightingSummary?mostRecentRef="
|
||||
url="http://192.168.75.11/SightingListRear/sightingSummary?mostRecentRef="
|
||||
side="Rear"
|
||||
>
|
||||
<RearCameraOverviewCard className="order-2" />
|
||||
|
||||
@@ -18,7 +18,7 @@ const FrontCamera = () => {
|
||||
>
|
||||
<OverviewVideoContainer
|
||||
title={"Front Camera"}
|
||||
side="TargetDetectionFront"
|
||||
side="CameraFront"
|
||||
settingsPage={true}
|
||||
/>
|
||||
<CameraSettings title="Front Camera Settings" />
|
||||
|
||||
@@ -19,7 +19,7 @@ const RearCamera = () => {
|
||||
<CameraSettings title="Rear Camera Settings" />
|
||||
<OverviewVideoContainer
|
||||
title={"Rear Camera"}
|
||||
side={"TargetDetectionRear"}
|
||||
side={"CameraRear"}
|
||||
settingsPage={true}
|
||||
/>
|
||||
<Toaster />
|
||||
|
||||
13
src/pages/Session.tsx
Normal file
13
src/pages/Session.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import HitSearchCard from "../components/SessionForm/HitSearchCard.tsx";
|
||||
import SessionCard from "../components/SessionForm/SessionCard.tsx";
|
||||
|
||||
const Session = () => {
|
||||
return (
|
||||
<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">
|
||||
<HitSearchCard />
|
||||
<SessionCard />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Session;
|
||||
@@ -3,6 +3,9 @@ import "react-tabs/style/react-tabs.css";
|
||||
import NPEDCard from "../components/SettingForms/NPED/NPEDCard";
|
||||
import SettingForms from "../components/SettingForms/SettingForms/SettingForms";
|
||||
import NPEDHotlistCard from "../components/SettingForms/NPED/NPEDHotlistCard";
|
||||
import WiFiCard from "../components/SettingForms/WiFi&Modem/WiFiCard";
|
||||
import ModemCard from "../components/SettingForms/WiFi&Modem/ModemCard";
|
||||
import SystemCard from "../components/SettingForms/System/SystemCard";
|
||||
import { Toaster } from "sonner";
|
||||
import { useNPEDAuth } from "../hooks/useNPEDAuth";
|
||||
|
||||
@@ -13,9 +16,16 @@ const SystemSettings = () => {
|
||||
<div className="m-4">
|
||||
<Tabs selectedTabClassName="bg-gray-300 text-gray-900 font-semibold border-none">
|
||||
<TabList>
|
||||
<Tab>System</Tab>
|
||||
<Tab>Output</Tab>
|
||||
<Tab>Integrations</Tab>
|
||||
<Tab>WiFi and Modem</Tab>
|
||||
</TabList>
|
||||
<TabPanel>
|
||||
<div className="flex flex-col space-y-3">
|
||||
<SystemCard />
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<div className="flex flex-col space-y-3">
|
||||
<SettingForms />
|
||||
@@ -27,6 +37,12 @@ const SystemSettings = () => {
|
||||
<NPEDHotlistCard />
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<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">
|
||||
<WiFiCard />
|
||||
<ModemCard />
|
||||
</div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
<Toaster />
|
||||
</div>
|
||||
|
||||
@@ -102,6 +102,18 @@ export type SightingWidgetType = {
|
||||
// list of rects normalized 0..1
|
||||
};
|
||||
|
||||
export type VersionFieldType = {
|
||||
version: string;
|
||||
revision: string;
|
||||
buildtime: string;
|
||||
MAC: string;
|
||||
timeStamp: number;
|
||||
UUID: string;
|
||||
"Serial No.": string;
|
||||
"Model No.": string;
|
||||
};
|
||||
|
||||
|
||||
export type Metadata = {
|
||||
npedJSON: NpedJSON;
|
||||
"hotlist-matches": HotlistMatches;
|
||||
|
||||
Reference in New Issue
Block a user