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