Go to file
Michael Mainguy 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
.idea Track vrek export, MCP config and IDE VCS mapping 2026-09-19 08:52:49 -05:00
.vrek Name cameras here, and rename them on the camera 2026-09-20 07:42:28 -05:00
cli Say what a scan is doing, and remember where MediaMTX went (0.1.3) 2026-09-19 20:00:30 -05:00
public Initial commit from Create Next App 2026-09-19 07:15:19 -05:00
scripts Fix the published package: compile the CLI, bundle onvif (0.1.1) 2026-09-19 19:21:53 -05:00
src Name cameras here, and rename them on the camera 2026-09-20 07:42:28 -05:00
test Keep camera fixtures byte-for-byte as the device sent them 2026-09-19 18:18:48 -05:00
.gitattributes Keep camera fixtures byte-for-byte as the device sent them 2026-09-19 18:18:48 -05:00
.gitignore Name cameras here, and rename them on the camera 2026-09-20 07:42:28 -05:00
.mcp.json Track vrek export, MCP config and IDE VCS mapping 2026-09-19 08:52:49 -05:00
AGENTS.md Initial commit from Create Next App 2026-09-19 07:15:19 -05:00
CLAUDE.md Initial commit from Create Next App 2026-09-19 07:15:19 -05:00
eslint.config.mjs Initial commit from Create Next App 2026-09-19 07:15:19 -05:00
LICENSE Prepare to publish: onvif-dashboard under MIT, paths in one module 2026-09-19 18:55:08 -05:00
next.config.ts Name cameras here, and rename them on the camera 2026-09-20 07:42:28 -05:00
package-lock.json Add image and stream editing with audit log, manual exposure controls 2026-09-19 14:44:00 -05:00
package.json Release 0.1.4: recordings play in a pop-out window 2026-09-20 05:47:11 -05:00
postcss.config.mjs Initial commit from Create Next App 2026-09-19 07:15:19 -05:00
README.md Record motion clips from ONVIF events 2026-09-19 17:09:13 -05:00
tsconfig.cli.json Fix the published package: compile the CLI, bundle onvif (0.1.1) 2026-09-19 19:21:53 -05:00
tsconfig.json Add admin login foundations: scrypt admin file, CLI, sessions 2026-09-19 09:46:40 -05:00
vitest.config.mts Add the first-run experience and the onvif-dashboard command 2026-09-19 19:00:16 -05:00

Cameras

A web interface for discovering and managing Hikvision and Annke IP cameras on your local network.

Securing the web interface

The dashboard and its API are protected by a single admin login. The login is stored in a file, never in plain text: the password is hashed with scrypt (random salt, N=65536, r=8, p=1), so it can't be recovered from the file.

Setting Default
ADMIN_AUTH_FILE (environment variable, e.g. in .env.local) ./.data/admin.json

If the file doesn't exist, the app starts unsecured: anyone who can reach it can view your cameras and change their logins. To avoid ever running it that way, create the admin file before the first start (below).

Setting it up from the browser instead

With no admin file, the server prints a one-time setup code in its console at startup:

====================================================================
  No admin login is set up: the camera dashboard is NOT secured.

  To secure it from a browser, open /setup and enter this one-time code:

      XLZ7-Q4MB
  ...

Opening the app sends you to /setup, which asks for that code plus the new admin username and password. Only someone who can see the server console can claim the login. After 5 wrong codes a new one is printed.

You can also choose Skip for now and run unsecured. Every page then shows a red warning banner, the API stays open to anyone on the network, and the setup prompt returns the next time the browser is restarted.

Signing in

Once an admin exists, every page and API call needs a session: pages redirect to /login, and the API answers 401. A session lasts 12 hours from your last activity. After 10 wrong passwords from one address (or 100 from all addresses) within 15 minutes, logins are paused for up to 15 minutes. Sign out (top right) ends the session in that browser; to sign out everywhere, change the password.

npm run admin:create

It asks for a username and a password (at least 12 characters; typing is hidden, and you confirm it), then writes the file with owner-only permissions (0600). Options:

npm run admin:create -- --username admin      # only ask for the password
npm run admin:create -- --force               # replace the existing admin (reset the password)
printf '%s\n' "$PW" | npm run admin:create -- --username admin   # non-interactive, e.g. provisioning

Without npm: a Node one-liner

Any Node 24+ can produce the same file. The password is read from the terminal without echoing, so it never appears in your shell history or the process list:

(read -rs PW && export PW && umask 077 && mkdir -p .data && node -e '
const c = require("node:crypto"), N = 65536, r = 8, p = 1, salt = c.randomBytes(16);
const key = c.scryptSync(process.env.PW, salt, 64, { N, r, p, maxmem: 256 * N * r });
const passwordHash = ["scrypt", N, r, p, salt.toString("base64"), key.toString("base64")].join("$");
console.log(JSON.stringify({ version: 1, username: "admin", passwordHash }, null, 2));
' > .data/admin.json)

The parentheses run it in a subshell, so the password variable and the stricter umask end with it.

Change username: "admin" to taste. Usernames are 1–64 letters, digits, or . _ @ -.

File format

{
  "version": 1,
  "username": "admin",
  "passwordHash": "scrypt$65536$8$1$<salt, base64>$<64-byte key, base64>"
}

A file that exists but is malformed stops the app from authenticating anyone rather than quietly turning security off; fix it or delete it.

Forgot the password?

Run npm run admin:create -- --force (or delete the file and create it again). Changing the password signs out every existing session.

Live video (MediaMTX)

Browsers can't play the cameras' RTSP streams, so live video goes through MediaMTX, a single MIT-licensed binary that the app will run on this machine only (localhost). Install the pinned build with:

npm run video:install

This downloads the MediaMTX release pinned in src/lib/mediamtx-install.ts for your platform (macOS or Linux, Intel or ARM), checks its SHA-256 against the value committed there, and puts the binary in ./bin/ (not checked in). A download that doesn't match is refused and nothing is written. Re-running it does nothing if that build is already installed.

To upgrade, change version in MEDIAMTX_RELEASE and replace every sha256 with the values from that release's checksums.sha256 on GitHub (cross-check them against the per-file digests on the release page), then run npm run video:install again.

Motion recording

When a camera reports motion over ONVIF, the app records a clip and stops a few seconds after the motion ends. Clips are fragmented MP4 and play in the browser.

Variable Default Meaning
RECORDINGS_DIR .data/recordings Where clips are written; point it at an external drive or NAS if you like. One folder per camera.
RECORD_STREAM sub Which stream is recorded. sub is about 5 MB per minute of motion, main about 38 MB.
RECORD_KEEP_DAYS 7 Clips older than this are deleted by an hourly sweep.
MOTION_RECORDING on Set to off to record nothing.

There is no pre-roll: a clip starts when the camera reports motion, which is a second or two after movement begins. Recording pulls the camera only while motion lasts.


This is a Next.js project bootstrapped with create-next-app.

Getting Started

First, run the development server:

npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev

Open http://localhost:3000 with your browser to see the result.

You can start editing the page by modifying app/page.tsx. The page auto-updates as you edit the file.

This project uses next/font to automatically optimize and load Geist, a new font family for Vercel.

Learn More

To learn more about Next.js, take a look at the following resources:

You can check out the Next.js GitHub repository - your feedback and contributions are welcome!

Deploy on Vercel

The easiest way to deploy your Next.js app is to use the Vercel Platform from the creators of Next.js.

Check out our Next.js deployment documentation for more details.