Commit Graph

62 Commits

Author SHA1 Message Date
622d24b4be Say why a rename was refused, instead of the library's generic error
Your camera answered SetScopes with a SOAP fault, ter:NotAuthorized: the
Operator login may not rename it. The onvif library reports every
SetScopes failure as "Wrong `SetScopes` response", so that cause was
invisible. We now keep the raw reply and turn a NotAuthorized fault into
our own error, and the page says the login isn't allowed to rename the
camera and that the name kept here still works.

Also give credential-store an explicit .ts import so plain Node can run
it, as the admin script's modules already do.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 13:48:27 -05:00
10d08e3b87 Name cameras here, and rename them on the camera
Both of your cameras call themselves "I91ET", so the slimmed dashboard
showed two identical names.

- A nickname kept in the registry now wins wherever a camera is named:
  cards, camera page, live view and its window title, pop-outs and the
  recordings list. It survives a rescan, and clearing it falls back to
  the camera's own name, then its model.
- Rename on the camera page also writes the camera's own name and
  location over ONVIF SetScopes, so everything else on the network sees
  them. Since SetScopes replaces every configurable scope, the others are
  read and sent back untouched; the result is re-read from the camera and
  reported as applied or adjusted, and the change is audited.
- cameraName() lives in a client-safe module, because the registry is
  server-only and the dashboard names cameras in the browser.
- NEXT_DIST_DIR lets a build run while dev servers hold .next.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 07:42:28 -05:00
814079d44d vrek: issues for naming cameras, in this app and on the camera
Both of the user's cameras call themselves "I91ET", so the slimmed-down
dashboard shows two identical names. One issue adds a nickname kept here;
the other changes what the camera itself advertises over ONVIF SetScopes,
which needs its own checks first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 07:34:36 -05:00
81e55a092b Centre the login buttons in the space below the fields
The button row now takes the room left under the username and password
and centres in it, so Save sits in the middle of that space rather than
hugging the last field.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 07:30:33 -05:00
f953f38716 Fit the login form to the picture's box
The form now sits in the same 16:9 box as the snapshot, so a card keeps
its height whichever it shows. A compact variant drops the aside about
which user to create and tightens the spacing, since that space is fixed;
the camera page keeps the full form, where there is room. Anything that
still doesn't fit scrolls inside the box rather than stretching the card.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 07:28:43 -05:00
6a663a5d82 Put the camera login where the picture would be
A camera that can't log in has no picture to show, so the form stands in
for the snapshot instead of appearing under it, with the status badge
above it so the card still says what is wrong. Saving brings the picture
back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 07:27:00 -05:00
f63fb664dd Click the picture to pop out; put settings behind an icon
The snapshot is now the way into the live pop-out, as a link, so keyboard
and a blocked popup both still work. The icon over the top right is the
camera's own page instead: a pencil, named "Settings for <camera>". The
pop-out icon is gone, so a card carries one icon, not two.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 07:25:29 -05:00
96de9092ab Move card detail to the camera page; keep your status and icon tweaks
A card is now the picture with the status over its top left and Pop out
over its top right, the camera's name, and nothing else. The Details
disclosure is gone: the address, model and firmware, main stream,
location, web page link and Change login all live on the camera page,
which gained them beside Recordings and Refresh. Anything needing
attention still appears on the card.

The login form and web page link moved to src/app/camera-login.tsx so
both screens share them. StatusBadge now takes a className for placement
rather than positioning itself, keeps inline-flex so its icon and label
line up, and gains a faint ring so it stays legible over a bright frame.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 07:21:50 -05:00
d28694d545 Move Pop out onto the picture as an icon
It sits over the top right of the snapshot instead of taking a line
beside the camera's name. Icon-only, so it carries a name that says what
it does and which camera ("Pop out Porch"), and it stays an ordinary link
so a blocked popup still opens a tab.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 06:46:18 -05:00
efe49d7363 Simplify the dashboard: a picture, a name, a status
A card now leads with the frame, the camera's name, its status and Pop
out. The address, device line, stream summary, location, web page and
login links and the profile table all moved behind Details, whose closed
summary shows the device line. Scanning is one button, with timeout and
sweep behind Options summarised as "5s, sweep".

