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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
- 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>
- 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>
- 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>
- 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>
- /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>
- 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>
- 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>
- 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>
- "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>
- 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>
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>
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>
Moves the IPv4/internal/link-local/prefix-size checks out of
localSubnetHosts() into a helper. No behaviour change: the 30 discovery
tests pass and onvif.ts stays at 100% coverage.
Completes securing the web front end (vrek gol-wqf95dq, iss-r5vrjx7).
- /login: Server Action with a generic error, same-site-only redirect
back to ?next=, and throttling of failed logins (10 per address and
100 overall per 15 min). The header shows "Signed in as" with
Sign out (iss-nj9wmwp).
- First run with no admin: instrumentation prints a one-time setup
code, shared with the app through globalThis. /setup requires it,
and 5 wrong codes rotate it. "Skip for now" runs unsecured for the
browser session behind a red warning banner on every page
(iss-9nxdndr).
- src/proxy.ts: optimistic redirects to /login or /setup, 401 for
the API, and the 12 h sliding session refresh. requirePageAccess()
and apiAccessDenied() re-check in the page and all 6 route handlers
(iss-76d5wrb).
- An expired session now shows "Your session has ended" instead of
the camera-login form.
- README documents in-app setup, skipping and signing in.
Verified with unit tests (383, 99.9% line coverage), end to end
against `next start`, and manually in a browser by the user.
Refreshes the vrek export.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Groundwork for securing the web front end (vrek gol-wqf95dq). The app
does not enforce login yet.
- src/lib/admin-file.ts: the admin file at ADMIN_AUTH_FILE (default
.data/admin.json). scrypt hashing (N=2^16, random salt, bounded
parameters, constant-time compare), zod-validated reads where a
malformed file is an error, and atomic 0600 writes that won't
replace an existing admin without overwrite. Plain Node, so the
CLI can share it (iss-mffqscg).
- src/lib/admin-auth.ts: server-only app layer; failed logins always
cost one hash.
- scripts/create-admin.mts + `npm run admin:create`: create or reset
the admin outside the app, interactive (hidden, confirmed) or piped
(iss-7xmka20). The README documents it, a no-npm Node one-liner,
the file format, and password reset.
- src/lib/session-token.ts and session.ts: stateless HMAC-signed
session cookie, keyed from the password hash so a password change
ends every session, with a 12 h sliding window (iss-e27nb70,
dec-f0xar8r).
281 tests, 99.8% line coverage. Refreshes the vrek export.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
next build warned that the env-dependent registry and credential-store
paths made Turbopack trace the whole project (vrek iss-jvxcd1n).
Marking only the readFile calls cleared the warning, but it still
traced .data/cameras.json and the encrypted .data/credentials.json,
which a standalone deployment would copy from the build machine.
A /* turbopackIgnore: true */ on each default path.join(process.cwd(),
".data", ...) fixes both. The traced files for every route now include
nothing from src/, public/ or .data/. The env-var overrides the tests
use still work. Refreshes the vrek export.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Route tests for camera info, snapshot and credentials (vrek
iss-nc2tj7c): input validation, store-only-after-verify, reconnects
after login changes, and fixed error messages with no upstream text.
- Providers and layout tests (iss-nc5w0j8).
- Discovery tests for onvif.ts (iss-3bg4r6e): multicast parsing,
unicast sweep batching and subnet limits, de-duplication and
merging. They run on a fake network, replacing onvif Discovery,
node:dgram and node:os, per new vrek principle pri-e14bahk
(replaceable transport; tests never touch the real network).
- coverage.thresholds.lines = 95, so `npm run coverage` fails below
the goal (iss-zjpc22k).
200 tests, 99.77% line coverage. Refreshes the vrek export.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds 47 tests for camera.ts (vrek iss-qak2mz8), bringing it to 100%
line coverage. A scripted stand-in for the onvif Cam class drives the
connect outcomes: profiles, warnings, inactive devices, and login
errors. Snapshots run over real HTTP against a local server with real
Digest and Basic 401 challenges. Also covers isAllowedHost edge cases.
Overall line coverage is 72.4% (143 tests). Refreshes the vrek export.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
cameraErrorResponse now returns fixed text for each error class and
logs the original server-side. Upstream ONVIF/SOAP responses, socket
errors and credential-file paths no longer reach the browser (vrek
iss-tz5s098). CameraAuthError gains a missingLogin flag so the UI can
still tell "no login saved" apart from "login rejected".
Tests throw errors carrying a fake password and file path, and check
neither appears in any response. Closes the dashboard migration (vrek
iss-qbh3541). Refreshes the vrek export.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
Sets up Vitest (jsdom, Testing Library, v8 coverage) per the Next 16
testing guide, using Vite's native tsconfig path resolution instead of
vite-tsconfig-paths. A server-only stub and a temp-data helper let
server modules run in isolation. @types/node moves to ^24 to match the
Node 24 runtime (a vitest 5 peer requirement).
First 68 tests cover the discover route (including iss-dbwgww8: no raw
errors in responses), the credential store, the camera registry,
camera-route helpers and the refresh-rate schema. Line coverage is
31.2%, toward the 95% goal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Discovers ONVIF cameras (Hikvision/Annke) over WS-Discovery, keeps a
server-side registry and an encrypted credential store, and serves
info, snapshot and credentials routes for each camera.
The home page is now a Server Component that lists known cameras from
the registry on load, without a scan (vrek iss-a0hz0py). The snapshot
refresh rate lives in the URL (?refresh=), and a scan refreshes the
server-rendered list.
Route input is validated with zod (iss-rjqy3hy), and /api/discover no
longer returns raw error messages (iss-dbwgww8). Adds
@tanstack/react-query and zod as dependencies, with a QueryClient
provider in the root layout.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>