cameras/src/app/camera-scanner.tsx
Michael Mainguy 870b4cd2af Move dashboard client onto React Query hooks
Adds src/app/camera-queries.ts: React Query hooks over our own routes,
with each camera's queries keyed ['camera', id]. The camera card,
snapshot polling, login form and network scan use these hooks instead
of hand-rolled fetch/useEffect state (vrek iss-2fm6x2y, iss-ksxmctm,
iss-m032zwq, iss-8hfq2y2).

- Snapshot polling pauses in hidden tabs, never overlaps a slow
  frame, and stops after a failure until Retry. Each frame's object
  URL is created and revoked in one effect, so none leak under Strict
  Mode.
- Saving or forgetting a login resets only that camera's queries.
- New "Forget saved login" action, shown for stored logins.
- Fix: a scan timeout typed below 1 s now clamps to 1 s instead of
  falling back to 5 s.

Adds jsdom component tests (test/dom.tsx helpers): 93 tests, line
coverage 54.3%. Refreshes the vrek export.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 09:01:34 -05:00

59 lines
1.9 KiB
TypeScript

"use client";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useScanNetwork } from "./camera-queries";
/**
* Runs a network scan. The scan writes the server-side registry, so afterwards the
* page is refreshed and the server re-renders the camera list.
*/
export default function CameraScanner() {
const router = useRouter();
const scan = useScanNetwork();
const [timeoutSecs, setTimeoutSecs] = useState(5);
const [unicastSweep, setUnicastSweep] = useState(true);
const found = scan.data?.cameras.length;
return (
<>
<button
onClick={() =>
scan.mutate({ timeoutSecs, unicastSweep }, { onSuccess: () => router.refresh() })
}
disabled={scan.isPending}
className="rounded-md bg-black px-4 py-2 text-sm font-medium text-white disabled:opacity-50 dark:bg-white dark:text-black"
>
{scan.isPending ? "Scanning…" : "Scan network"}
</button>
<label className="flex items-center gap-2 text-sm">
Timeout
<input
type="number"
min={1}
max={30}
value={timeoutSecs}
onChange={(e) => setTimeoutSecs(Number(e.target.value))}
className="w-16 rounded border border-zinc-300 bg-transparent px-2 py-1 dark:border-zinc-700"
/>
s
</label>
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={unicastSweep}
onChange={(e) => setUnicastSweep(e.target.checked)}
/>
Unicast sweep of local subnet
</label>
{found !== undefined && (
<p className="w-full text-sm text-zinc-600 dark:text-zinc-400">
Scan found {found} camera{found === 1 ? "" : "s"}
</p>
)}
{scan.isError && <p className="w-full text-sm text-red-600">Error: {scan.error.message}</p>}
</>
);
}