cameras/src/app/camera-scanner.tsx
Michael Mainguy 190191df92 Add Lucide icons: camera status badge, actions and warnings
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>
2026-09-19 12:33:16 -05:00

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>
)}
</>
);
}