More messing about, and learning how it all works. Added time to header. Changed some padding. Changed video to preview not target detection. Added TODO list.

This commit is contained in:
2025-09-09 15:57:35 +01:00
parent 0c405d2038
commit 70d0d3234d
14 changed files with 163 additions and 113 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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={() => {

View File

@@ -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"

View File

@@ -10,7 +10,7 @@ const Card = ({ children, className }: CardProps) => {
return (
<div
className={clsx(
"bg-[#253445] rounded-lg mt-4 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
)}
>

View File

@@ -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<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 */}
@@ -13,17 +63,20 @@ const Header = () => {
<img src={Logo} alt="Logo" width={150} height={150} />
</Link>
</div>
<div className="flex items-center space-x-8">
{/* 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 icon={faListCheck} />
<FontAwesomeIcon className="text-white" icon={faListCheck} />
</Link>
<Link to={"/system-settings"}>
<FontAwesomeIcon icon={faGear} />
<FontAwesomeIcon className="text-white" icon={faGear} />
</Link>
</div>
</div>
);
};
export default Header;
}

View File

@@ -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"

View File

@@ -40,7 +40,7 @@ export function useGetOverviewSnapshot(cameraSide: string) {
queryKey: ["overviewSnapshot", cameraSide],
queryFn: () => fetchSnapshot(cameraSide),
refetchOnWindowFocus: false,
// refetchInterval: 1000,
refetchInterval: 250,
});
useEffect(() => {

View File

@@ -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<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();
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,
};
}

View File

@@ -18,7 +18,7 @@ const FrontCamera = () => {
>
<OverviewVideoContainer
title={"Front Camera"}
side="TargetDetectionFront"
side="CameraFront"
settingsPage={true}
/>
<CameraSettings title="Front Camera Settings" />

View File

@@ -18,7 +18,7 @@ const RearCamera = () => {
<CameraSettings title="Rear Camera Settings" />
<OverviewVideoContainer
title={"Rear Camera"}
side={"TargetDetectionRear"}
side={"CameraRear"}
settingsPage={true}
/>
</div>

View File

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