Anything needing attention stays outside any disclosure: setup required,
camera login, session ended, a stopped stream.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 06:41:46 -05:00
213d1c05f3 Add the shared disclosure, the basis for showing less by default
Native details/summary, so keyboard, screen readers and find-in-page keep
working and closed is not hidden. What someone opens stays open for them,
remembered per section in that browser only; anything needing attention
is forced open regardless, and a closed section still carries the useful
value in its summary. Safe when storage is blocked and on the server.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 06:39:35 -05:00
8be9b1c154 vrek: break the simplification goal into one issue per screen
Five: the shared disclosure and its rules, then the dashboard, the camera
page, the recordings page, and what a new install sees first. Target is
those five, measured at 0 today.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 06:34:56 -05:00
c9caf12922 vrek: goal to simplify the interface, showing less by default
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 06:32:59 -05:00
54f1efe895 vrek: retire the npm packaging goal, met at 5 of 5
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 06:29:32 -05:00
c517b9e4fc vrek: npm packaging goal met, 5 of 5
Published and installed on another server, with the clip pop-out checked
there. It took four releases: 0.1.0 exposed shipping TypeScript for the
CLI and an externalized git dependency, and the rest fixed those, the
working-directory disagreement over MediaMTX, and added scan progress.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 06:24:13 -05:00
db615e9e2a Release 0.1.4: recordings play in a pop-out window
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 05:47:11 -05:00
0acd69b9ca Play recordings in a pop-out window, like the live view
Play now opens /recordings/play?clip=… in a window of its own: the video
on black, fitted to the window, with a fading overlay naming the camera
and the time. One window is reused for whichever clip is opened next, and
it stays an ordinary link so a blocked popup still opens a tab. The page
checks the session and resolves the clip inside the recordings folder.

Also add DOM cleanup to three test files that had none: renders piled up
in the document and tests read each other's markup, which is what made
the new test pass or fail depending on its position in the file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 05:38:51 -05:00
5886c010ea vrek: remote access stays on the VPN, no internet-facing video
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 20:21:25 -05:00
2667967cf0 Say what a scan is doing, and remember where MediaMTX went (0.1.3)
- A scan no longer looks hung: the button counts seconds against the
  chosen timeout, the page says in words that it is listening for
  cameras and asking every address in turn, the refresh afterwards has
  its own state, and the result reports how many cameras were found, how
  long it took and how many addresses were checked. scanNetwork() returns
  that report; discoverCameras() still exists on top of it.
- The config pointer now remembers the binary folder beside the data
  folder, so install-video and start can't disagree about where MediaMTX
  lives, whichever directory each is run from.

Checked through an installed tarball against the real network: 2 cameras
found, 4,093 addresses probed, 3.0 s.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 20:00:30 -05:00
731206be8e Resolve data and binary folders before starting the server (0.1.2)
onvif-dashboard install-video put MediaMTX in ./bin relative to where it
was run, but start launches the server from inside the package, so the
app looked for it there and reported it missing. serverEnvironment() now
settles CAMERAS_DATA_DIR and CAMERAS_BIN_DIR while still in the user's
directory, and start passes them on. Checked from an installed tarball:
MediaMTX in ./bin is found and the video bridge comes up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 19:35:50 -05:00
daaecb7452 Fix the published package: compile the CLI, bundle onvif (0.1.1)
Installing 0.1.0 failed twice, both my mistakes.

- Node refuses to strip types under node_modules, so shipping .ts for the
  CLI could never work from an installed package. tsconfig.cli.json now
  compiles those modules to dist/ as ES modules, and the helper scripts
  are plain .mjs.
- serverExternalPackages made Turbopack emit require("onvif-<hash>"),
  a name that resolves nowhere, because onvif comes from a git URL.
  Bundling it fixes that; checked from an installed tarball, where
  /api/discover answered 200 and a real camera's info and snapshot came
  back through the packaged server.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 19:21:53 -05:00
