diff --git a/src/components/UI/Header.tsx b/src/components/UI/Header.tsx
index d0445b8..4d71685 100644
--- a/src/components/UI/Header.tsx
+++ b/src/components/UI/Header.tsx
@@ -1,10 +1,60 @@
+import * as React from "react";
import { Link } from "react-router";
import Logo from "/MAV.svg";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { faGear } from "@fortawesome/free-solid-svg-icons";
-import { faListCheck } 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
{
+ 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(null);
+ const [nowMs, setNowMs] = React.useState(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 (
{/* Left: Logo */}
@@ -13,17 +63,20 @@ const Header = () => {
-
+ {/* Right: Texts stacked + icons */}
+
+
+
Local: {localStr}
+ UTC: {utcStr}
+
+
-
+
-
+
);
-};
-
-
-export default Header;
+}
diff --git a/src/components/UI/NavigationArrow.tsx b/src/components/UI/NavigationArrow.tsx
index 7a15c62..ebdc43e 100644
--- a/src/components/UI/NavigationArrow.tsx
+++ b/src/components/UI/NavigationArrow.tsx
@@ -26,7 +26,7 @@ const NavigationArrow = ({ side, settingsPage }: NavigationArrowProps) => {
if (settingsPage) {
return (
<>
- {side === "TargetDetectionFront" ? (
+ {side === "CameraFront" ? (
fetchSnapshot(cameraSide),
refetchOnWindowFocus: false,
- // refetchInterval: 1000,
+ refetchInterval: 250,
});
useEffect(() => {
diff --git a/src/hooks/useSightingFeed.ts b/src/hooks/useSightingFeed.ts
index cc3632c..d117335 100644
--- a/src/hooks/useSightingFeed.ts
+++ b/src/hooks/useSightingFeed.ts
@@ -1,89 +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 {
+ 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(
- () => Array(7).fill(null) as unknown as SightingWidgetType[]
- );
- const [noSighting, setNoSighting] = useState(false);
+ const [sightings, setSightings] = useState([]);
const [selectedRef, setSelectedRef] = useState(null);
const [mostRecent, setMostRecent] = useState(null);
- const mostRecentRef = useRef(-1);
-
- const lastSeenRef = useRef(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(-1);
+ const pollingTimeout = useRef | null>(null);
+ const lastValidTimestamp = useRef(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();
- setMostRecent(data);
+ 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;
+ }
- setSightings((prev) => {
- const existing = prev.find((p) => p?.ref === data.ref);
- const next = existing
- ? prev
- : [data, ...prev.filter(Boolean)].slice(0, 7);
+ pollingTimeout.current = setTimeout(poll, 400);
+ } else {
+ currentRef.current = data.ref;
+ lastValidTimestamp.current = now;
- const stillHasSelection =
- selectedRef != null && next.some((s) => s?.ref === selectedRef);
- if (!stillHasSelection) {
- setSelectedRef(data.ref);
+ 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]);
- // setMostRecent(data);
- 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,
};
}
diff --git a/src/pages/FrontCamera.tsx b/src/pages/FrontCamera.tsx
index 774904c..054c335 100644
--- a/src/pages/FrontCamera.tsx
+++ b/src/pages/FrontCamera.tsx
@@ -18,7 +18,7 @@ const FrontCamera = () => {
>
diff --git a/src/pages/RearCamera.tsx b/src/pages/RearCamera.tsx
index 34b3c99..40e368b 100644
--- a/src/pages/RearCamera.tsx
+++ b/src/pages/RearCamera.tsx
@@ -18,7 +18,7 @@ const RearCamera = () => {
diff --git a/src/types/types.ts b/src/types/types.ts
index 6f2e18c..3eb823a 100644
--- a/src/types/types.ts
+++ b/src/types/types.ts
@@ -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;