Added working System config, NOT working WiFi & modem and Session pages as placeholders. Also added NPED images.

This commit is contained in:
2025-09-08 15:21:17 +01:00
parent 4fd3bd4319
commit 0c405d2038
26 changed files with 5195 additions and 659 deletions

4308
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

BIN
public/DVLA_Cat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
public/NPED.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

BIN
public/NPED_Cat_A.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
public/NPED_Cat_B.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
public/NPED_Cat_C.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -4,6 +4,7 @@ import { Route, Routes } from "react-router";
import FrontCamera from "./pages/FrontCamera"; import FrontCamera from "./pages/FrontCamera";
import RearCamera from "./pages/RearCamera"; import RearCamera from "./pages/RearCamera";
import SystemSettings from "./pages/SystemSettings"; import SystemSettings from "./pages/SystemSettings";
import Session from "./pages/Session";
import { NPEDUserProvider } from "./context/providers/NPEDUserContextProvider"; import { NPEDUserProvider } from "./context/providers/NPEDUserContextProvider";
function App() { function App() {
@@ -15,6 +16,7 @@ function App() {
<Route path="front-camera-settings" element={<FrontCamera />} /> <Route path="front-camera-settings" element={<FrontCamera />} />
<Route path="rear-camera-settings" element={<RearCamera />} /> <Route path="rear-camera-settings" element={<RearCamera />} />
<Route path="system-settings" element={<SystemSettings />} /> <Route path="system-settings" element={<SystemSettings />} />
<Route path="session-settings" element={<Session />} />
</Route> </Route>
</Routes> </Routes>
</NPEDUserProvider> </NPEDUserProvider>

View File

@@ -83,7 +83,7 @@ const CameraSettingFields = () => {
name="cameraAddress" name="cameraAddress"
type="text" type="text"
className="p-2 border border-gray-400 rounded-lg" className="p-2 border border-gray-400 rounded-lg"
placeholder="123, London Road..." placeholder="RTSP://..."
autoComplete="street-address" autoComplete="street-address"
/> />
</div> </div>

View 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;

View 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;

View File

@@ -5,7 +5,7 @@ import NPEDFields from "./NPEDFields";
const NPEDCard = () => { const NPEDCard = () => {
return ( return (
<Card> <Card>
<CardHeader title={"NPED Config"} /> <CardHeader title={"NPED Config"} img={"/NPED.jpg"} />
<NPEDFields /> <NPEDFields />
</Card> </Card>
); );

View 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!");
}

View 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);
}
}

View 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;

View 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);
}
}

View 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;

View 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;

View File

@@ -10,7 +10,7 @@ const Card = ({ children, className }: CardProps) => {
return ( return (
<div <div
className={clsx( 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-4 px-4 py-4 shadow-2xl overflow-y-auto md:row-span-1",
className className
)} )}
> >

View File

@@ -1,24 +1,29 @@
import type { IconProp } from "@fortawesome/fontawesome-svg-core"; import type { IconProp } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import clsx from "clsx"; import clsx from "clsx";
type CameraOverviewHeaderProps = { type CameraOverviewHeaderProps = {
title: string; title: string;
icon?: IconProp; icon?: IconProp;
img?: string;
}; };
const CardHeader = ({ title, icon }: CameraOverviewHeaderProps) => {
const CardHeader = ({ title, icon, img }: CameraOverviewHeaderProps) => {
return ( return (
<div <div
className={clsx( className={clsx(
"w-full border-b border-gray-600 flex flex-row items-center space-x-2 md:mb-6" "w-full border-b border-gray-600 flex flex-row items-center md:mb-6"
)} )}
> >
<div className="flex items-center space-x-2">
{icon && <FontAwesomeIcon icon={icon} className="size-4" />} {icon && <FontAwesomeIcon icon={icon} className="size-4" />}
<h2 className="text-xl">{title}</h2> <h2 className="text-xl">{title}</h2>
</div> </div>
{img && <img src={img} alt="Logo" width={100} height={50} className="ml-auto" />}
</div>
); );
}; };
export default CardHeader; export default CardHeader;

View File

@@ -1,21 +1,29 @@
import { Link } from "react-router"; import { Link } from "react-router";
import Logo from "/MAV.svg"; import Logo from "/MAV.svg";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSliders } from "@fortawesome/free-solid-svg-icons"; import { faGear } from "@fortawesome/free-solid-svg-icons";
import { faListCheck } from "@fortawesome/free-solid-svg-icons";
const Header = () => { const Header = () => {
return ( 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"> <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"> <div className="w-30">
<Link to={"/"}> <Link to={"/"}>
<img src={Logo} alt="Logo" width={100} height={100} /> <img src={Logo} alt="Logo" width={150} height={150} />
</Link> </Link>
</div> </div>
<Link to={"/system-settings"}> <div className="flex items-center space-x-8">
<FontAwesomeIcon icon={faSliders} /> <Link to={"/session-settings"}>
<FontAwesomeIcon icon={faListCheck} />
</Link> </Link>
<Link to={"/system-settings"}>
<FontAwesomeIcon icon={faGear} />
</Link>
</div>
</div> </div>
); );
}; };
export default Header; export default Header;

