Adds lucide-react (vrek dec-1vnz5fw, chosen over Heroicons and React Icons) for the icons goal gol-m8je1wc. - Camera status badge on each card (iss-scj42jq): Live, Connecting, Needs login, Not activated, Offline, Session ended. Each is an icon plus a visible label, derived from existing query state with no extra device calls. Logic in src/app/camera-status.tsx. - Action icons (iss-chrdd0v): scan (radar, then spinner), refresh rate, change or forget login, retry, the camera web page link (replaces ↗), save, sign in and sign out. - Warning and error icons (iss-egy082x): unsecured banner, setup panel, inline errors, session-ended notice, login and setup headings. Icons are decorative (aria-hidden), spinners respect reduced motion, and every accessible name is unchanged (all 403 prior tests pass untouched). 416 tests, 99.9% line coverage. The vitest threshold comment now points to principle pri-be2smzk. Refreshes the vrek export. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
70 lines
2.3 KiB
TypeScript
70 lines
2.3 KiB
TypeScript
"use client";
|
|
|
|
import { CircleAlert, LoaderCircle, Radar } from "lucide-react";
|
|
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="inline-flex items-center gap-2 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 ? (
|
|
<LoaderCircle aria-hidden className="size-4 animate-spin motion-reduce:animate-none" />
|
|
) : (
|
|
<Radar aria-hidden className="size-4" />
|
|
)}
|
|
{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="flex w-full items-center gap-1.5 text-sm text-red-600">
|
|
<CircleAlert aria-hidden className="size-4 shrink-0" />
|
|
Error: {scan.error.message}
|
|
</p>
|
|
)}
|
|
</>
|
|
);
|
|
}
|