cameras/.vrek/log.ndjson
Michael Mainguy 462141aa35 Add admin login foundations: scrypt admin file, CLI, sessions
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>
2026-09-19 09:46:40 -05:00

341 lines
167 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{"id":"evt-4gnef5mwb0p7","type":"principal.created","subject":"prn-cmzz6ey","actor":"prn-cmzz6ey","actor_kind":"human","session":null,"payload":{"id":"prn-cmzz6ey","kind":"human","handle":"unknown","model":null},"at":"2026-09-19T13:17:08.655Z","parents":[],"hash":"0e584dd0f4ecaf44943b98a046324b13ea8254ad742e1d47cb5e22a34068a27a"}
{"id":"evt-4m6c69a7ff07","type":"workspace.configured","subject":null,"actor":"prn-cmzz6ey","actor_kind":"human","session":null,"payload":{"name":"cameras"},"at":"2026-09-19T13:17:08.656Z","parents":["evt-4gnef5mwb0p7"],"hash":"b67d67e17571c463cc48d389590627d1c4b2230dc931ccf1f47ddfd231c7c0e3"}
{"id":"evt-p29p53002whg","type":"principal.created","subject":"prn-q80g8mz","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"id":"prn-q80g8mz","kind":"agent","handle":"claude","model":"claude-opus-5"},"at":"2026-09-19T13:18:09.387Z","parents":["evt-4m6c69a7ff07"],"hash":"50647a106f7e629b3218bba488066d44905e7dc9176d7d631d499f983fa36b2a"}
{"id":"evt-y950xfjmdrpm","type":"node.created","subject":"gol-6q1q5mr","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"goal","title":"Easy-to-use web interface for managing Hisense and Annke cameras","body":"Provide a simple web UI (this Next.js app) to discover, view, and manage Hisense and Annke IP cameras from one place, without needing each vendor's own tools.","status":"active","owner":null,"attrs":{},"weight":null,"target":null,"direction":"up","unit":null},"at":"2026-09-19T13:18:09.389Z","parents":["evt-p29p53002whg"],"hash":"e500905a99b7cb971cf0261772b8ba2a537b460bacd562318301cf2446641155"}
{"id":"evt-9rw30xydxg03","type":"node.updated","subject":"gol-6q1q5mr","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"title":"Easy-to-use web interface for managing Hikvision and Annke cameras","body":"Provide a simple web UI (this Next.js app) to discover, view, and manage Hikvision and Annke IP cameras from one place, without needing each vendor's own tools. Annke cameras are largely Hikvision OEM hardware, so both are expected to share the Hikvision ISAPI surface."}},"at":"2026-09-19T13:18:29.271Z","parents":["evt-y950xfjmdrpm"],"hash":"3362e9eeaff74e9091f485c6fa719708893fc27f9d4575dd66d0e929ba28ae59"}
{"id":"evt-t662qrse8wf5","type":"node.created","subject":"pri-mz2jxpb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"principle","title":"Minimal dependencies: no new package without a recorded, answered justification","body":"The baseline is what package.json holds as of 2026-09-19 — runtime: next, react, react-dom, onvif, server-only; dev: typescript, eslint, eslint-config-next, tailwindcss, @tailwindcss/postcss, @types/*. Anything beyond that (runtime or dev) is not added until a vrek question has been opened with vrek_ask and answered by a human. The question must state: (1) what problem the package solves, (2) why it can't reasonably be done with the platform (Node built-ins, Web APIs, Next.js/React features) or code already here, (3) size / transitive dependency count and whether it is server-only or ships to the client, (4) maintenance and security health (last release, open advisories, license), (5) how hard it would be to remove later. Why: every dependency is attack surface on a box that holds camera credentials, adds upgrade churn, and this app's job (talk ONVIF/ISAPI over HTTP, render a UI) is well covered by fetch, crypto and Next itself. Rules out: installing a package 'to try it', pulling a library for a helper that is a few lines of code, and adding a UI component kit. Upgrading an existing package's version is not a new dependency.","status":"active","owner":"prn-q80g8mz","attrs":{},"weight":null},"at":"2026-09-19T13:20:37.601Z","parents":["evt-9rw30xydxg03"],"hash":"f76135bece320651018a7d8bcf3fc71b1844e2430d17364d1b2bc823df710bac"}
{"id":"evt-hw63e1h6hsq0","type":"edge.added","subject":"pri-mz2jxpb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"guides","from":"pri-mz2jxpb","to":"gol-6q1q5mr"},"at":"2026-09-19T13:20:37.603Z","parents":["evt-t662qrse8wf5"],"hash":"4555857423c119c5a51e931b135617297d9f59539cdb88a39ea68d0a8f6a8335"}
{"id":"evt-37er6eht710q","type":"node.created","subject":"pri-p9h51hx","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"principle","title":"Modular code: small single-purpose modules with one-way dependencies, vendor quirks behind a common camera interface","body":"Layers, top to bottom, each depending only on the ones below it: (1) UI — src/app pages and components; (2) entry points — src/app/api/**/route.ts and Server Actions, kept thin: parse and validate input, call one lib function, map the result or error to a response (as info/route.ts does today via camera-route.ts); (3) domain — src/lib modules, one responsibility each: discovery (onvif.ts), device protocol (camera.ts), camera registry (camera-registry.ts), credential store (credential-store.ts), HTTP glue (camera-route.ts); (4) platform — Node built-ins and the few allowed packages. Vendor-specific behaviour (Hikvision ISAPI endpoints, Annke firmware quirks) lives in its own module behind a shared CameraTarget/CameraInfo-style interface, so a page or route never branches on brand. A module exports types and functions, not mutable state; errors are typed classes (CameraAuthError, CredentialStoreError) that the route layer maps to HTTP status. No import cycles, no reaching into another module's private helpers, and nothing in src/lib imports from src/app. A file that grows a second responsibility gets split. Why: device protocols are the part most likely to change or grow (ISAPI, PTZ, recording, new models); keeping them isolated means a new camera feature is a new module plus a thin route, not an edit across the UI. Rules out: business logic in route handlers or components, one big 'utils' file, and vendor checks scattered through the UI.","status":"active","owner":"prn-q80g8mz","attrs":{},"weight":null},"at":"2026-09-19T13:22:42.461Z","parents":["evt-hw63e1h6hsq0"],"hash":"f92df5ca42bc6b1dc444b1589e32ca30abec0c959f73fd8f7fe96bb761debf75"}
{"id":"evt-5yncr2v9h2qx","type":"edge.added","subject":"pri-p9h51hx","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"guides","from":"pri-p9h51hx","to":"gol-6q1q5mr"},"at":"2026-09-19T13:22:42.463Z","parents":["evt-37er6eht710q"],"hash":"fb27b46e1478c5c4b02c7c67851dcd533ed0275fbfd22d3129738b729d2df771"}
{"id":"evt-98rz8qgb6hyy","type":"node.created","subject":"pri-e0qjyy0","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"principle","title":"Write Next.js code against the bundled v16 docs (node_modules/next/dist/docs), not memory of older versions","body":"The installed next is 16.3.5, and the docs that match it ship in node_modules/next/dist/docs. Read the relevant page before writing Next-specific code. Next 16 rules that older habits break (source: 02-guides/upgrading/version-16.md unless noted): params, searchParams, cookies() and headers() are Promises and must be awaited; type route handlers with the global RouteContext<'/api/...'> helper. Request interception is src/proxy.ts exporting proxy; middleware.ts is deprecated, and proxy is for coarse checks, not authorization (01-getting-started/16-proxy.md). The edge runtime is deprecated, so don't export runtime='edge'. Turbopack is the default and a custom webpack config fails the build. revalidateTag takes (tag, profile); updateTag and refresh() exist for Server Actions. unstable_cache is replaced by 'use cache', which requires cacheComponents, and that is off here. unstable_noStore is replaced by connection(). serverRuntimeConfig and publicRuntimeConfig are removed; read env at runtime after await connection(). next lint is removed, so run eslint directly. next/image blocks local IPs by default. Why: training data and blog posts describe Next 13–15; code written from them compiles poorly or uses removed APIs. Rules out: middleware.ts, synchronous params access, unstable_cache, and edge runtime exports.","status":"active","owner":"prn-q80g8mz","attrs":{},"weight":null},"at":"2026-09-19T13:22:48.194Z","parents":["evt-5yncr2v9h2qx"],"hash":"7fded33ab649be556cdf36b9659a280000aa67a629ec7ac857f000f28553f566"}
{"id":"evt-paffx15pwdkd","type":"edge.added","subject":"pri-e0qjyy0","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"guides","from":"pri-e0qjyy0","to":"gol-6q1q5mr"},"at":"2026-09-19T13:22:48.195Z","parents":["evt-98rz8qgb6hyy"],"hash":"d96db3a8657808d0f5d92fe682f3e3039e2889722bea9705ba19dbe934afad50"}
{"id":"evt-n3wssys9zfmf","type":"node.created","subject":"pri-tyrxdz9","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"principle","title":"Camera credentials and device access stay on the server; the client only ever sees minimal DTOs","body":"Every src/lib module that touches devices, credentials, the registry or process.env starts with import 'server-only', so importing it into a Client Component is a build error. Client code may import types only (import type). Routes and Server Actions return only what the UI needs: never a password, never a raw device record or SOAP/ISAPI response, never an upstream error message verbatim, since those can echo URLs with user:pass. Secrets are non-NEXT_PUBLIC_ env vars (e.g. CAMERA_CREDENTIALS_KEY) kept in .env* at the project root and out of git; stored credentials stay encrypted at rest in .data/ (gitignored). Why: the server holds the logins for every camera on the network, and props, action return values and error bodies are all serialized to the browser. Sources: 02-guides/data-security.md ('Data Access Layer', 'Controlling return values', 'Preventing client-side execution of server-only code'); 02-guides/environment-variables.md; 02-guides/backend-for-frontend.md ('Working with headers'). Rules out: an API that returns a password, even to 'pre-fill' a form; NEXT_PUBLIC_ secrets; and passing lib objects straight into client props.","status":"active","owner":"prn-q80g8mz","attrs":{},"weight":null},"at":"2026-09-19T13:22:52.351Z","parents":["evt-paffx15pwdkd"],"hash":"3fdca55b4cf156d12738d48ccf71f448449290937a23332eea8494d18f3e2939"}
{"id":"evt-b3xyagkdfnjk","type":"edge.added","subject":"pri-tyrxdz9","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"guides","from":"pri-tyrxdz9","to":"gol-6q1q5mr"},"at":"2026-09-19T13:22:52.353Z","parents":["evt-n3wssys9zfmf"],"hash":"e3c2d213cb1bf774055ea91fa602eb791689adeb94992345715f0f1b26c98773"}
{"id":"evt-j4tn6nn59and","type":"node.created","subject":"pri-m1csgrm","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"principle","title":"Every route handler and Server Action is a public, untrusted entry point: validate input and only talk to known private cameras","body":"Next's docs are explicit: Route Handlers are public HTTP endpoints, and Server Functions are reachable by direct POST, not just through the UI. Being LAN-only doesn't change that. So each entry point validates its own input: the [id] param, the JSON body, and FormData fields for type, length and format. Validation is small hand-written checks in the lib layer, with no schema library (see the minimal-dependencies principle). The device address is always resolved server-side from the camera registry by id and must pass isAllowedHost (private IPv4); a client-supplied host or URL is never fetched, which prevents SSRF. Every outbound ONVIF/ISAPI call has a timeout. Why: this server can reach every device on the LAN with stored admin logins, so an unvalidated endpoint is a pivot into the network. Sources: 02-guides/backend-for-frontend.md ('Public Endpoints', 'Verify payloads', 'Proxying to a backend'); 02-guides/server-actions.md ('Security'); 02-guides/data-security.md ('Validating client input'). Rules out: accepting host or port from the request, trusting that a request came from our own UI, and device calls with no timeout.","status":"active","owner":"prn-q80g8mz","attrs":{},"weight":null},"at":"2026-09-19T13:22:57.221Z","parents":["evt-b3xyagkdfnjk"],"hash":"e371efae71f617d7493fe4d2042f63db6259c45ad084ba901c47f7788a355f59"}
{"id":"evt-z4eev123b4a3","type":"edge.added","subject":"pri-m1csgrm","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"guides","from":"pri-m1csgrm","to":"gol-6q1q5mr"},"at":"2026-09-19T13:22:57.223Z","parents":["evt-j4tn6nn59and"],"hash":"d42a599e928f814ffef62557e5d7c1604f553bd2f98f5b256f944fe3418b939b"}
{"id":"evt-7ccp8csngzyx","type":"node.created","subject":"pri-01np850","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"principle","title":"Server Components by default, 'use client' only on small interactive leaves; server code calls src/lib directly, never our own /api","body":"Pages and layouts are Server Components that read data by calling src/lib functions directly. Route Handlers exist for Client Components, e.g. snapshot refresh or polling, not for server-to-server calls to ourselves. 'use client' goes on the smallest component that needs state, effects or event handlers, because everything a client file imports ships to the browser. Only serializable props cross the boundary. Slow per-camera reads are fetched in parallel (Promise.allSettled, so one dead camera doesn't fail the page) and wrapped in <Suspense> so they stream. Client polling uses plain fetch in an effect, with no SWR or React Query unless a dependency question justifies one. Why: this keeps the client bundle and the attack surface small and pages fast when some cameras are offline. Sources: 01-getting-started/05-server-and-client-components.md ('Reducing JS bundle size'); 02-guides/server-and-client-boundary.md; 02-guides/backend-for-frontend.md ('Caveats > Server Components'); 01-getting-started/06-fetching-data.md ('Streaming', 'Parallel data fetching'). Rules out: 'use client' at page level by reflex and server components fetching http://localhost/api/....","status":"active","owner":"prn-q80g8mz","attrs":{},"weight":null},"at":"2026-09-19T13:23:01.925Z","parents":["evt-z4eev123b4a3"],"hash":"cc4c0c5d19f1694aad4824bfd3e21d1135c6f6d82f3abec557aa411aaac51f11"}
{"id":"evt-ce37bvhnb4k2","type":"edge.added","subject":"pri-01np850","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"guides","from":"pri-01np850","to":"gol-6q1q5mr"},"at":"2026-09-19T13:23:01.926Z","parents":["evt-7ccp8csngzyx"],"hash":"c7a1cf9eeeecacffccbe906d108f1c92003b838180c061fe8c1554aa850c1ee8"}
{"id":"evt-2r7e4hajwf8v","type":"node.created","subject":"pri-qqrp49f","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"principle","title":"Live device state is never cached; mutations go through Server Actions that return expected errors as values","body":"Camera status, snapshots and settings read from a device are always fetched fresh. Route Handlers and fetch are uncached by default in Next 16 with cacheComponents off, so don't opt them into caching. React.cache is fine for de-duplicating within a single request. Changes to a device or to stored credentials go through Server Actions (or POST/PUT route handlers when a Client Component must call them imperatively). Expected failures, like a wrong password, an unreachable camera or an unsupported feature, come back as return values shown via useActionState, not thrown. Unexpected errors fall through to error.tsx. After a mutation, call refresh() or revalidatePath so the UI re-reads the device. Actions run one at a time per client, so they are not used for reads or fan-out. Why: a stale view of a security camera is worse than a slow one, and users need to know why a change failed. Sources: 01-getting-started/15-route-handlers.md ('Caching'); 01-getting-started/10-error-handling.md ('Handling expected errors'); 02-guides/server-actions.md ('Sequential dispatch', 'Choosing a cache update'); 01-getting-started/07-mutating-data.md. Rules out: 'use cache' or revalidate timers on device data, and using Server Actions to fetch.","status":"active","owner":"prn-q80g8mz","attrs":{},"weight":null},"at":"2026-09-19T13:23:07.390Z","parents":["evt-ce37bvhnb4k2"],"hash":"25affdb731f0b0b9de1544af9c88d9a722b7a1cf92f931221c4c923bcf120e65"}
{"id":"evt-hzgt5xtm5vdj","type":"edge.added","subject":"pri-qqrp49f","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"guides","from":"pri-qqrp49f","to":"gol-6q1q5mr"},"at":"2026-09-19T13:23:07.391Z","parents":["evt-2r7e4hajwf8v"],"hash":"87b0716e6168dfa876648077dde07fff396c3dfaf65f2718983e99b8316b31f9"}
{"id":"evt-hzehfrtjnrzz","type":"node.created","subject":"doc-e0nsr0v","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"doc","title":"Coding guide","body":"How code in this repo is written. The rules are the workspace's active principles, listed live below, so this guide can't drift from them. To change a rule, add, update or retire a principle; don't edit this page. Next.js specifics come from the version-matched docs in node_modules/next/dist/docs (next 16.3.5).","status":"active","owner":"prn-q80g8mz","attrs":{"blocks":[{"type":"text","text":"Before writing code: read these principles, and the bundled Next.js doc page for any Next API you touch. Adding a package needs an answered vrek question first.","by":"prn-q80g8mz"},{"type":"query","query":{"kind":"principle","status":"active"},"by":"prn-q80g8mz"}],"acknowledged_at_seq":null}},"at":"2026-09-19T13:23:15.029Z","parents":["evt-hzgt5xtm5vdj"],"hash":"a826220db53e93531025a319bb38b2cbfc1e786bb2927085c30a2bc982cf1d11"}
{"id":"evt-4mye2zcxvw4a","type":"edge.added","subject":"doc-e0nsr0v","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"covers","from":"doc-e0nsr0v","to":"gol-6q1q5mr"},"at":"2026-09-19T13:23:15.030Z","parents":["evt-hzehfrtjnrzz"],"hash":"365fff6be3ccf625c4d035a0723f77d0435c82c06b15517503729b78218887c2"}
{"id":"evt-bq6m9vmr6q6x","type":"node.created","subject":"que-prbknze","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"question","title":"Add zod as a runtime dependency for validating route and Server Action input?","body":"Dependency justification, as pri-mz2jxpb requires. (1) Problem: every route handler and Server Action must validate [id] params, JSON bodies and FormData (pri-m1csgrm), and the Next 16 docs' own examples use zod for this (02-guides/forms.md, data-security.md). (2) Why not hand-written: hand-written checks work, but schemas give one declarative definition per input, with typed output via z.infer and consistent error messages; hand-rolling that across every endpoint duplicates code. (3) Size and reach: zod 4.6.5, MIT, zero runtime dependencies. It is already in node_modules transitively (eslint-config-next → eslint-plugin-react-hooks), so installing adds no new code to the tree. It is used server-side; it only ships to the client if a client component imports a schema. (4) Health: actively maintained, latest release 4.6.5, registry updated 2026-09-13. (5) Removal: schemas sit in the lib/entry layer only, so swapping them for hand-written checks later is local.","status":"open","owner":null,"attrs":{"options":[{"option":"Add zod","consequence":"Input validation uses zod schemas; pri-m1csgrm is updated to name it."},{"option":"Don't add","consequence":"Validation stays hand-written in src/lib."}],"revisit":null}},"at":"2026-09-19T13:28:08.909Z","parents":["evt-4mye2zcxvw4a"],"hash":"75421c3cb917ade396bd4ea32af54aa9dfbdf22ce96c54503503af803d9159e4"}
{"id":"evt-jxbx4dggh0c6","type":"edge.added","subject":"que-prbknze","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"que-prbknze","to":"gol-6q1q5mr"},"at":"2026-09-19T13:28:08.910Z","parents":["evt-bq6m9vmr6q6x"],"hash":"3efe61fc9fe22addf2ef448c01114a24adc39ed6aaf83a031c59707ddca93b0e"}
{"id":"evt-yymjyzvqsfr1","type":"edge.added","subject":"pri-mz2jxpb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"guides","from":"pri-mz2jxpb","to":"que-prbknze"},"at":"2026-09-19T13:28:08.911Z","parents":["evt-jxbx4dggh0c6"],"hash":"05524cc1dbd4f0cd000b66ba671f4a9997dcee800ab19d34ffd0411fca40de32"}
{"id":"evt-bxhvfre330xz","type":"edge.added","subject":"pri-m1csgrm","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"guides","from":"pri-m1csgrm","to":"que-prbknze"},"at":"2026-09-19T13:28:08.912Z","parents":["evt-yymjyzvqsfr1"],"hash":"8b169d43d4752636b3e1e3fa4d89d2f3205d8a34f59261eef99d1e468b29c4b8"}
{"id":"evt-wzt776dwyqsz","type":"node.created","subject":"dec-jfrds1r","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"decision","title":"Accept zod as a runtime dependency for input validation","body":"Accepted by the user (Michael Mainguy) in conversation on 2026-09-19: \"I accept zod as a new dependency.\" Rejected alternative: hand-written validators in src/lib. Scope: validating input at route handlers and Server Actions (pri-m1csgrm). Schemas live server-side; client components don't import them unless deliberately sharing a form schema.","status":"recorded","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T13:28:13.267Z","parents":["evt-bxhvfre330xz"],"hash":"3cdcfc53a095dc5179c16784101778182099db12fc22284ddef84764408b727e"}
{"id":"evt-qxaa2xakyq8z","type":"edge.added","subject":"dec-jfrds1r","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"answers","from":"dec-jfrds1r","to":"que-prbknze"},"at":"2026-09-19T13:28:13.268Z","parents":["evt-wzt776dwyqsz"],"hash":"eabfc130338918159b53d0378c92f2fe3f3d87fef6b4ebedc4655f9042365487"}
{"id":"evt-3p1c7h9d742h","type":"question.answered","subject":"que-prbknze","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"answer":"dec-jfrds1r"},"at":"2026-09-19T13:28:13.269Z","parents":["evt-qxaa2xakyq8z"],"hash":"39980da9b7331c634fde44c99b8d4e58564b978c87b2276635eb8e780b29bf48"}
{"id":"evt-8cjj740y7psa","type":"edge.added","subject":"dec-jfrds1r","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"dec-jfrds1r","to":"gol-6q1q5mr"},"at":"2026-09-19T13:28:13.270Z","parents":["evt-3p1c7h9d742h"],"hash":"6ce58f1a24d4a2b1b61807ae054bfc0b450df4bb53e689902387b52d9a0c4ec0"}
{"id":"evt-6jxj3bk4fjv7","type":"node.updated","subject":"pri-m1csgrm","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"body":"Next's docs are explicit: Route Handlers are public HTTP endpoints, and Server Functions are reachable by direct POST, not just through the UI. Being LAN-only doesn't change that. So each entry point validates its own input with a zod schema: the [id] param, the JSON body, and FormData fields. It uses safeParse and turns failures into a 400, or into a returned error value for actions. zod was accepted as a dependency on 2026-09-19 (que-prbknze). The device address is always resolved server-side from the camera registry by id and must pass isAllowedHost (private IPv4); a client-supplied host or URL is never fetched, which prevents SSRF. Every outbound ONVIF/ISAPI call has a timeout. Why: this server can reach every device on the LAN with stored admin logins, so an unvalidated endpoint is a pivot into the network. Sources: 02-guides/backend-for-frontend.md ('Public Endpoints', 'Verify payloads', 'Proxying to a backend'); 02-guides/server-actions.md ('Security'); 02-guides/data-security.md ('Validating client input'); 02-guides/forms.md ('Validation errors'). Rules out: accepting host or port from the request, trusting that a request came from our own UI, and device calls with no timeout."}},"at":"2026-09-19T13:28:16.723Z","parents":["evt-8cjj740y7psa"],"hash":"84ad70eb5f9874a21f9b8a2af57c797ab3b1abdae899021241a8c5f5cc5d2b63"}
{"id":"evt-m6srta3gzyr7","type":"node.created","subject":"gol-146a3cb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"goal","title":"Multi-camera dashboard: server does the work, the browser is a thin display layer","body":"One dashboard showing every known Hikvision/Annke camera at once: live status, snapshot and key settings, with changes made from the same screen. As much as possible runs on the server: discovery, device protocol, credential use, validation, data shaping and error mapping, in src/lib, Server Components, route handlers and Server Actions. The client is a thin wrapper that renders what the server returns and keeps it fresh (React Query for polling, dedupe and invalidation). No device logic, protocol knowledge or business rules live in client components. Why: one screen is the easy-to-use part of the parent goal, and keeping logic server-side keeps credentials off the browser and makes the client trivially replaceable.","status":"active","owner":null,"attrs":{},"weight":null,"target":null,"direction":"up","unit":null},"at":"2026-09-19T13:30:56.600Z","parents":["evt-6jxj3bk4fjv7"],"hash":"0ab4c61b6907eee8b59b4cf8ae81fde70f5ee1c435b494ff4874387437b76435"}
{"id":"evt-vv0xvpvv6zfx","type":"edge.added","subject":"gol-146a3cb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"gol-146a3cb","to":"gol-6q1q5mr"},"at":"2026-09-19T13:30:56.601Z","parents":["evt-m6srta3gzyr7"],"hash":"68e5f155c75dfd32ff903f4ab9a2af0062ff0159f69badec370738eca0a497d9"}
{"id":"evt-85tshfa63ytf","type":"edge.added","subject":"gol-146a3cb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"upholds","from":"gol-146a3cb","to":"pri-01np850"},"at":"2026-09-19T13:30:56.602Z","parents":["evt-vv0xvpvv6zfx"],"hash":"0b301286c71f915b7fb145c209a087538d4ee57d3dd8ac9afd7f84fb897d00a1"}
{"id":"evt-0mwt26qccg1k","type":"edge.added","subject":"gol-146a3cb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"upholds","from":"gol-146a3cb","to":"pri-tyrxdz9"},"at":"2026-09-19T13:30:56.603Z","parents":["evt-85tshfa63ytf"],"hash":"04c6e77da29e0926083659938078ffccdb3254a3a16b17efe59c794f01e25317"}
{"id":"evt-010bwknvqz8z","type":"edge.added","subject":"gol-146a3cb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"upholds","from":"gol-146a3cb","to":"pri-p9h51hx"},"at":"2026-09-19T13:30:56.604Z","parents":["evt-0mwt26qccg1k"],"hash":"0f81bf9681ed97070454e08844ad6e70b3ce1354cfd2d0888929c64ace1b22a6"}
{"id":"evt-nbdfdvdwr56z","type":"node.created","subject":"que-m9zjfbk","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"question","title":"Add @tanstack/react-query as a runtime dependency for client-side polling and cache invalidation on the multi-camera dashboard?","body":"Dependency justification, as pri-mz2jxpb requires. (1) Problem: the multi-camera dashboard polls status and snapshots for many cameras from the browser, and after a change it must refresh exactly the affected camera. camera-card.tsx already hand-rolls this with useState, useEffect and an attempt counter, and that code would repeat per camera and per widget. (2) Why not the platform: Server Components handle the initial render, but live polling has to happen in the client. React Query gives keyed dedupe, refetchInterval that pauses in hidden tabs, retry control and invalidateQueries in one tested place; rebuilding it by hand amounts to writing a small query library. (3) Size and reach: @tanstack/react-query 5.103.1, MIT, one dependency (@tanstack/query-core), peer react ^18 || ^19. It ships to the client, and that is its purpose. Client only: it is never imported from src/lib. (4) Health: actively maintained, registry updated 2026-09-16. (5) Removal: confined to a providers.tsx wrapper plus thin hooks that call our route handlers; the server side is unaffected. Configuration required: retry off or limited for device calls (don't hammer offline cameras), and staleTime 0 for device state (pri-qqrp49f).","status":"open","owner":null,"attrs":{"options":[{"option":"Add @tanstack/react-query","consequence":"Client polling and invalidation go through React Query; pri-01np850 is updated to name it."},{"option":"Don't add","consequence":"Client polling stays hand-written fetch plus useEffect."}],"revisit":null}},"at":"2026-09-19T13:31:03.437Z","parents":["evt-010bwknvqz8z"],"hash":"2a84077c71d45acb9498c101a59a372299807a3a08394c49a93823c402d7e6a3"}
{"id":"evt-w405pzy5qyma","type":"edge.added","subject":"que-m9zjfbk","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"que-m9zjfbk","to":"gol-6q1q5mr"},"at":"2026-09-19T13:31:03.439Z","parents":["evt-nbdfdvdwr56z"],"hash":"00950c5b90d025e4bfebb9057963d13d17f3727f58c03363a547391965a94648"}
{"id":"evt-3aznwgtd39gn","type":"edge.added","subject":"pri-mz2jxpb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"guides","from":"pri-mz2jxpb","to":"que-m9zjfbk"},"at":"2026-09-19T13:31:03.440Z","parents":["evt-w405pzy5qyma"],"hash":"df7443b854b2083b2bf9143e8a46ad25d337e66d8b83e033ecf39c27904476cc"}
{"id":"evt-18ds9jgaa1dp","type":"edge.added","subject":"pri-01np850","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"guides","from":"pri-01np850","to":"que-m9zjfbk"},"at":"2026-09-19T13:31:03.441Z","parents":["evt-3aznwgtd39gn"],"hash":"7476c65f7c4b32e7392b1969f2b68db23721d3bdde2e43c59c6dfe4cb4ce5cfd"}
{"id":"evt-s4cd65cmnz3m","type":"edge.added","subject":"pri-qqrp49f","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"guides","from":"pri-qqrp49f","to":"que-m9zjfbk"},"at":"2026-09-19T13:31:03.442Z","parents":["evt-18ds9jgaa1dp"],"hash":"50401fd609c3b724f006f26c60a2bc5a5ca3ce4da5c15cea5d1489e67f394318"}
{"id":"evt-harmqb674m2t","type":"node.created","subject":"dec-7qhxvts","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"decision","title":"Accept @tanstack/react-query as the client-side data layer for the dashboard","body":"Accepted by the user (Michael Mainguy) on 2026-09-19: \"go ahead and add react query, the point is to build a multi-camera dashboard and shift as much as possible to server with client being only a thin wrapper.\" Rejected alternative: hand-written fetch plus useEffect polling per component. Scope: client components only, calling our own route handlers. It holds no device logic. Defaults: staleTime 0, limited retry, polling pauses in hidden tabs.","status":"recorded","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T13:31:08.446Z","parents":["evt-s4cd65cmnz3m"],"hash":"89cb7736ca1ead353c63c44861a49c23f3542be59668ca41887da39ef036d2cb"}
{"id":"evt-wrz2yyk59bag","type":"edge.added","subject":"dec-7qhxvts","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"answers","from":"dec-7qhxvts","to":"que-m9zjfbk"},"at":"2026-09-19T13:31:08.447Z","parents":["evt-harmqb674m2t"],"hash":"999d8850b82ea37b57a98a7c7d007e2215b6bf665de6af8240b8b3f92e1de8c0"}
{"id":"evt-p3f3p9nwckyw","type":"question.answered","subject":"que-m9zjfbk","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"answer":"dec-7qhxvts"},"at":"2026-09-19T13:31:08.448Z","parents":["evt-wrz2yyk59bag"],"hash":"8e4473df470e33e7e35080c8f4c2c86a1bdad90e0a8e83d4ef805a14e9560759"}
{"id":"evt-ns7dz8z369pp","type":"edge.added","subject":"dec-7qhxvts","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"dec-7qhxvts","to":"gol-146a3cb"},"at":"2026-09-19T13:31:08.449Z","parents":["evt-p3f3p9nwckyw"],"hash":"32805a2901b2945e2d046df02413eb2a64515b0fb1b1aa8a456d37dd7dcdb2c4"}
{"id":"evt-xn4se6xetcrf","type":"edge.added","subject":"dec-7qhxvts","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"dec-7qhxvts","to":"gol-6q1q5mr"},"at":"2026-09-19T13:31:08.450Z","parents":["evt-ns7dz8z369pp"],"hash":"bb0fcf37677b85a4ba0a33b3d529c5149f14592eed169a5a50c8d2c406356ab1"}
{"id":"evt-wh3sekj42gxm","type":"node.updated","subject":"pri-01np850","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"body":"Pages and layouts are Server Components that read data by calling src/lib functions directly. Route Handlers exist for Client Components, e.g. snapshot refresh or polling, not for server-to-server calls to ourselves. 'use client' goes on the smallest component that needs state, effects or event handlers, because everything a client file imports ships to the browser. Only serializable props cross the boundary. Slow per-camera reads are fetched in parallel (Promise.allSettled, so one dead camera doesn't fail the page) and wrapped in <Suspense> so they stream. Client-side reads and polling go through @tanstack/react-query (accepted 2026-09-19, que-m9zjfbk). Query functions only call our own route handlers and contain no device logic. Keys are ['camera', id, ...] so a mutation can invalidate one camera. Why: this keeps the client bundle and the attack surface small and pages fast when some cameras are offline. Sources: 01-getting-started/05-server-and-client-components.md ('Reducing JS bundle size', 'Context providers'); 02-guides/server-and-client-boundary.md; 02-guides/backend-for-frontend.md ('Caveats > Server Components'); 01-getting-started/06-fetching-data.md ('Streaming', 'Parallel data fetching'). Rules out: 'use client' at page level by reflex, server components fetching http://localhost/api/..., and hand-rolled useEffect polling."}},"at":"2026-09-19T13:31:11.869Z","parents":["evt-xn4se6xetcrf"],"hash":"c55327598ee2c255ef1252798c9adb228f358337070a3ab3d9a7d83d2e18c793"}
{"id":"evt-bj1hp8jppa8r","type":"node.created","subject":"iss-qbh3541","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Migrate camera UI to a server-rendered multi-camera dashboard with a thin React Query client","body":"Today page.tsx renders a client-only CameraScanner. Cameras appear only after a scan (client state), and camera-card.tsx hand-rolls info, snapshot and credentials fetching with useState, useEffect and an attempt counter. Target: the page is a Server Component that lists known cameras from the server registry. Client components only render and refresh data through React Query hooks that call our route handlers, and inputs are validated with zod. Done when every child is closed, no client component contains hand-rolled fetch or useEffect polling, and page.tsx has no 'use client' ancestor above the per-camera leaves.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T13:32:25.210Z","parents":["evt-wh3sekj42gxm"],"hash":"b9891f935256fa03a0be32775df7679cc36e9d72f8dcbc35bf015b4cab931a23"}
{"id":"evt-n8h164qxhthn","type":"edge.added","subject":"iss-qbh3541","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-qbh3541","to":"gol-146a3cb"},"at":"2026-09-19T13:32:25.212Z","parents":["evt-bj1hp8jppa8r"],"hash":"5043eaf2402dc1ab1e002d39298e495c5f15472893fe8cb84c5fa4161c27ad8c"}
{"id":"evt-7vjqhew52agq","type":"edge.added","subject":"iss-qbh3541","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-qbh3541","to":"gol-6q1q5mr"},"at":"2026-09-19T13:32:25.213Z","parents":["evt-n8h164qxhthn"],"hash":"8a3fd66af60971eff214f45c46aacf49bf563d730f79ed167c6759c580a9574e"}
{"id":"evt-yx7zx5xbrjr8","type":"edge.added","subject":"iss-qbh3541","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-qbh3541","to":"src/app/page.tsx"},"at":"2026-09-19T13:32:25.214Z","parents":["evt-7vjqhew52agq"],"hash":"02153cd1d188ef89b6fe67786bc4a376d248875615726015206257ce913bff0c"}
{"id":"evt-efxgker80c3y","type":"edge.added","subject":"iss-qbh3541","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-qbh3541","to":"src/app/camera-scanner.tsx"},"at":"2026-09-19T13:32:25.215Z","parents":["evt-yx7zx5xbrjr8"],"hash":"e6f60b94d64416f28debaa9b54569fb389d9cb4d51d77ecd95d181a4ae0abad7"}
{"id":"evt-mnb3x4spazf5","type":"edge.added","subject":"iss-qbh3541","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-qbh3541","to":"src/app/camera-card.tsx"},"at":"2026-09-19T13:32:25.216Z","parents":["evt-efxgker80c3y"],"hash":"5854f1849862d88c0107396c78264b4e039d134662b424f9235782da6cf5d6c0"}
{"id":"evt-g0az0dgqen16","type":"edge.added","subject":"iss-qbh3541","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-qbh3541","to":"src/lib/camera-registry.ts"},"at":"2026-09-19T13:32:25.217Z","parents":["evt-mnb3x4spazf5"],"hash":"36bf189a9365c67a977de40ce78cb94338bd38c75664cf31e4e48741dd7af177"}
{"id":"evt-canp029vey0s","type":"edge.added","subject":"iss-qbh3541","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-qbh3541","to":"area:ui"},"at":"2026-09-19T13:32:25.218Z","parents":["evt-g0az0dgqen16"],"hash":"878e4055c40d3c4ae4952fe6024188c7287dce393704c79f3b563448bc2fef0c"}
{"id":"evt-5x26rrcvq478","type":"edge.added","subject":"iss-qbh3541","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-qbh3541","to":"area:dashboard"},"at":"2026-09-19T13:32:25.219Z","parents":["evt-canp029vey0s"],"hash":"255d51c7b86a4376546b37744d862125ab237f4788c49c47ec1b18b728bf14b8"}
{"id":"evt-m4gdbhc211pf","type":"node.created","subject":"iss-a0hz0py","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Render the dashboard page as a Server Component listing known cameras from the registry","body":"Add a listCameras() to src/lib/camera-registry.ts that returns a client-safe DTO per camera (id, name, host, port, lastSeen, hasCredentials). page.tsx calls it directly (no fetch to /api) and renders one client camera tile per record, so known cameras appear on load without a scan. Define the DTO type in a module client code can import with `import type`. Acceptance: with a populated .data/cameras.json, the page shows all cameras before any scan, and page.tsx has no 'use client'.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T13:32:30.973Z","parents":["evt-5x26rrcvq478"],"hash":"1f5c14c5e3430145946359df9b116fdf56d94ec28537c8f1b2e49dbb0c35b862"}
{"id":"evt-jqc49z6ae4vp","type":"edge.added","subject":"iss-a0hz0py","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-a0hz0py","to":"gol-146a3cb"},"at":"2026-09-19T13:32:30.976Z","parents":["evt-m4gdbhc211pf"],"hash":"2dbddc3d6411974e2639fe4cfcf3824a0d11e4fa49e47abddaedec8c10805f0d"}
{"id":"evt-zdrhfnrxzrsb","type":"edge.added","subject":"iss-a0hz0py","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-a0hz0py","to":"src/app/page.tsx"},"at":"2026-09-19T13:32:30.977Z","parents":["evt-jqc49z6ae4vp"],"hash":"7614aa0b3a649bd60af8219738094a9eb19ead2ebc29c52119c83581d85599a9"}
{"id":"evt-fn78qthkd0e3","type":"edge.added","subject":"iss-a0hz0py","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-a0hz0py","to":"src/lib/camera-registry.ts"},"at":"2026-09-19T13:32:30.978Z","parents":["evt-zdrhfnrxzrsb"],"hash":"25dfe36fb525bb0d896f114d2f94032332fd1ed45067e4ff98e7f71c534e8424"}
{"id":"evt-s5x9a3mncwt6","type":"edge.added","subject":"iss-qbh3541","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"parent_of","from":"iss-qbh3541","to":"iss-a0hz0py"},"at":"2026-09-19T13:32:30.979Z","parents":["evt-fn78qthkd0e3"],"hash":"bff05b6fcce77eeb8004cbc84151ceab160bde9b1d9b0d1f5375d8b48642d25e"}
{"id":"evt-79vjabrmdk1t","type":"edge.added","subject":"iss-a0hz0py","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-a0hz0py","to":"area:dashboard"},"at":"2026-09-19T13:32:30.980Z","parents":["evt-s5x9a3mncwt6"],"hash":"14588eb10fb82dfb082ae6c3509e101f693b444a916e2dab8cda59a2a2e65b0f"}
{"id":"evt-jd4xemnve4w2","type":"node.created","subject":"iss-rjqy3hy","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Validate all route handler input with zod schemas","body":"Replace the hand-rolled checks with zod safeParse: the [id] param (isValidCameraId in camera-route.ts), the PUT /api/cameras/[id]/credentials body (username required, both strings, sensible max lengths), and the POST /api/discover body (timeout clamped 1–30 s, unicastSweep boolean). Put schemas in src/lib next to what they validate. A failure returns 400 with a generic message. Per pri-m1csgrm.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T13:32:34.293Z","parents":["evt-79vjabrmdk1t"],"hash":"9143ee4c6a2461848d0b27915563aef851c6c4f2cfc6c9b63c92a07697221e34"}
{"id":"evt-h85t4x4x7tjn","type":"edge.added","subject":"iss-rjqy3hy","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-rjqy3hy","to":"gol-6q1q5mr"},"at":"2026-09-19T13:32:34.294Z","parents":["evt-jd4xemnve4w2"],"hash":"480a22b6b4cdc9fc1ae34a989c1ab53d0938b2603c705ab18651b0cd9826cd93"}
{"id":"evt-b78sv8p32110","type":"edge.added","subject":"iss-rjqy3hy","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-rjqy3hy","to":"src/lib/camera-route.ts"},"at":"2026-09-19T13:32:34.295Z","parents":["evt-h85t4x4x7tjn"],"hash":"1c5e295ecd44b482eda112f29d790c8de296ce0d7bf39ada4f1f55d0433fda73"}
{"id":"evt-pz7ndaxk01xw","type":"edge.added","subject":"iss-rjqy3hy","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-rjqy3hy","to":"src/app/api/cameras/[id]/credentials/route.ts"},"at":"2026-09-19T13:32:34.296Z","parents":["evt-b78sv8p32110"],"hash":"e5fdd0e2e7ec6b6d26dd7286ce36b9d29ee4de78ec5a25e02bdc86b81da04fde"}
{"id":"evt-da99bcqnvg82","type":"edge.added","subject":"iss-rjqy3hy","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-rjqy3hy","to":"src/app/api/discover/route.ts"},"at":"2026-09-19T13:32:34.297Z","parents":["evt-pz7ndaxk01xw"],"hash":"d8c20c8d682a8ccfddeaf74d67031f9fd18c7cb1c3f4ef2a4b9043bcb1e2f53e"}
{"id":"evt-bs1qk9pwt6n8","type":"edge.added","subject":"iss-qbh3541","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"parent_of","from":"iss-qbh3541","to":"iss-rjqy3hy"},"at":"2026-09-19T13:32:34.298Z","parents":["evt-da99bcqnvg82"],"hash":"5a8fbe4ba2b81177da92361d1262d3d2ad82ab2fda4b897f06173acbf01b1130"}
{"id":"evt-pd484j5j3hq5","type":"edge.added","subject":"iss-rjqy3hy","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-rjqy3hy","to":"area:api"},"at":"2026-09-19T13:32:34.299Z","parents":["evt-bs1qk9pwt6n8"],"hash":"a9abb91ad00c8327224db8d4adb267efa45d45f75fff36b95091472cecaa2541"}
{"id":"evt-ytfrpb79ah3b","type":"edge.added","subject":"iss-rjqy3hy","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-rjqy3hy","to":"area:security"},"at":"2026-09-19T13:32:34.300Z","parents":["evt-pd484j5j3hq5"],"hash":"b739aef21d7680d428ea744d2fecae2885844f1a7dc2f13c6aa17fc6e171a8f4"}
{"id":"evt-49834dyrfxsz","type":"node.created","subject":"iss-dbwgww8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Stop /api/discover returning raw error messages to the client","body":"src/app/api/discover/route.ts returns `err.message` verbatim in its 500 response, which breaks pri-tyrxdz9: upstream and OS errors can leak internal detail. Log the error server-side and return a generic message, as cameraErrorResponse does for the camera routes.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T13:32:36.415Z","parents":["evt-ytfrpb79ah3b"],"hash":"fa183e0add83870e060363268199dc28c43998bfed489828a439e4486297040e"}
{"id":"evt-452arxb0d76h","type":"edge.added","subject":"iss-dbwgww8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-dbwgww8","to":"gol-6q1q5mr"},"at":"2026-09-19T13:32:36.417Z","parents":["evt-49834dyrfxsz"],"hash":"c62021b0a468a7f9e32626f7ab1569057cc7dbeb274b05a4c9ed7bf1db409e9f"}
{"id":"evt-8z642b5kxf22","type":"edge.added","subject":"iss-dbwgww8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-dbwgww8","to":"src/app/api/discover/route.ts"},"at":"2026-09-19T13:32:36.418Z","parents":["evt-452arxb0d76h"],"hash":"e8a46cae5779e4ad0e819459b54e650b68bf99b5aabd62ad8f7a2891ad450208"}
{"id":"evt-bza19sahqv8k","type":"edge.added","subject":"iss-qbh3541","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"parent_of","from":"iss-qbh3541","to":"iss-dbwgww8"},"at":"2026-09-19T13:32:36.419Z","parents":["evt-8z642b5kxf22"],"hash":"d8ee57c5c6add36a9a1f3472653b8b38c0ec4642c483fd3d02c19b2c8784503e"}
{"id":"evt-031tzsx4b57t","type":"edge.added","subject":"iss-dbwgww8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-dbwgww8","to":"area:api"},"at":"2026-09-19T13:32:36.420Z","parents":["evt-bza19sahqv8k"],"hash":"03d635f91a32d1e46b0dbcfbf68f2f877ed1e0115e82391baec08b49a36c18d6"}
{"id":"evt-j5f7zhptkzzw","type":"edge.added","subject":"iss-dbwgww8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-dbwgww8","to":"area:security"},"at":"2026-09-19T13:32:36.421Z","parents":["evt-031tzsx4b57t"],"hash":"9e7c2c17a96ce0bfc223cecdea037e25f8c58198540a0632ed2f2108fa5dec08"}
{"id":"evt-z8cb3qepq17p","type":"node.created","subject":"iss-ksxmctm","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Move camera info fetching in camera-card.tsx to a React Query hook","body":"Replace the useState, useEffect and `attempt` counter in CameraCard with useCameraInfo(id): useQuery with key ['camera', id, 'info'] calling /api/cameras/[id]/info. Map auth and inactive failures (problemFrom) to typed query errors. Retry becomes refetch(). The hook lives in a small client module of camera hooks with no device logic.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T13:32:41.746Z","parents":["evt-j5f7zhptkzzw"],"hash":"c5d03bb173afaf24302d907d1cc08cdfce0041da925d2b0cb96d358a0a05d172"}
{"id":"evt-7pjd58ehewqd","type":"edge.added","subject":"iss-ksxmctm","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-ksxmctm","to":"gol-146a3cb"},"at":"2026-09-19T13:32:41.748Z","parents":["evt-z8cb3qepq17p"],"hash":"77f9c673df77ee4b96ce4aa5d03a893f3fe43d2a71805b0ff7f23604822381bd"}
{"id":"evt-1bn3taydyynf","type":"edge.added","subject":"iss-ksxmctm","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-ksxmctm","to":"src/app/camera-card.tsx"},"at":"2026-09-19T13:32:41.749Z","parents":["evt-7pjd58ehewqd"],"hash":"de6f16af867d2abf6c5ec25be528c5497372688419d57c1c855ff086ff2fe846"}
{"id":"evt-w5s3ytrr891y","type":"edge.added","subject":"iss-a0hz0py","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"blocks","from":"iss-a0hz0py","to":"iss-ksxmctm"},"at":"2026-09-19T13:32:41.750Z","parents":["evt-1bn3taydyynf"],"hash":"728be8299128949c7b165811d4e7bed487d869f486c324107915f3e578a8917c"}
{"id":"evt-3ans5g5azk3n","type":"edge.added","subject":"iss-qbh3541","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"parent_of","from":"iss-qbh3541","to":"iss-ksxmctm"},"at":"2026-09-19T13:32:41.751Z","parents":["evt-w5s3ytrr891y"],"hash":"ad3cd0aa38c7c34442df04fceb99dc9a34a75a39e17b20996dff1d11e6b9edbb"}
{"id":"evt-3vfengbvgv4s","type":"edge.added","subject":"iss-ksxmctm","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-ksxmctm","to":"area:ui"},"at":"2026-09-19T13:32:41.752Z","parents":["evt-3ans5g5azk3n"],"hash":"2fedeb07fa40965c5ae7b817b5fa30634860da687aeed9d1f04e8124318ba63b"}
{"id":"evt-f0egr0w1ghpp","type":"node.created","subject":"iss-m032zwq","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Move snapshot polling to React Query refetchInterval","body":"Replace the setTimeout poll loop in Snapshot with useQuery on key ['camera', id, 'snapshot'], using refetchInterval set to the chosen refresh rate and enabled only while info has loaded and there's no problem. Polling stops in hidden tabs by default. Keep revoking the previous object URL when a new frame arrives and on unmount. Acceptance: no memory growth from object URLs over a few minutes of polling, and an offline camera is not retried more than once per interval.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T13:32:44.362Z","parents":["evt-3vfengbvgv4s"],"hash":"e75ff82160e26d8ceb967b22f057f2b814559b4581a7210c2847fcd40a501c5f"}
{"id":"evt-wzwx3h77wt1d","type":"edge.added","subject":"iss-m032zwq","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-m032zwq","to":"gol-146a3cb"},"at":"2026-09-19T13:32:44.363Z","parents":["evt-f0egr0w1ghpp"],"hash":"f2e391563a14c83c0f84344b291a38eec61c9699026816c56b858532a95cf030"}
{"id":"evt-0z0hjypy0he2","type":"edge.added","subject":"iss-m032zwq","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-m032zwq","to":"src/app/camera-card.tsx"},"at":"2026-09-19T13:32:44.364Z","parents":["evt-wzwx3h77wt1d"],"hash":"e0d31ab72c76d4ad67bf24553b1325d7645b9b8163e15b217013132209cc9875"}
{"id":"evt-yhnsfn4nrk9j","type":"edge.added","subject":"iss-a0hz0py","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"blocks","from":"iss-a0hz0py","to":"iss-m032zwq"},"at":"2026-09-19T13:32:44.365Z","parents":["evt-0z0hjypy0he2"],"hash":"d51b1ea8e3201c5530ccba16c89bee6f11d53149e3cdb3ad7e4e74992dd0fec9"}
{"id":"evt-5ptc6qptgwz4","type":"edge.added","subject":"iss-qbh3541","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"parent_of","from":"iss-qbh3541","to":"iss-m032zwq"},"at":"2026-09-19T13:32:44.366Z","parents":["evt-yhnsfn4nrk9j"],"hash":"7dcd20a5779a10b131d1bb8e61d88d1d209ef16d6f24e8eefe1cd380b5926df3"}
{"id":"evt-yg5j4gfmm9ft","type":"edge.added","subject":"iss-m032zwq","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-m032zwq","to":"area:ui"},"at":"2026-09-19T13:32:44.367Z","parents":["evt-5ptc6qptgwz4"],"hash":"99743510db7b977aaeff18ea5314a4af7cdc09fb3cc046a99a391957da613709"}
{"id":"evt-yw9gex33ay6s","type":"node.created","subject":"iss-8hfq2y2","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Save and clear camera credentials with useMutation and invalidate that camera's queries","body":"The login form in camera-card.tsx currently calls fetch on /api/cameras/[id]/credentials (GET, PUT, DELETE) by hand. Use useQuery for the GET status and useMutation for PUT and DELETE. On success, invalidateQueries(['camera', id]) so info and snapshot refetch with the new login. Expected failures, such as a wrong password or an unreachable camera, are shown as form errors, not thrown.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T13:32:47.302Z","parents":["evt-yg5j4gfmm9ft"],"hash":"30d4ead146b4be11a7013042d8f0bc6ef4bc87567e2f66cc0733afa3f91489cf"}
{"id":"evt-xhk82b9vcjyv","type":"edge.added","subject":"iss-8hfq2y2","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-8hfq2y2","to":"gol-146a3cb"},"at":"2026-09-19T13:32:47.304Z","parents":["evt-yw9gex33ay6s"],"hash":"e9521406aee626ea78bd26102ed4f622b69b93163954bf1ba0aa9eeab54b00e2"}
{"id":"evt-ypx40vcx02p2","type":"edge.added","subject":"iss-8hfq2y2","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-8hfq2y2","to":"src/app/camera-card.tsx"},"at":"2026-09-19T13:32:47.305Z","parents":["evt-xhk82b9vcjyv"],"hash":"476497b321039f3484c7392d5fdf8060475d1ea911859a8676993598c4310ab8"}
{"id":"evt-3ahcyz6tk1hm","type":"edge.added","subject":"iss-a0hz0py","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"blocks","from":"iss-a0hz0py","to":"iss-8hfq2y2"},"at":"2026-09-19T13:32:47.306Z","parents":["evt-ypx40vcx02p2"],"hash":"91e02eb11b06fb465f7398cb86eabae98951b6d9baf7b5ee02224f9dd22bbd19"}
{"id":"evt-wj33w9feevzd","type":"edge.added","subject":"iss-qbh3541","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"parent_of","from":"iss-qbh3541","to":"iss-8hfq2y2"},"at":"2026-09-19T13:32:47.307Z","parents":["evt-3ahcyz6tk1hm"],"hash":"ad983a4baa411032461f5ad212166063f9f0ee413e7ec9c26117986993d724b8"}
{"id":"evt-06rcrscag1zm","type":"edge.added","subject":"iss-8hfq2y2","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-8hfq2y2","to":"area:ui"},"at":"2026-09-19T13:32:47.308Z","parents":["evt-wj33w9feevzd"],"hash":"6e49a9b98136ba155a24aae418584b6343029825944ea04e58eae1116a9153b4"}
{"id":"evt-z8hz7wfbmm83","type":"node.created","subject":"iss-2fm6x2y","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Turn \"Scan network\" into a mutation that refreshes the server-rendered camera list","body":"Scan results currently live only in CameraScanner's client state. Make scanning a small client control that runs useMutation against POST /api/discover (which already writes the registry), then calls router.refresh() so the server-rendered list re-reads the registry. The camera list is no longer held in client state, and the scan options (timeout, unicast sweep) and the snapshot refresh-rate selector stay as small client controls.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T13:32:50.459Z","parents":["evt-06rcrscag1zm"],"hash":"df2afcf23dedfcdb378b77b905ffe886f8b7d62687ce2a47635e25ce50cf29e9"}
{"id":"evt-jfa6d5nyk7sb","type":"edge.added","subject":"iss-2fm6x2y","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-2fm6x2y","to":"gol-146a3cb"},"at":"2026-09-19T13:32:50.460Z","parents":["evt-z8hz7wfbmm83"],"hash":"047c5efc04151937a615d0b04ab0c185e2d9e6093b82ca9035ef65884535347e"}
{"id":"evt-va8rts0h81x1","type":"edge.added","subject":"iss-2fm6x2y","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-2fm6x2y","to":"src/app/camera-scanner.tsx"},"at":"2026-09-19T13:32:50.461Z","parents":["evt-jfa6d5nyk7sb"],"hash":"e2c7ddad03fde0a4da0092fc7804ac226da5fbfdffc33273ec90d5cb4f9952d2"}
{"id":"evt-8a93gxsa25z7","type":"edge.added","subject":"iss-2fm6x2y","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-2fm6x2y","to":"src/app/page.tsx"},"at":"2026-09-19T13:32:50.462Z","parents":["evt-va8rts0h81x1"],"hash":"ddd85037b2a90d97e95a948f5dfb691763aa32aab4f966739ff74bfd87bad1a2"}
{"id":"evt-k59zqha559f5","type":"edge.added","subject":"iss-a0hz0py","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"blocks","from":"iss-a0hz0py","to":"iss-2fm6x2y"},"at":"2026-09-19T13:32:50.463Z","parents":["evt-8a93gxsa25z7"],"hash":"e6c7d1f6db7c099edbf123721428544042936841fb4c38d6ffdecb6fe3677871"}
{"id":"evt-qcgz6258gtkq","type":"edge.added","subject":"iss-qbh3541","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"parent_of","from":"iss-qbh3541","to":"iss-2fm6x2y"},"at":"2026-09-19T13:32:50.464Z","parents":["evt-k59zqha559f5"],"hash":"1a87d28e7fe4a3c16ff56d78e60497498aaf1b408ad0b52c414ee7c7cb95cac7"}
{"id":"evt-0srks6bg9fav","type":"edge.added","subject":"iss-2fm6x2y","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-2fm6x2y","to":"area:ui"},"at":"2026-09-19T13:32:50.465Z","parents":["evt-qcgz6258gtkq"],"hash":"9110e2a468620653c4a0e61757192c20ade73e621db785e5c67b72f3fdd3775b"}
{"id":"evt-et2mscnws7qe","type":"edge.added","subject":"iss-2fm6x2y","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-2fm6x2y","to":"area:dashboard"},"at":"2026-09-19T13:32:50.466Z","parents":["evt-0srks6bg9fav"],"hash":"b1a17f2117ba6b00613ce1a492dbda4e85decd188c5490e87db79be38d62766f"}
{"id":"evt-bm8mry4yvret","type":"node.created","subject":"gol-sjabnh3","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"goal","title":"Serve as an API server so third-party integrations can discover and use cameras","body":"Besides the web UI, the app exposes a documented, stable HTTP API that other systems (home automation, NVRs, scripts) can call to discover cameras on the network and use them: list known cameras, read info and status, fetch snapshots, and change settings. The dashboard and third parties go through the same server-side lib layer, so device logic is written once. Implies things the UI-only app can skip: a stable, versioned contract, authentication for non-browser callers, and error responses a machine can act on. Priority is low for now (weight 0.2), below the dashboard goals (1).","status":"active","owner":null,"attrs":{},"weight":0.2,"target":null,"direction":"up","unit":null},"at":"2026-09-19T13:36:39.284Z","parents":["evt-et2mscnws7qe"],"hash":"de5a9833e81ec4d55d23635be591ec929d64a401f3aa2d35fbdc2ba3eae7c6d5"}
{"id":"evt-py79tb5z5cjx","type":"edge.added","subject":"gol-sjabnh3","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"gol-sjabnh3","to":"gol-6q1q5mr"},"at":"2026-09-19T13:36:39.285Z","parents":["evt-bm8mry4yvret"],"hash":"cd30b7d83d5a33737079ce7cc2f8e8ee62d209135f9381957fd90a8835dd8338"}
{"id":"evt-9cwvv3a1h7wg","type":"edge.added","subject":"gol-sjabnh3","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"upholds","from":"gol-sjabnh3","to":"pri-m1csgrm"},"at":"2026-09-19T13:36:39.286Z","parents":["evt-py79tb5z5cjx"],"hash":"9c8b1e45dea5dbcbae33527617681d1a3fc221150a0d31dfbb9ed3c0ffde2f07"}
{"id":"evt-4g852pmnbmj5","type":"edge.added","subject":"gol-sjabnh3","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"upholds","from":"gol-sjabnh3","to":"pri-tyrxdz9"},"at":"2026-09-19T13:36:39.287Z","parents":["evt-9cwvv3a1h7wg"],"hash":"54535842c40f2aa4ff9901f95ba8b480b439fa573ce0ce070494dc341bf019a2"}
{"id":"evt-16g6xgs4xxzy","type":"edge.added","subject":"gol-sjabnh3","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"upholds","from":"gol-sjabnh3","to":"pri-p9h51hx"},"at":"2026-09-19T13:36:39.288Z","parents":["evt-4g852pmnbmj5"],"hash":"3e208d85a2d02e4cbac421ec2a67c482cf0f03be5539a9e7cad19e08f807e8cc"}
{"id":"evt-pa0a7gftmx91","type":"node.updated","subject":"gol-6q1q5mr","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"weight":1}},"at":"2026-09-19T13:36:40.386Z","parents":["evt-16g6xgs4xxzy"],"hash":"7514e2d5b58786dff238503e0a1e352ad3cb393010287f82661c0f4fd6599482"}
{"id":"evt-vqapqnjkfh90","type":"node.updated","subject":"gol-146a3cb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"weight":1}},"at":"2026-09-19T13:36:41.628Z","parents":["evt-pa0a7gftmx91"],"hash":"1764590f7adbec128eaaa2f7151c967250d9699337c5a4d27a0469c8e9f24012"}
{"id":"evt-4axgmcdj2fh8","type":"node.created","subject":"ver-6xyjfyx","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"With a populated .data/cameras.json, GET / server-renders one tile per registry entry before any scan; page.tsx has no 'use client'; / is dynamic (not prerendered at build).","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T13:37:12.746Z","parents":["evt-vqapqnjkfh90"],"hash":"db199cd95b5d0b60f51c89842354da0295948de9a5ad168d7ed7f13517dc6fca"}
{"id":"evt-p8szc580ewf0","type":"edge.added","subject":"ver-6xyjfyx","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-6xyjfyx","to":"iss-a0hz0py"},"at":"2026-09-19T13:37:12.748Z","parents":["evt-4axgmcdj2fh8"],"hash":"aa57e4e53ba530dae5376362f04b8af9284899e967e1df8ad7c8aa5d78f5694a"}
{"id":"evt-nqd77pqc6ktb","type":"verification.recorded","subject":"ver-6xyjfyx","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"2026-09-19: `npx next build` lists `ƒ /` (dynamic). `npx next start -p 3917`, then `curl -s localhost:3917/` rendered 2 tiles (192.168.17.129:80, 192.168.31.2:80), matching the 2 entries in .data/cameras.json. `?refresh=abc` falls back to 1 s (200). tsc --noEmit and eslint src are clean. Not checked in a browser: hydration and the snapshot polling UI weren't exercised."},"at":"2026-09-19T13:37:12.749Z","parents":["evt-p8szc580ewf0"],"hash":"84a1c6e46bf3d01af5fae5e7aac86bfd02afcc36c6370bede38ae0e02d934bfd"}
{"id":"evt-4r4dsj0ghyc1","type":"node.created","subject":"ver-mgxv8kn","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"Invalid camera id, credentials body and discover body are rejected by zod with a 400 and a generic message, before any device or registry write.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T13:37:15.201Z","parents":["evt-nqd77pqc6ktb"],"hash":"18e25e9feed819afe7e0c936b97cc8986486a767f506634ef1d49437de5422a7"}
{"id":"evt-7n706y9fhcx3","type":"edge.added","subject":"ver-mgxv8kn","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-mgxv8kn","to":"iss-rjqy3hy"},"at":"2026-09-19T13:37:15.203Z","parents":["evt-4r4dsj0ghyc1"],"hash":"b4e2ccac63d4da17adfa5b2208e2669a73d597a47b38be974309053fafda0231"}
{"id":"evt-y3j43c3y6ew5","type":"verification.recorded","subject":"ver-mgxv8kn","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"2026-09-19 against `next start -p 3917`: GET /api/cameras/NOTANID/info returned 400 \"Invalid camera ID\"; an unknown valid UUID returned 404; PUT credentials with {\"username\":\"\"}, {\"username\":5} and non-JSON each returned 400 \"Username is required\"; POST /api/discover with {\"timeout\":\"x\"} and {\"timeout\":99999} each returned 400 \"Invalid scan options\". Schemas: cameraIdSchema (camera-registry.ts), credentialsSchema (credential-store.ts), discoverRequestSchema (onvif.ts), refreshMsSchema (app/refresh-rate.ts)."},"at":"2026-09-19T13:37:15.204Z","parents":["evt-7n706y9fhcx3"],"hash":"6976b7669f44e610435a7dae6a3479bceee3f25e61d50eb0254fbbcc34accc05"}
{"id":"evt-p92q927w98nj","type":"node.created","subject":"ver-n9sa5s4","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"/api/discover never returns err.message; failures are logged server-side and the client gets a generic \"Network scan failed\".","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T13:37:17.620Z","parents":["evt-y3j43c3y6ew5"],"hash":"9dbc1d29a7cdcc1e08a6f3466179a73ac72e73c375fa682e7897d219ed7ff471"}
{"id":"evt-ej5ww93txf38","type":"edge.added","subject":"ver-n9sa5s4","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-n9sa5s4","to":"iss-dbwgww8"},"at":"2026-09-19T13:37:17.621Z","parents":["evt-p92q927w98nj"],"hash":"6026f7b80c70e985e17a060cae5f3c9a57b5114278708a0c3071728c462d40e5"}
{"id":"evt-9gh8610vnppz","type":"verification.recorded","subject":"ver-n9sa5s4","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pending","evidence":"Code change in src/app/api/discover/route.ts: the catch block now calls console.error and returns {error:\"Network scan failed\"} with a 500. Reviewed, but the 500 path was not exercised because I couldn't force a scan failure without breaking the host's network stack."},"at":"2026-09-19T13:37:17.622Z","parents":["evt-ej5ww93txf38"],"hash":"ee6a6c340af604313daf5de480ca59b8102d4712ff75b88b2dceef33fed09bb6"}
{"id":"evt-7tg6sh0x2gzp","type":"node.status_changed","subject":"iss-a0hz0py","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T13:37:18.863Z","parents":["evt-9gh8610vnppz"],"hash":"9d7dd789eee12f2fe490feac05341ac4a68c8e4d2a9d3130667e0e82ed710777"}
{"id":"evt-7czexnaf6c4h","type":"node.status_changed","subject":"iss-rjqy3hy","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T13:37:20.103Z","parents":["evt-7tg6sh0x2gzp"],"hash":"72b343bd059aba47d5e85d224272dadc74f4c98b656dc0477b92b89a64442e7d"}
{"id":"evt-jrxhb8h0h4t5","type":"node.status_changed","subject":"iss-dbwgww8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T13:37:21.177Z","parents":["evt-7czexnaf6c4h"],"hash":"a454ae1749521ad7719d436b0b0800665f5be74e15243355e817ccde3eb65255"}
{"id":"evt-jvt1zp0n2y3a","type":"node.created","subject":"iss-tz5s098","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Stop cameraErrorResponse passing upstream camera error messages to the client","body":"iss-dbwgww8 described cameraErrorResponse (src/lib/camera-route.ts) as the generic-message model. It isn't: it returns err.message verbatim for every case, including the 502 fallback, so raw ONVIF/SOAP and socket errors reach the browser. That breaks pri-tyrxdz9. Keep the codes (inactive, auth, store) and write fixed user-facing messages for each; log the original server-side. Check that camera-card.tsx's auth 'reason' text still makes sense.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T13:37:29.051Z","parents":["evt-jrxhb8h0h4t5"],"hash":"3ed02e77abd89b30665a9568bb4cd876684d3f2a9cbef5ea5adf59c574c6842b"}
{"id":"evt-dx3n4xph4z8a","type":"edge.added","subject":"iss-tz5s098","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-tz5s098","to":"gol-6q1q5mr"},"at":"2026-09-19T13:37:29.052Z","parents":["evt-jvt1zp0n2y3a"],"hash":"a671815f9b2e041c844f23de37c049ce9175dafaa7afbf6fe0ceea6494de0779"}
{"id":"evt-ddsqrs5kejjk","type":"edge.added","subject":"iss-tz5s098","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-tz5s098","to":"gol-sjabnh3"},"at":"2026-09-19T13:37:29.053Z","parents":["evt-dx3n4xph4z8a"],"hash":"4d922fc99ea57f56eacac43a92b8f878be63962769419817cee50254c2486258"}
{"id":"evt-7hwp833a6qzn","type":"edge.added","subject":"iss-tz5s098","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-tz5s098","to":"src/lib/camera-route.ts"},"at":"2026-09-19T13:37:29.054Z","parents":["evt-ddsqrs5kejjk"],"hash":"d3c4710e44252760e5f592dcecb82de987f5dbf2647c7947c29d60aa15699548"}
{"id":"evt-z23sf86wmkwp","type":"edge.added","subject":"iss-tz5s098","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-tz5s098","to":"src/app/camera-card.tsx"},"at":"2026-09-19T13:37:29.055Z","parents":["evt-7hwp833a6qzn"],"hash":"fcb24029db67d49dbb24809de2d131595782bc0ded3b1869faf6d1940fa880ca"}
{"id":"evt-fc3z9kkr5ss8","type":"edge.added","subject":"iss-qbh3541","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"parent_of","from":"iss-qbh3541","to":"iss-tz5s098"},"at":"2026-09-19T13:37:29.056Z","parents":["evt-z23sf86wmkwp"],"hash":"8083cf086de63c50174b7f4620907f9b3a78bdeacec989bea4cd8ae500d82ad8"}
{"id":"evt-hdg4ccp9f02m","type":"edge.added","subject":"iss-tz5s098","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-tz5s098","to":"area:api"},"at":"2026-09-19T13:37:29.057Z","parents":["evt-fc3z9kkr5ss8"],"hash":"6fbde4947b2de5d42b6c40665fa07cccfd93d5d89abca465e469881e9e223770"}
{"id":"evt-mnerbabmd40b","type":"edge.added","subject":"iss-tz5s098","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-tz5s098","to":"area:security"},"at":"2026-09-19T13:37:29.058Z","parents":["evt-hdg4ccp9f02m"],"hash":"cc19bbe88a3215d5cd7c69414c98a843cd5a5f047ace982adc41e41d69ccb309"}
{"id":"evt-nt4sbgm3hsaj","type":"edge.added","subject":"iss-tz5s098","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"discovered_from","from":"iss-tz5s098","to":"iss-dbwgww8"},"at":"2026-09-19T13:37:29.059Z","parents":["evt-mnerbabmd40b"],"hash":"ba8f0d9b1b08e8278a0b6290de43d8e4a49a1b38d1a765708d44f3e281ac8b1a"}
{"id":"evt-55fx8tkcd512","type":"node.created","subject":"iss-jvxcd1n","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Fix build warning: dynamic filesystem paths make Turbopack trace the whole project","body":"`next build` warns \"Dynamic filesystem access causes tracing of the whole project\" at camera-registry.ts load() and credential-store.ts load(). Both build their paths from process.env and path.join(process.cwd(), '.data', ...). This existed before; it surfaced while building for iss-a0hz0py. Effect: all source files, including the public folder, get included in the server output. Fix per the warning's guidance, e.g. a static path or outputFileTracingExcludes; check the bundled docs first. Acceptance: next build shows no tracing warnings.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T13:37:31.978Z","parents":["evt-nt4sbgm3hsaj"],"hash":"bb43c0ff197a21ff431dbe4b147733cac73fe839059b6187fc68a436c51a5c63"}
{"id":"evt-yvjync8ah2j4","type":"edge.added","subject":"iss-jvxcd1n","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-jvxcd1n","to":"gol-6q1q5mr"},"at":"2026-09-19T13:37:31.979Z","parents":["evt-55fx8tkcd512"],"hash":"e24a93e08902e7ed2cbee7ca0e1e945c8b225deba7b4a7d4bc0e2f824cbeb098"}
{"id":"evt-89tftvm9qcta","type":"edge.added","subject":"iss-jvxcd1n","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-jvxcd1n","to":"src/lib/camera-registry.ts"},"at":"2026-09-19T13:37:31.980Z","parents":["evt-yvjync8ah2j4"],"hash":"fa413abe9fa1262adc26c7f5346cf4a923dfaaaf245114d3d9d60737b502a480"}
{"id":"evt-6ra9093yhc00","type":"edge.added","subject":"iss-jvxcd1n","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-jvxcd1n","to":"src/lib/credential-store.ts"},"at":"2026-09-19T13:37:31.981Z","parents":["evt-89tftvm9qcta"],"hash":"f0cee7c33b4834b8f0791799a875e44685e3217f4e79696508682678acab9d22"}
{"id":"evt-xjtjda2yy9np","type":"edge.added","subject":"iss-jvxcd1n","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-jvxcd1n","to":"next.config.ts"},"at":"2026-09-19T13:37:31.982Z","parents":["evt-6ra9093yhc00"],"hash":"32218b8c9894d2288f0ad46de2c4538bc92f561694fd86cfedd16c647e84436a"}
{"id":"evt-6qtsfz3rv9rz","type":"edge.added","subject":"iss-jvxcd1n","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-jvxcd1n","to":"area:build"},"at":"2026-09-19T13:37:31.983Z","parents":["evt-xjtjda2yy9np"],"hash":"d1054fb6b27a995c2f8f12f97455799e5c3bccb02e99c7cb28805ff902e453e5"}
{"id":"evt-eev5ay33cf08","type":"edge.added","subject":"iss-jvxcd1n","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"discovered_from","from":"iss-jvxcd1n","to":"iss-a0hz0py"},"at":"2026-09-19T13:37:31.984Z","parents":["evt-6qtsfz3rv9rz"],"hash":"29c7239c6322b95973c7b414e81c947dd211fb9bf28033d9715fce3afafbaf60"}
{"id":"evt-1snexdqy14kf","type":"node.updated","subject":"iss-2fm6x2y","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"body":"Scan results used to live only in CameraScanner's client state. Partly done under iss-a0hz0py: the camera list is now server-rendered from the registry, the scanner calls router.refresh() after a successful scan, and the refresh-rate selector moved into the URL (?refresh=) as its own control (refresh-rate-select.tsx). Remaining: replace the scanner's hand-written fetch, useState and try/finally with useMutation against POST /api/discover, with onSuccess calling router.refresh()."}},"at":"2026-09-19T13:37:34.073Z","parents":["evt-eev5ay33cf08"],"hash":"f6c615fd26a426793a56b4595dbb8c27785477a3595417ca475a66510ad8c68f"}
{"id":"evt-8kaa72vcy7bt","type":"node.created","subject":"ver-zfcakm1","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"The change is committed and can be re-checked from the repo.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T13:40:36.667Z","parents":["evt-1snexdqy14kf"],"hash":"bc5f648ce98a710bc2fb73d569f609532cb23a1d2b425c35004d1cee2ebddf96"}
{"id":"evt-fznxevbm4afh","type":"edge.added","subject":"ver-zfcakm1","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-zfcakm1","to":"iss-a0hz0py"},"at":"2026-09-19T13:40:36.668Z","parents":["evt-8kaa72vcy7bt"],"hash":"2fed91eee6c65fe1a656819863ae91f47f85677447ee6635a11431bf587dd95c"}
{"id":"evt-px9ckhw092qh","type":"verification.recorded","subject":"ver-zfcakm1","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"Commit f39d16a on branch camera-dashboard. Re-check: `git checkout f39d16a && npx next build`; the route table should show `ƒ /`, then `npx next start` and GET / should list the registry cameras."},"at":"2026-09-19T13:40:36.669Z","parents":["evt-fznxevbm4afh"],"hash":"686d03f4eb2fefcd1edbdf2b0a83c6b3ce01045e50ab777dc4ccba969bd97ec6"}
{"id":"evt-35fqwhnzvtkn","type":"node.created","subject":"gol-9zxah3p","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"goal","title":"95% automated test coverage","body":"Line coverage of the app's source (src/**), measured by the test runner's coverage report on every run, is at least 95%. The coverage command and exact file scope get fixed once the test tooling is chosen (see the tooling question). Why: this app holds admin logins for every camera on the network and is headed toward a third-party API (gol-sjabnh3), so security properties need automated regression checks instead of one-off manual curl runs. Examples: no secrets or upstream errors in responses (iss-dbwgww8, iss-tz5s098), input validation, and private-IP-only targets. Device-facing code (ONVIF discovery, SOAP, snapshots) is tested against local fake devices built on Node's http and dgram, never real cameras. Baseline 2026-09-19: 0%, since there are no tests.","status":"active","owner":null,"attrs":{},"weight":null,"target":95,"direction":"up","unit":"%"},"at":"2026-09-19T13:44:39.758Z","parents":["evt-px9ckhw092qh"],"hash":"9933926b8e0271cc9347a2536950b103c8c1bd1cd3c22d26cc9c77b3a4d17f8f"}
{"id":"evt-xybetm8tvpjn","type":"edge.added","subject":"gol-9zxah3p","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"gol-9zxah3p","to":"gol-6q1q5mr"},"at":"2026-09-19T13:44:39.759Z","parents":["evt-35fqwhnzvtkn"],"hash":"9f61d961c88fbc7820e295c9c9c3fab115afd502258150270af79c85a0c426df"}
{"id":"evt-m3z241wyf7qq","type":"edge.added","subject":"gol-9zxah3p","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"gol-9zxah3p","to":"gol-sjabnh3"},"at":"2026-09-19T13:44:39.760Z","parents":["evt-xybetm8tvpjn"],"hash":"57add2064648b76af7bf8747d6459d39e7921a98638354b1b9fc34052bec7bda"}
{"id":"evt-s65d56015qpr","type":"edge.added","subject":"gol-9zxah3p","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"upholds","from":"gol-9zxah3p","to":"pri-tyrxdz9"},"at":"2026-09-19T13:44:39.761Z","parents":["evt-m3z241wyf7qq"],"hash":"5c9af0a7dbbd92350285a1e531e99ff4a7f279c018eb098057f502fe5b1c6dea"}
{"id":"evt-7kr43tjk3bd0","type":"edge.added","subject":"gol-9zxah3p","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"upholds","from":"gol-9zxah3p","to":"pri-m1csgrm"},"at":"2026-09-19T13:44:39.762Z","parents":["evt-s65d56015qpr"],"hash":"986006ae0e59e8d75edca980b3939a6bbd6a5104e477829cae1eb2bc4d41af32"}
{"id":"evt-gvf2zr8741c7","type":"node.created","subject":"que-gsp8rhh","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"question","title":"Which test tooling should we use to reach 95% coverage: Node's built-in test runner, or Vitest + Testing Library?","body":"Dependency justification per pri-mz2jxpb. Node is v24.14.1, which runs .ts directly (type stripping) and has node:test with built-in coverage (--experimental-test-coverage, --test-coverage-lines=95). That needs zero new packages, but it can't run .tsx: no JSX transform, and no DOM for client components. The Next 16 bundled guide (02-guides/testing/vitest.md) recommends: npm install -D vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/dom vite-tsconfig-paths, plus @vitest/coverage-v8 for coverage. That's about 7 dev dependencies with a large transitive tree (vite, a bundler, jsdom). It runs .ts and .tsx with one runner and one coverage report. Either way, the Next docs say async Server Components (our page.tsx) are unsupported by unit runners and recommend E2E (Playwright, which downloads browsers) for those. Everything here is dev-only; nothing ships to the client. Removal is easy either way, since tests are isolated.","status":"open","owner":null,"attrs":{"options":[{"option":"node:test only (zero dependencies)","consequence":"src/lib and route handlers get tested, which is where the security logic lives. Client .tsx components and page.tsx can't be run, so either the 95% scope excludes them or they need component logic pulled out into plain .ts."},{"option":"Vitest + Testing Library + jsdom + coverage-v8 (~7 dev deps)","consequence":"One runner covers lib, routes and client components; 95% over all of src except the async page.tsx."},{"option":"Either of the above plus Playwright for E2E","consequence":"Also covers page.tsx and full flows against next start, at the cost of a browser download and slower runs."}],"revisit":null}},"at":"2026-09-19T13:44:46.870Z","parents":["evt-7kr43tjk3bd0"],"hash":"fe297caac30b10317d8b0498d3d533dd48cbb60a05384a0c1c011cf53f06c18a"}
{"id":"evt-18tex4vv69sz","type":"edge.added","subject":"que-gsp8rhh","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"que-gsp8rhh","to":"gol-6q1q5mr"},"at":"2026-09-19T13:44:46.871Z","parents":["evt-gvf2zr8741c7"],"hash":"22cb6705f0138a595881bf0fc55de1e7c75cb58dcd2a46cafc17c4ba193743f3"}
{"id":"evt-b5wtcn58qr6s","type":"edge.added","subject":"pri-mz2jxpb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"guides","from":"pri-mz2jxpb","to":"que-gsp8rhh"},"at":"2026-09-19T13:44:46.872Z","parents":["evt-18tex4vv69sz"],"hash":"570b426c32267ebb36110aa5df7c18b833c5f0002565d3f5324d647c0dad3640"}
{"id":"evt-jde40zstj5a9","type":"node.created","subject":"dec-f62003t","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"decision","title":"Use Vitest + Testing Library (jsdom, coverage-v8) as the test tooling","body":"Chosen by the user on 2026-09-19. Dev dependencies: vitest, @vitejs/plugin-react, jsdom, @testing-library/react, @testing-library/dom, vite-tsconfig-paths, @vitest/coverage-v8. Rejected: node:test only (can't run .tsx client components) and adding Playwright E2E now (browser download, slower; can be revisited for page.tsx). Coverage is measured with v8 over src/**, excluding the async page.tsx, which Next's docs say unit runners can't render.","status":"recorded","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T13:45:09.379Z","parents":["evt-b5wtcn58qr6s"],"hash":"773176803cf90466556fa4580782e0fed6631093757ca9e48af49b93b56ac2ed"}
{"id":"evt-2d1ty0mpd6gg","type":"edge.added","subject":"dec-f62003t","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"answers","from":"dec-f62003t","to":"que-gsp8rhh"},"at":"2026-09-19T13:45:09.380Z","parents":["evt-jde40zstj5a9"],"hash":"a94aab756339eeb72ab45456afa8ea26d1ab5fd55e2b8073f02356dc09feb5e2"}
{"id":"evt-w51qwmfgbdnj","type":"question.answered","subject":"que-gsp8rhh","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"answer":"dec-f62003t"},"at":"2026-09-19T13:45:09.381Z","parents":["evt-2d1ty0mpd6gg"],"hash":"76ee63bc78183bc102fcc9606962e0ac82860b4ce65223e61cc626b41a184c6f"}
{"id":"evt-gzczh9apfdqp","type":"edge.added","subject":"dec-f62003t","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"dec-f62003t","to":"gol-9zxah3p"},"at":"2026-09-19T13:45:09.382Z","parents":["evt-w51qwmfgbdnj"],"hash":"50ce48ffce0ad5deab75ba8ffa83a3e60f9e2f5698bf0cb2bb993f7355e4e3ce"}
{"id":"evt-97k3q00prk28","type":"node.updated","subject":"gol-9zxah3p","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"weight":1}},"at":"2026-09-19T13:45:10.389Z","parents":["evt-gzczh9apfdqp"],"hash":"09d84be013a1b1886f0dc4de592de83910423c195aad0d25485b535d1a20cdf0"}
{"id":"evt-vtx2d6pd5cm3","type":"node.created","subject":"mea-gwsw0mg","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"measurement","title":"Line coverage of src/** (excluding page.tsx, *.d.ts, tests)","body":"`npm run coverage` (vitest run --coverage, v8 provider, scope from vitest.config.mts) on branch camera-dashboard, 2026-09-19: 140/449 lines, 68 tests. Per file: camera-registry.ts 96.8%, credential-store.ts 100%, api/discover/route.ts 100%, refresh-rate.ts 100%, camera.ts 13.5%, onvif.ts 7.3%; the other routes and all client components 0%.","status":"recorded","owner":null,"attrs":{"value":31.18,"applies_at":"2026-09-19T13:48:06.459Z"}},"at":"2026-09-19T13:48:06.459Z","parents":["evt-97k3q00prk28"],"hash":"befdf8422be0bb96af307380b4a907bbec1fcd94b1be66f3e9c3f987a830fac2"}
{"id":"evt-afxnff1fbntk","type":"edge.added","subject":"mea-gwsw0mg","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"measures","from":"mea-gwsw0mg","to":"gol-9zxah3p"},"at":"2026-09-19T13:48:06.461Z","parents":["evt-vtx2d6pd5cm3"],"hash":"a6c2cd2d488d9d0a539af6f4840990072c0d1db577f501bb86f80c00f131dc73"}
{"id":"evt-88gwxwnpfb9q","type":"node.created","subject":"ver-wx8n3pt","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"When discovery throws, recording results throws, or a non-Error is thrown, POST /api/discover returns 500 {\"error\":\"Network scan failed\"}, the body contains none of the original message, and the error is logged server-side.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T13:48:08.940Z","parents":["evt-afxnff1fbntk"],"hash":"23468a62705a056e210d76ad210a849ca2fd2fc370daf94ba2a462a5a58df5ae"}
{"id":"evt-0feac5f7j5k3","type":"edge.added","subject":"ver-wx8n3pt","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-wx8n3pt","to":"iss-dbwgww8"},"at":"2026-09-19T13:48:08.942Z","parents":["evt-88gwxwnpfb9q"],"hash":"3d4ef51bdba198c66f89bbe3a80e938beccd561f0207112dd8d70493eb977b8e"}
{"id":"evt-negjfqhsg0tt","type":"verification.recorded","subject":"ver-wx8n3pt","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"src/app/api/discover/route.test.ts, 'when the scan fails' (3 cases) with a thrown message containing a fake EISDIR path. Run `npx vitest run src/app/api/discover`: 11/11 pass on 2026-09-19."},"at":"2026-09-19T13:48:08.943Z","parents":["evt-0feac5f7j5k3"],"hash":"baf7834a9152cb1a7a91fb5244be0a13262321a0143e43bf921b9586b45d03b4"}
{"id":"evt-4qn792azvgn3","type":"node.created","subject":"iss-fp7qh13","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Set up Vitest with coverage reporting","body":"vitest.config.mts: react plugin, native tsconfigPaths, a server-only stub (test/server-only.ts), node environment by default with jsdom opt-in per file, and v8 coverage over src/** excluding page.tsx, *.d.ts and tests. Scripts: test, test:watch, coverage. test/temp-data.ts gives each test an isolated registry and credential store. vite-tsconfig-paths from the Next guide was dropped because Vite 8 resolves tsconfig paths natively. @types/node was bumped from ^20 to ^24 to match the Node 24 runtime (vitest 5 peer requirement).","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T13:48:11.642Z","parents":["evt-negjfqhsg0tt"],"hash":"ba67b1f95d4531bc6935477fc561e3edf424cf75781128ebdc70f40ed4d7484b"}
{"id":"evt-jvkf01egsdzc","type":"edge.added","subject":"iss-fp7qh13","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-fp7qh13","to":"gol-9zxah3p"},"at":"2026-09-19T13:48:11.643Z","parents":["evt-4qn792azvgn3"],"hash":"290e1d4e3d18be01a592f825cc50fc614dd654739a92687d780b98f076e5cb2c"}
{"id":"evt-aygkt9sma1g1","type":"edge.added","subject":"iss-fp7qh13","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-fp7qh13","to":"vitest.config.mts"},"at":"2026-09-19T13:48:11.644Z","parents":["evt-jvkf01egsdzc"],"hash":"6ed844b0efb14169a848978ffca2f16130d2d68fce5582966d7ec2e81bd25f6d"}
{"id":"evt-ec6jdy5gcakv","type":"edge.added","subject":"iss-fp7qh13","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-fp7qh13","to":"package.json"},"at":"2026-09-19T13:48:11.645Z","parents":["evt-aygkt9sma1g1"],"hash":"7b827c9dc3452e819ea7c98cf3b0d706a924fbb1fea0b63509a933e689bc7a22"}
{"id":"evt-fd8fq7v39gg9","type":"edge.added","subject":"iss-fp7qh13","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-fp7qh13","to":"test/"},"at":"2026-09-19T13:48:11.646Z","parents":["evt-ec6jdy5gcakv"],"hash":"ef4bd565012d1c0b386dd748fc2f9dfae2d57dfa68ea2d5918cec0b0d69f884b"}
{"id":"evt-gg0g109sjkkf","type":"edge.added","subject":"iss-fp7qh13","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-fp7qh13","to":"area:testing"},"at":"2026-09-19T13:48:11.647Z","parents":["evt-fd8fq7v39gg9"],"hash":"85f7d95708c6369b16102690d65b4b1d71bb9be8841d335473a25fd105eeb721"}
{"id":"evt-fskrsqn0c5db","type":"node.created","subject":"ver-5nx699s","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"`npm test` runs the suite green, `npm run coverage` prints a v8 report for src/**, and tsc and eslint are clean with the test files included.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T13:48:16.296Z","parents":["evt-gg0g109sjkkf"],"hash":"0efccba5342755be86de3bda42aafd7186c6d640d137bfab43ecb32b7784b803"}
{"id":"evt-mkrpremw61he","type":"edge.added","subject":"ver-5nx699s","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-5nx699s","to":"iss-fp7qh13"},"at":"2026-09-19T13:48:16.298Z","parents":["evt-fskrsqn0c5db"],"hash":"6f5a81eb28d6911ec28cb762bce6dc8b38e8288c2d78053ce9abc3e48bd934f9"}
{"id":"evt-1jn7ct7mv04m","type":"verification.recorded","subject":"ver-5nx699s","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"2026-09-19: npx vitest run gave 5 files and 68 tests passing; coverage report 31.18% lines; npx tsc --noEmit and npx eslint src test vitest.config.mts clean."},"at":"2026-09-19T13:48:16.299Z","parents":["evt-mkrpremw61he"],"hash":"5f2c70dcef7be27a9929c596f45fbeeaf6953b2ae6104a201cc43240d5c7936b"}
{"id":"evt-jkrbz6k6tzc5","type":"node.status_changed","subject":"iss-fp7qh13","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T13:48:17.363Z","parents":["evt-1jn7ct7mv04m"],"hash":"10c06b7a73b9d5a6510d36beb60dbab835cf266d51e94dd5406d612e1809c314"}
{"id":"evt-bwfkby1sn3kz","type":"node.created","subject":"iss-qak2mz8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Test camera.ts against a fake ONVIF/HTTP device","body":"camera.ts is at 13.5% line coverage. Stand up a local node:http server on 127.0.0.1 that answers the ONVIF SOAP calls (GetDeviceInformation, GetProfiles, GetSnapshotUri) and serves a snapshot with digest auth. Tests must bypass isAllowedHost, since the fake runs on loopback, by calling lib functions with a CameraTarget directly. Cover: info mapping, snapshot content-type and body, digest auth, auth-failure → CameraAuthError, inactive device → CameraInactiveError, timeouts, connection reuse and resetConnection. Also cover isAllowedHost edge cases: 172.15/172.32, octets above 255, IPv6.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T13:48:20.308Z","parents":["evt-jkrbz6k6tzc5"],"hash":"ab889741116691b0fe059dfc03e672ebed458367ee428d5b2110856e5029c852"}
{"id":"evt-ps83bhknrgcz","type":"edge.added","subject":"iss-qak2mz8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-qak2mz8","to":"gol-9zxah3p"},"at":"2026-09-19T13:48:20.309Z","parents":["evt-bwfkby1sn3kz"],"hash":"842b3b8363813141238744ef69db71eabe09851b22f75b76ac86c9aa63c1af30"}
{"id":"evt-x0feparnhmmz","type":"edge.added","subject":"iss-qak2mz8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-qak2mz8","to":"src/lib/camera.ts"},"at":"2026-09-19T13:48:20.310Z","parents":["evt-ps83bhknrgcz"],"hash":"ed7f9db7b26badb71a59548b86535f41a4b62ab636dfc8b5e2bec491e385f9a3"}
{"id":"evt-xtdmjnt8ddm1","type":"edge.added","subject":"iss-qak2mz8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-qak2mz8","to":"area:testing"},"at":"2026-09-19T13:48:20.311Z","parents":["evt-x0feparnhmmz"],"hash":"8a9ac6b2ffc60b629e8fd6e45e6c3ad2a5055265d558c62ab21a0e18ad26ac3a"}
{"id":"evt-ws9ef8fkm6qj","type":"node.created","subject":"iss-3bg4r6e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Test onvif.ts discovery against a fake WS-Discovery responder","body":"onvif.ts is at 7.3% line coverage. Use node:dgram to answer probes with canned ProbeMatch SOAP. Cover response parsing (XAddrs, scopes → name, hardware and location, endpoint reference → id), de-duplicating the same camera found by multicast and unicast (foundBy), the subnet-size limit for the unicast sweep (/20), timeouts, and malformed responses. Where real multicast is flaky in CI, inject the socket or interfaces instead of sending real multicast; open a design issue if that needs a code seam.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T13:48:23.727Z","parents":["evt-xtdmjnt8ddm1"],"hash":"1b1b0e2cc289eddfd224f95ff753048db66f2e3028758eae4c6f99848446acc5"}
{"id":"evt-68eqe8bgyy7p","type":"edge.added","subject":"iss-3bg4r6e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-3bg4r6e","to":"gol-9zxah3p"},"at":"2026-09-19T13:48:23.728Z","parents":["evt-ws9ef8fkm6qj"],"hash":"50331a3cc0bc6fcd4c8207e0b262377a503e5373f47179100d3dd8affac9f5b4"}
{"id":"evt-mh4x418v3x2q","type":"edge.added","subject":"iss-3bg4r6e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-3bg4r6e","to":"src/lib/onvif.ts"},"at":"2026-09-19T13:48:23.729Z","parents":["evt-68eqe8bgyy7p"],"hash":"7f9375c8ab6cb0efa2264d27b02df989060d05c2d88e5422a1e7b2a05c128df7"}
{"id":"evt-wem03cgmd3af","type":"edge.added","subject":"iss-3bg4r6e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-3bg4r6e","to":"area:testing"},"at":"2026-09-19T13:48:23.730Z","parents":["evt-mh4x418v3x2q"],"hash":"9c56f42d9ac19c5ce940e2d49a6bc4c2103551a093c4e6eae40dcc96c88962fe"}
{"id":"evt-8f99b8nrsd0a","type":"node.created","subject":"iss-nc2tj7c","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Test the camera info, snapshot and credentials route handlers","body":"All three are at 0% line coverage. Mock @/lib/camera, and the credential store where needed. Cover: bad id → 400, unknown → 404; info passthrough; snapshot bytes, Content-Type, Cache-Control no-store and the ?profile param; credentials GET never includes a password; PUT validates with zod, stores only after testCredentials succeeds, and calls resetConnection; DELETE clears; each error class maps via cameraErrorResponse. Write the error-leak assertions so they encode iss-tz5s098's fixed behaviour.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T13:48:27.546Z","parents":["evt-wem03cgmd3af"],"hash":"faff1c604c1bf647dd45e1f27dca1b965a6e0e89dc291f0b1fcc97fe80a0c421"}
{"id":"evt-zy1r77zzm925","type":"edge.added","subject":"iss-nc2tj7c","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-nc2tj7c","to":"gol-9zxah3p"},"at":"2026-09-19T13:48:27.547Z","parents":["evt-8f99b8nrsd0a"],"hash":"10f96109b30d634cec5c143d980768d97ce447ce3af7b196547bc978b08e65f9"}
{"id":"evt-ej6sj2fzfwd6","type":"edge.added","subject":"iss-nc2tj7c","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-nc2tj7c","to":"src/app/api/cameras/[id]/info/route.ts"},"at":"2026-09-19T13:48:27.548Z","parents":["evt-zy1r77zzm925"],"hash":"9bb3032a339b45dc1b3827d0206796026123678f2affb264375e323cc38c4847"}
{"id":"evt-p34m0fbkpe11","type":"edge.added","subject":"iss-nc2tj7c","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-nc2tj7c","to":"src/app/api/cameras/[id]/snapshot/route.ts"},"at":"2026-09-19T13:48:27.549Z","parents":["evt-ej6sj2fzfwd6"],"hash":"2c0a4ce242138cf030f9e88579fdf35d82dc99111f5b870b926849d11dcc0a20"}
{"id":"evt-d8ft09wp5tmk","type":"edge.added","subject":"iss-nc2tj7c","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-nc2tj7c","to":"src/app/api/cameras/[id]/credentials/route.ts"},"at":"2026-09-19T13:48:27.550Z","parents":["evt-p34m0fbkpe11"],"hash":"30f88782f74bdb5d99260b116923f9ea52d7281d4b2a70fa94e327d1cf67f3d0"}
{"id":"evt-vb0vkr6qda21","type":"edge.added","subject":"iss-nc2tj7c","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-nc2tj7c","to":"area:testing"},"at":"2026-09-19T13:48:27.551Z","parents":["evt-d8ft09wp5tmk"],"hash":"7a0b378b31697ce002c74f733c1344fafebca29f122f3baa33ff6c75577db27e"}
{"id":"evt-j6f03bgr07aw","type":"edge.added","subject":"iss-nc2tj7c","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-nc2tj7c","to":"area:api"},"at":"2026-09-19T13:48:27.552Z","parents":["evt-vb0vkr6qda21"],"hash":"5b3fb45ed96e8b28ddc08e1bab2d2b9e45e0ff0d6dacb4a54b1ef29d5ed14e32"}
{"id":"evt-n4bsjrhgbzb2","type":"node.created","subject":"iss-nc5w0j8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Component tests for camera-card, camera-scanner, refresh-rate-select, providers and layout","body":"All client components are at 0% line coverage. Use jsdom with Testing Library, and stub fetch with vi.stubGlobal plus next/navigation's useRouter and usePathname. Best done after the React Query migration issues (iss-ksxmctm, iss-m032zwq, iss-8hfq2y2, iss-2fm6x2y), so the tests are written against the new hooks rather than code about to be replaced. Cover the setup, login and error panels, snapshot pause and resume, the scan flow calling router.refresh, and the refresh selector writing ?refresh=. layout.tsx needs next/font/google mocked.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T13:48:32.191Z","parents":["evt-j6f03bgr07aw"],"hash":"7b0ef50b53b3ea4bf0cbc88cbeb1ced59fe7f82ec0fbd4f7b5520e700999fd45"}
{"id":"evt-yv8ep34jkagb","type":"edge.added","subject":"iss-nc5w0j8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-nc5w0j8","to":"gol-9zxah3p"},"at":"2026-09-19T13:48:32.192Z","parents":["evt-n4bsjrhgbzb2"],"hash":"6eedbbb04ef7464317c25a9ef206cf477e469d874c70e9d924b8600ebe55fade"}
{"id":"evt-mpnncz77ahfk","type":"edge.added","subject":"iss-nc5w0j8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-nc5w0j8","to":"gol-146a3cb"},"at":"2026-09-19T13:48:32.193Z","parents":["evt-yv8ep34jkagb"],"hash":"921bd2e707196cfa5a0bdcd3a622c7df8e5d9eb73826e1a9b9a3b2f842ebdab5"}
{"id":"evt-jmgs8twwfpa1","type":"edge.added","subject":"iss-nc5w0j8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-nc5w0j8","to":"src/app/camera-card.tsx"},"at":"2026-09-19T13:48:32.194Z","parents":["evt-mpnncz77ahfk"],"hash":"53f9469ee293c53bfd281ccc06047c1100619c2f05a71ee6e2b0586d3881d034"}
{"id":"evt-w208y81sz70m","type":"edge.added","subject":"iss-nc5w0j8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-nc5w0j8","to":"src/app/camera-scanner.tsx"},"at":"2026-09-19T13:48:32.195Z","parents":["evt-jmgs8twwfpa1"],"hash":"7692d17365f8a74c1d1809f60ad6c1e1bb0bc609062ef01700925c0f370b7017"}
{"id":"evt-dpw94b847yqh","type":"edge.added","subject":"iss-nc5w0j8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-nc5w0j8","to":"src/app/refresh-rate-select.tsx"},"at":"2026-09-19T13:48:32.196Z","parents":["evt-w208y81sz70m"],"hash":"ef61e6216cd5fb4bcc8fa04b237aab8ac6575bae62b172c2fdcf2f698c147b02"}
{"id":"evt-d9a1pyzsavm0","type":"edge.added","subject":"iss-nc5w0j8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-nc5w0j8","to":"src/app/providers.tsx"},"at":"2026-09-19T13:48:32.197Z","parents":["evt-dpw94b847yqh"],"hash":"318f40626918950a0f7e0a2ce4fe31f04eaef70962d9547bc9885a2db2dc5df9"}
{"id":"evt-4qxyc2kyq2cr","type":"edge.added","subject":"iss-nc5w0j8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-nc5w0j8","to":"src/app/layout.tsx"},"at":"2026-09-19T13:48:32.198Z","parents":["evt-d9a1pyzsavm0"],"hash":"ab5a8f9cb35167d3df6374d3fa4c60d90f01e0bfb8370cae5bd13feb3217d6b7"}
{"id":"evt-f5f5c36cmdwx","type":"edge.added","subject":"iss-ksxmctm","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"blocks","from":"iss-ksxmctm","to":"iss-nc5w0j8"},"at":"2026-09-19T13:48:32.199Z","parents":["evt-4qxyc2kyq2cr"],"hash":"6e288cdbcb20e5500ef8a8a09207a28068f8ae65868017f73da6ac5aa02955a7"}
{"id":"evt-p9e0jd6d0q3n","type":"edge.added","subject":"iss-m032zwq","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"blocks","from":"iss-m032zwq","to":"iss-nc5w0j8"},"at":"2026-09-19T13:48:32.200Z","parents":["evt-f5f5c36cmdwx"],"hash":"f03d5eafa729c5f06843c18798c58b82d1e59f57d95458f6766a20c846c751a5"}
{"id":"evt-te4s5w0j0ngr","type":"edge.added","subject":"iss-8hfq2y2","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"blocks","from":"iss-8hfq2y2","to":"iss-nc5w0j8"},"at":"2026-09-19T13:48:32.201Z","parents":["evt-p9e0jd6d0q3n"],"hash":"4fa9a7286a62d04a9ce1babb721e5c514167f61997202f867d355f654b7ec41e"}
{"id":"evt-enyfg19tyw7b","type":"edge.added","subject":"iss-2fm6x2y","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"blocks","from":"iss-2fm6x2y","to":"iss-nc5w0j8"},"at":"2026-09-19T13:48:32.202Z","parents":["evt-te4s5w0j0ngr"],"hash":"5d9a430cbf16a56be8597ed53a6dd2224668525f633c90a1aafeb0db51854f69"}
{"id":"evt-1b7znc85203x","type":"edge.added","subject":"iss-nc5w0j8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-nc5w0j8","to":"area:testing"},"at":"2026-09-19T13:48:32.203Z","parents":["evt-enyfg19tyw7b"],"hash":"61627b32dbda33ae92ec7f27d354f386d2e1ef31b87001a19f5519cbb22deb13"}
{"id":"evt-p2kpk0wtdsgv","type":"edge.added","subject":"iss-nc5w0j8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-nc5w0j8","to":"area:ui"},"at":"2026-09-19T13:48:32.204Z","parents":["evt-1b7znc85203x"],"hash":"5dce2df80199d0e3f98bf31002604aae457629297b61bdb468acf1364fe82da9"}
{"id":"evt-b5118ev8dnhf","type":"node.created","subject":"iss-zjpc22k","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Enforce the 95% line-coverage threshold in vitest.config.mts","body":"Once coverage reaches 95%, set coverage.thresholds.lines = 95 so `npm run coverage` fails if it drops. Until then, measurements are recorded on gol-9zxah3p after each batch of tests.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T13:48:34.119Z","parents":["evt-p2kpk0wtdsgv"],"hash":"db62b67eb50e908053f5b4b4a96aaf0d571a592eda56ffe85f468ad61d6c5822"}
{"id":"evt-zb67vbj9gq3f","type":"edge.added","subject":"iss-zjpc22k","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-zjpc22k","to":"gol-9zxah3p"},"at":"2026-09-19T13:48:34.121Z","parents":["evt-b5118ev8dnhf"],"hash":"47a2a3f02b139eab3f36583eb856278791a46e9e6035cad834de35f8aeca3f49"}
{"id":"evt-9jqc4c9akpvd","type":"edge.added","subject":"iss-zjpc22k","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-zjpc22k","to":"vitest.config.mts"},"at":"2026-09-19T13:48:34.122Z","parents":["evt-zb67vbj9gq3f"],"hash":"2dce8ab9c28c069b400e6bfbcbb1859ea66e49e72c4c36dab1e7c1386c2cd67e"}
{"id":"evt-tgnhg3bzctxe","type":"edge.added","subject":"iss-zjpc22k","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-zjpc22k","to":"area:testing"},"at":"2026-09-19T13:48:34.123Z","parents":["evt-9jqc4c9akpvd"],"hash":"f5502f9006c88d881121d0b5ea5750d9a179ac754b1d53aab5469bb2e5d9ebaf"}
{"id":"evt-0ep7e88wpm1m","type":"node.created","subject":"ver-k6ndd19","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"Scan runs through useScanNetwork (useMutation); on success it calls router.refresh() and shows the count; on failure it shows the error and does not refresh; the button is disabled while pending; the timeout is clamped to 1–30 s with an empty field meaning 5 s.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T13:58:30.768Z","parents":["evt-tgnhg3bzctxe"],"hash":"228254a84db314e5d0bcdccc8cf513bc5a457734f3cc8b3df2aad552de7ba5a7"}
{"id":"evt-gy197h670rnw","type":"edge.added","subject":"ver-k6ndd19","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-k6ndd19","to":"iss-2fm6x2y"},"at":"2026-09-19T13:58:30.771Z","parents":["evt-0ep7e88wpm1m"],"hash":"43d375d7ed15df69443fa05d8153467b55d16b7ced4d4ea339eee7b5c05188a5"}
{"id":"evt-anhn0b0z57dq","type":"verification.recorded","subject":"ver-k6ndd19","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"src/app/camera-scanner.test.tsx (9 tests), `npx vitest run src/app/camera-scanner.test.tsx`, 2026-09-19. The test caught a real bug, fixed: a typed timeout under 1 s rounded to 0 and fell back to 5 s. Not exercised in a real browser."},"at":"2026-09-19T13:58:30.772Z","parents":["evt-gy197h670rnw"],"hash":"e51323701717157533b89c853bda296e5a91d3b71231980f2414b0a813714be1"}
{"id":"evt-s3m83caq68et","type":"node.created","subject":"ver-5f7rnb2","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"CameraCard gets info via useCameraInfo (['camera', id, 'info']), with no useState/useEffect/attempt counter; auth → login form, inactive → setup panel, other failures retried once then shown with Retry; Retry resets the camera's queries.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T13:58:33.140Z","parents":["evt-anhn0b0z57dq"],"hash":"bb78b9114fa6856fd437db70a91590eda976e7814e2d6937f4a825089aafbe89"}
{"id":"evt-6by9j3pw9thh","type":"edge.added","subject":"ver-5f7rnb2","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-5f7rnb2","to":"iss-ksxmctm"},"at":"2026-09-19T13:58:33.142Z","parents":["evt-s3m83caq68et"],"hash":"29bb2a1c1e5361c2379e3e3916e27ddeaf2bd2d6459b67a209587286e0cd1124"}
{"id":"evt-7628nyyf5hnp","type":"verification.recorded","subject":"ver-5f7rnb2","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"src/app/camera-card.test.tsx: 'retries a plain info failure once' (exactly 2 info fetches, 0 snapshot), 'shows setup steps…', 'asks for a login…', 'reports a network error…'. Hooks are in src/app/camera-queries.ts. 2026-09-19; not exercised in a real browser."},"at":"2026-09-19T13:58:33.143Z","parents":["evt-6by9j3pw9thh"],"hash":"335bc8d637583724617a6432fad5176a0c1cf22dba19dee1921fedf3efe4ba39"}
{"id":"evt-cbz0k7fvr9ez","type":"node.created","subject":"ver-e7k77p6","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"Snapshots poll via useCameraSnapshot (refetchInterval, only once info has loaded); every object URL is revoked, including on unmount; after a failed frame, polling stops with no further requests until Retry.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T13:58:36.163Z","parents":["evt-7628nyyf5hnp"],"hash":"e9417f97d166f402922124c794a56a667daf2d5685b2cc8fcce8c0b9f4b73c23"}
{"id":"evt-g0fcfn3fav15","type":"edge.added","subject":"ver-e7k77p6","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-e7k77p6","to":"iss-m032zwq"},"at":"2026-09-19T13:58:36.164Z","parents":["evt-cbz0k7fvr9ez"],"hash":"f05e75428f603fe9d9e3d4ec616212532d1e75e1f13f16f2eea4ea68f39c8388"}
{"id":"evt-3ezj7nyaet8c","type":"verification.recorded","subject":"ver-e7k77p6","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"src/app/camera-card.test.tsx: 'frees every frame's object URL, including on unmount' (created ≥4, revoked set == created set) and 'stops polling after a failed frame' (0 extra requests over 150 ms at a 20 ms interval, then resumes after Retry). Interval refetches reuse an in-flight request (query-core queryObserver #executeFetch has no cancelRefetch), so a slow camera doesn't pile up requests. Object URLs are created and revoked in one effect, so they're safe under Strict Mode. 2026-09-19; the 'few minutes in a real browser' memory check was not run."},"at":"2026-09-19T13:58:36.165Z","parents":["evt-g0fcfn3fav15"],"hash":"e03c0fe1e2cb3aa4566b5d8b8ba37f18bd45a2f9f7b01cd152733972744893d9"}
{"id":"evt-q24pzht5swew","type":"node.created","subject":"ver-9z27bka","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"The credentials GET is a useQuery (username prefill, never a password); PUT and DELETE are useMutations whose success resets only that camera's ['camera', id] queries; a rejected login shows as a form error.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T13:58:39.168Z","parents":["evt-3ezj7nyaet8c"],"hash":"f273b46aa473eb2403e6a8f5cb9d917b7504443207d8f39a70308bfb92647222"}
{"id":"evt-b83nqk81tsyh","type":"edge.added","subject":"ver-9z27bka","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-9z27bka","to":"iss-8hfq2y2"},"at":"2026-09-19T13:58:39.170Z","parents":["evt-q24pzht5swew"],"hash":"97ba093b1d98a4a277e711e86a7a2553ee23d6c57823b98b709914ff1359126f"}
{"id":"evt-dwsp2442bbq6","type":"verification.recorded","subject":"ver-9z27bka","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"src/app/camera-card.test.tsx 'login' block (6 tests: prefill, typed username wins, rejected login, other failure, cancel, forget saved login via DELETE) and 'two cameras' (saving camera A refetches A's info, while B's info stays at 1 fetch). resetQueries is used rather than invalidateQueries so that error state clears. Adds a 'Forget saved login' button, shown only for stored logins. 2026-09-19; not exercised in a real browser."},"at":"2026-09-19T13:58:39.171Z","parents":["evt-b83nqk81tsyh"],"hash":"9fbc058401774aeb723683396a65a69e43b0cbbb3b2ecb8dd45dbaf7c7905786"}
{"id":"evt-edzjpeex02wj","type":"node.created","subject":"mea-s1kf91q","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"measurement","title":"Line coverage of src/** (excluding page.tsx, *.d.ts, tests)","body":"`npm run coverage`, 2026-09-19, after the React Query migration and its component tests: 235/433 lines, 93 tests. At 100%: camera-card, camera-queries, camera-scanner, refresh-rate-select, refresh-rate, discover route, camera-route, credential-store. Remaining gaps: camera.ts 13.5%, onvif.ts 7.3%, the info/snapshot/credentials routes 0%, layout and providers 0%.","status":"recorded","owner":null,"attrs":{"value":54.27,"applies_at":"2026-09-19T13:58:41.458Z"}},"at":"2026-09-19T13:58:41.458Z","parents":["evt-dwsp2442bbq6"],"hash":"d758ed6a96554f9d760f0214fc04b50a34ae97bed44cdbdcc1570fc654d42d21"}
{"id":"evt-0wdpqz1n37pf","type":"edge.added","subject":"mea-s1kf91q","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"measures","from":"mea-s1kf91q","to":"gol-9zxah3p"},"at":"2026-09-19T13:58:41.459Z","parents":["evt-edzjpeex02wj"],"hash":"c5b74cc09fb476bed847f38fbd2a2dc5d25a10d92f4dfc00da775175a9f4d24b"}
{"id":"evt-4zw0z9bpe55j","type":"node.status_changed","subject":"iss-2fm6x2y","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T13:58:44.403Z","parents":["evt-0wdpqz1n37pf"],"hash":"89ba0614e63465ee0a196031731d6d66fbd9510e1b99af04ca4b02f51a686557"}
{"id":"evt-4v0jw11be2tj","type":"node.status_changed","subject":"iss-ksxmctm","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T13:58:45.459Z","parents":["evt-4zw0z9bpe55j"],"hash":"a112adef63186440af148e6b8f448284a136e185f04c0d4285603357e756fec2"}
{"id":"evt-0v8z82ap6vw8","type":"node.status_changed","subject":"iss-m032zwq","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T13:58:46.508Z","parents":["evt-4v0jw11be2tj"],"hash":"2a1d315573688aa3502db53ecc5a39e073e4a9a6820861e01d710cf30db511b1"}
{"id":"evt-gdbnzswzw52f","type":"node.status_changed","subject":"iss-8hfq2y2","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T13:58:47.573Z","parents":["evt-0v8z82ap6vw8"],"hash":"967a983d4444f32f6624a17b0cf15d2f520bf62e88b6a4fca57dd23a0dcd8904"}
{"id":"evt-5q5femhczhwy","type":"node.updated","subject":"iss-nc5w0j8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"title":"Component tests for providers and layout","body":"Mostly done alongside the React Query migration: camera-card, camera-queries, camera-scanner and refresh-rate-select are at 100% line coverage (test/dom.tsx has the render and fetch helpers). Remaining: providers.tsx (QueryClient defaults: staleTime 0, retry 1) and layout.tsx (mock next/font/google; check it wraps children in Providers)."}},"at":"2026-09-19T13:58:48.649Z","parents":["evt-gdbnzswzw52f"],"hash":"86128137c9d72e962190179549cc17c4970115c46958d76c06185693736ad65c"}
{"id":"evt-f6skgy2z97a7","type":"node.created","subject":"ver-h8wgh7r","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"In a real browser against real cameras, 'Change login' and 'Forget saved login' work end to end.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T14:01:24.882Z","parents":["evt-5q5femhczhwy"],"hash":"c6c5f6af9cf551e1cfeae733656cd853d9d712dccab3da0914b87198a99c90a8"}
{"id":"evt-2xx680y4h043","type":"edge.added","subject":"ver-h8wgh7r","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-h8wgh7r","to":"iss-8hfq2y2"},"at":"2026-09-19T14:01:24.884Z","parents":["evt-f6skgy2z97a7"],"hash":"e276e752fb4db6dff1c9a931f93047d819a47f367298e1417c814aa43d89804a"}
{"id":"evt-e2r4bptapxrq","type":"verification.recorded","subject":"ver-h8wgh7r","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"Manual test by the user (Michael Mainguy) on 2026-09-19: both flows reported working."},"at":"2026-09-19T14:01:24.885Z","parents":["evt-2xx680y4h043"],"hash":"edcb6d01b6770914dc084b7542709bcbc8eba7129391cf6fc4a548191e7e05cd"}
{"id":"evt-x7z0pgaa5x2r","type":"node.created","subject":"ver-axagv0j","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"cameraErrorResponse returns fixed text per error class (inactive 409, auth 401 in 'rejected' and 'missing login' forms, store 500, other 502) and never echoes the original message; unexpected failures and rejected logins are logged server-side; no API route passes err.message to the client.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T14:02:56.605Z","parents":["evt-e2r4bptapxrq"],"hash":"cbc4392c1e36b178d229ca87d653a003779b8e42c199e8bc9e90a492f06edff1"}
{"id":"evt-xr3hmxyydv24","type":"edge.added","subject":"ver-axagv0j","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-axagv0j","to":"iss-tz5s098"},"at":"2026-09-19T14:02:56.606Z","parents":["evt-x7z0pgaa5x2r"],"hash":"b24fb843e703f39972383240ec84d316fb9f697cccda24b03f151e70d9283aea"}
{"id":"evt-zyatdc21q8ja","type":"verification.recorded","subject":"ver-axagv0j","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"src/lib/camera-route.test.ts 'cameraErrorResponse' (8 tests; each case throws a message containing a fake password and a /Users/ path and checks neither appears in the body). `grep -rn \"message|String(err)\" src/app/api --include=route.ts` finds nothing. Full suite 96/96, tsc and eslint clean, 2026-09-19. CameraAuthError gained a missingLogin flag so the UI can still tell 'no login saved' from 'rejected'."},"at":"2026-09-19T14:02:56.607Z","parents":["evt-xr3hmxyydv24"],"hash":"ac177142e64cb1619ae0c37a9eaebbcf071affa6cfad1a19ed2259bdada13d9b"}
{"id":"evt-nxc9zdmq28zq","type":"node.status_changed","subject":"iss-tz5s098","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T14:02:57.690Z","parents":["evt-zyatdc21q8ja"],"hash":"ada40e8bb503d18898af64208ad09f07feca6fc2d19c243805360576e1a20d3f"}
{"id":"evt-s95vt9k8f63d","type":"node.created","subject":"ver-d925rqw","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"All children are closed; no client component contains hand-rolled fetch or useEffect polling; page.tsx is a Server Component with no 'use client' ancestor above the per-camera tiles.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T14:03:00.599Z","parents":["evt-nxc9zdmq28zq"],"hash":"ad8b4d8baff2ae889a92115d2cfd77530855cdc3b70d28f935b2db085c0bceba"}
{"id":"evt-pqwgd02ntpka","type":"edge.added","subject":"ver-d925rqw","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-d925rqw","to":"iss-qbh3541"},"at":"2026-09-19T14:03:00.600Z","parents":["evt-s95vt9k8f63d"],"hash":"13c905b15e00750c6d5380978fad98e4e9ecc1adfee1ed1b3880b712cdbb4052"}
{"id":"evt-41e116z167g3","type":"verification.recorded","subject":"ver-d925rqw","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"Children iss-a0hz0py, iss-rjqy3hy, iss-dbwgww8, iss-2fm6x2y, iss-ksxmctm, iss-m032zwq, iss-8hfq2y2 and iss-tz5s098 are all done with passing evidence. The only fetch calls in client code are in src/app/camera-queries.ts, wrapped in React Query; the only useEffect in camera-card.tsx manages object URLs, not polling. page.tsx has no 'use client', and next build lists `ƒ /`. The user manually tested the login flows in a browser, 2026-09-19."},"at":"2026-09-19T14:03:00.601Z","parents":["evt-pqwgd02ntpka"],"hash":"f1cffc889f8bf493746e287f36ab195418460ccfc909c39975f11126ffe74888"}
{"id":"evt-kd9pxqn4bcd8","type":"node.status_changed","subject":"iss-qbh3541","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T14:03:01.991Z","parents":["evt-41e116z167g3"],"hash":"b22b1be718202b8f41e5491d630fa52dd1f46083789a98c8a89a4aa273bb796d"}
{"id":"evt-kan6asvzm60c","type":"node.created","subject":"ver-rqk3ktd","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"camera.ts is at 100% line coverage. Tests cover info mapping (Media1 and Media2 profiles), connection reuse and resetConnection, inactive/auth/missing-login/no-profile failures, testCredentials not caching, snapshot fetch from the target address, Digest and Basic challenges, a refused login, HTTP errors, an unreachable host, snapshot URI caching and failure recovery, and isAllowedHost edge cases.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T14:07:14.315Z","parents":["evt-kd9pxqn4bcd8"],"hash":"56ec4ef908aebc91798a7244c7e114ac806e30c8f4c7042c7563c2d3f0442f43"}
{"id":"evt-j9mdfr9msk1v","type":"edge.added","subject":"ver-rqk3ktd","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-rqk3ktd","to":"iss-qak2mz8"},"at":"2026-09-19T14:07:14.317Z","parents":["evt-kan6asvzm60c"],"hash":"6b0824745dde7fa28c6b00f93afd25f1f7032929b2c02d7c0ab525c21be44e45"}
{"id":"evt-2amh1dbbb8v2","type":"verification.recorded","subject":"ver-rqk3ktd","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"src/lib/camera.test.ts, 47 tests; `npx vitest run src/lib/camera.test.ts --coverage --coverage.include=src/lib/camera.ts` gives 100% lines and 93.3% branches, 2026-09-19. Deviation from the issue text: the onvif Cam class is a scripted test double (vi.mock) rather than a fake SOAP server. Emulating onvif's SOAP parsing would test the library rather than our code. Snapshots use a real node:http server on 127.0.0.1 with real 401 challenges. Not covered: the 10 s socket-timeout callback, and the https branch (no self-signed cert without a new dependency). Full suite 143/143, tsc and eslint clean."},"at":"2026-09-19T14:07:14.318Z","parents":["evt-j9mdfr9msk1v"],"hash":"8e594820c4c2eab47b002b459d9c02cd6ab391e3e4f8710f50016807e4f62f37"}
{"id":"evt-vpr581bxp7sw","type":"node.status_changed","subject":"iss-qak2mz8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T14:07:15.570Z","parents":["evt-2amh1dbbb8v2"],"hash":"c83dad23d807ecca00d847f3a30a682df460eba2e5ec6c65d2f3779232aef003"}
{"id":"evt-2gyj0y16gz93","type":"node.created","subject":"mea-prapanf","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"measurement","title":"Line coverage of src/** (excluding page.tsx, *.d.ts, tests)","body":"`npm run coverage`, 2026-09-19, after camera.ts tests (iss-qak2mz8): 317/438 lines, 143 tests. camera.ts is now at 100%. Remaining gaps: onvif.ts (76 lines), the info/snapshot/credentials routes (36), layout and providers (8), camera-registry (1).","status":"recorded","owner":null,"attrs":{"value":72.37,"applies_at":"2026-09-19T14:07:16.619Z"}},"at":"2026-09-19T14:07:16.619Z","parents":["evt-vpr581bxp7sw"],"hash":"80890f1f1c559bca0f6f33fc114338073510bc94fa2fd84153c123a19f1cf9a8"}
{"id":"evt-f17cnwzvhjqf","type":"edge.added","subject":"mea-prapanf","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"measures","from":"mea-prapanf","to":"gol-9zxah3p"},"at":"2026-09-19T14:07:16.621Z","parents":["evt-2gyj0y16gz93"],"hash":"e26fa3671b95cc3b9dae9ffcf894da0d704f61588d0b9111e25fce22e0b6ed7d"}
{"id":"evt-d7btd1jh3x6m","type":"node.created","subject":"ver-8ah7n8j","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"The info, snapshot and credentials routes are at 100% line coverage. Tests cover bad id → 400, unknown → 404, info passthrough, snapshot bytes plus Content-Type, Cache-Control no-store and ?profile, GET without a password, PUT validation with zod (5 bad-body cases, no camera contact), testCredentials before setCredentials, a rejected login never stored, resetConnection, DELETE, and fixed error messages that never echo upstream text.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T14:10:26.665Z","parents":["evt-f17cnwzvhjqf"],"hash":"b08d82d4a692c8484e5f4ea05e65cfc854f8dd32077c9d7d095a6365e87c8592"}
{"id":"evt-hbz7w2ryaycd","type":"edge.added","subject":"ver-8ah7n8j","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-8ah7n8j","to":"iss-nc2tj7c"},"at":"2026-09-19T14:10:26.667Z","parents":["evt-d7btd1jh3x6m"],"hash":"b6154eeba8b209b02119b534cea98ef1e943d96ecafa0be356bcc7f00905cc52"}
{"id":"evt-fwvf11neacg5","type":"verification.recorded","subject":"ver-8ah7n8j","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"src/app/api/cameras/[id]/{info,snapshot,credentials}/route.test.ts with shared fixtures in test/camera-routes.ts; `npx vitest run src/app/api/cameras` passes. Full suite 170/170, coverage 82.42% lines, tsc and eslint clean, 2026-09-19."},"at":"2026-09-19T14:10:26.668Z","parents":["evt-hbz7w2ryaycd"],"hash":"655d82d5f8c58b7ecdabb8e3a4bf3c6467a30c22aab7d081d05fa6509689e353"}
{"id":"evt-jhdv7dna7xdb","type":"node.created","subject":"ver-kvtf8ek","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"providers.tsx and layout.tsx are at 100% line coverage: Providers gives children a QueryClient with staleTime 0 and retry 1, stable across re-renders; RootLayout applies both font variables, wraps children in Providers, and exports the page metadata.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T14:10:28.551Z","parents":["evt-fwvf11neacg5"],"hash":"21b79d3b3b272c2273e3617ce157899b0062c166be43983bce1857d5a101406e"}
{"id":"evt-3gjva3dbwp6s","type":"edge.added","subject":"ver-kvtf8ek","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-kvtf8ek","to":"iss-nc5w0j8"},"at":"2026-09-19T14:10:28.553Z","parents":["evt-jhdv7dna7xdb"],"hash":"e7ff4025cc3047584fe405151ac225678c5069f6e41f66d242956e99dbd51f36"}
{"id":"evt-99j1aj1ga9yq","type":"verification.recorded","subject":"ver-kvtf8ek","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"src/app/providers.test.tsx (jsdom) and src/app/layout.test.tsx (renderToStaticMarkup with next/font/google, globals.css and ./providers mocked). Full suite 170/170, 2026-09-19."},"at":"2026-09-19T14:10:28.554Z","parents":["evt-3gjva3dbwp6s"],"hash":"ed7d00c2419cb8900552e9143fc8c68208893d36b3405896e3f798717faf1ab9"}
{"id":"evt-ryb2rh1a8xb9","type":"node.status_changed","subject":"iss-nc2tj7c","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T14:10:29.575Z","parents":["evt-99j1aj1ga9yq"],"hash":"ded9c0616ec8364ded4539303ac715fea08a291935eb0cee6466614774e7f5b7"}
{"id":"evt-8qw1mcgcjck1","type":"node.status_changed","subject":"iss-nc5w0j8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T14:10:30.712Z","parents":["evt-ryb2rh1a8xb9"],"hash":"4c1c6328d74246f4a8244414db67faac3a498f07429f24da71ea42bc913acb73"}
{"id":"evt-bmpbynnw50md","type":"node.created","subject":"mea-hbkgsng","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"measurement","title":"Line coverage of src/** (excluding page.tsx, *.d.ts, tests)","body":"`npm run coverage`, 2026-09-19, after the route, layout and providers tests (iss-nc2tj7c, iss-nc5w0j8): 361/438 lines, 170 tests. Every file is at 100% except onvif.ts (7.3%, 76 lines) and camera-registry.ts (96.8%, 1 line).","status":"recorded","owner":null,"attrs":{"value":82.42,"applies_at":"2026-09-19T14:10:31.945Z"}},"at":"2026-09-19T14:10:31.945Z","parents":["evt-8qw1mcgcjck1"],"hash":"1db162dfb571958670e0b0b30d3204b2e8a74fed3b4d82aa4af201b9a7b2ffe8"}
{"id":"evt-9cst8tvwh4rv","type":"edge.added","subject":"mea-hbkgsng","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"measures","from":"mea-hbkgsng","to":"gol-9zxah3p"},"at":"2026-09-19T14:10:31.947Z","parents":["evt-bmpbynnw50md"],"hash":"f29ad1acdae0375d7d3ebeebc8da21d5661243cbce804e722b1df3af00cfb580"}
{"id":"evt-t0mkh4jxh4xg","type":"node.created","subject":"pri-e14bahk","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"principle","title":"Network and device code uses a replaceable transport; tests never touch the real network or real cameras","body":"Code that talks to the network or to hardware (ONVIF/SOAP, WS-Discovery UDP, snapshot HTTP, interface enumeration) reaches it only through a transport tests can replace: an imported module (onvif, node:dgram, node:http, node:os) that a test swaps with vi.mock, or an explicitly injected parameter where module replacement isn't enough. Logic never hard-wires a socket in a way a test can't intercept. Tests use scripted fakes, such as a fake Cam, a FakeSocket that records sends, fixed interface lists, or local loopback HTTP servers, and never send traffic to the LAN or to real devices. Each test file that does this says so in a header comment naming this principle. Why: the app's core value is device I/O, which must be tested deterministically and quickly, and a test that probes the real LAN is flaky, slow and can disturb real cameras. Rules out: tests that need a real camera or network, and production code that opens sockets in ways a test can't replace. Accepted by the user on 2026-09-19. Current implementation: module replacement in src/lib/camera.test.ts and src/lib/onvif.test.ts, with no test-only parameters in production code.","status":"active","owner":"prn-q80g8mz","attrs":{},"weight":null},"at":"2026-09-19T14:16:30.087Z","parents":["evt-9cst8tvwh4rv"],"hash":"fc6c7888d94fa4bc9b5660111c816d89c3db1d943ccaca9631bfc76378d4c43d"}
{"id":"evt-xqqyfer28b0w","type":"edge.added","subject":"pri-e14bahk","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"guides","from":"pri-e14bahk","to":"gol-9zxah3p"},"at":"2026-09-19T14:16:30.088Z","parents":["evt-t0mkh4jxh4xg"],"hash":"e96a3df29fd539fdd802b88603edad1e50a750d0c88ad4a9f9b70d4ca304f394"}
{"id":"evt-86yc27j0mn11","type":"edge.added","subject":"pri-e14bahk","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"guides","from":"pri-e14bahk","to":"gol-6q1q5mr"},"at":"2026-09-19T14:16:30.089Z","parents":["evt-xqqyfer28b0w"],"hash":"e7b1c05610a16870dec21de87d0ba30035ea9ee824a7a41bb9e14cd0c83743ac"}
{"id":"evt-nrj7b4kpagfy","type":"node.created","subject":"ver-eyg08v6","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"onvif.ts is at 100% line, branch and function coverage, with no real network traffic. Covered: multicast probe parsing (IPv4 XAddr preference, default port, scopes as a string or text node, endpoint fallback, name/hardware/location decoding, skipping bad shapes, partial parse errors, socket errors, the single error listener); the unicast sweep (hosts excluding self, port 3702, the probe message, batches of 64 per 15 ms, the /20 limit and /31 refusal, skipping loopback/IPv6/link-local/CIDR-less interfaces, de-duplicating hosts, parsing real ProbeMatches XML, duplicate and malformed replies, a socket error closing once and stopping sends); merging multicast and unicast results; the 5 s default; discoverRequestSchema.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T14:16:54.197Z","parents":["evt-86yc27j0mn11"],"hash":"65c0ad4ba94cd7337fd0240051ca784a3cabe8bfb54eb9dc7709bcbf4620a709"}
{"id":"evt-tym6m3ee1hty","type":"edge.added","subject":"ver-eyg08v6","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-eyg08v6","to":"iss-3bg4r6e"},"at":"2026-09-19T14:16:54.200Z","parents":["evt-nrj7b4kpagfy"],"hash":"2a3d27d9faabddbd77a94882e7553b81c4a26ec0c35cb1186c6e716ab98e231d"}
{"id":"evt-17mwajn90f51","type":"verification.recorded","subject":"ver-eyg08v6","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"src/lib/onvif.test.ts, 30 tests; `npx vitest run src/lib/onvif.test.ts --coverage --coverage.include=src/lib/onvif.ts` gives 100/100/100, 2026-09-19. The network is replaced per pri-e14bahk: onvif Discovery, node:dgram (FakeSocket) and node:os networkInterfaces via vi.mock, with no production code change. Full suite 200/200, tsc and eslint clean."},"at":"2026-09-19T14:16:54.201Z","parents":["evt-tym6m3ee1hty"],"hash":"b638224eda01459955020e8d5f6f0775145529cdfc8acea9b87b3d8ce6a2b657"}
{"id":"evt-n0wsrk8gcjam","type":"node.status_changed","subject":"iss-3bg4r6e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T14:16:55.511Z","parents":["evt-17mwajn90f51"],"hash":"7c38b317239e7b5a1c78344a62770662d9712931976a0be6bc0f3209996009e4"}
{"id":"evt-xyakqc0x1w21","type":"node.created","subject":"mea-a5fjckt","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"measurement","title":"Line coverage of src/** (excluding page.tsx, *.d.ts, tests)","body":"`npm run coverage`, 2026-09-19, after the discovery tests (iss-3bg4r6e): 437/438 lines, 200 tests. The only uncovered line is in camera-registry.ts (96.8%). The 95% target is met; iss-zjpc22k (enforce the threshold) is now unblocked in practice.","status":"recorded","owner":null,"attrs":{"value":99.77,"applies_at":"2026-09-19T14:16:56.623Z"}},"at":"2026-09-19T14:16:56.623Z","parents":["evt-n0wsrk8gcjam"],"hash":"c641ecaf5a2f3de7ea54af6b7f2001c7d57e6d01ca19de6495e508ee26604d5d"}
{"id":"evt-pcaq1kmtr4bt","type":"edge.added","subject":"mea-a5fjckt","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"measures","from":"mea-a5fjckt","to":"gol-9zxah3p"},"at":"2026-09-19T14:16:56.624Z","parents":["evt-xyakqc0x1w21"],"hash":"d7d08d75d1290064414a35e60e5dde61af9c727923ceb374192d0fdf3ef05897"}
{"id":"evt-m1kfa935btnf","type":"node.created","subject":"ver-ata01qf","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"vitest.config.mts sets coverage.thresholds.lines = 95; `npm run coverage` exits 0 at the current coverage and non-zero when coverage is below the threshold.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T14:17:42.237Z","parents":["evt-pcaq1kmtr4bt"],"hash":"3e18b5e97fd7879201938766444783ae98c43739c642c48225028e35c8bc3a7a"}
{"id":"evt-2xzed3xn0deh","type":"edge.added","subject":"ver-ata01qf","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-ata01qf","to":"iss-zjpc22k"},"at":"2026-09-19T14:17:42.238Z","parents":["evt-m1kfa935btnf"],"hash":"78885766ed2ce868f7a734174ff90de45073c05061ccc499b76221e2076ef182"}
{"id":"evt-3prszpdhev6p","type":"verification.recorded","subject":"ver-ata01qf","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"2026-09-19: `npm run coverage` exit=0 at 99.77% lines. `npx vitest run --coverage --coverage.thresholds.lines=100` exit=1 with \"ERROR: Coverage for lines (99.77%) does not meet global threshold (100%)\", which shows the check fails the run."},"at":"2026-09-19T14:17:42.239Z","parents":["evt-2xzed3xn0deh"],"hash":"0a506113db391f0bc0cda852d58e807b5a4e4a5bceca7d689171c28fdf807ea3"}
{"id":"evt-3s0p3p5jac04","type":"node.status_changed","subject":"iss-zjpc22k","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T14:17:43.311Z","parents":["evt-3prszpdhev6p"],"hash":"af762768a2a8b2255f400662c7a83742c5f9e4039fdb7aa498de25fc922a0425"}
{"id":"evt-8x1b7dp5t0f0","type":"node.created","subject":"ver-bnbvcep","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"`next build` shows no tracing warnings, and the traced files for /, /api/discover and /api/cameras/[id]/{info,snapshot,credentials} include nothing from src/, public/ or .data/; the env-var path overrides still work (store tests pass).","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T14:23:23.401Z","parents":["evt-3s0p3p5jac04"],"hash":"6fdea2b6bb0cf260b3255c9183addad0ae5dfaa1cdb4bccabcbd696e09742059"}
{"id":"evt-bez2zz3sb8xn","type":"edge.added","subject":"ver-bnbvcep","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-bnbvcep","to":"iss-jvxcd1n"},"at":"2026-09-19T14:23:23.405Z","parents":["evt-8x1b7dp5t0f0"],"hash":"cd4b45af93b765ba2bbfa6a665b3b63cea2bc067db740bd6609acd4678b1ca26"}
{"id":"evt-8pf21y5tkn3y","type":"verification.recorded","subject":"ver-bnbvcep","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"2026-09-19: `npx next build` gives 0 'Dynamic filesystem access' warnings. The .next/server/app/**.nft.json for all five routes have 0 src/, 0 public/ and 0 .data/ entries. Before, the whole project was traced; with only readFile ignored, .data/cameras.json and .data/credentials.json were still traced, which would have copied the local camera registry and encrypted logins into a standalone bundle. Fix: a /* turbopackIgnore: true */ on the default path.join(process.cwd(), '.data', ...) in camera-registry.ts and credential-store.ts, which alone also clears the readFile warning. Full suite 200/200, coverage 99.77%, tsc and eslint clean."},"at":"2026-09-19T14:23:23.406Z","parents":["evt-bez2zz3sb8xn"],"hash":"c1c680a72c02c6076893f6d942a4a8a1a445fcf6d50fc2f264e16d2f2eb9625c"}
{"id":"evt-mbng7jjqd16c","type":"node.status_changed","subject":"iss-jvxcd1n","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T14:23:24.548Z","parents":["evt-8pf21y5tkn3y"],"hash":"2e53812074a3a2ebaf7c5447d79c34f9ef8eed4387177d3d0e6ca4bef9c82b5f"}
{"id":"evt-1pqxvsfa964w","type":"node.created","subject":"gol-wqf95dq","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"goal","title":"Secure the web front end with an admin login","body":"The dashboard and every /api/* route require an admin session once an admin exists. The admin's username and scrypt password hash live in a file named by ADMIN_AUTH_FILE (default ./.data/admin.json). If the file is missing, users are prompted to create an admin; creating one needs a one-time setup code printed on the server console. Users may skip, but then every page shows a severe warning banner and the prompt returns each browser session. The file can also be created entirely outside the app with a documented CLI or one-liner, so the app never has to run unsecured. Passwords are never stored or logged in recoverable form. Third-party API tokens are out of scope here (gol-sjabnh3).","status":"active","owner":null,"attrs":{},"weight":null,"target":null,"direction":"up","unit":null},"at":"2026-09-19T14:29:30.169Z","parents":["evt-mbng7jjqd16c"],"hash":"f75f435d71be601c3ed6894197a8286d6f3b5dd5f636711a5c3896dac5695b1b"}
{"id":"evt-m2r3gjx34a8v","type":"edge.added","subject":"gol-wqf95dq","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"gol-wqf95dq","to":"gol-6q1q5mr"},"at":"2026-09-19T14:29:30.171Z","parents":["evt-1pqxvsfa964w"],"hash":"6e42f4bc8e7ee8417b900d6a23ddf6ecc972a793bc9625cce47ff1b533148021"}
{"id":"evt-kfx40y2wt20z","type":"edge.added","subject":"gol-wqf95dq","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"upholds","from":"gol-wqf95dq","to":"pri-tyrxdz9"},"at":"2026-09-19T14:29:30.172Z","parents":["evt-m2r3gjx34a8v"],"hash":"dcb62609776f7e7dcdde59ba1c2d17d5e361cc1341dcca9c29b9de72d4e3c088"}
{"id":"evt-tdtqt0v5fa72","type":"edge.added","subject":"gol-wqf95dq","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"upholds","from":"gol-wqf95dq","to":"pri-m1csgrm"},"at":"2026-09-19T14:29:30.173Z","parents":["evt-kfx40y2wt20z"],"hash":"ef284fae8ab79c68978718a62f5d23601658c85887019c15c81a1c1d3cbff402"}
{"id":"evt-5gq14vadrcym","type":"edge.added","subject":"gol-wqf95dq","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"upholds","from":"gol-wqf95dq","to":"pri-mz2jxpb"},"at":"2026-09-19T14:29:30.174Z","parents":["evt-tdtqt0v5fa72"],"hash":"57d9cb2fc6fca19e5197c1967f43c6e1a515c3ab9b3374c04a83d9d8deacb878"}
{"id":"evt-0zjk90y63ad5","type":"node.created","subject":"dec-nw2hvff","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"decision","title":"Admin auth design: scrypt hashes, setup code, protect everything, banner + re-prompt","body":"User's choices (2026-09-19): (1) Hash with scrypt, Node built-in, random 16-byte salt, stored as `scrypt$N$r$p$saltB64$hashB64`. Rejected: plain salted SHA-512 (fast, brute-forceable if the file leaks) and PBKDF2-SHA512. (2) Creating the first admin in the browser requires a one-time setup code printed to the server console at startup. Rejected: anyone on the LAN, and localhost-only. (3) Once an admin exists, a session is required for all pages and all /api/*. Rejected: pages only. (4) Skipping setup shows a red banner on every page with a 'Set up admin' button, and the prompt reappears each browser session. Defaults chosen by Claude, open to change: ADMIN_AUTH_FILE defaults to ./.data/admin.json (gitignored); the file is JSON {version, username, passwordHash} written 0600; the CLI is `npm run admin:create` (scripts/, no dependencies) plus a documented node one-liner; sessions are a stateless HMAC-SHA256-signed HttpOnly SameSite=Lax cookie whose key is HKDF-derived from the stored hash, so a password change invalidates all sessions; proxy.ts does optimistic redirects, and a verifySession() in the lib layer does the authoritative check in each page and route (Next 16 authentication guide: 02-guides/authentication.md, 'Optimistic checks with Proxy' and 'Creating a Data Access Layer').","status":"recorded","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T14:29:35.107Z","parents":["evt-5gq14vadrcym"],"hash":"836970b61f269e88861971d8af54f2ce5197ad77ac13be91eeb2e3f49bb85b98"}
{"id":"evt-0taaeh9gca0r","type":"edge.added","subject":"dec-nw2hvff","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"dec-nw2hvff","to":"gol-6q1q5mr"},"at":"2026-09-19T14:29:35.109Z","parents":["evt-0zjk90y63ad5"],"hash":"64f2821c8808f6bc4f5d560ace813d400f6a6fe9cff6716a1639693204859110"}
{"id":"evt-6sgd2s2ry2h1","type":"node.created","subject":"iss-r5vrjx7","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Add admin login to the web front end","body":"Parent for gol-wqf95dq, following the design in dec-nw2hvff. Done when every child is closed. With no admin file: the banner and setup prompt are shown, and setup requires the console code. With an admin file: every page and /api/* route rejects requests without a valid session. The file can be created with the CLI. Coverage stays at or above 95%.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T14:29:39.998Z","parents":["evt-0taaeh9gca0r"],"hash":"9fc73a6d145b670814f181616f86933ddbd349b9f228e3f5677d04276851c438"}
{"id":"evt-ax0hhtgvrgjz","type":"edge.added","subject":"iss-r5vrjx7","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-r5vrjx7","to":"gol-wqf95dq"},"at":"2026-09-19T14:29:39.999Z","parents":["evt-6sgd2s2ry2h1"],"hash":"a86650a8661fbd06a14bac910d62876c93df9f79b1c83285fb683280de099968"}
{"id":"evt-f7yqykk7b5q1","type":"edge.added","subject":"iss-r5vrjx7","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-r5vrjx7","to":"area:security"},"at":"2026-09-19T14:29:40.000Z","parents":["evt-ax0hhtgvrgjz"],"hash":"356641c0b22c84f63fdae42c311f2b28ccabd887768771d5a3bf92ed0f799002"}
{"id":"evt-179d7m068jwm","type":"edge.added","subject":"iss-r5vrjx7","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-r5vrjx7","to":"area:auth"},"at":"2026-09-19T14:29:40.001Z","parents":["evt-f7yqykk7b5q1"],"hash":"5fe36d695ce2bdb0e1fbf89a1dedc4ae578e38d7e9c5db87f05c7058e255a0ed"}
{"id":"evt-j5pgdj02tv33","type":"node.created","subject":"iss-mffqscg","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Admin credential file module: scrypt hash, verify, and read/write ADMIN_AUTH_FILE","body":"src/lib/admin-auth.ts (server-only). The path is ADMIN_AUTH_FILE, defaulting to .data/admin.json with the turbopackIgnore marker as in the other stores. hashPassword uses scrypt, a random salt, and the format `scrypt$N$r$p$salt$hash`. verifyPassword recomputes with the stored parameters and compares with timingSafeEqual; it also runs a dummy hash when there's no admin, so response timing doesn't reveal whether the username exists. Reading the file validates it with zod and treats a missing file as 'no admin'; a malformed file is an error, not 'no admin', so a typo can't silently disable auth. Writes are atomic with mode 0600. Password rules: at least 12 characters, at most 256. Must be plain Node so the CLI can share the hashing code.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T14:29:46.654Z","parents":["evt-179d7m068jwm"],"hash":"fc770c0e252c6c2b3385c01a8efc99cc4d7cf67d9df6e74f557a96abe50ddd5b"}
{"id":"evt-hcxmw6es699m","type":"edge.added","subject":"iss-mffqscg","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-mffqscg","to":"gol-wqf95dq"},"at":"2026-09-19T14:29:46.656Z","parents":["evt-j5pgdj02tv33"],"hash":"2c5c7b8be61b8a97857b9a754a4347e91eaa0ea8a75406caf759df84231c9c82"}
{"id":"evt-0kjtasc8sz01","type":"edge.added","subject":"iss-mffqscg","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-mffqscg","to":"src/lib/admin-auth.ts"},"at":"2026-09-19T14:29:46.657Z","parents":["evt-hcxmw6es699m"],"hash":"59646d59aa9fb83fed6e9f0fa1978668c2e022a4dfb2f7e65c7bde2caf646046"}
{"id":"evt-rsxe1s00mem7","type":"edge.added","subject":"iss-r5vrjx7","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"parent_of","from":"iss-r5vrjx7","to":"iss-mffqscg"},"at":"2026-09-19T14:29:46.658Z","parents":["evt-0kjtasc8sz01"],"hash":"be09b6c47860856bd21c7907fc561f7c6f44105b09de25f8fe444b212337ff07"}
{"id":"evt-0s253d2ejnvb","type":"edge.added","subject":"iss-mffqscg","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-mffqscg","to":"area:auth"},"at":"2026-09-19T14:29:46.659Z","parents":["evt-rsxe1s00mem7"],"hash":"d2652f6e0abccf8f2100144c20d20d9667c579ffa3e413f91e10a184cd852de5"}
{"id":"evt-7bmke4j98p58","type":"node.created","subject":"iss-7xmka20","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"CLI and docs for creating the admin file outside the app","body":"`npm run admin:create` runs a scripts/ file with no dependencies. It prompts for a username and a hidden password with confirmation, applies the same password rules, and writes ADMIN_AUTH_FILE (or the default) with mode 0600, refusing to overwrite without --force. It uses the same hashing code as the app. Document it in the README's security section, along with a fallback node one-liner that prints a hash, the file format, how to reset a forgotten password (delete the file or re-run with --force), and a note that changing the password signs out every session.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T14:29:55.552Z","parents":["evt-0s253d2ejnvb"],"hash":"b083d59fdb1a0a5ec22f2cdff77ced083505041b297d1a689ec418b8f30270e6"}
{"id":"evt-4nc7dw3y8y0y","type":"edge.added","subject":"iss-7xmka20","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-7xmka20","to":"gol-wqf95dq"},"at":"2026-09-19T14:29:55.554Z","parents":["evt-7bmke4j98p58"],"hash":"cc62e3ac7284604c6bc726a892beafeb51bd4c9ec24459c3194ceb087c86ff33"}
{"id":"evt-wk3ettr00cah","type":"edge.added","subject":"iss-7xmka20","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-7xmka20","to":"scripts/"},"at":"2026-09-19T14:29:55.555Z","parents":["evt-4nc7dw3y8y0y"],"hash":"e37c899459f6649fef8a008f2dea5d176c06a9bddc3fa69e5c614541fad201cb"}
{"id":"evt-4sdy640mqbd5","type":"edge.added","subject":"iss-7xmka20","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-7xmka20","to":"package.json"},"at":"2026-09-19T14:29:55.556Z","parents":["evt-wk3ettr00cah"],"hash":"0d4471f316c0985c9b9329a7f06e026656bdb3dd77ed4d2fe7b9189ac6b04318"}
{"id":"evt-fe4a889tm84s","type":"edge.added","subject":"iss-7xmka20","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-7xmka20","to":"README.md"},"at":"2026-09-19T14:29:55.557Z","parents":["evt-4sdy640mqbd5"],"hash":"9c7bd39f3db11f042f6745b25c44e5e763feb503e855efb63cbd9832083623b5"}
{"id":"evt-eqh0qw4ctrww","type":"edge.added","subject":"iss-mffqscg","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"blocks","from":"iss-mffqscg","to":"iss-7xmka20"},"at":"2026-09-19T14:29:55.558Z","parents":["evt-fe4a889tm84s"],"hash":"1a450ada5c3a83b015c155f63ba0d18ca2944cb1eda62ab1a0b837c765d99577"}
{"id":"evt-jpy7rfyv8tpy","type":"edge.added","subject":"iss-r5vrjx7","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"parent_of","from":"iss-r5vrjx7","to":"iss-7xmka20"},"at":"2026-09-19T14:29:55.559Z","parents":["evt-eqh0qw4ctrww"],"hash":"5f325cc0a869f1528d4542ac1b8eed299d40b72bc5c0e3e73a5810eeb66efcc7"}
{"id":"evt-dmapwtv13yhv","type":"edge.added","subject":"iss-7xmka20","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-7xmka20","to":"area:auth"},"at":"2026-09-19T14:29:55.560Z","parents":["evt-jpy7rfyv8tpy"],"hash":"32f6741bbe17cb0ddece1a0913365fb20a257d71753669bbaddcc7a81d0d7ff1"}
{"id":"evt-q6jz9zrt5018","type":"edge.added","subject":"iss-7xmka20","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-7xmka20","to":"area:docs"},"at":"2026-09-19T14:29:55.561Z","parents":["evt-dmapwtv13yhv"],"hash":"bb1e9c8a651ac5a53dbc2cdbcc7100a00cdafe3539858dc82adf50199c7462e7"}
{"id":"evt-e4j7ft9jx4r0","type":"node.created","subject":"iss-e27nb70","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Signed session cookie: create, verify and clear an admin session","body":"src/lib/session.ts (server-only). The cookie holds {username, expiresAt}, signed with HMAC-SHA256; the key is HKDF-derived from the stored password hash. It is HttpOnly, SameSite=Lax, Path=/, Secure when the request is HTTPS, and lasts 7 days. verifySession() is the authoritative check used by pages and routes: it returns the session or null, and rejects on a bad signature, expiry, a username mismatch, or a missing admin file. authState() returns 'no-admin' | 'signed-out' | 'signed-in'. Comparisons are constant-time. Uses cookies() from next/headers, async in Next 16.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T14:29:57.645Z","parents":["evt-q6jz9zrt5018"],"hash":"9953ca57de2584589038186db241a3a1bbc091f31f3e11c3eb368ed52f3893b3"}
{"id":"evt-3gqyqs3rh8nw","type":"edge.added","subject":"iss-e27nb70","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-e27nb70","to":"gol-wqf95dq"},"at":"2026-09-19T14:29:57.647Z","parents":["evt-e4j7ft9jx4r0"],"hash":"bbf45ea175e341c98ddbfd754d7724ffcd737b3305e2e50b58ac5d596b2c8d26"}
{"id":"evt-e8sc5kb7f8c4","type":"edge.added","subject":"iss-e27nb70","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-e27nb70","to":"src/lib/session.ts"},"at":"2026-09-19T14:29:57.648Z","parents":["evt-3gqyqs3rh8nw"],"hash":"8d254260c8a56244cded78f736322a99dcecadfdfecb67954355ab18b47545d0"}
{"id":"evt-h02tdapx3sbe","type":"edge.added","subject":"iss-mffqscg","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"blocks","from":"iss-mffqscg","to":"iss-e27nb70"},"at":"2026-09-19T14:29:57.649Z","parents":["evt-e8sc5kb7f8c4"],"hash":"cd23f26b03c5d87ad3b193b6417b5aabc49e85e5be7fca111085e6f4f1c1d6af"}
{"id":"evt-kywn3ewrqgc3","type":"edge.added","subject":"iss-r5vrjx7","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"parent_of","from":"iss-r5vrjx7","to":"iss-e27nb70"},"at":"2026-09-19T14:29:57.650Z","parents":["evt-h02tdapx3sbe"],"hash":"ce7c98887055e1c6b435d58a2574f596c562f306b27fa01bead564b859558065"}
{"id":"evt-g8x1rdk8f3ax","type":"edge.added","subject":"iss-e27nb70","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-e27nb70","to":"area:auth"},"at":"2026-09-19T14:29:57.651Z","parents":["evt-kywn3ewrqgc3"],"hash":"2f95c8bccf7cab5797b3a7992678011724c10f0734710e9b17147e6eb122e3e1"}
{"id":"evt-eedhstp8sjjw","type":"node.created","subject":"iss-nj9wmwp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Login and sign-out: /login page, Server Action, and throttling of failed attempts","body":"A /login page with a form backed by a Server Action. The input is validated with zod, and the result comes back through useActionState with a generic 'Wrong username or password' error. On success it sets the session and redirects to the page the user came from; the destination is checked to be a same-origin path, so it can't be used as an open redirect. A sign-out control appears in the header. Failed attempts are throttled in memory per client IP, e.g. an increasing delay and a temporary lockout after 10 failures in 15 minutes, since this is a LAN brute-force surface.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T14:30:04.136Z","parents":["evt-g8x1rdk8f3ax"],"hash":"97d4dfd81f8faaee3a21291980904583e51567655300a8b6e357b00ebf837509"}
{"id":"evt-hzjr818geyz8","type":"edge.added","subject":"iss-nj9wmwp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-nj9wmwp","to":"gol-wqf95dq"},"at":"2026-09-19T14:30:04.139Z","parents":["evt-eedhstp8sjjw"],"hash":"7b72c7a8045c34eac51db2614e5c54382a8ecb1e08b7a5f8e278a6af0eb3b455"}
{"id":"evt-jz3da7vpwdsd","type":"edge.added","subject":"iss-nj9wmwp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-nj9wmwp","to":"src/app/login/"},"at":"2026-09-19T14:30:04.140Z","parents":["evt-hzjr818geyz8"],"hash":"3c90b3d1743a901352aea13888645399108c495d993528c6e9f2ba81aaa637ec"}
{"id":"evt-7cybsryn99pg","type":"edge.added","subject":"iss-nj9wmwp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-nj9wmwp","to":"src/app/layout.tsx"},"at":"2026-09-19T14:30:04.141Z","parents":["evt-jz3da7vpwdsd"],"hash":"a60f3c3deb8529889e93ce8769a5130233d852d974d829a8875f063cfa5eea69"}
{"id":"evt-8m0zts40x6fn","type":"edge.added","subject":"iss-e27nb70","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"blocks","from":"iss-e27nb70","to":"iss-nj9wmwp"},"at":"2026-09-19T14:30:04.142Z","parents":["evt-7cybsryn99pg"],"hash":"4730ca23f65a27d125f26c5aa0461be5bede3bae4592e082c42f02ce9873facb"}
{"id":"evt-3ygvy2f243sr","type":"edge.added","subject":"iss-r5vrjx7","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"parent_of","from":"iss-r5vrjx7","to":"iss-nj9wmwp"},"at":"2026-09-19T14:30:04.143Z","parents":["evt-8m0zts40x6fn"],"hash":"ea3dd391959465718a07c1582a72302bf0c5ed5bc05775aa57841d91ab762413"}
{"id":"evt-wf5t9tkxc6xj","type":"edge.added","subject":"iss-nj9wmwp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-nj9wmwp","to":"area:auth"},"at":"2026-09-19T14:30:04.144Z","parents":["evt-3ygvy2f243sr"],"hash":"f28abc6de41d8b13e015f20c7991d693b960466c62d5dfc46c4b5e031c4b5ffc"}
{"id":"evt-cyk05sa8df4v","type":"edge.added","subject":"iss-nj9wmwp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-nj9wmwp","to":"area:ui"},"at":"2026-09-19T14:30:04.145Z","parents":["evt-wf5t9tkxc6xj"],"hash":"894478623f4288f450b13b9efeffac0bb730f264d54c344147a61e309750f6a6"}
{"id":"evt-7jzra01bg238","type":"node.created","subject":"iss-9nxdndr","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"First-run setup: console setup code, /setup page, skip option and severe warning banner","body":"When no admin file exists: instrumentation.ts (register) generates a random one-time setup code in memory and prints it with instructions (including the CLI alternative) to the server console. A /setup page takes the username, password, confirmation and setup code; its Server Action checks the code in constant time, refuses if an admin already exists, writes the file, signs the user in, and invalidates the code. 'Skip for now' sets a browser-session cookie so the prompt doesn't reappear until the browser restarts. While there's no admin, every page shows a red banner ('Not secured: anyone on your network can view your cameras and change their logins') with a 'Set up admin' link. Check whether instrumentation register() runs under `next start` in the bundled docs.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T14:30:08.171Z","parents":["evt-cyk05sa8df4v"],"hash":"826d855a40bfc444256c44ecfc27ef9847bfdbb1e754307ff56a8ed6891bae2e"}
{"id":"evt-d272az9jgq3b","type":"edge.added","subject":"iss-9nxdndr","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-9nxdndr","to":"gol-wqf95dq"},"at":"2026-09-19T14:30:08.172Z","parents":["evt-7jzra01bg238"],"hash":"9da836d0ab606c5123bdbf2cdf79242adf58113c77e00b2651e8568ece6e572e"}
{"id":"evt-w2xe6ytb00vv","type":"edge.added","subject":"iss-9nxdndr","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-9nxdndr","to":"src/instrumentation.ts"},"at":"2026-09-19T14:30:08.173Z","parents":["evt-d272az9jgq3b"],"hash":"154f604495dbdaad2d3ed23302028ecd38085e398934240591ea18d7b0b70057"}
{"id":"evt-z38mczsczkke","type":"edge.added","subject":"iss-9nxdndr","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-9nxdndr","to":"src/app/setup/"},"at":"2026-09-19T14:30:08.174Z","parents":["evt-w2xe6ytb00vv"],"hash":"541a9e935df8254f1166455a96e3e16f0e7717a89e3b28fe1b6a00533e2dde1b"}
{"id":"evt-5y2etwe9qnfp","type":"edge.added","subject":"iss-9nxdndr","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-9nxdndr","to":"src/app/layout.tsx"},"at":"2026-09-19T14:30:08.175Z","parents":["evt-z38mczsczkke"],"hash":"a5b400831e93e48dd2600c59ebebd5a850cb08c312cac14a0807cf1f071218aa"}
{"id":"evt-0s6g16vw92t0","type":"edge.added","subject":"iss-e27nb70","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"blocks","from":"iss-e27nb70","to":"iss-9nxdndr"},"at":"2026-09-19T14:30:08.176Z","parents":["evt-5y2etwe9qnfp"],"hash":"1be8ce8bb513ff702bdd27d345a2dd53bbbe595f9d3cb7201ef29a7ebec7bea5"}
{"id":"evt-137wdzt3ytb2","type":"edge.added","subject":"iss-r5vrjx7","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"parent_of","from":"iss-r5vrjx7","to":"iss-9nxdndr"},"at":"2026-09-19T14:30:08.177Z","parents":["evt-0s6g16vw92t0"],"hash":"67eadabc015e5191e3c8f96c0f33e9bd77495b198855e1ef48a05125c2f6d8e0"}
{"id":"evt-q34ren2s73ey","type":"edge.added","subject":"iss-9nxdndr","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-9nxdndr","to":"area:auth"},"at":"2026-09-19T14:30:08.178Z","parents":["evt-137wdzt3ytb2"],"hash":"f158aafee35c1741245006c76692f3c8918710d96d14c4f09d105b74cb27e0fa"}
{"id":"evt-qhb2r7x5cgr8","type":"edge.added","subject":"iss-9nxdndr","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-9nxdndr","to":"area:ui"},"at":"2026-09-19T14:30:08.179Z","parents":["evt-q34ren2s73ey"],"hash":"8328c616919fbad349325770a3510734ccbb2e2ad107a918faef4f771f69c155"}
{"id":"evt-2zb4wga46rth","type":"node.created","subject":"iss-76d5wrb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Enforce the admin session on every page and /api/* route","body":"src/proxy.ts (Next 16's replacement for middleware, Node runtime) does optimistic checks. With an admin: no valid cookie → redirect pages to /login?next=..., and return 401 JSON for /api/*. Without an admin: pages go to /setup unless the skip cookie is set, and the API stays open, matching 'skip = unsecured'. Excluded: /login, /setup, and _next/static assets. The authoritative check is verifySession() in page.tsx and in every route handler via camera-route.ts, so skipping the proxy still can't reach data. Tests cover each route unauthenticated → 401 when an admin exists.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T14:30:11.902Z","parents":["evt-qhb2r7x5cgr8"],"hash":"cc89ad454eeebd15067b8cd7a4d1c1bbe21aa877bfda1685eb1d811101c0e8a5"}
{"id":"evt-tyfyar8vdzjr","type":"edge.added","subject":"iss-76d5wrb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-76d5wrb","to":"gol-wqf95dq"},"at":"2026-09-19T14:30:11.903Z","parents":["evt-2zb4wga46rth"],"hash":"e19b8669be2c5b5ddebf0df27d10007ac0727a1e7ffb711f33d99b32304c7a6b"}
{"id":"evt-3pggm88grjrm","type":"edge.added","subject":"iss-76d5wrb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-76d5wrb","to":"src/proxy.ts"},"at":"2026-09-19T14:30:11.904Z","parents":["evt-tyfyar8vdzjr"],"hash":"a0061824a53c0baaa6737f56e9536c4f7d9d04d794596ebc5da28afdc7bc6b78"}
{"id":"evt-6ctn1h7tcjdf","type":"edge.added","subject":"iss-76d5wrb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-76d5wrb","to":"src/lib/camera-route.ts"},"at":"2026-09-19T14:30:11.905Z","parents":["evt-3pggm88grjrm"],"hash":"8dde0239f8564efaa6dbc1131ec059304addafb37db42b4f482ab96953e7f46d"}
{"id":"evt-91zr543pgj4y","type":"edge.added","subject":"iss-76d5wrb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-76d5wrb","to":"src/app/page.tsx"},"at":"2026-09-19T14:30:11.906Z","parents":["evt-6ctn1h7tcjdf"],"hash":"729b670454ec0846357c9c1d09972cd5ad3372dee137f894ca07515471aaa6f7"}
{"id":"evt-x90vnksepxyq","type":"edge.added","subject":"iss-76d5wrb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-76d5wrb","to":"src/app/api/"},"at":"2026-09-19T14:30:11.907Z","parents":["evt-91zr543pgj4y"],"hash":"72846fc4fcb2971ea167710e3c0afb5163116a40096cc33dc7d9e06370360358"}
{"id":"evt-2m8rqzdwvk0w","type":"edge.added","subject":"iss-e27nb70","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"blocks","from":"iss-e27nb70","to":"iss-76d5wrb"},"at":"2026-09-19T14:30:11.908Z","parents":["evt-x90vnksepxyq"],"hash":"755faaa1999460db3bf20decb4c6cc3b2ec4fa4d0e0c4707cee661f9e7d66531"}
{"id":"evt-p9byhy01ym59","type":"edge.added","subject":"iss-r5vrjx7","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"parent_of","from":"iss-r5vrjx7","to":"iss-76d5wrb"},"at":"2026-09-19T14:30:11.909Z","parents":["evt-2m8rqzdwvk0w"],"hash":"5abc3ecc9b1747c1a1d8e72f1891fb62a8c7f86f56ef76859a994701068ab87a"}
{"id":"evt-bdt2wdyym6g1","type":"edge.added","subject":"iss-76d5wrb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-76d5wrb","to":"area:auth"},"at":"2026-09-19T14:30:11.910Z","parents":["evt-p9byhy01ym59"],"hash":"d374932c8fbd8b810c571965467d1cdf88b67594683da49cb68eac968a22dbaf"}
{"id":"evt-g1xr6vqnj3k6","type":"edge.added","subject":"iss-76d5wrb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-76d5wrb","to":"area:security"},"at":"2026-09-19T14:30:11.911Z","parents":["evt-bdt2wdyym6g1"],"hash":"2cc6f29d6e4954065fccc5c3df7b4dc7c4e57900f4a93517ff3fc73facec75f9"}
{"id":"evt-n6kgan9e08pn","type":"edge.added","subject":"iss-76d5wrb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-76d5wrb","to":"area:api"},"at":"2026-09-19T14:30:11.912Z","parents":["evt-g1xr6vqnj3k6"],"hash":"16c8d887f3d200c407c636efbc740954e0fa867337a864f85f5e040966d1c6c1"}
{"id":"evt-8e1730343hfr","type":"node.updated","subject":"gol-wqf95dq","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"weight":0.9}},"at":"2026-09-19T14:34:00.588Z","parents":["evt-n6kgan9e08pn"],"hash":"0482776c0995b425ad1b1fe22da7e132f0ad9ed67172dc1bf6bfd1c024f82d1e"}
{"id":"evt-1yckgsb8pvnt","type":"node.created","subject":"ver-5m61y5m","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"Admin file module: scrypt hash/verify (random salt, stored params, constant-time compare, bounded params), ADMIN_AUTH_FILE with a .data/admin.json default, zod-validated read (missing → null, malformed → AdminFileError), atomic 0600 write that can't replace an existing admin without overwrite, and checkLogin that always spends one hash.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T14:34:31.571Z","parents":["evt-8e1730343hfr"],"hash":"8e42720718ec8e89a6813d76aef3235c6aa0d491f1e80b2050866c7455fea1e7"}
{"id":"evt-5fr8erspmktk","type":"edge.added","subject":"ver-5m61y5m","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-5m61y5m","to":"iss-mffqscg"},"at":"2026-09-19T14:34:31.573Z","parents":["evt-1yckgsb8pvnt"],"hash":"2eed13ae55a7bac1b53bd14309ff1d9396dbc90df8b87b7b477b4cbe5b21dc70"}
{"id":"evt-jg6sfntc43cp","type":"verification.recorded","subject":"ver-5m61y5m","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"src/lib/admin-file.ts (plain Node, shared with the future CLI) and src/lib/admin-auth.ts (server-only app layer); tests in src/lib/admin-file.test.ts and src/lib/admin-auth.test.ts, 43 tests, 100% lines for both. Full suite 243/243, coverage 99.8%, tsc and eslint clean, 2026-09-19. Split from the issue text: the hashing and file code is in admin-file.ts rather than admin-auth.ts, so the CLI can import it without Next."},"at":"2026-09-19T14:34:31.574Z","parents":["evt-5fr8erspmktk"],"hash":"518d9c4c8813d8bccd44ab1c34a92944cef8ba087e4de8f437c94fc4edf463ac"}
{"id":"evt-et9z26yd6dat","type":"node.status_changed","subject":"iss-mffqscg","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T14:34:32.733Z","parents":["evt-jg6sfntc43cp"],"hash":"d4b9203a0d63789a16fde5731f32c7637095e62b47aefbeb2e66f48cb88a95bd"}
{"id":"evt-ktw4j5b38wm0","type":"node.created","subject":"ver-6a8cs5y","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"`npm run admin:create` creates ADMIN_AUTH_FILE (or .data/admin.json) owner-only with a scrypt hash: interactive with a hidden, confirmed password, or piped; it refuses to overwrite without --force and rejects weak or invalid input. The README documents the CLI, a no-npm Node one-liner, the file format, and password reset.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T14:37:27.499Z","parents":["evt-et9z26yd6dat"],"hash":"becc70dde87809c16f8c9970957528f27d7bccfa97d01d5a88e2f27eed31070f"}
{"id":"evt-h2dw7m3h5wah","type":"edge.added","subject":"ver-6a8cs5y","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-6a8cs5y","to":"iss-7xmka20"},"at":"2026-09-19T14:37:27.502Z","parents":["evt-ktw4j5b38wm0"],"hash":"65fe15671bf53cf588c46b11d4cfebf26f5c8e6b6ead8d51542e56960a85cb62"}
{"id":"evt-t416xnmprmnp","type":"verification.recorded","subject":"ver-6a8cs5y","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"scripts/create-admin.mts shares src/lib/admin-file.ts and runs on plain `node` (Node 24 type stripping; tsconfig allowImportingTsExtensions). scripts/create-admin.test.ts: 9 process-level tests (create, stdin username, refuse without --force, --force replace, short password, bad username, empty input, corrupt file, --help). Interactive mode checked by hand via a pty (`script`): password not echoed; mismatched confirmation refused. The README one-liner was extracted verbatim and run in bash and zsh through a pty, producing a -rw------- file that verifyPassword accepts. Full suite 252/252, tsc, eslint and next build clean, 2026-09-19."},"at":"2026-09-19T14:37:27.503Z","parents":["evt-h2dw7m3h5wah"],"hash":"6764ee63d4f00f8af0aa7a5029ada2fd4a6aef2832f2e1da07bc83134642f950"}
{"id":"evt-3jj78jc2jtra","type":"node.status_changed","subject":"iss-7xmka20","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T14:37:28.623Z","parents":["evt-t416xnmprmnp"],"hash":"48dd11b3f5b33812b650821013a633fa57fac932bf87b3b23990a6640d75016d"}
{"id":"evt-me637e20qx4a","type":"node.created","subject":"dec-f0xar8r","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"decision","title":"Admin sessions: 12-hour sliding window; locking out other sessions means changing the password","body":"User's choice (2026-09-19), replacing the 7-day default in dec-nw2hvff. A session expires 12 hours after the last activity. The signed token carries its own expiry, which is renewed to now+12h when it's older than 5 minutes, so the dashboard's 1 s polling doesn't rewrite the cookie on every request. Next 16 can't set cookies during Server Component rendering (03-api-reference/04-functions/cookies.md), so the refresh happens in proxy.ts (iss-76d5wrb), Server Actions and route handlers; verifySession() during render only reads. Sessions stay stateless: signing out clears only that browser, and the accepted way to cut off other sessions is changing the password, which rotates the HKDF-derived key. Rejected: a fixed 7-day session, and a server-side session store.","status":"recorded","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T14:42:27.204Z","parents":["evt-3jj78jc2jtra"],"hash":"172672fb31586e94a5ab483b00394d35be5b4754a3e8388b69bda463e1a60a65"}
{"id":"evt-d18rdkvv0z2f","type":"edge.added","subject":"dec-f0xar8r","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"dec-f0xar8r","to":"gol-wqf95dq"},"at":"2026-09-19T14:42:27.205Z","parents":["evt-me637e20qx4a"],"hash":"227b45629b5c4ec0837ee3317ceb5bac4b8797ddf110e2c793cb0817f7edab9e"}
{"id":"evt-k075xnkjjazc","type":"node.status_changed","subject":"iss-e27nb70","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"in_progress"},"at":"2026-09-19T14:42:28.474Z","parents":["evt-d18rdkvv0z2f"],"hash":"867a9ae8503654e3b632ff01afc669efa0afd904d2257373b96db35bdf033554"}
{"id":"evt-qzwpjb3mdebm","type":"node.updated","subject":"iss-e27nb70","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"body":"src/lib/session-token.ts (pure: issue, read and refresh tokens) and src/lib/session.ts (server-only: cookies via next/headers). The token is `<username b64url>.<expiresAt>.<HMAC-SHA256>`; the key is HKDF-SHA256 from the stored password hash, so a password change ends all sessions. The cookie `cameras_session` is HttpOnly, SameSite=Lax, Path=/, Secure behind HTTPS (x-forwarded-proto), expiring with the token. Lifetime is a 12-hour sliding window (dec-f0xar8r): re-issued at most every 5 minutes. verifySession() only reads, so it's safe during render; touchSession() slides the window in Server Actions and route handlers; proxy.ts will slide it on page loads (iss-76d5wrb). authState() returns 'no-admin' | 'signed-out' | 'signed-in'. createSession() and deleteSession() are used by login and sign-out. Signature comparison is constant-time."}},"at":"2026-09-19T14:43:44.948Z","parents":["evt-k075xnkjjazc"],"hash":"e17c5608ce4126fc5e7a4d2558ec36b0e5d0fb9ae029ca8c2bb2061dab29fa68"}
{"id":"evt-vmhgde1h8656","type":"node.created","subject":"ver-9h5hthr","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"Tokens round-trip; expire at 12 h idle; slide to now+12h when older than 5 min (an active user stays signed in past 30 h, an idle one lapses); reject a forged, tampered, other-username, or password-changed token, garbage, and a missing admin. The cookie is HttpOnly, SameSite=Lax, Path=/, Secure only behind HTTPS; verifySession, authState, createSession, touchSession and deleteSession behave as specified.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T14:43:48.423Z","parents":["evt-qzwpjb3mdebm"],"hash":"aceb8345fa1ae05fc0c8ea9137a2d641a88e6e597392d032515394e2fdb829a5"}
{"id":"evt-3zd0afwt73jz","type":"edge.added","subject":"ver-9h5hthr","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-9h5hthr","to":"iss-e27nb70"},"at":"2026-09-19T14:43:48.424Z","parents":["evt-vmhgde1h8656"],"hash":"54f3637661f1a3dbefd062c1c7a7cdae0485735d6e848e425265fed528906ee9"}
{"id":"evt-ys0ns2v7xa4g","type":"verification.recorded","subject":"ver-9h5hthr","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"src/lib/session-token.test.ts and src/lib/session.test.ts, 29 tests; 100% statements, branches, functions and lines for both modules (next/headers replaced by an in-memory cookie jar). Full suite 281/281, coverage 99.81%, tsc, eslint and next build clean, 2026-09-19. Nothing calls these yet: login, setup and enforcement (iss-nj9wmwp, iss-9nxdndr, iss-76d5wrb) wire them in."},"at":"2026-09-19T14:43:48.425Z","parents":["evt-3zd0afwt73jz"],"hash":"68a859ece84f4bd548a2e8590a4526b302e661785bd8f2428adcb9caf59cb5a0"}
{"id":"evt-gxsrqxkcrrtn","type":"node.status_changed","subject":"iss-e27nb70","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"in_progress","to":"done"},"at":"2026-09-19T14:43:49.506Z","parents":["evt-ys0ns2v7xa4g"],"hash":"2a55c37b48d5a13eb0bacb0e76956f129d58b045bfb92b06092f572b607d1c19"}