View File

@@ -4,7 +4,7 @@ import { useCallback, useEffect, useRef } from "react";
async function fetchSighting() { async function fetchSighting() {
const response = await fetch( const response = await fetch(
// `http://100.82.205.44/api` // `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"); if (!response.ok) throw new Error("Failed to fetch sighting");
return response.json(); return response.json();
@@ -31,7 +31,7 @@ export function useLatestSighting() {
const { data } = useQuery({ const { data } = useQuery({
queryKey: ["latestSighting"], queryKey: ["latestSighting"],
queryFn: fetchSighting, queryFn: fetchSighting,
refetchInterval: 500, refetchInterval: 100,
}); });
useEffect(() => { useEffect(() => {

View File

@@ -45,6 +45,8 @@ export function useSightingFeed(url: string) {
if (data.ref === lastSeenRef.current) return; // duplicate payload → do nothing if (data.ref === lastSeenRef.current) return; // duplicate payload → do nothing
lastSeenRef.current = data.ref; lastSeenRef.current = data.ref;
setMostRecent(data);
setSightings((prev) => { setSightings((prev) => {
const existing = prev.find((p) => p?.ref === data.ref); const existing = prev.find((p) => p?.ref === data.ref);
const next = existing const next = existing
@@ -59,7 +61,8 @@ export function useSightingFeed(url: string) {
return next; return next;
}); });
setMostRecent(sightings[0]); // setMostRecent(sightings[0]);
// setMostRecent(data);
mostRecentRef.current = data.ref ?? -1; mostRecentRef.current = data.ref ?? -1;
}, [data, selectedRef, sightings]); }, [data, selectedRef, sightings]);

View File

@@ -6,11 +6,10 @@ import { SightingFeedProvider } from "../context/providers/SightingFeedProvider"
const Dashboard = () => { const Dashboard = () => {
return ( 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 <SightingFeedProvider
url={ url={
"http://100.82.205.44/SightingListFront/sightingSummary?mostRecentRef=" "http://192.168.75.11/SightingListFront/sightingSummary?mostRecentRef="
// "http://100.116.253.81/mergedHistory/sightingSummary?mostRecentRef="
} }
side="Front" side="Front"
> >
@@ -19,7 +18,7 @@ const Dashboard = () => {
</SightingFeedProvider> </SightingFeedProvider>
<SightingFeedProvider <SightingFeedProvider
url="http://100.82.205.44/SightingListRear/sightingSummary?mostRecentRef=" url="http://192.168.75.11/SightingListRear/sightingSummary?mostRecentRef="
side="Rear" side="Rear"
> >
<RearCameraOverviewCard className="order-2" /> <RearCameraOverviewCard className="order-2" />

13
src/pages/Session.tsx Normal file
View 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;

View File

@@ -3,6 +3,9 @@ import "react-tabs/style/react-tabs.css";
import NPEDCard from "../components/SettingForms/NPED/NPEDCard"; import NPEDCard from "../components/SettingForms/NPED/NPEDCard";
import SettingForms from "../components/SettingForms/SettingForms/SettingForms"; import SettingForms from "../components/SettingForms/SettingForms/SettingForms";
import NPEDHotlistCard from "../components/SettingForms/NPED/NPEDHotlistCard"; 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 { Toaster } from "sonner";
import { useNPEDAuth } from "../hooks/useNPEDAuth"; import { useNPEDAuth } from "../hooks/useNPEDAuth";
@@ -13,9 +16,16 @@ const SystemSettings = () => {
<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">
<TabList> <TabList>
<Tab>System</Tab>
<Tab>Output</Tab> <Tab>Output</Tab>
<Tab>Integrations</Tab> <Tab>Integrations</Tab>
<Tab>WiFi and Modem</Tab>
</TabList> </TabList>
<TabPanel>
<div className="flex flex-col space-y-3">
<SystemCard />
</div>
</TabPanel>
<TabPanel> <TabPanel>
<div className="flex flex-col space-y-3"> <div className="flex flex-col space-y-3">
<SettingForms /> <SettingForms />
@@ -27,6 +37,12 @@ const SystemSettings = () => {
<NPEDHotlistCard /> <NPEDHotlistCard />
</div> </div>
</TabPanel> </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> </Tabs>
<Toaster /> <Toaster />
</div> </div>

906
yarn.lock

File diff suppressed because it is too large Load Diff