diff --git a/TODO.txt b/TODO.txt new file mode 100644 index 0000000..e2ff0e0 --- /dev/null +++ b/TODO.txt @@ -0,0 +1,18 @@ +TODO: + +Hotlist upload (Question for Dion about API) and hits popping up in sighting stack. +NPED API working and catagories popping up in sighting stack. Images added to public folder. +Make the friendly name of each camera permeate throughout. +Make favicon MAV logo. +Swipe down to get to session page. +I have made an error I don't know how to fix in SightingFeedProvider.tsx +There is a bug in /front-camera-settings where the navigation arrow doesn't have a transparent background. I don't know why it is only that one and I can't find out why. Very strange. +The selected sighting in the sighting stack seems a tad buggy. Sometimes multiple get selected. +Can the selected sighting be shown in full detail. How this will look is still up for debate. Either as a pop up card as in AiQ Flexi, or in the OVerview card?? +How do you know if the time has sync? Make UTC red if not sync. +Can the relative aspect ratio in SightingOverview.tsx be the ratio of image pixel size of the image to best take advantage of the space? + + +FYI: + +Session, WiFi and Modem stuff isn't implimented in the backend. Those are just placeholders for now. diff --git a/public/homepage-banner.jpg b/public/homepage-banner.jpg new file mode 100644 index 0000000..e0440ac Binary files /dev/null and b/public/homepage-banner.jpg differ diff --git a/src/components/FrontCameraOverview/FrontCameraOverviewCard.tsx b/src/components/FrontCameraOverview/FrontCameraOverviewCard.tsx index 8c543ed..6862fd2 100644 --- a/src/components/FrontCameraOverview/FrontCameraOverviewCard.tsx +++ b/src/components/FrontCameraOverview/FrontCameraOverviewCard.tsx @@ -26,7 +26,7 @@ const FrontCameraOverviewCard = ({ className }: CardProps) => { )} >
- + {/* */}
diff --git a/src/components/RearCameraOverview/RearCameraOverviewCard.tsx b/src/components/RearCameraOverview/RearCameraOverviewCard.tsx index 3b82fe3..1e9f467 100644 --- a/src/components/RearCameraOverview/RearCameraOverviewCard.tsx +++ b/src/components/RearCameraOverview/RearCameraOverviewCard.tsx @@ -25,7 +25,7 @@ const RearCameraOverviewCard = ({ className }: CardProps) => { )} >
- + {/* */}
diff --git a/src/components/SightingOverview/SightingOverview.tsx b/src/components/SightingOverview/SightingOverview.tsx index 7092e08..47aad61 100644 --- a/src/components/SightingOverview/SightingOverview.tsx +++ b/src/components/SightingOverview/SightingOverview.tsx @@ -35,7 +35,7 @@ const SightingOverview = () => {
-
+
{ diff --git a/src/components/SightingsWidget/SightingWidgetDetails.tsx b/src/components/SightingsWidget/SightingWidgetDetails.tsx index b91385a..2bde728 100644 --- a/src/components/SightingsWidget/SightingWidgetDetails.tsx +++ b/src/components/SightingsWidget/SightingWidgetDetails.tsx @@ -9,6 +9,14 @@ const SightingWidgetDetails = ({ }: SightingWidgetDetailsProps) => { return (
+
+ VRM:{" "} + {effectiveSelected?.vrm ?? "—"} +
+
+ Timestamp:{" "} + {effectiveSelected?.timeStamp ?? "—"} +
Make:{" "} {effectiveSelected?.make ?? "—"} @@ -17,6 +25,16 @@ const SightingWidgetDetails = ({ Model:{" "} {effectiveSelected?.model ?? "—"}
+
+ Country:{" "} + {effectiveSelected?.countryCode ?? "—"} +
+
+ Seen:{" "} + + {effectiveSelected?.seenCount ?? "—"} + +
Colour:{" "} {effectiveSelected?.color ?? "—"} @@ -43,40 +61,8 @@ const SightingWidgetDetails = ({ {effectiveSelected?.overviewSize ?? "—"}
-
- Motion:{" "} - {effectiveSelected?.motion ?? "—"} -
-
- Seen:{" "} - - {effectiveSelected?.seenCount ?? "—"} - -
-
- Location:{" "} - - {effectiveSelected?.locationName ?? "—"} - -
-
- Lane:{" "} - {effectiveSelected?.laneID ?? "—"} -
-
- Radar:{" "} - - {effectiveSelected?.radarSpeed ?? "—"} - -
-
- Track:{" "} - - {effectiveSelected?.trackSpeed ?? "—"} - -
{effectiveSelected?.detailsUrl ? ( -
+
{ return (
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 = () => { Logo
-
+ {/* 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;