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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
- 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>
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>
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>
- 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>
- 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>
- 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>
- /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>
- 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>
- 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>
- 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>
- "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>
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>
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>
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>