30b1f36e1b Ship a standalone build and an onvif-dashboard start command
- next.config.ts builds standalone output with image optimization off,
  and scripts/bundle-standalone.mjs puts the static files beside the
  server, then drops what tracing swept in but the app never runs (the
  MediaMTX binary and sharp's platform binaries, 79 MB). The build is
  27 MB and npm pack is 4.9 MB.
- The CLI gained start, which runs that server from any directory with
  the remembered data folder and stored key in its environment, and
  admin, so the messages in the UI can name a command that exists.
- Messages that said "npm run …" now say "onvif-dashboard …".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 19:09:13 -05:00
65345d0984 Add the first-run experience and the onvif-dashboard command
- src/lib/first-run.ts settles where data lives (asked once, remembered
  in a pointer under the user's config dir), creates the owner-only key
  that protects stored camera logins, and reports whether MediaMTX is
  installed. Without a terminal nothing prompts: defaults are taken and
  logged, so a service still starts. Running it again changes nothing.
- cli/onvif-dashboard.mjs is the published command: setup, install-video
  and help. Node runs the TypeScript in src/lib directly, so the CLI
  needs no build of its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 19:00:16 -05:00
0556b3913e Prepare to publish: onvif-dashboard under MIT, paths in one module
- Rename the package to onvif-dashboard, drop private, add the MIT
  LICENSE and the metadata npm needs; a files list keeps the tarball to
  473 kB instead of sweeping in the MediaMTX binary.
- src/lib/paths.ts now decides where everything lives, so the app can run
  from any directory. CAMERAS_DATA_DIR moves the data folder;
  CAMERAS_BIN_DIR moves the helper binaries; every existing per-file
  override still wins, and the defaults are exactly what running from the
  repo meant before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 18:55:08 -05:00
6b0f093248 vrek: targets for the npm and Debian packaging goals
Each is met by one check on a clean machine: installed, running, with a
camera visible.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 18:42:49 -05:00
47b5759ae3 vrek: goals to publish on npm and as a Debian package
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 18:41:12 -05:00
e6a6aa33d3 vrek: retire the top-level web interface goal
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 18:37:49 -05:00
9b20a0b32d vrek: record upstream PR agsh/onvif#500, defer the v1 fork work
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 18:36:09 -05:00
b576bb44a7 vrek: advanced image settings checked in the browser, issue closed
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 18:34:34 -05:00
daabcf74ab vrek: both cameras' existing accounts do reach ISAPI
Corrects the claim that one camera needed a separate web account; that
came from a single failed request while accounts were being changed on
that camera. The advanced settings work with the logins already stored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 18:33:55 -05:00
d49be1b933 vrek: correct the lock theory, the web account was deleted
The later 401s are explained by the account no longer existing, so the
illegal-login-lock hypothesis is withdrawn. The HTTPS refusal observed
while the account worked over HTTP still stands unexplained.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 18:32:29 -05:00
c0e987d06e vrek: camera traffic is plain HTTP; TLS investigation deferred to tomorrow
- Record that ONVIF, snapshots and ISAPI all use plain HTTP and that
  MediaMTX pulls RTSP with the login in the URL; no password crosses the
  wire in the clear, but everything else does.
- Both cameras answer HTTPS with a self-signed certificate, yet digest
  that works on port 80 is refused on 443; repeated attempts tripped the
  camera's login lock, so testing resumes tomorrow, one attempt at a time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 18:31:09 -05:00
12d0604dfd vrek: defer the forum survey and the dashboard marker
Probing the cameras directly answered what the desk research was for, and
the camera page already says whether advanced settings are available.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 18:23:49 -05:00
a74e737d64 vrek: ISAPI stays read-only
An Operator account can read ISAPI imaging but not write it (403,
lowPrivilege, proven with a no-op PUT that changed nothing). Rather than
give the app an Administrator camera login, the ISAPI-only settings stay
view-only and changes keep going through ONVIF; editing them is deferred.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 18:22:04 -05:00
5cc54036bd Keep camera fixtures byte-for-byte as the device sent them
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 18:18:48 -05:00
8e87fd9d3d Show the camera's own advanced image settings over ISAPI
- Read /ISAPI/Image/channels/1 and its capabilities with digest auth and
  the camera's stored login, behind a vendor module.
- New "Advanced image" panel: shutter and its limits, gain limit, WDR,
  highlight and backlight compensation, noise reduction, white balance,
  day/night switching, lens distortion correction, digital zoom, the
  camera's lights and power line frequency.
- Availability comes from the camera's own capabilities, so only settings
  it advertises are shown; a refused login says a camera web account is
  needed, anything else says the camera doesn't offer more. Answers are
  cached per camera and path for five minutes.
- Parser and options are tested against real documents from a camera,
  kept as fixtures (imaging data only).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 18:18:35 -05:00
e319b0198f vrek: goal to research the cameras' own web interface
- New goal: understand the Hikvision/Annke web UI and decide between
  embedding it and rebuilding its features natively.
- Issue for surveying forums and open-source clients, explicitly
  excluding firmware flashing, backdoors and auth bypasses.
- Finding: an activated Operator account reaches most of ISAPI; the
  alert stream needs more rights, main-stream stills are refused on both
  cameras, and the two cameras' account setups differ.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 17:36:13 -05:00
817310569d vrek: live video and recording goal met (6/6) and retired
- Both cameras watched live on main and sub, and motion clips recorded
  and played back from both; audio plays too, correcting the earlier
  assumption that browsers can't decode G711 in MP4.
- Retention check deferred to 2026-09-27, when today's clips age out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 17:19:37 -05:00
4fc68cacf1 Add a Recordings page to browse, play and delete motion clips
- /recordings lists clips newest first with camera, time and size, plays
  them in the browser and deletes after a confirmation.
- /api/recordings lists clips; /api/recordings/clip serves one with Range
  support so seeking works, and deletes it. A clip is named by folder and
  file name, both matched against fixed patterns and resolved inside the
  recordings folder, so no request can reach another file.
- Each camera's page links to its own clips.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 17:16:14 -05:00
4a0790565f Record motion clips from ONVIF events
- Watch each camera's ONVIF VideoSource/MotionAlarm; record while it
  reports movement and stop after a 10s post-roll, through a per-stream
  MediaMTX path so viewers are never interrupted.
- Clips go to RECORDINGS_DIR (default .data/recordings), one folder per
  recorded stream; RECORD_STREAM, RECORD_KEEP_DAYS and MOTION_RECORDING
  configure the rest. An hourly sweep deletes clips past retention.
- Add allCameraRecords(): listCameras() calls connection(), which never
  resolves outside a request and silently stalled startup. Background
  work is no longer awaited in instrumentation either.
- Document the settings in the README.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 17:09:13 -05:00
2edafdeeaf vrek: live video slice done; recording design and motion event findings
- First live-video slice closed: both cameras watched live on main and
  sub (4 of the goal's 6 checks), leftover-MediaMTX cleanup confirmed.
- Recording decided: motion-triggered clips, no pre-roll, 7-day
  retention, stored under .data with an env override, stream by config.
- Motion source decided: ONVIF VideoSource/MotionAlarm; Hikvision's
  alert stream refuses the ONVIF login (403).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 16:51:18 -05:00
41021a62ad Add live video: WHEP relay route and WebRTC player
- POST/DELETE /api/cameras/[id]/live/whep check the session, resolve the
  camera from the registry, and relay WebRTC signaling to MediaMTX on
  localhost. The browser never sees MediaMTX's address, its error text or
  a camera login; video flows browser↔MediaMTX, not through Next.
- LivePlayer negotiates with the browser's own RTCPeerConnection and
  shows connecting, reconnecting and failed states with Retry.
- The camera page gains a Live section; the pop-out plays live video and
  falls back to snapshot polling if it can't. Both offer Main/Sub.
- Stop a leftover MediaMTX from a server that didn't exit cleanly: record
  its pid and, on startup, stop it only if that pid is still our binary
  with our config.
- Export vrek log: both cameras watched live on main and sub (4 of the
  goal's 6 checks).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 16:40:21 -05:00
5824db8ea9 Add MediaMTX video bridge: pinned installer and supervised localhost process
- npm run video:install downloads the pinned MediaMTX v1.21.0 for this
  platform, verifies its SHA-256 against checksums committed in
  src/lib/mediamtx-install.ts, and installs it into gitignored bin/.
- On server start, instrumentation launches MediaMTX with a generated
  owner-only config: API and WebRTC signaling on 127.0.0.1, only the ICE
  port (UDP 8189) on the LAN, a hashed per-boot API password, no
  anonymous users, unused protocols off. Restarts on crash with backoff,
  stops with the server; VIDEO_BRIDGE=off skips it.
- Camera streams are added to MediaMTX at runtime, on demand, with the
  stored login; nothing with a password is written to disk or echoed.
- rtspSourceWithLogin() reads a profile's RTSP URI over ONVIF.
- Export vrek log (live video goal, decisions, codec finding).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 15:33:55 -05:00
1c9270c7c0 vrek: measure gol-2nxgqjn as met on one user observation
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 15:10:48 -05:00
2aac38146d vrek: ISAPI snapshot findings, retire met goals
- Record that camera …af54 has no full-resolution still over ONVIF or
  ISAPI (fnd-dw1pqcw, fnd-k1jyt83); close iss-3vck3js.
- Retire gol-2nxgqjn, and gol-wqf95dq and gol-8v1wqvp as met on one
  user observation each.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 15:08:50 -05:00
ebd488b672 Add camera pop-out window
- "Pop out" on each dashboard card opens /cameras/[id]/live in a named
  popup sized to the main stream (a plain link if popups are blocked).
- The live page shows only the frame, never stretched past its own size,
  with a fading name/time/refresh-rate overlay and error handling.
- The card pauses its own polling while its pop-out is open; the two
  coordinate over a BroadcastChannel.
- Share frame rendering (FrameImage) between card and pop-out.
- Ignore a stray build cache folder; export vrek log.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 15:02:39 -05:00
f4d40dbea6 Add image and stream editing with audit log, manual exposure controls
- Per-section Edit/Save for image settings and streams (Server Actions),
  validated against the camera's own ranges; stream saves ask to confirm
  the restart. Results report applied or adjusted values.
- Every write is appended to a local audit log (.data/audit.jsonl).
- In Manual exposure, edit exposure time (with approximate shutter speed)
  and gain; the read-only page shows them, and a hint explains that
  Brightness mainly works in Auto.
- Pin onvif to the mikemainguy fork for complete imaging/encoder setters;
  set turbopack.root automatically when onvif is a linked checkout.
- Export vrek log.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 14:44:00 -05:00
0ec434ca26 Add read-only camera settings page and dashboard stream summary
Read-only settings and stream URLs (vrek gol-2nxgqjn), shaped by a
read-only probe of the real cameras (findings fnd-*) and decisions
dec-s205dvs and dec-gtw493s.

- src/lib/camera-settings.ts: reads streams, image, time/NTP and
  network over ONVIF behind a CameraSettingsSource interface, so an
  ISAPI source can be added later. Sections are read independently
  and fail on their own with fixed text. RTSP URLs have credentials
  stripped and use the registry host. camera.ts exports its cached
  connect() and the profile helpers it shares with it (iss-7eaywtf).
- /cameras/[id]: server-rendered page with an access check, 404 for
  unknown ids or non-private hosts, and a streamed Suspense section.
  Shows streams (with copyable RTSP URLs and an ONVIF-login note),
  image, time/NTP with a drift warning, and network, plus Refresh
  and back. Copy falls back to execCommand over plain LAN HTTP,
  where the clipboard API is unavailable (iss-s935a4n).
- Dashboard card: the name links to the camera page; a main-stream
  summary line ("Main 4096×1860 · H.264 · 20 fps"); a manufacturer
  of "ONVIF" is hidden (iss-pv2bvzj).

522 tests, 99.9% line coverage. Checked by the user in a browser
against both cameras. Refreshes the vrek export (the icon principle
now covers the page's new icons).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 13:00:37 -05:00
b05047d8e5 vrek: retire icons goal, add icon principle
Retires gol-m8je1wc (icons done for now) and records principle
pri-8ev5m89, derived from dec-1vnz5fw. Icons come from lucide-react,
always accompany text, are aria-hidden, use standard Tailwind sizes,
map one concept to one icon, and are tested by lucide-<name> class.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 12:35:54 -05:00
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