cameras/.vrek/log.ndjson
Michael Mainguy db615e9e2a Release 0.1.4: recordings play in a pop-out window
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 05:47:11 -05:00

1132 lines
595 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"}
{"id":"evt-40tzp9w71pwf","type":"node.created","subject":"ver-pttw4js","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"/login (a Server Action through useActionState) signs in with a generic error on failure, redirects only to same-site paths, and throttles failures (10 per client, 100 overall, per 15 min, keyed on x-forwarded-for, with a client-count cap); the sign-out form in the header clears the session; /login redirects to /setup with no admin and onward when already signed in.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T14:54:01.115Z","parents":["evt-gxsrqxkcrrtn"],"hash":"b58d0c7f31d5318cf00e87ed22960fbeaba9bb756e3724ee60cbcce63a1d3af9"}
{"id":"evt-pghp99arbst3","type":"edge.added","subject":"ver-pttw4js","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-pttw4js","to":"iss-nj9wmwp"},"at":"2026-09-19T14:54:01.121Z","parents":["evt-40tzp9w71pwf"],"hash":"3a3a5aa69c59bcb2ee8a2fa07b7e8e25e075f6dbd00badd169493dd7dc354fde"}
{"id":"evt-3xm5jkh26xd1","type":"verification.recorded","subject":"ver-pttw4js","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"Unit: src/app/auth-actions.test.ts (login, logout), src/lib/login-throttle.test.ts, src/lib/auth-shared.test.ts (safeNextPath: //, /\\, absolute URL, control chars), src/app/auth-pages.test.tsx, src/app/auth-forms.test.tsx, src/app/security-bar.test.tsx. End to end against `next start` with a temp ADMIN_AUTH_FILE: /login → 307 /setup with no admin; the header shows 'Signed in as admin' with a valid session. 2026-09-19. Not exercised: submitting the login form in a real browser (a Server Action round trip); tracked as pending on the parent iss-r5vrjx7."},"at":"2026-09-19T14:54:01.122Z","parents":["evt-pghp99arbst3"],"hash":"6e0fc6b4722975520531a71446ff457770e3bbd2b1a8e77f3ebd289c27f8bc4f"}
{"id":"evt-8wfcr54fxde3","type":"node.created","subject":"ver-q1ae1wj","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"With no admin: the setup code is printed once at startup (instrumentation) and /setup reuses it (globalThis); /setup requires the code (constant-time; rotated after 5 wrong), validates the username, password and confirmation before spending an attempt, creates the admin, signs in, and clears the code and skip cookie; skipping sets a browser-session cookie; a red banner shows on every page while no admin exists.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T14:54:05.126Z","parents":["evt-3xm5jkh26xd1"],"hash":"e56458fbd7adc9b1ee912a5dbde6eb274874cb9456b06cebfcdae2246abe6758"}
{"id":"evt-4z6vesr3k55h","type":"edge.added","subject":"ver-q1ae1wj","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-q1ae1wj","to":"iss-9nxdndr"},"at":"2026-09-19T14:54:05.128Z","parents":["evt-8wfcr54fxde3"],"hash":"9301574565238450cc903551cce08946cb5727a0882d6ba8aafe473ede1da38a"}
{"id":"evt-2fxckhhhs7mf","type":"verification.recorded","subject":"ver-q1ae1wj","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"Unit: src/lib/setup-code.test.ts, src/instrumentation.test.ts, src/app/auth-actions.test.ts (setupAdmin, skipSetup incl. an EEXIST race), src/app/auth-pages.test.tsx, src/app/security-bar.test.tsx. End to end with `next start`: exactly 1 'one-time code' block in the server log after start and still 1 after two GET /setup (so the code is shared between instrumentation and app bundles); GET / → 307 /setup; with the skip cookie → 200; the banner 'Not secured:' is rendered; with an admin, no code is printed and /setup → 307 /login. README documents the flow. 2026-09-19. Not exercised: submitting the setup and skip forms in a real browser; pending on iss-r5vrjx7."},"at":"2026-09-19T14:54:05.129Z","parents":["evt-4z6vesr3k55h"],"hash":"442901d20c4da3b276bab6966bb9f5d60ea14004e1fadb0766a7082dcb7a348a"}
{"id":"evt-arehf43fcbj4","type":"node.created","subject":"ver-bj79asq","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"src/proxy.ts: with an admin, pages without a valid session → /login?next=… (query kept), /api/* → 401 JSON, and a valid session passes and slides when older than 5 min; with no admin, the API is open and pages go to /setup unless skipped. Authoritative checks: requirePageAccess() in page.tsx and apiAccessDenied() first in all 6 route handlers.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T14:54:09.877Z","parents":["evt-2fxckhhhs7mf"],"hash":"e04de035e56bb4780c54910eb8c21dabec05bbfb74162e8eab97174dea90e3ee"}
{"id":"evt-c5zqnqn59ad1","type":"edge.added","subject":"ver-bj79asq","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-bj79asq","to":"iss-76d5wrb"},"at":"2026-09-19T14:54:09.879Z","parents":["evt-arehf43fcbj4"],"hash":"ff647282511532f16b85469596b5268d176e75b87be5d77a61e2b6b8b09bb28e"}
{"id":"evt-dyb79cnf4jpa","type":"verification.recorded","subject":"ver-bj79asq","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"Unit: src/proxy.test.ts (14, real admin file, NextRequest; caught and fixed a bug where /?refresh=500 lost its query in ?next=), src/lib/access.test.ts, src/app/api/access.test.ts (each of the 6 handlers returns 401 and touches no camera or store code when signed out). End to end with `next start` + admin: GET /?refresh=500 → 307 /login?next=%2F%3Frefresh%3D500; the skip cookie doesn't bypass; API without a session → 401 {\"error\":\"Sign in required\"}; forged cookie → 401; valid session → 200 (API) and 200 (page); a 10-minute-old session gets Set-Cookie with a new token, Expires +12h, HttpOnly, SameSite=lax. Build lists 'ƒ Proxy (Middleware)'. Full suite 382/382, coverage 99.86%, tsc, eslint and build clean, 2026-09-19."},"at":"2026-09-19T14:54:09.880Z","parents":["evt-c5zqnqn59ad1"],"hash":"3a47fb0141472a72df76102f88cc73069f1d1806d5361ec681bbea98db503460"}
{"id":"evt-1frkwn4trvyt","type":"node.created","subject":"ver-qz5sev9","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"In a real browser: complete /setup with the console code, sign out, sign in (including a wrong password and a redirect back to ?next=), and skip setup to see the banner.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T14:54:13.002Z","parents":["evt-dyb79cnf4jpa"],"hash":"4b5037365460d9462c678d505c391ee270def03792dc041796d88dae113b6e6c"}
{"id":"evt-4pqk1wyfhf0s","type":"edge.added","subject":"ver-qz5sev9","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-qz5sev9","to":"iss-r5vrjx7"},"at":"2026-09-19T14:54:13.003Z","parents":["evt-1frkwn4trvyt"],"hash":"b387b8062288b31bc36fda4e095e8881dc90744f4ba5e5dc2e7d52263e955fa4"}
{"id":"evt-qa5z1v7chc02","type":"verification.recorded","subject":"ver-qz5sev9","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pending","evidence":"Server Action round trips from real browser forms haven't been run: Claude doesn't start dev servers here, and curl can't easily drive Server Actions. Everything else is verified by unit tests and `next start` + curl (see iss-nj9wmwp, iss-9nxdndr, iss-76d5wrb). Needs a manual check by the user."},"at":"2026-09-19T14:54:13.004Z","parents":["evt-4pqk1wyfhf0s"],"hash":"f31c050167824784bbd13d5b0a35e015afa8194011d1693aafb9da418f30bf9a"}
{"id":"evt-c410xxjgthrh","type":"node.status_changed","subject":"iss-nj9wmwp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T14:54:14.116Z","parents":["evt-qa5z1v7chc02"],"hash":"370db1ceeadad76ad355d77dd2a0168ce3170a91f9dae928b19c2e2e7127a947"}
{"id":"evt-0zfw9s1039e8","type":"node.status_changed","subject":"iss-9nxdndr","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T14:54:15.515Z","parents":["evt-c410xxjgthrh"],"hash":"a26951573416be01590b0486119cde90a7c99b615690167c4f41a71e03f927f3"}
{"id":"evt-7jmwek1hzwy0","type":"node.status_changed","subject":"iss-76d5wrb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T14:54:17.226Z","parents":["evt-0zfw9s1039e8"],"hash":"782451c37447ce0594da68efb324db8cf3925a1fbd9793fb8b366fe9bbfb0edd"}
{"id":"evt-5hdbhp5bn7t0","type":"node.created","subject":"ver-xz17rpj","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"When the app session ends, dashboard API calls (401 without a camera error code) show 'Your session has ended. Sign in again', not the camera-login form, and aren't retried.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T14:54:54.309Z","parents":["evt-7jmwek1hzwy0"],"hash":"7054ae0335f04c4300edcc3a059818725902e8fd93267f4bb7cc527e4ecc0aed"}
{"id":"evt-xqg7nwtyhdf3","type":"edge.added","subject":"ver-xz17rpj","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-xz17rpj","to":"iss-76d5wrb"},"at":"2026-09-19T14:54:54.311Z","parents":["evt-5hdbhp5bn7t0"],"hash":"2a1cec7150beff04c58cca6c76dd65701d390092e00f303e64f44a2e5a16d24b"}
{"id":"evt-d7j19r27at24","type":"verification.recorded","subject":"ver-xz17rpj","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"Found after enforcement: camera-queries.ts mapped every 401 to the camera 'auth' problem, so an expired session would have shown the camera-login form. Fixed: code 'auth' means a camera login problem, and any other 401 means the new 'signed-out' kind, shown on camera-card.tsx with a /login link. Test: camera-card.test.tsx 'tells the user to sign in again…' (1 info request, no camera-login form). Suite 383/383, coverage 99.86%, tsc and eslint clean, 2026-09-19."},"at":"2026-09-19T14:54:54.312Z","parents":["evt-xqg7nwtyhdf3"],"hash":"cd46e3dfd1afd7710b28def1c9fa26177050936bad5e9e99956719907245c4fe"}
{"id":"evt-z4k3aqs5whz1","type":"node.created","subject":"ver-86zabcp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"In a real browser: complete /setup with the console code, sign out, sign in (including a wrong password and a redirect back to ?next=), and skip setup to see the banner.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T16:56:11.239Z","parents":["evt-d7j19r27at24"],"hash":"c2271075a643fce52efa076efef36cdf58601239e3b54c1f6ec588139c582927"}
{"id":"evt-3j6gy3qctp2f","type":"edge.added","subject":"ver-86zabcp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-86zabcp","to":"iss-r5vrjx7"},"at":"2026-09-19T16:56:11.242Z","parents":["evt-z4k3aqs5whz1"],"hash":"c292ee79097215281246b9eb8f6e111ffdfadb0ed2e30764244f9ba1da799923"}
{"id":"evt-qv7r0x32bhrq","type":"verification.recorded","subject":"ver-86zabcp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"Manual test by the user (Michael Mainguy) on 2026-09-19, who went through the full flow and reported that everything works correctly. This resolves the earlier pending check ver-qz5sev9."},"at":"2026-09-19T16:56:11.243Z","parents":["evt-3j6gy3qctp2f"],"hash":"bcceb6a19d3adc70e59f72bb44ca0ee76d8f0d79f197018ce0f53ed841c5a122"}
{"id":"evt-h5c9jc9hak9q","type":"node.status_changed","subject":"iss-r5vrjx7","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T16:56:12.460Z","parents":["evt-qv7r0x32bhrq"],"hash":"8c7c6c6e0a55282116ad4421d2f7491da248227185abea89a815e4c147cbfea9"}
{"id":"evt-cbmv07cqwtwp","type":"node.created","subject":"pri-be2smzk","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"principle","title":"Keep automated line coverage at or above 95%, and ship every change with its tests","body":"`npm run coverage` must pass: vitest.config.mts sets coverage.thresholds.lines = 95 over src/**. Excluded: tests, *.d.ts, and the async Server Component src/app/page.tsx, which unit runners can't render (Next docs, 02-guides/testing/vitest.md). New or changed code comes with tests in the same change, so coverage never dips and gets repaid later. Security properties are pinned by tests, not by manual checks: no secrets or upstream error text in responses, input validation, private-IP-only camera targets, and auth on every route. Tests never touch the real network or cameras (pri-e14bahk). Where something genuinely can't be unit-tested, such as a real browser Server Action round trip, record a pending verification in vrek and get it checked by hand rather than skipping it. Why: the app holds admin logins for every camera on the network and is headed toward a third-party API (gol-sjabnh3), so regressions must be caught automatically. Origin: formerly goal gol-9zxah3p, met on 2026-09-19 at 99.8% (retired); the user asked to keep it as a standing rule. Rules out: merging untested code, lowering the threshold to get a change through, and tests that pass only because of what's on the developer's machine.","status":"active","owner":"prn-q80g8mz","attrs":{},"weight":null},"at":"2026-09-19T17:12:32.802Z","parents":["evt-h5c9jc9hak9q"],"hash":"94146a47643f63ba092c212e5500915638169745f126befbab4944c1413ca8df"}
{"id":"evt-5r63syak8qcb","type":"edge.added","subject":"pri-be2smzk","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"guides","from":"pri-be2smzk","to":"gol-6q1q5mr"},"at":"2026-09-19T17:12:32.806Z","parents":["evt-cbmv07cqwtwp"],"hash":"991197bd763ab9f0698bac15915ed1667d3319836529e96e275afedb8ef6167a"}
{"id":"evt-qbrfjfjn7zj3","type":"edge.added","subject":"pri-be2smzk","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"guides","from":"pri-be2smzk","to":"gol-146a3cb"},"at":"2026-09-19T17:12:32.807Z","parents":["evt-5r63syak8qcb"],"hash":"c05d7ae3fd03f67bc7ebc1f79678212a5b368663ced53ffaf819e08be306bd70"}
{"id":"evt-h30p9pk188sa","type":"edge.added","subject":"pri-be2smzk","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"guides","from":"pri-be2smzk","to":"gol-sjabnh3"},"at":"2026-09-19T17:12:32.808Z","parents":["evt-qbrfjfjn7zj3"],"hash":"10653662eef7815940253e652978d7730cd07085a2aa22c926ad2db354fbc1df"}
{"id":"evt-kcr6skmhvt7z","type":"edge.added","subject":"pri-be2smzk","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"guides","from":"pri-be2smzk","to":"gol-wqf95dq"},"at":"2026-09-19T17:12:32.809Z","parents":["evt-h30p9pk188sa"],"hash":"78723618b64324e5669712afe7684d8bdcaff4b34ababa5793154085e0e42082"}
{"id":"evt-wjm17pkv4g76","type":"node.status_changed","subject":"gol-9zxah3p","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"active","to":"retired"},"at":"2026-09-19T17:12:33.983Z","parents":["evt-kcr6skmhvt7z"],"hash":"679a458798391a3d160472493901cb255c922bc46ce9fdc7b64936dd899357bf"}
{"id":"evt-g90ppyyh82e9","type":"node.created","subject":"gol-m8je1wc","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"goal","title":"Informative icons throughout the dashboard","body":"Use icons to make state and actions recognizable at a glance: camera online/offline/needs-login/not-activated status, actions (scan, refresh, change login, sign out, retry), and warnings (unsecured banner, errors). Icons supplement text labels rather than replace them, stay accessible (decorative icons aria-hidden; icon-only buttons get an aria-label), and match Tailwind sizing and colors in light and dark mode. Use an established icon library rather than hand-drawn SVGs (user preference, 2026-09-19); which library is decided through a dependency question per pri-mz2jxpb.","status":"active","owner":null,"attrs":{},"weight":null,"target":null,"direction":"up","unit":null},"at":"2026-09-19T17:20:55.493Z","parents":["evt-wjm17pkv4g76"],"hash":"578f03fecd8025f62b1e62d619d777dc79166ebe2994a3dc1845435bda2487a9"}
{"id":"evt-yv7jfyphnqjg","type":"edge.added","subject":"gol-m8je1wc","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"gol-m8je1wc","to":"gol-6q1q5mr"},"at":"2026-09-19T17:20:55.495Z","parents":["evt-g90ppyyh82e9"],"hash":"e09e14f4e7cbe51e9edf4043cf731b7712316a9fb0fd398f75511154a166e55c"}
{"id":"evt-pzjjv5eczc9q","type":"edge.added","subject":"gol-m8je1wc","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"gol-m8je1wc","to":"gol-146a3cb"},"at":"2026-09-19T17:20:55.496Z","parents":["evt-yv7jfyphnqjg"],"hash":"6d4d4c4df9385316ccba3ba62ea7c01f7d9a25670cd76ed45ff34762dbb4230c"}
{"id":"evt-a6c5zqdn04kk","type":"edge.added","subject":"gol-m8je1wc","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"upholds","from":"gol-m8je1wc","to":"pri-mz2jxpb"},"at":"2026-09-19T17:20:55.497Z","parents":["evt-pzjjv5eczc9q"],"hash":"cd38ae5da658b943a330a45101c5b2779974d35f395ef11bd0cfadcdb737f810"}
{"id":"evt-vtstx24gawxm","type":"node.created","subject":"que-nm0xbg4","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"question","title":"Which icon library should the dashboard use: lucide-react, @heroicons/react, or react-icons?","body":"Dependency justification per pri-mz2jxpb, for gol-m8je1wc. The user prefers a library over hand-drawn SVGs. Facts checked on 2026-09-19 against the npm registry and package contents: all three have zero runtime dependencies, sideEffects:false, React 19 peer support and permissive licenses, and all are in Next 16's default optimizePackageImports list (03-api-reference/05-config/01-next-config-js/optimizePackageImports.md), so only the icons used ship to the browser, the same code Next's own import optimization produces. (1) lucide-react 1.47.0, ISC: about 1,500 icons in one consistent 24px outline style with adjustable stroke width; 85 releases in the last 12 months, the latest on 2026-09-17; 35 MB unpacked. It has camera-domain icons (Cctv, Camera/CameraOff, Video/VideoOff, Wifi/WifiOff, ShieldAlert, KeyRound). (2) @heroicons/react 2.2.0, MIT, by the Tailwind team: 324 icons in 24 outline, 24 solid, 20 solid and 16 solid; 1 release in 12 months, the latest on 2024-11-18, so it's in maintenance mode; 3.7 MB. It covers the basics (VideoCamera/VideoCameraSlash, Wifi, ShieldExclamation, ArrowPath, Key) but has no CCTV icon. (3) react-icons 5.7.0, MIT: about 51,000 icons from 31 sets, including copies of Lucide (1,541) and Heroicons, plus Font Awesome, Material and Tabler; 3 releases in 12 months, the latest on 2026-06-30; 88 MB unpacked. Its bundled copies lag upstream, and mixing sets undermines visual consistency. Removal: icons are leaf imports in components, so swapping libraries later is a find-and-replace.","status":"open","owner":null,"attrs":{"options":[{"option":"lucide-react","consequence":"One consistent set of about 1,500 icons, including camera-specific ones; frequent releases mean occasional renames to handle."},{"option":"@heroicons/react","consequence":"Smallest install and a Tailwind-native look, but only 324 icons and near-dormant maintenance; some camera concepts would need workarounds."},{"option":"react-icons","consequence":"Access to about 51,000 icons from many sets; the largest install, copies of upstream sets that lag behind, and a style drift risk unless the project sticks to one set."}],"revisit":null}},"at":"2026-09-19T17:21:58.438Z","parents":["evt-a6c5zqdn04kk"],"hash":"2699fabf05b65194c6b8293ee07f9f608786adfac81c6ed037a573239b178228"}
{"id":"evt-6ae5qyh48d59","type":"edge.added","subject":"que-nm0xbg4","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"que-nm0xbg4","to":"gol-m8je1wc"},"at":"2026-09-19T17:21:58.440Z","parents":["evt-vtstx24gawxm"],"hash":"eae8ecb88e0679d4d7d6049ddd51533082251f5efa81956d998c2cc7fed6c4fe"}
{"id":"evt-xcsfxy4913d3","type":"edge.added","subject":"pri-mz2jxpb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"guides","from":"pri-mz2jxpb","to":"que-nm0xbg4"},"at":"2026-09-19T17:21:58.441Z","parents":["evt-6ae5qyh48d59"],"hash":"6da97fed371a923ad20b0bcea75c47d73dc93cf12f93e924a83ccc25fec51f21"}
{"id":"evt-xzzpcyr12c4n","type":"node.created","subject":"dec-1vnz5fw","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"decision","title":"Use lucide-react for dashboard icons","body":"Chosen by the user on 2026-09-19. Why: about 1,500 icons in one consistent outline style, including camera-domain icons (Cctv, CameraOff, VideoOff, WifiOff, ShieldAlert, KeyRound); actively maintained; ISC; no dependencies; optimized by Next 16 by default. Rejected: @heroicons/react (324 icons, no release since 2024-11) and react-icons (88 MB, lagging copies, style-mixing risk). Conventions: import named icons directly from 'lucide-react'; size with Tailwind (className=\"size-4\"); decorative icons get aria-hidden; icon-only buttons get an aria-label; icons accompany text, never replace it for state.","status":"recorded","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T17:24:55.568Z","parents":["evt-xcsfxy4913d3"],"hash":"777497d2f7871963f174326847256cf0664a6f69ba8e72a8ff09996bde9de3f2"}
{"id":"evt-gr4kzj719v0z","type":"edge.added","subject":"dec-1vnz5fw","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"answers","from":"dec-1vnz5fw","to":"que-nm0xbg4"},"at":"2026-09-19T17:24:55.570Z","parents":["evt-xzzpcyr12c4n"],"hash":"ab2049fcb5ddf40a335875c92b0d7171f11f8324cd127fabada337e3bfc8a305"}
{"id":"evt-7gxfmdx2mxdz","type":"question.answered","subject":"que-nm0xbg4","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"answer":"dec-1vnz5fw"},"at":"2026-09-19T17:24:55.571Z","parents":["evt-gr4kzj719v0z"],"hash":"d86136a10f3124a03be2ea54367dcdd7bfacfab1bd9bb7752aa84c58701038db"}
{"id":"evt-5ks734vysg8z","type":"edge.added","subject":"dec-1vnz5fw","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"dec-1vnz5fw","to":"gol-m8je1wc"},"at":"2026-09-19T17:24:55.572Z","parents":["evt-7gxfmdx2mxdz"],"hash":"87d1b4bb262a92c01e090cadc458c53f716183de381dd9a6e48db9ce5636a6bf"}
{"id":"evt-6zvrt762e8fa","type":"node.created","subject":"iss-scj42jq","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Camera status badge with icons on each dashboard card","body":"Each camera card gets a status badge, an icon plus a text label, derived from the existing query state with no new device calls: Live (Cctv, green) when the snapshot is polling; Connecting (LoaderCircle, spinning) while info loads; Needs login (KeyRound, amber) for the 'auth' problem; Not activated (ShieldAlert, amber) for 'inactive'; Offline or error (WifiOff, red) for 'error'; Session ended (LogIn) for 'signed-out'. Colors work in light and dark mode, and the label is always visible text, never color or icon alone. This also partly fills the 'live status' gap noted on gol-146a3cb. Tests: each state shows the right label and icon.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T17:25:14.428Z","parents":["evt-5ks734vysg8z"],"hash":"46507c2c5cc26321e5d73d36f40049d2f8532346dea16f3c71a2268a414a7315"}
{"id":"evt-7yvhajjxnk65","type":"edge.added","subject":"iss-scj42jq","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-scj42jq","to":"gol-m8je1wc"},"at":"2026-09-19T17:25:14.429Z","parents":["evt-6zvrt762e8fa"],"hash":"431fd3e35f5f5c79005633edd272ef044e94ae1a1fd3578ee1e23f0ed467133a"}
{"id":"evt-cemey6n87hbb","type":"edge.added","subject":"iss-scj42jq","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-scj42jq","to":"gol-146a3cb"},"at":"2026-09-19T17:25:14.430Z","parents":["evt-7yvhajjxnk65"],"hash":"96759ba2f174eb984bf1de213c402d542ac6e179c1a35e9b8011f65c68ca5cc6"}
{"id":"evt-1h5qv8r2k2g8","type":"edge.added","subject":"iss-scj42jq","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-scj42jq","to":"src/app/camera-card.tsx"},"at":"2026-09-19T17:25:14.431Z","parents":["evt-cemey6n87hbb"],"hash":"3e8431ac5007e9f0974f09b4d11df7cc07c62f45484809c0ac5c77487fb5a89b"}
{"id":"evt-h06gyawk0r36","type":"edge.added","subject":"iss-scj42jq","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-scj42jq","to":"area:ui"},"at":"2026-09-19T17:25:14.432Z","parents":["evt-1h5qv8r2k2g8"],"hash":"28d65c77e75835350fdc2b8c7429db2255f1609f359fb1d094afe6727832728b"}
{"id":"evt-zbc6epbtz66f","type":"edge.added","subject":"iss-scj42jq","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-scj42jq","to":"area:icons"},"at":"2026-09-19T17:25:14.433Z","parents":["evt-h06gyawk0r36"],"hash":"79d4d06eb0641509ebfd3228661415421ac612c094b0269280f7694b5ebb0a74"}
{"id":"evt-xjpveszhvt1k","type":"node.created","subject":"iss-chrdd0v","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Icons on dashboard actions and controls","body":"Add leading icons to actions, keeping their text labels: Scan network (Radar; LoaderCircle spinning while scanning); Snapshot refresh selector (Timer); Change login (KeyRound); Forget saved login (Trash2); Retry and Check again (RefreshCw); camera Web page link (ExternalLink, which also signals a new tab); Sign out (LogOut); Save (Lock). Icons are decorative (aria-hidden), since each control already has a text label, and are sized with Tailwind (size-4 in buttons, size-3.5 in small text). Tests check the icons don't change the accessible names existing tests rely on.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T17:25:17.898Z","parents":["evt-zbc6epbtz66f"],"hash":"6f35203c9d59e5a76c7afa07c5b6ef371c2d14bc1e02c93a152dae7d33075410"}
{"id":"evt-mkmvsqezshj4","type":"edge.added","subject":"iss-chrdd0v","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-chrdd0v","to":"gol-m8je1wc"},"at":"2026-09-19T17:25:17.899Z","parents":["evt-xjpveszhvt1k"],"hash":"4b5c61fdeb3b22fc7905a8ae2f317b8f73a4ea818a042ae1def09452c4a1074b"}
{"id":"evt-hzk5w34zbmjf","type":"edge.added","subject":"iss-chrdd0v","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-chrdd0v","to":"src/app/camera-card.tsx"},"at":"2026-09-19T17:25:17.900Z","parents":["evt-mkmvsqezshj4"],"hash":"136c3a8f219d78465b161fce1da0865905f9876c298bed382c31abb1362cd19e"}
{"id":"evt-w90nhvbcvtka","type":"edge.added","subject":"iss-chrdd0v","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-chrdd0v","to":"src/app/camera-scanner.tsx"},"at":"2026-09-19T17:25:17.901Z","parents":["evt-hzk5w34zbmjf"],"hash":"565a6aca5b4a7853da5e012fba9c127e3bb8b3d175d999cda5332239c687ff05"}
{"id":"evt-y4fe0srp615m","type":"edge.added","subject":"iss-chrdd0v","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-chrdd0v","to":"src/app/refresh-rate-select.tsx"},"at":"2026-09-19T17:25:17.902Z","parents":["evt-w90nhvbcvtka"],"hash":"18de00519ef81831ba82ba79020e90eb09fd316bbbcf96c03cba1b0d64880834"}
{"id":"evt-c34h3hbdqyzr","type":"edge.added","subject":"iss-chrdd0v","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-chrdd0v","to":"src/app/security-bar.tsx"},"at":"2026-09-19T17:25:17.903Z","parents":["evt-y4fe0srp615m"],"hash":"2e78c43b8c1f37fa772d48ccf92b3b765ea351b87b986cb0fb513c21e27fc6eb"}
{"id":"evt-tp7htnkxh47f","type":"edge.added","subject":"iss-chrdd0v","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-chrdd0v","to":"area:ui"},"at":"2026-09-19T17:25:17.904Z","parents":["evt-c34h3hbdqyzr"],"hash":"f6a807ec9f5d7644ed64e4bd7705d9c56c108f9825518347eadf45efc5acf5e5"}
{"id":"evt-vh7ppk61cbqh","type":"edge.added","subject":"iss-chrdd0v","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-chrdd0v","to":"area:icons"},"at":"2026-09-19T17:25:17.905Z","parents":["evt-tp7htnkxh47f"],"hash":"89462173bfd30f97081825fda19a2b60dc8bd24e839e60f156f2222a0be233e7"}
{"id":"evt-ze812dw52pr4","type":"node.created","subject":"iss-egy082x","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Icons on warnings, errors and the login and setup screens","body":"Unsecured banner and the setup page's skip box: ShieldAlert. Camera setup panel ('not activated'): TriangleAlert. Inline errors (form errors, 'Stopped: …', scan errors): CircleAlert. Session-ended notice: LogIn. The signed-in bar: ShieldCheck next to 'Signed in as'. /login heading: Lock. /setup heading: ShieldCheck. Warnings keep role=\"alert\" and their text; icons are aria-hidden.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T17:25:20.490Z","parents":["evt-vh7ppk61cbqh"],"hash":"aa0aca07465145074b15207ecc9993590bafd0571ac0dc2f48e9b79ef0a08ed6"}
{"id":"evt-d9kz9rhbbqw9","type":"edge.added","subject":"iss-egy082x","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-egy082x","to":"gol-m8je1wc"},"at":"2026-09-19T17:25:20.491Z","parents":["evt-ze812dw52pr4"],"hash":"0c18c688c7a1bf1532c0fd0bd9592fcd7adb5fcf17c0c5e130ef762f34621da3"}
{"id":"evt-85mdv4ynn7th","type":"edge.added","subject":"iss-egy082x","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-egy082x","to":"gol-wqf95dq"},"at":"2026-09-19T17:25:20.492Z","parents":["evt-d9kz9rhbbqw9"],"hash":"7404dcdcd35bc2091543e6b248c8f0461ef7559b9db25af80ba41a67da89aee4"}
{"id":"evt-whmkdf58tawj","type":"edge.added","subject":"iss-egy082x","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-egy082x","to":"src/app/security-bar.tsx"},"at":"2026-09-19T17:25:20.493Z","parents":["evt-85mdv4ynn7th"],"hash":"70b84ce6063b4829ce29cdb1a6a53cc1de32adb2cb045045cad65a34e74c9a29"}
{"id":"evt-rwx2q6mvd5qh","type":"edge.added","subject":"iss-egy082x","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-egy082x","to":"src/app/setup/"},"at":"2026-09-19T17:25:20.494Z","parents":["evt-whmkdf58tawj"],"hash":"6ca157f483df0146e21075232066d93b797270c6888132b4eb46f1225891d40b"}
{"id":"evt-rym686zqdrr4","type":"edge.added","subject":"iss-egy082x","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-egy082x","to":"src/app/login/"},"at":"2026-09-19T17:25:20.495Z","parents":["evt-rwx2q6mvd5qh"],"hash":"2b1a71bd44cb7b12cf83e606a39c37d7b952b2032f969b7a9f8e8538286e19fb"}
{"id":"evt-690ksyrzr514","type":"edge.added","subject":"iss-egy082x","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-egy082x","to":"src/app/camera-card.tsx"},"at":"2026-09-19T17:25:20.496Z","parents":["evt-rym686zqdrr4"],"hash":"52b3d1c3818c8b2457aa2601e94a9c2667fe32ad2bef5b681fec4e5c22571be5"}
{"id":"evt-vt5aasrmse8a","type":"edge.added","subject":"iss-egy082x","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-egy082x","to":"src/app/camera-scanner.tsx"},"at":"2026-09-19T17:25:20.497Z","parents":["evt-690ksyrzr514"],"hash":"b63cc4033152e29a99633dd1745c90588b7cfefc8ceeb3d684535a15e39ff43f"}
{"id":"evt-dcf01c8jjq40","type":"edge.added","subject":"iss-egy082x","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-egy082x","to":"area:ui"},"at":"2026-09-19T17:25:20.498Z","parents":["evt-vt5aasrmse8a"],"hash":"c8ee2b55a1e527b3f54740326f1258cb71b00856a99eb417c3a74a7e95bf8340"}
{"id":"evt-1f1j4caqqfqg","type":"edge.added","subject":"iss-egy082x","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-egy082x","to":"area:icons"},"at":"2026-09-19T17:25:20.499Z","parents":["evt-dcf01c8jjq40"],"hash":"b4fe0ed0451b794a630e68d534e0ea84036eb803721d9bf14bf8c49e8b78c35a"}
{"id":"evt-e463qfq0k6n1","type":"node.updated","subject":"gol-m8je1wc","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"weight":1}},"at":"2026-09-19T17:25:48.717Z","parents":["evt-1f1j4caqqfqg"],"hash":"fa13ede027745fbd1ab0c75a78f7c58731e8c47a15588d0d6ae21abccac67c81"}
{"id":"evt-at6ht9pamwwf","type":"node.created","subject":"ver-aqgngd8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"Each camera card shows a status badge (icon plus visible text label) derived from existing query state: Live (Cctv), Connecting (spinning LoaderCircle, respecting reduced motion), Needs login (KeyRound), Not activated (ShieldAlert), Offline (WifiOff), Session ended (LogIn); icons are aria-hidden; light and dark tones; problems take precedence over a stale frame.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T17:27:30.170Z","parents":["evt-e463qfq0k6n1"],"hash":"20d8509e94afbe24dae9604c1cecb46eaf762a333a8981642e51bfe42eee0f53"}
{"id":"evt-p2bahjnbk0yz","type":"edge.added","subject":"ver-aqgngd8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-aqgngd8","to":"iss-scj42jq"},"at":"2026-09-19T17:27:30.171Z","parents":["evt-at6ht9pamwwf"],"hash":"6ec36bc805ecaaf5248e12faa594ab3d2adf151ba714b71fbdd721c309cc1e5a"}
{"id":"evt-f5628h5wpygb","type":"verification.recorded","subject":"ver-aqgngd8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"src/app/camera-status.tsx (pure cameraStatus() plus the StatusBadge component), wired into the camera-card.tsx header. Tests: src/app/camera-status.test.tsx (each state's label, lucide-<icon> class and aria-hidden; spin only while connecting, with motion-reduce; dark tones) and the camera-card.test.tsx 'status badge' block (Connecting→Live on the first frame; auth, inactive, error and signed-out from real 401/409/502 responses; Live→Offline when polling fails). Full suite 403/403, coverage 99.86%, tsc, eslint and next build clean, 2026-09-19. Not checked: how it looks in a real browser."},"at":"2026-09-19T17:27:30.172Z","parents":["evt-p2bahjnbk0yz"],"hash":"fbb1d2d79453a16bc11df354f57165f1f0d862bf38d0b9b30c3e8cadc2a07717"}
{"id":"evt-ffbztk22ttbh","type":"node.status_changed","subject":"iss-scj42jq","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T17:27:31.300Z","parents":["evt-f5628h5wpygb"],"hash":"3c08b99276d86bae8115e97ebd63de011db3a730e4863b1c38f5bc22a152a69d"}
{"id":"evt-j4xqd53rstf9","type":"node.created","subject":"ver-k6w31xe","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"The status badge looks right in a real browser: placement, colors and states.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T17:29:37.139Z","parents":["evt-ffbztk22ttbh"],"hash":"3524ef9c0470967b15d8f6728108e646d471d495aca4da87173d2c2db77f40ec"}
{"id":"evt-6w7f3v6b7nwe","type":"edge.added","subject":"ver-k6w31xe","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-k6w31xe","to":"iss-scj42jq"},"at":"2026-09-19T17:29:37.140Z","parents":["evt-j4xqd53rstf9"],"hash":"892875d77ff0f97113eee3bc978059c38b0643f1a2e49748a74b70705ff3fd85"}
{"id":"evt-9qdgf9nzdtqw","type":"verification.recorded","subject":"ver-k6w31xe","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"Manual check by the user (Michael Mainguy) on 2026-09-19: \"Looks good\"."},"at":"2026-09-19T17:29:37.141Z","parents":["evt-6w7f3v6b7nwe"],"hash":"02f1539222835a74e530e16271f1af785037430392d024675d8ba928f1332780"}
{"id":"evt-sj24gaby174p","type":"node.created","subject":"ver-qg053tc","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"Actions carry the agreed Lucide icons while keeping their text: Scan network (Radar, spinning LoaderCircle while scanning), refresh selector (Timer), Change login (KeyRound), Forget saved login (Trash2), Retry and Check again (RefreshCw), Web page link (ExternalLink, replacing the ↗ glyph), Sign out (LogOut), camera-login Save (Lock, spinner while checking), login submit (LogIn), setup submit (ShieldCheck); icons are aria-hidden and accessible names are unchanged.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T17:32:33.331Z","parents":["evt-9qdgf9nzdtqw"],"hash":"8a12849b543c9a888201cccd42ed1b77adbe04284f990394d8cfe828c1d92778"}
{"id":"evt-c3qc77nankwd","type":"edge.added","subject":"ver-qg053tc","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-qg053tc","to":"iss-chrdd0v"},"at":"2026-09-19T17:32:33.332Z","parents":["evt-sj24gaby174p"],"hash":"37f36e688bd004b1ce8239c789e1f1a0a048f9d21547e428489e57b1e2e21bdc"}
{"id":"evt-adact6kfkb6g","type":"verification.recorded","subject":"ver-qg053tc","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"src/app/icons.test.tsx (13 tests: each control's icon, looked up by its unchanged accessible name, plus every svg.lucide rendered is aria-hidden); spinners carry motion-reduce:animate-none. All 403 pre-existing tests pass unchanged, so names and text are intact. Full suite 416/416, coverage 99.86%, tsc, eslint and next build clean, 2026-09-19. Not yet checked in a real browser."},"at":"2026-09-19T17:32:33.333Z","parents":["evt-c3qc77nankwd"],"hash":"90f0932a8bb6d61ac078f2e0dfa63165d3df4f5a19fe7a26dddf52c3cf57143f"}
{"id":"evt-x3d2tg336gr8","type":"node.created","subject":"ver-7ctbkz0","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"Warnings and errors carry icons while keeping role=\"alert\" and their text: unsecured banner and setup skip warning (ShieldAlert), camera setup panel (TriangleAlert), inline errors in the camera card, scanner and login/setup forms (CircleAlert), session-ended notice (CircleAlert) with a Sign in again link (LogIn), signed-in bar (ShieldCheck), /login heading (Lock), /setup heading (ShieldCheck).","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T17:32:35.909Z","parents":["evt-adact6kfkb6g"],"hash":"0006e18df20cff77506e325da0dee2b307e2ccc3bc90c1c14df9f2ee545568d5"}
{"id":"evt-6qf34pxqayr8","type":"edge.added","subject":"ver-7ctbkz0","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-7ctbkz0","to":"iss-egy082x"},"at":"2026-09-19T17:32:35.910Z","parents":["evt-x3d2tg336gr8"],"hash":"a3d24d83a2b5f75a4af1edc393bfd85c22db9837328ca6ca8d0776c7d042cfd3"}
{"id":"evt-2m9f36a8ak2r","type":"verification.recorded","subject":"ver-7ctbkz0","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"src/app/icons.test.tsx covers each placement (server components rendered via renderToStaticMarkup, client ones in jsdom); existing security-bar, auth-pages and auth-forms tests still pass unchanged. Full suite 416/416, 2026-09-19. Not yet checked in a real browser."},"at":"2026-09-19T17:32:35.911Z","parents":["evt-6qf34pxqayr8"],"hash":"839cd33945cb92668b475bce329d7688ed9b5aed8c31a72725606ee84643a366"}
{"id":"evt-pj9asf1mqfa2","type":"node.status_changed","subject":"iss-chrdd0v","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T17:32:37.022Z","parents":["evt-2m9f36a8ak2r"],"hash":"4cedeee71ddeff681f58d31259537489eee4a9ca314c172b61a2915365e040e3"}
{"id":"evt-k12ns9yp4phn","type":"node.status_changed","subject":"iss-egy082x","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T17:32:38.169Z","parents":["evt-pj9asf1mqfa2"],"hash":"1a722c19e825f011d1514d9e760af22eb456a129ea8799e1565badfb51d1e88f"}
{"id":"evt-30k66rv28vma","type":"node.status_changed","subject":"gol-m8je1wc","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"active","to":"retired"},"at":"2026-09-19T17:34:19.099Z","parents":["evt-k12ns9yp4phn"],"hash":"0e48e6b9a2c915c1d6105233ca3f22d5212c005c941515955f138687500b7c0e"}
{"id":"evt-7avw131w2f8w","type":"node.created","subject":"pri-8ev5m89","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"principle","title":"Icons come from lucide-react, always accompany text, and are accessible","body":"Standing rule derived from dec-1vnz5fw (Lucide chosen 2026-09-19) and applied in iss-scj42jq, iss-chrdd0v and iss-egy082x.\n\n1. Source: icons come from lucide-react, imported by name (`import { KeyRound } from \"lucide-react\"`); Next 16 optimizes those imports by default. No other icon library, icon font or hand-drawn SVG icon without a new dependency question (pri-mz2jxpb). Check that a name exists in the installed version before using it.\n2. Meaning: an icon never carries meaning alone. Status, warnings and actions always have visible text (e.g. the status badge's 'Needs login'), and state is never conveyed by color or icon alone.\n3. Accessibility: icons next to text are decorative and get `aria-hidden`. A control with only an icon must have an `aria-label` that says what it does. Warnings keep role=\"alert\" and their text. Adding an icon must not change a control's accessible name.\n4. Sizing and layout: size with Tailwind classes, not the `size` prop: `size-3.5` in small text and badges, `size-4` in buttons and inline messages, `size-5` in banners, `size-6` in page headings. Lay out with `inline-flex items-center gap-1…2`, and use `shrink-0` beside wrapping text.\n5. Motion and color: spinners use LoaderCircle with `animate-spin motion-reduce:animate-none`. Colored icons and badges have `dark:` variants.\n6. Consistency: one concept, one icon across the app. Current mapping: Cctv = live camera; LoaderCircle = in progress; KeyRound = login; ShieldAlert = unsecured or not activated; ShieldCheck = secured or signed in; WifiOff = offline; CircleAlert = error message; TriangleAlert = setup needed; RefreshCw = retry; Radar = scan; Timer = refresh rate; Trash2 = forget; Lock = save/sign-in heading; LogIn/LogOut = session; ExternalLink = opens another site in a new tab. Reuse these; if a new concept needs an icon, add it here.\n7. Tests: assert icons by their `lucide-<name>` class with toContain, since Lucide also adds alias classes (Trash2 → 'trash' and 'trash-2'), and assert every rendered `svg.lucide` is aria-hidden (see src/app/icons.test.tsx).\n\nWhy: icons should make the dashboard faster to scan without making it less accessible or less consistent. Rules out: icon-only status, icons that change accessible names, ad-hoc SVGs, and mixing icon sets.","status":"active","owner":"prn-q80g8mz","attrs":{},"weight":null},"at":"2026-09-19T17:35:04.935Z","parents":["evt-30k66rv28vma"],"hash":"0b9358f4789dd8dbb79f43cc0e9be9b072aa32331d876bb8e1250014cb3850d0"}
{"id":"evt-20am9fjb935n","type":"edge.added","subject":"pri-8ev5m89","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"guides","from":"pri-8ev5m89","to":"gol-6q1q5mr"},"at":"2026-09-19T17:35:04.937Z","parents":["evt-7avw131w2f8w"],"hash":"d28ca0d0de6cf2b2edeb395f8fb1cba0a447192ace809c31b68f68994e37155b"}
{"id":"evt-0aznmpcdcg5p","type":"edge.added","subject":"pri-8ev5m89","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"guides","from":"pri-8ev5m89","to":"gol-146a3cb"},"at":"2026-09-19T17:35:04.938Z","parents":["evt-20am9fjb935n"],"hash":"3a3810eedfea771a59f6c16952efbf7486403f311c578f2b0aa722195231fa54"}
{"id":"evt-gatzwhb87efd","type":"edge.added","subject":"pri-8ev5m89","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"guides","from":"pri-8ev5m89","to":"gol-wqf95dq"},"at":"2026-09-19T17:35:04.939Z","parents":["evt-0aznmpcdcg5p"],"hash":"a9535734194fc658c2fb9c0094e8b8bcf6be3e3f1f0015f5c3ab985ccba7765c"}
{"id":"evt-n0bphgm2fydj","type":"edge.added","subject":"pri-8ev5m89","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"discovered_from","from":"pri-8ev5m89","to":"dec-1vnz5fw"},"at":"2026-09-19T17:35:09.439Z","parents":["evt-gatzwhb87efd"],"hash":"4594421840fb035833e09ea618381aea810035288b5410e209bd321fae616bc4"}
{"id":"evt-60ngdx4pts6d","type":"node.created","subject":"gol-2nxgqjn","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"goal","title":"Read-only camera settings and stream URLs on the dashboard","body":"Each camera card shows its key settings and how to reach its video, without changing anything on the camera. Settings: per stream (main/sub) codec, resolution, frame rate and bitrate; image basics such as day/night mode; clock and time sync (NTP) state; model, firmware and serial. Stream URLs: the RTSP address per stream, ready to paste into VLC or an NVR, with credentials never embedded in the URL or sent to the browser. Read over ONVIF where it's enough and over Hikvision ISAPI (shared by Annke) where it isn't, behind a vendor module so the UI never branches on brand (pri-p9h51hx). Read-only by design: nothing here writes to a camera. Picks up the 'key settings' part of the retired gol-146a3cb; changing settings is gol for camera configuration.","status":"active","owner":null,"attrs":{},"weight":null,"target":null,"direction":"up","unit":null},"at":"2026-09-19T17:37:49.859Z","parents":["evt-n0bphgm2fydj"],"hash":"a817a6b82651a1af75d87e7e9dd17eefeb1631733966399fb21d2346febe2d49"}
{"id":"evt-a7bezmmb9qc0","type":"edge.added","subject":"gol-2nxgqjn","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"gol-2nxgqjn","to":"gol-6q1q5mr"},"at":"2026-09-19T17:37:49.860Z","parents":["evt-60ngdx4pts6d"],"hash":"c3ebe8aaca2e78a038d59deae99912a7031833d0de0739575aab071c3cc5210a"}
{"id":"evt-erpc1hn1d6ay","type":"edge.added","subject":"gol-2nxgqjn","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"upholds","from":"gol-2nxgqjn","to":"pri-01np850"},"at":"2026-09-19T17:37:49.861Z","parents":["evt-a7bezmmb9qc0"],"hash":"44719ea94206797eeff9a188ba733c6bc1118c638a621c640a7fd13d2d87a141"}
{"id":"evt-9bh86smy574k","type":"edge.added","subject":"gol-2nxgqjn","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"upholds","from":"gol-2nxgqjn","to":"pri-p9h51hx"},"at":"2026-09-19T17:37:49.862Z","parents":["evt-erpc1hn1d6ay"],"hash":"fb0c753fc9654a394ef7dac64ff439d80c1ddfc0586eea7612dda1e264e6c641"}
{"id":"evt-1rzt5s87wpb9","type":"edge.added","subject":"gol-2nxgqjn","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"upholds","from":"gol-2nxgqjn","to":"pri-tyrxdz9"},"at":"2026-09-19T17:37:49.863Z","parents":["evt-9bh86smy574k"],"hash":"fa13d2d915a77581eede322859e714109c34a7fbdef1671a353883ead693be94"}
{"id":"evt-y34bdvfbdb8k","type":"edge.added","subject":"gol-2nxgqjn","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"upholds","from":"gol-2nxgqjn","to":"pri-e14bahk"},"at":"2026-09-19T17:37:49.864Z","parents":["evt-1rzt5s87wpb9"],"hash":"c5a51b9fae66d91a3809921b705ce30fbbef9ecc5b8821e4abfda9f2bd862b59"}
{"id":"evt-eg049qzbbwgb","type":"edge.added","subject":"gol-2nxgqjn","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"upholds","from":"gol-2nxgqjn","to":"pri-be2smzk"},"at":"2026-09-19T17:37:49.865Z","parents":["evt-y34bdvfbdb8k"],"hash":"05ec19d38ecef1fc9484de0d0a7eacdc4c3387e87b282de01a0028419347e9d1"}
{"id":"evt-9by655qt0xf7","type":"node.created","subject":"gol-8v1wqvp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"goal","title":"Configure cameras from the dashboard","body":"Change camera settings from the same screen instead of each camera's own web page: e.g. stream encoding (resolution, frame rate, bitrate), image and day/night settings, clock and NTP, and actions such as reboot; pan/tilt/zoom where supported. Writes go through Server Actions or validated route handlers, over ONVIF or Hikvision ISAPI (Annke shares it) behind the vendor module. Every change is validated with zod, confirmed for disruptive actions (reboot, anything that drops the stream), re-read from the camera afterwards to show what actually applied, and reported as a clear success or error. Requires an admin session (gol-wqf95dq). Picks up the 'changes made from the same screen' part of the retired gol-146a3cb, and builds on the read-only settings goal.","status":"active","owner":null,"attrs":{},"weight":null,"target":null,"direction":"up","unit":null},"at":"2026-09-19T17:37:54.299Z","parents":["evt-eg049qzbbwgb"],"hash":"5381314045a8c4f5449d14c15de474bac263102458791da199daa16bf9d2b8e2"}
{"id":"evt-hgjd5n8q793c","type":"edge.added","subject":"gol-8v1wqvp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"gol-8v1wqvp","to":"gol-6q1q5mr"},"at":"2026-09-19T17:37:54.300Z","parents":["evt-9by655qt0xf7"],"hash":"a19dea7cc9e625db9195110b3cb1dfeb903b31f50aa79ab6be1d5d73876dc338"}
{"id":"evt-af4q8j0hgsye","type":"edge.added","subject":"gol-8v1wqvp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"upholds","from":"gol-8v1wqvp","to":"pri-01np850"},"at":"2026-09-19T17:37:54.301Z","parents":["evt-hgjd5n8q793c"],"hash":"76c5c362ea623fb8465308f30c0803afcc01be04b3529f7b720ced31e3f3ed3a"}
{"id":"evt-vrkawxerr38x","type":"edge.added","subject":"gol-8v1wqvp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"upholds","from":"gol-8v1wqvp","to":"pri-p9h51hx"},"at":"2026-09-19T17:37:54.302Z","parents":["evt-af4q8j0hgsye"],"hash":"4d7f8beaba527b3f6a68afe4564cd49cc53debe1302169f2bd14ee6b734a45a2"}
{"id":"evt-6j5vdg23ttb6","type":"edge.added","subject":"gol-8v1wqvp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"upholds","from":"gol-8v1wqvp","to":"pri-tyrxdz9"},"at":"2026-09-19T17:37:54.303Z","parents":["evt-vrkawxerr38x"],"hash":"7b739238e317346073baa36d3e28ca80b46c09e8bd7bee54ec300fef8af26537"}
{"id":"evt-5vm4bzt7kyew","type":"edge.added","subject":"gol-8v1wqvp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"upholds","from":"gol-8v1wqvp","to":"pri-m1csgrm"},"at":"2026-09-19T17:37:54.304Z","parents":["evt-6j5vdg23ttb6"],"hash":"96ec81d9b47ce74a43efbaa432de702b3d11d4191aa9d022cb335c872d88a1a7"}
{"id":"evt-b6n8hpkdqjsy","type":"edge.added","subject":"gol-8v1wqvp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"upholds","from":"gol-8v1wqvp","to":"pri-qqrp49f"},"at":"2026-09-19T17:37:54.305Z","parents":["evt-5vm4bzt7kyew"],"hash":"0c66482887917c79db820bfbb904004831e192d22e6f78feddf71a29866f7554"}
{"id":"evt-qd98rwj32rde","type":"edge.added","subject":"gol-8v1wqvp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"upholds","from":"gol-8v1wqvp","to":"pri-e14bahk"},"at":"2026-09-19T17:37:54.306Z","parents":["evt-b6n8hpkdqjsy"],"hash":"c748376638a82e71e4f7ed8bfe6700f55a9cd4a497fde714555ceea89d722eb6"}
{"id":"evt-gjs051d8727f","type":"edge.added","subject":"gol-8v1wqvp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"upholds","from":"gol-8v1wqvp","to":"pri-be2smzk"},"at":"2026-09-19T17:37:54.307Z","parents":["evt-qd98rwj32rde"],"hash":"738203f17d37395ced7d66cfa843fa68117aa9fce1e389d5ec6b7e0a1b1a0734"}
{"id":"evt-nykjsnmwvxzj","type":"node.updated","subject":"gol-2nxgqjn","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"body":"Each camera card shows its key settings and how to reach its video, without changing anything on the camera. Settings: per stream (main/sub) codec, resolution, frame rate and bitrate; image basics such as day/night mode; clock and time sync (NTP) state; model, firmware and serial. Stream URLs: the RTSP address per stream, ready to paste into VLC or an NVR, with credentials never embedded in the URL or sent to the browser. Read over ONVIF where it's enough and over Hikvision ISAPI (shared by Annke) where it isn't, behind a vendor module so the UI never branches on brand (pri-p9h51hx). Read-only by design: nothing here writes to a camera. Picks up the 'key settings' part of the retired gol-146a3cb; changing settings belongs to gol-8v1wqvp."}},"at":"2026-09-19T17:38:01.500Z","parents":["evt-gjs051d8727f"],"hash":"c7163c8951f71cd4873ca19cf0461d4713f4c62c23885d7827525284b8dc0af6"}
{"id":"evt-c7h5r80v9ceq","type":"edge.added","subject":"gol-8v1wqvp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"depends_on","from":"gol-8v1wqvp","to":"gol-2nxgqjn"},"at":"2026-09-19T17:38:02.692Z","parents":["evt-nykjsnmwvxzj"],"hash":"bb333c60431c0d49158a6e565ae959c617244aff8a13c4c9cf910a1ed981ae8c"}
{"id":"evt-ss7yazceygd9","type":"node.status_changed","subject":"gol-146a3cb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"active","to":"retired"},"at":"2026-09-19T17:38:04.010Z","parents":["evt-c7h5r80v9ceq"],"hash":"2e31be3b571523013547401bed8137756fe71f0293a40de3406a8cacc3ace43e"}
{"id":"evt-3ezkdkzyhrk2","type":"node.updated","subject":"gol-2nxgqjn","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"weight":1}},"at":"2026-09-19T17:38:38.157Z","parents":["evt-ss7yazceygd9"],"hash":"6865d5928a06a2c9577ba47023f0bb7531f48fae0ff85ec380eadd408a92f2d6"}
{"id":"evt-cqg4b4159vxk","type":"node.updated","subject":"gol-8v1wqvp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"weight":0.9}},"at":"2026-09-19T17:38:39.226Z","parents":["evt-3ezkdkzyhrk2"],"hash":"a5554cd400a167ac0ae20a142663a1c50b3216cde1b2c936f2852c0a31a78456"}
{"id":"evt-ejj3adqg8gn1","type":"node.created","subject":"dec-s205dvs","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"decision","title":"Read-only settings: ONVIF first (ISAPI later), credential-free stream URLs, per-camera page","body":"User's choices, 2026-09-19, for gol-2nxgqjn. (1) Source: ONVIF only for now, using the stored Operator ONVIF login. The camera-reading code goes behind a vendor module so Hikvision ISAPI can be added later as its own goal, once we know what's missing. Rejected for now: ONVIF + ISAPI, which likely needs a second, device-level login stored per camera. (2) Stream URLs are shown without credentials (e.g. rtsp://host:554/Streaming/Channels/101), with a copy button and a note on which camera account to enter in VLC or an NVR; passwords never reach the browser (pri-tyrxdz9). Rejected: a USERNAME:PASSWORD placeholder URL. (3) Layout: a per-camera page /cameras/[id] holds all settings, and the dashboard stays an overview; this page is also the natural home for configuration (gol-8v1wqvp). Rejected: an expandable details panel on the card, and showing everything on the card. (4) The user permitted a one-off read-only probe of the real cameras (ONVIF reads, RTSP DESCRIBE, ISAPI GET; nothing written) to see what they actually report. Automated tests still use fakes (pri-e14bahk).","status":"recorded","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T17:41:35.925Z","parents":["evt-cqg4b4159vxk"],"hash":"b769a2494005167f9bf489455c4c1d84b03a33b78f79e8c3e00ad7b4b79f42b0"}
{"id":"evt-br7qz2w9t36s","type":"edge.added","subject":"dec-s205dvs","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"dec-s205dvs","to":"gol-2nxgqjn"},"at":"2026-09-19T17:41:35.926Z","parents":["evt-ejj3adqg8gn1"],"hash":"d01a6d86536dc481dffdd4ac9cee5646916fef4c646c42f8c57d4668a13ccbc2"}
{"id":"evt-k85dbe7d3ftj","type":"node.created","subject":"fnd-j1s725e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"finding","title":"Both cameras report full per-stream encoding settings and RTSP URIs over ONVIF with the stored Operator login","body":"Two I91ET cameras, firmware V5.8.10, at 192.168.17.129 and 192.168.31.2. Profile_1 'mainStream': H.264 Main, 4096×1860, 20 fps limit, variable bitrate (ConstantBitRate=false), bitrateLimit 10240 kbps, GOP 50, quality 3. Profile_2 'subStream': H.264 Main, 1200×536, 20 fps, VBR 4096 kbps, GOP 50, quality 3. GetStreamUri (RTSP) returns rtsp://<host>/Streaming/Channels/101 (main) and /102 (sub), with ?transportmode=unicast&profile=Profile_N; no credentials are embedded and the port is the default 554. The app already receives bitrate, GOP and quality in the profile data but doesn't show them.","status":"current","owner":"prn-q80g8mz","attrs":{"sources":[{"node":"dec-s205dvs","note":"one-off read-only probe run by Claude on 2026-09-19 with the user's permission; ONVIF GetProfiles/GetStreamUri"}],"as_of":"2026-09-19"}},"at":"2026-09-19T17:42:40.302Z","parents":["evt-br7qz2w9t36s"],"hash":"db4d5a9b9545811b76df90a5c169128dcdd806e3bbcf0777e8d0979025ae4589"}
{"id":"evt-mr233cpb084k","type":"edge.added","subject":"fnd-j1s725e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-j1s725e","to":"gol-2nxgqjn"},"at":"2026-09-19T17:42:40.303Z","parents":["evt-k85dbe7d3ftj"],"hash":"226479c8604b5b4265e93e8ef862167a82876a3e6c7b04f6ad1e2bf58da19d70"}
{"id":"evt-ccxmzx4prcmt","type":"edge.added","subject":"fnd-j1s725e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-j1s725e","to":"gol-8v1wqvp"},"at":"2026-09-19T17:42:40.304Z","parents":["evt-mr233cpb084k"],"hash":"9594c0b1e1d8d431e3c6b1059e5d87310854c6db3910fdd6fc5ebb8fe97daf6c"}
{"id":"evt-9wfmkcc39hqm","type":"node.created","subject":"fnd-g4z7c6d","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"finding","title":"ONVIF imaging settings are readable: day/night (IR-cut), exposure, white balance, WDR, BLC, picture levels, noise reduction","body":"GetImagingSettings(VideoSource_1) on both cameras: irCutFilter AUTO (day/night switching automatic); brightness, contrast, colorSaturation and sharpness all 50; exposure AUTO (10–33333 µs, gain 0–100); whiteBalance AUTO; wideDynamicRange OFF; backlightCompensation OFF; noiseReduction level 0.5 (nested under extension.extension.extension). So the goal's 'day/night mode' and image basics need no ISAPI.","status":"current","owner":"prn-q80g8mz","attrs":{"sources":[{"node":"dec-s205dvs","note":"read-only probe 2026-09-19; ONVIF GetImagingSettings"}],"as_of":"2026-09-19"}},"at":"2026-09-19T17:42:43.116Z","parents":["evt-ccxmzx4prcmt"],"hash":"bb5f691a87f061edd875104735aa44f4b44add6d7173519d8fabe17c8e99e24f"}
{"id":"evt-9yvajdtwc985","type":"edge.added","subject":"fnd-g4z7c6d","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-g4z7c6d","to":"gol-2nxgqjn"},"at":"2026-09-19T17:42:43.117Z","parents":["evt-9wfmkcc39hqm"],"hash":"fce71707585af783791666255828b115a2446973dd15addb64e7e66d0272a409"}
{"id":"evt-vbfksk3rgsev","type":"edge.added","subject":"fnd-g4z7c6d","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-g4z7c6d","to":"gol-8v1wqvp"},"at":"2026-09-19T17:42:43.118Z","parents":["evt-9yvajdtwc985"],"hash":"7e398c7e19cf86ddf4aff7f18d47b694a266ddd46b7d654325134e886ad51801"}
{"id":"evt-yfwzyk1kn6cf","type":"node.created","subject":"fnd-b73cav4","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"finding","title":"Clock, NTP and network settings are readable over ONVIF","body":"Clock drift versus the server is within 1 s on both. NTP is manual, via DNS name time.windows.com (not from DHCP). Network: eth0, IPv4 by DHCP with a /20 prefix (192.168.16.0/20, gateway 192.168.16.1), 100 Mb full duplex, MACs d0:3b:f4:04:af:2e and d0:3b:f4:04:af:54. IPv6 is enabled with a link-local and a global SLAAC address (2001:470:…). The hostname reports 'localhost', which isn't useful as a display name.","status":"current","owner":"prn-q80g8mz","attrs":{"sources":[{"node":"dec-s205dvs","note":"read-only probe 2026-09-19; ONVIF GetSystemDateAndTime, GetNTP, GetNetworkInterfaces, GetNetworkDefaultGateway, GetHostname"}],"as_of":"2026-09-19"}},"at":"2026-09-19T17:42:46.597Z","parents":["evt-vbfksk3rgsev"],"hash":"01ba415c8e3edeb4c8e5c1e285597fa58a2525dc357164aae1521042035edfb5"}
{"id":"evt-xvfjkb6gfz5f","type":"edge.added","subject":"fnd-b73cav4","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-b73cav4","to":"gol-2nxgqjn"},"at":"2026-09-19T17:42:46.599Z","parents":["evt-yfwzyk1kn6cf"],"hash":"d6d7c590946c8cfb4ead73c403b8ea71d19ed57a85d32388e2d49547889d204b"}
{"id":"evt-esfr9ypd97ks","type":"node.created","subject":"fnd-nj8n90g","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"finding","title":"The RTSP stream accepts the stored ONVIF user on both cameras","body":"An RTSP DESCRIBE on Channels/101 with Digest auth as the stored ONVIF Operator user 'camera' returned 'RTSP/1.0 200 OK' on both cameras. So the credential-free stream URLs can tell users to sign in with that same ONVIF account in VLC or an NVR; no device (web) account is needed for streaming.","status":"current","owner":"prn-q80g8mz","attrs":{"sources":[{"node":"dec-s205dvs","note":"read-only probe 2026-09-19; RTSP DESCRIBE only, no PLAY"}],"as_of":"2026-09-19"}},"at":"2026-09-19T17:42:48.834Z","parents":["evt-xvfjkb6gfz5f"],"hash":"0d9bf4669e8f7879d9e6f8cfe6daa29d42970fe4db9f1a8d3e6e60fa341d9d05"}
{"id":"evt-6h02q06frnwr","type":"edge.added","subject":"fnd-nj8n90g","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-nj8n90g","to":"gol-2nxgqjn"},"at":"2026-09-19T17:42:48.835Z","parents":["evt-esfr9ypd97ks"],"hash":"3be08e13456aee6a89b6141871b05c8982ab97af38ef28d228384f20f529a781"}
{"id":"evt-a0ntbmwd27pv","type":"node.created","subject":"fnd-hcnbvht","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"finding","title":"ISAPI with the ONVIF user is inconsistent: 401 on one camera, 200 on the other","body":"GET /ISAPI/System/deviceInfo with Digest auth as 'camera': 192.168.17.129 → HTTP 401; 192.168.31.2 → HTTP 200 (model I91ET). Likely the second camera also has a device (web) account named 'camera' with the same password, or its ONVIF and device users are configured differently. Implication for the future ISAPI goal: ISAPI access can't be assumed from the ONVIF login; it needs per-camera verification or its own credentials.","status":"current","owner":"prn-q80g8mz","attrs":{"sources":[{"node":"dec-s205dvs","note":"read-only probe 2026-09-19; ISAPI GET only"}],"as_of":"2026-09-19"}},"at":"2026-09-19T17:42:51.646Z","parents":["evt-6h02q06frnwr"],"hash":"65dd78de0461f84ac721fd77b40800b21cd2c00cbca3a442e62a9c16a44e2faa"}
{"id":"evt-x059s99sa5x3","type":"edge.added","subject":"fnd-hcnbvht","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-hcnbvht","to":"gol-2nxgqjn"},"at":"2026-09-19T17:42:51.647Z","parents":["evt-a0ntbmwd27pv"],"hash":"bc0dadfdc5a916b998b74643392ab0e2d94619da5fb6e18cf93833153a644bd2"}
{"id":"evt-b6j6tnjvkcpj","type":"edge.added","subject":"fnd-hcnbvht","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-hcnbvht","to":"gol-8v1wqvp"},"at":"2026-09-19T17:42:51.648Z","parents":["evt-x059s99sa5x3"],"hash":"00d7d68951b7026d979cef0b170d2fd07fa4a12d19cde6b654924e28db8348b5"}
{"id":"evt-xby2yehfxjfv","type":"node.created","subject":"fnd-msm9prc","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"finding","title":"The cameras report their manufacturer as 'ONVIF', not a brand","body":"GetDeviceInformation returns manufacturer 'ONVIF', model 'I91ET', firmware 'V5.8.10', serials I91ET20251117AAWRGK8004273 and …8004311, hardwareId 88. The card currently shows 'ONVIF I91ET · fw V5.8.10'; 'ONVIF' as a manufacturer is noise to users. Brand detection (Hikvision vs Annke) can't rely on this field.","status":"current","owner":"prn-q80g8mz","attrs":{"sources":[{"node":"dec-s205dvs","note":"read-only probe 2026-09-19; ONVIF GetDeviceInformation"}],"as_of":"2026-09-19"}},"at":"2026-09-19T17:42:53.435Z","parents":["evt-b6j6tnjvkcpj"],"hash":"287226826bdedb2338f1adcba16d0f35b594520b9a98a3b7017a9cd138fccce0"}
{"id":"evt-tfxg7dxw16f6","type":"edge.added","subject":"fnd-msm9prc","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-msm9prc","to":"gol-2nxgqjn"},"at":"2026-09-19T17:42:53.437Z","parents":["evt-xby2yehfxjfv"],"hash":"a3a1a47b8e2c4314fc168f5e71526ad13f1705394afd200e618c01d8beb6d035"}
{"id":"evt-234mkjnf06xr","type":"node.created","subject":"dec-gtw493s","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"decision","title":"Camera page: streams+URLs, image, time/NTP, network; read on open with Refresh; name links; hide 'ONVIF' brand","body":"User's choices, 2026-09-19, for gol-2nxgqjn (following the probe findings fnd-j1s725e, fnd-g4z7c6d, fnd-b73cav4, fnd-nj8n90g, fnd-hcnbvht, fnd-msm9prc). (1) /cameras/[id] shows four sections: Streams + URLs (per profile: codec and profile, resolution, fps, bitrate with VBR/CBR, GOP, quality; RTSP URL with a copy button and a note to use the camera's ONVIF login, which RTSP accepts); Image (IR-cut day/night, exposure, white balance, WDR, BLC, picture levels, noise reduction); Time & NTP (camera clock, drift from the server with a warning above 30 s, NTP server and source); Network (IPv4 address/prefix, DHCP or static, gateway, MAC, link speed/duplex, IPv6). (2) Freshness: read once when the page opens, plus a Refresh button; no polling. Rejected: polling every 30 s. (3) Navigation: the camera name on each dashboard card links to its page, and the card gains a one-line summary (e.g. 'Main 4096×1860 · H.264 · 20 fps'). Rejected: a Details button only, or a fully clickable card. (4) A manufacturer of 'ONVIF' (or empty) counts as unknown, so the card shows 'I91ET · fw V5.8.10'; real brand names still show. Rejected: showing it as reported, or a per-camera brand field.","status":"recorded","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T17:44:33.625Z","parents":["evt-tfxg7dxw16f6"],"hash":"f53838266524af249d7562b7f14b969b9d51ff1525cc1811af24953d1bf59b5c"}
{"id":"evt-3hqybbe8wprr","type":"edge.added","subject":"dec-gtw493s","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"dec-gtw493s","to":"gol-2nxgqjn"},"at":"2026-09-19T17:44:33.626Z","parents":["evt-234mkjnf06xr"],"hash":"ca6c5aaeec39f901974615a60903fae3f2486d71055be3c318c3505017ecf44d"}
{"id":"evt-875z735p1rhh","type":"node.created","subject":"iss-cngxt98","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Per-camera settings page /cameras/[id] (read-only)","body":"Parent for gol-2nxgqjn, following dec-s205dvs and the camera-page decision. Done when every child is closed and the user has checked the page against both real cameras. Coverage stays at or above 95% (pri-be2smzk); tests use fakes (pri-e14bahk); icons follow pri-8ev5m89.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T17:44:35.653Z","parents":["evt-3hqybbe8wprr"],"hash":"f9374d2ba8e93f556efe5581d0e0b727ab92b0864caf4720baf071ae5aa6e8da"}
{"id":"evt-w0c4q5njj8xn","type":"edge.added","subject":"iss-cngxt98","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-cngxt98","to":"gol-2nxgqjn"},"at":"2026-09-19T17:44:35.654Z","parents":["evt-875z735p1rhh"],"hash":"bff45c0b24fe36ffc7aa46d94abfa5c6a2e7fcc75fe3e827a021c010b6132b6b"}
{"id":"evt-zyb1vp2fxmng","type":"edge.added","subject":"iss-cngxt98","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-cngxt98","to":"area:settings"},"at":"2026-09-19T17:44:35.655Z","parents":["evt-w0c4q5njj8xn"],"hash":"2d305815f1abed34a844dd73ee9253e7117fc168dd92b650116a69e1f2b3bad1"}
{"id":"evt-nvh5gb4t22nq","type":"edge.added","subject":"iss-cngxt98","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-cngxt98","to":"area:ui"},"at":"2026-09-19T17:44:35.656Z","parents":["evt-zyb1vp2fxmng"],"hash":"a9f0fa118a168ca441ffbf1407b48eac143b548fe96d7fdef4f58c0ccd746599"}
{"id":"evt-gqgzzcb0xfxq","type":"node.created","subject":"iss-7eaywtf","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Camera settings reader: ONVIF implementation behind a vendor-neutral interface","body":"src/lib/camera-settings.ts (server-only). getCameraSettings(target) returns a client-safe DTO: streams[] (token, name, codec, H.264/H.265 profile, width, height, fps, bitrate kbps, constantBitRate, GOP, quality, rtspUrl), image (irCut, exposure, whiteBalance, wdr, blc, brightness, contrast, saturation, sharpness, noiseReduction), time (cameraTime, driftSeconds, ntp: source and servers), network (per interface: name, MAC, IPv4 address/prefix, dhcp, IPv6 addresses, speed/duplex; default gateway). Implemented with ONVIF calls on the cached connection from camera.ts (GetProfiles data, GetStreamUri, GetImagingSettings, GetSystemDateAndTime, GetNTP, GetNetworkInterfaces, GetNetworkDefaultGateway), shaped by a documented `CameraSettingsSource` interface so an ISAPI source can be added later (pri-p9h51hx). Each section is read independently: one failing call (e.g. imaging unsupported) yields that section as 'unavailable' without failing the page. RTSP URLs are sanitized: any userinfo is stripped, and the host is forced to the registry address. Auth and inactive errors map as in camera.ts. Tests use a scripted fake Cam with shapes taken from the probe findings (fnd-j1s725e, fnd-g4z7c6d, fnd-b73cav4), including Media2-style and missing fields.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T17:44:44.220Z","parents":["evt-nvh5gb4t22nq"],"hash":"a57e15bc20bd231f5aa1d420e4e2985fa96391fc9b3a0cb84a434c7b73ffbf2b"}
{"id":"evt-z274n0kxst0r","type":"edge.added","subject":"iss-7eaywtf","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-7eaywtf","to":"gol-2nxgqjn"},"at":"2026-09-19T17:44:44.221Z","parents":["evt-gqgzzcb0xfxq"],"hash":"154896f66f69f28067b7dc40effbb4481005e5a216c91359dfa9d6e78f613c80"}
{"id":"evt-gqk010w07854","type":"edge.added","subject":"iss-7eaywtf","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-7eaywtf","to":"src/lib/camera-settings.ts"},"at":"2026-09-19T17:44:44.222Z","parents":["evt-z274n0kxst0r"],"hash":"c7a0bd30401e64c5158a7c492ee5c94af2a25edd548c264cab724625d26a5e04"}
{"id":"evt-373vmvxm7sq2","type":"edge.added","subject":"iss-7eaywtf","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-7eaywtf","to":"src/lib/camera.ts"},"at":"2026-09-19T17:44:44.223Z","parents":["evt-gqk010w07854"],"hash":"75bb6c815ea0224375627575604f5890e42171f9fd0401e333f8ce4b4d2ecaa1"}
{"id":"evt-cxetb15ctatx","type":"edge.added","subject":"iss-cngxt98","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"parent_of","from":"iss-cngxt98","to":"iss-7eaywtf"},"at":"2026-09-19T17:44:44.224Z","parents":["evt-373vmvxm7sq2"],"hash":"069ab9298c441d45a1e1ddba8caa2db1c1e870dd42492ac5324d1db5a4ae906a"}
{"id":"evt-9qsbjnz19hyv","type":"edge.added","subject":"iss-7eaywtf","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-7eaywtf","to":"area:settings"},"at":"2026-09-19T17:44:44.225Z","parents":["evt-cxetb15ctatx"],"hash":"23f5f07f16477dbef80599212cfbd2ce8cb0e9ba0e09a783f1eebaa581b0f272"}
{"id":"evt-p21jybbv90p8","type":"node.created","subject":"iss-s935a4n","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Build the /cameras/[id] page: four sections, Refresh, copyable RTSP URLs","body":"A Server Component page (pri-01np850) that calls requirePageAccess(), resolves the camera from the registry by id (404 for unknown or invalid ids, and only private-IP hosts per isAllowedHost), and calls getCameraSettings directly, with no fetch to our own API. Its four sections sit inside <Suspense> so the header renders immediately. Streams: a table per profile, plus the RTSP URL with a small client 'Copy' leaf (Copy → Check icon) and the note 'Sign in with the camera's ONVIF login (e.g. camera)'. Image: day/night shown as 'Auto (IR-cut)', etc. Time & NTP: camera clock in the browser's locale, drift with a warning at over 30 s, and the NTP server and its source. Network: IPv4, DHCP or static, gateway, MAC, link, IPv6. Unavailable sections say so without failing the page, and auth or inactive problems show the same guidance as the dashboard card. A Refresh button (a client leaf calling router.refresh()) and a back link to the dashboard. Icons per pri-8ev5m89, adding mappings for new concepts (e.g. ArrowLeft = back, Copy/Check = copy, Video = streams, Image or Sun/Moon = image, Clock = time, Network = network). Tests render with a faked settings reader.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T17:44:52.989Z","parents":["evt-9qsbjnz19hyv"],"hash":"c556d9d2a1a89d005c502dc08b3cc639ff45e011d0c522e251737ef500a8343b"}
{"id":"evt-7qyyxx2mq7sc","type":"edge.added","subject":"iss-s935a4n","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-s935a4n","to":"gol-2nxgqjn"},"at":"2026-09-19T17:44:52.990Z","parents":["evt-p21jybbv90p8"],"hash":"73ad7d119a584f2c57fd54aaa685b2264b22ec454bdd4eaaf958665acdbf4aa1"}
{"id":"evt-wvb3vge5chvp","type":"edge.added","subject":"iss-s935a4n","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-s935a4n","to":"src/app/cameras/[id]/"},"at":"2026-09-19T17:44:52.991Z","parents":["evt-7qyyxx2mq7sc"],"hash":"7d47fb03f9123077747a4219f47f04c53ab59d5e7c1fd014d908dbcb08b6147c"}
{"id":"evt-2nyqg79f2m78","type":"edge.added","subject":"iss-7eaywtf","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"blocks","from":"iss-7eaywtf","to":"iss-s935a4n"},"at":"2026-09-19T17:44:52.992Z","parents":["evt-wvb3vge5chvp"],"hash":"6a3c77756b61e535baaea73d6fb78f57a37e2a428b9022ca610caddfbdf52de2"}
{"id":"evt-2r58e73geybc","type":"edge.added","subject":"iss-cngxt98","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"parent_of","from":"iss-cngxt98","to":"iss-s935a4n"},"at":"2026-09-19T17:44:52.993Z","parents":["evt-2nyqg79f2m78"],"hash":"76081b066c331a0bb846d7902846dd84d9b8c20dfab774868f7bca3fac01519e"}
{"id":"evt-akxgbvb58mwv","type":"edge.added","subject":"iss-s935a4n","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-s935a4n","to":"area:settings"},"at":"2026-09-19T17:44:52.994Z","parents":["evt-2r58e73geybc"],"hash":"94cab7c9e8055d67b2624bed255b0c423a86e89ca571976b97220e5d826d39d1"}
{"id":"evt-7h2qm57jgzkx","type":"edge.added","subject":"iss-s935a4n","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-s935a4n","to":"area:ui"},"at":"2026-09-19T17:44:52.995Z","parents":["evt-akxgbvb58mwv"],"hash":"abb24a1d00a444152618262d308f7cb9f035fdd4c253ba89593140fcc6d31d66"}
{"id":"evt-yw0mq8fcfzzr","type":"node.created","subject":"iss-pv2bvzj","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Dashboard card: link the name to the camera page, add a stream summary, hide 'ONVIF' as a brand","body":"On each camera card: the camera name becomes a next/link to /cameras/[id]; a one-line summary is built from the info the card already loads, e.g. 'Main 4096×1860 · H.264 · 20 fps' (main = the first or highest-resolution profile); the device line shows 'I91ET · fw V5.8.10', treating manufacturer 'ONVIF' or empty as unknown (fnd-msm9prc) while still showing real brand names. No new device calls. Tests: link href, summary text, and the brand rule for 'ONVIF', empty, and 'Hikvision'.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T17:44:56.153Z","parents":["evt-7h2qm57jgzkx"],"hash":"d45c80cf8e68882686fd6b262c5ae7efeeac92c1661afb694dc9ccaf689958ae"}
{"id":"evt-99qp337rvxfe","type":"edge.added","subject":"iss-pv2bvzj","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-pv2bvzj","to":"gol-2nxgqjn"},"at":"2026-09-19T17:44:56.155Z","parents":["evt-yw0mq8fcfzzr"],"hash":"73ac24a1372f32c3a15acc1548f575594be1f11f872d8ce823b6122fa931f9b7"}
{"id":"evt-7r964b8eckvn","type":"edge.added","subject":"iss-pv2bvzj","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-pv2bvzj","to":"src/app/camera-card.tsx"},"at":"2026-09-19T17:44:56.156Z","parents":["evt-99qp337rvxfe"],"hash":"0e54ab18305617b3d21c627f0fb2de19465348eb0a07f25b13764eae19b8f1a1"}
{"id":"evt-egqgafamqg6x","type":"edge.added","subject":"iss-cngxt98","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"parent_of","from":"iss-cngxt98","to":"iss-pv2bvzj"},"at":"2026-09-19T17:44:56.157Z","parents":["evt-7r964b8eckvn"],"hash":"91429b74b8ca1f3e18342bbe0a3e07cc7c6508a93d30d49230605a88a1f6b64f"}
{"id":"evt-422kqj834328","type":"edge.added","subject":"iss-pv2bvzj","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-pv2bvzj","to":"area:ui"},"at":"2026-09-19T17:44:56.158Z","parents":["evt-egqgafamqg6x"],"hash":"2469b54f3efc7f49df6fd1c903f86520b355b71a4178f368c2dfccb49c3ea032"}
{"id":"evt-872wg16xwxe3","type":"node.created","subject":"ver-az11m1q","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"getCameraSettings(target) returns a typed, client-safe DTO with four independently read sections (streams incl. credential-free RTSP URLs on the registry host, image, time/NTP with drift, network with gateways) over ONVIF, behind the CameraSettingsSource interface; one failing section is 'unavailable' with a fixed message while the others still load; connection and login errors propagate; only get* calls are made.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T17:48:09.526Z","parents":["evt-422kqj834328"],"hash":"66fc13edb2d27d89ae914660aab4fd2620a9b3f55931aff8e8ae37d698008dd9"}
{"id":"evt-dnd7z4yspnmd","type":"edge.added","subject":"ver-az11m1q","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-az11m1q","to":"iss-7eaywtf"},"at":"2026-09-19T17:48:09.528Z","parents":["evt-872wg16xwxe3"],"hash":"24383e89462dfb9f2fe29bdda9b81dd8fa48f891f9bcfe24222d2384d60a7ca9"}
{"id":"evt-zpmx1vjh23mq","type":"verification.recorded","subject":"ver-az11m1q","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"src/lib/camera-settings.ts; camera.ts now exports connect(), encoderConfig() and profileToken(), with toProfile() reusing them. Tests in src/lib/camera-settings.test.ts (25) use a scripted fake camera with the exact shapes from the real-camera probe (fnd-j1s725e, fnd-g4z7c6d, fnd-b73cav4), plus Media2 profiles, stringly typed numbers and booleans, a single non-array interface, static IPv4, disabled IPv6, NTP from DHCP by IP, a bare profile, no profiles, each section failing on its own (no 'hunter2' leak), and sanitizeRtspUrl (strips credentials, forces the host, keeps port and path, accepts rtsps, rejects http and garbage); they also assert only get* methods are called. 100% lines and functions and 95% branches for the module. Full suite 441/441, coverage 99.87%, tsc, eslint and next build clean, 2026-09-19. Not run against the real cameras through this module (the probe used the same calls); the parent iss-cngxt98 requires the user's check of the page."},"at":"2026-09-19T17:48:09.529Z","parents":["evt-dnd7z4yspnmd"],"hash":"1a15b1b4ea232301c2144d17ae437ece764bf924e1c529dac90cafcf47a7b8ba"}
{"id":"evt-9mk55qmy6amx","type":"node.status_changed","subject":"iss-7eaywtf","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T17:48:10.674Z","parents":["evt-zpmx1vjh23mq"],"hash":"2cac4816dbc162eeda4bcb7fe6251ec6a5f19b7ab8fae7e821acd7723ee70f92"}
{"id":"evt-x6cbjt1gx94r","type":"node.updated","subject":"pri-8ev5m89","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"body":"Standing rule derived from dec-1vnz5fw (Lucide chosen 2026-09-19) and applied in iss-scj42jq, iss-chrdd0v, iss-egy082x and iss-s935a4n.\n\n1. Source: icons come from lucide-react, imported by name (`import { KeyRound } from \"lucide-react\"`); Next 16 optimizes those imports by default. No other icon library, icon font or hand-drawn SVG icon without a new dependency question (pri-mz2jxpb). Check that a name exists in the installed version before using it.\n2. Meaning: an icon never carries meaning alone. Status, warnings and actions always have visible text (e.g. the status badge's 'Needs login'), and state is never conveyed by color or icon alone.\n3. Accessibility: icons next to text are decorative and get `aria-hidden`. A control with only an icon must have an `aria-label` that says what it does. Warnings keep role=\"alert\" and their text. Adding an icon must not change a control's accessible name.\n4. Sizing and layout: size with Tailwind classes, not the `size` prop: `size-3.5` in small text and badges, `size-4` in buttons, inline messages and section headings, `size-5` in banners, `size-6` in page headings. Lay out with `inline-flex items-center gap-1…2`, and use `shrink-0` beside wrapping text.\n5. Motion and color: spinners use LoaderCircle with `animate-spin motion-reduce:animate-none`. Colored icons and badges have `dark:` variants.\n6. Consistency: one concept, one icon across the app. Current mapping: Cctv = a camera (live status; camera page heading); LoaderCircle = in progress; KeyRound = login; ShieldAlert = unsecured or not activated; ShieldCheck = secured or signed in; WifiOff = offline; CircleAlert = error message; TriangleAlert = needs attention (setup needed, clock drift); RefreshCw = retry or refresh; Radar = scan; Timer = refresh rate; Trash2 = forget; Lock = save or sign-in heading; LogIn/LogOut = session; ExternalLink = opens another site in a new tab; ArrowLeft = back to the dashboard; Video = streams; SunMoon = image and day/night settings; Clock = time and NTP; Network = network settings; Copy → Check = copy to clipboard / copied. Reuse these; if a new concept needs an icon, add it here.\n7. Tests: assert icons by their `lucide-<name>` class with toContain, since Lucide also adds alias classes (Trash2 → 'trash' and 'trash-2'), and assert every rendered `svg.lucide` is aria-hidden (see src/app/icons.test.tsx).\n\nWhy: icons should make the dashboard faster to scan without making it less accessible or less consistent. Rules out: icon-only status, icons that change accessible names, ad-hoc SVGs, and mixing icon sets."}},"at":"2026-09-19T17:53:58.530Z","parents":["evt-9mk55qmy6amx"],"hash":"234f807d29c1359f6029e44ed4a88d4079f67f5f0d06f107cfea68151deeefe5"}
{"id":"evt-sw81v28rq819","type":"node.created","subject":"ver-ps9ex17","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"/cameras/[id] is a Server Component that checks access, 404s bad or unknown ids and non-private hosts, and renders the header immediately with the four settings sections streamed via Suspense (streams with copyable credential-free RTSP URLs and an ONVIF-login note; image; time and NTP with a >30 s drift warning; network), plus Refresh and back links; unavailable sections and camera errors show fixed text only.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T17:54:03.061Z","parents":["evt-x6cbjt1gx94r"],"hash":"d450a94ebcd51f8dc26d2a00b6d5b64789a2edc46d8221e79559cf7090eeef5a"}
{"id":"evt-rxj3dgznmqjb","type":"edge.added","subject":"ver-ps9ex17","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-ps9ex17","to":"iss-s935a4n"},"at":"2026-09-19T17:54:03.062Z","parents":["evt-sw81v28rq819"],"hash":"9ad696673ed8f995f367dfea23226ed47dc0a8d1bb02b6b2f9689a7bd3bc1c67"}
{"id":"evt-pxs1rddg4b2m","type":"verification.recorded","subject":"ver-ps9ex17","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"Built by a parallel agent, reviewed by Claude. src/app/cameras/[id]/{page.tsx, settings-sections.tsx, client-controls.tsx, format.ts} + 4 test files, 100% coverage on the new files. Claude's follow-up fix: the copy button now falls back to a hidden textarea + execCommand('copy') outside a secure context, because navigator.clipboard is unavailable when the app is opened over plain HTTP on a LAN IP (it would always have said 'Copy failed'); 3 new tests cover the fallback, refusal and throw paths and cleanup. New icons were added to pri-8ev5m89. Combined suite 522/522, coverage 99.89%, tsc, eslint and next build clean (the route list includes ƒ /cameras/[id]), 2026-09-19. Not yet viewed in a browser or run against the real cameras; tracked on the parent iss-cngxt98."},"at":"2026-09-19T17:54:03.063Z","parents":["evt-rxj3dgznmqjb"],"hash":"3731fef863384af1bc1597edc4c7172bd8f1c2f32b9517bd54b67067ee79360f"}
{"id":"evt-dngy3dgedbx5","type":"node.created","subject":"ver-kmkh9c4","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"The dashboard card's name links to /cameras/<id>; a summary line shows the main (highest-resolution) stream, e.g. 'Main 4096×1860 · H.264 · 20 fps'; the device line hides a manufacturer of 'ONVIF' or empty ('I91ET · fw V5.8.10') and keeps real brands; no new device calls.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T17:54:06.076Z","parents":["evt-pxs1rddg4b2m"],"hash":"0d08c24227697ca72921d7e64073e7cfee04de903975e550ea41bc0bbe20bd2e"}
{"id":"evt-jv9fdfdw5cwz","type":"edge.added","subject":"ver-kmkh9c4","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-kmkh9c4","to":"iss-pv2bvzj"},"at":"2026-09-19T17:54:06.079Z","parents":["evt-dngy3dgedbx5"],"hash":"5ab0761924edb26f38a82b9a21374e2745d8f8f93620f6102ddcb893bbb5a46e"}
{"id":"evt-1y4b5zgp33ph","type":"verification.recorded","subject":"ver-kmkh9c4","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"Built by a parallel agent, reviewed by Claude. src/app/camera-summary.ts (pure: mainStream, codecLabel, streamSummary, displayBrand, deviceLine) with camera-summary.test.ts, plus 3 new tests in camera-card.test.tsx (link href; probe-like profiles give 'I91ET · fw V5.8.10' and the summary, with no 'ONVIF'; nothing before info loads). Existing 'Hikvision DS-2CD · fw V5.7' test unchanged and passing. Summary covers the main stream only; no '+N more'. Combined suite 522/522, 2026-09-19. Not yet viewed in a browser."},"at":"2026-09-19T17:54:06.080Z","parents":["evt-jv9fdfdw5cwz"],"hash":"72f691f9f289c71e6c32284ec53f121784047b70f7676a778f510c48b6629fb9"}
{"id":"evt-2qgsw9d64b3g","type":"node.status_changed","subject":"iss-s935a4n","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T17:54:07.127Z","parents":["evt-1y4b5zgp33ph"],"hash":"4bd51a8c2627040dad1b79f354d51b6916280f4ff051548af1f1152340cf3bd3"}
{"id":"evt-t8jhasgkfb5a","type":"node.status_changed","subject":"iss-pv2bvzj","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T17:54:08.288Z","parents":["evt-2qgsw9d64b3g"],"hash":"c030d4aa2a94fad669d5c2cfbc72b347f8f1fd607def5c66bcf50d72d34feaae"}
{"id":"evt-wbe1mkb54hck","type":"node.created","subject":"ver-1fbexnj","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"In a real browser against both real cameras: open a camera's page from its name on the dashboard; the four sections show the values the probe found; Copy puts the RTSP URL on the clipboard (over the LAN URL you normally use); Refresh works; the card shows the summary line and 'I91ET · fw V5.8.10'.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T17:54:09.508Z","parents":["evt-t8jhasgkfb5a"],"hash":"02a1c3a22a3890dbd88b40d3248254a59deb780d5a756418360a839fd2d6b5ac"}
{"id":"evt-hmkqz65wra0a","type":"edge.added","subject":"ver-1fbexnj","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-1fbexnj","to":"iss-cngxt98"},"at":"2026-09-19T17:54:09.511Z","parents":["evt-wbe1mkb54hck"],"hash":"dd94aefa435b1a8a0325f8da753a2610a8fd27316d3671604e445b469109f0dd"}
{"id":"evt-mxv72kxw35fw","type":"verification.recorded","subject":"ver-1fbexnj","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pending","evidence":"Awaiting the user's manual check; Claude doesn't run dev servers here, and this module has only been exercised against fakes shaped like the probe results."},"at":"2026-09-19T17:54:09.512Z","parents":["evt-hmkqz65wra0a"],"hash":"eb0547b2d061037e896cf001df716bdba9cced08b4e3c24dac4fb9c93258a917"}
{"id":"evt-9syzrzp8ng8g","type":"node.created","subject":"ver-sgvq7se","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"In a real browser against both real cameras: open a camera's page from its name on the dashboard; the four sections show the values the probe found; Copy puts the RTSP URL on the clipboard (over the LAN URL you normally use); Refresh works; the card shows the summary line and 'I91ET · fw V5.8.10'.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T17:58:41.419Z","parents":["evt-mxv72kxw35fw"],"hash":"9dda8fb9a81aaa525b37c2ee0d094865400e814502032862388d9c60478f7a31"}
{"id":"evt-42geta3461gv","type":"edge.added","subject":"ver-sgvq7se","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-sgvq7se","to":"iss-cngxt98"},"at":"2026-09-19T17:58:41.421Z","parents":["evt-9syzrzp8ng8g"],"hash":"f5e9b08be9d7db2f9adad826e1ba44608c6c33478e83089cc918232db408caa4"}
{"id":"evt-k2c8c78v7jzf","type":"verification.recorded","subject":"ver-sgvq7se","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"Manual check by the user (Michael Mainguy) on 2026-09-19: \"checked, looks good\". This resolves pending ver-1fbexnj."},"at":"2026-09-19T17:58:41.422Z","parents":["evt-42geta3461gv"],"hash":"6e4b445b64c8448f454780a1dc817cd83e8b61394369e436f484d72d2c54252a"}
{"id":"evt-9nwcj1gp32rt","type":"node.status_changed","subject":"iss-cngxt98","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T17:58:42.661Z","parents":["evt-k2c8c78v7jzf"],"hash":"596c33315f0e7cfae98d4dc3e8cdcec9baa15d45726580d7e88373e6ddceac1c"}
{"id":"evt-xnpebsz5e53a","type":"node.created","subject":"dec-kgm44qw","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"decision","title":"Network identifiers in the committed vrek export stay for now; the user will redact later","body":"Recorded 2026-09-19 as input to a future process for handling sensitive information that lands in version control. What happened: commit 0ec434c (pushed to origin main at https://git.dasfad.com/mmainguy/cameras.git) includes .vrek/log.ndjson with the read-only probe findings (fnd-j1s725e, fnd-b73cav4, fnd-msm9prc, fnd-hcnbvht). Those findings contain camera LAN IPv4 addresses (192.168.17.129, 192.168.31.2), the gateway and subnet (192.168.16.1, /20), MAC addresses, a global IPv6 address (2001:470:1f11:137:…), serial numbers, and the ONVIF username 'camera'. The export contains no passwords, credential-store keys or password hashes (checked with a grep before each commit). Claude flagged it after the push. Decision (by the user): leave it in place for now; the user will handle redaction later. Consequences: vrek is append-only, so redacting means superseding the findings with redacted versions and, if the history must be cleaned, rewriting git history and force-pushing, which is the user's call. Until then, anyone with read access to the repo sees the network layout and device identifiers. Rejected for now: immediate redaction by Claude. Open question for the process: what counts as sensitive (credentials always; addresses, MACs, serials, usernames?) and when to check (before export, before commit, before push).","status":"recorded","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T18:04:43.106Z","parents":["evt-9nwcj1gp32rt"],"hash":"aad6b583cde0012427d178b7854ae78e6747cd9476a040d93182630a3e205b6e"}
{"id":"evt-vh2bjsh1fea8","type":"edge.added","subject":"dec-kgm44qw","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"dec-kgm44qw","to":"gol-6q1q5mr"},"at":"2026-09-19T18:04:43.108Z","parents":["evt-xnpebsz5e53a"],"hash":"0a0f5f04ecea19392081eb3b80ba7b9717dac133bcfc806a203eb3b6b0b69db8"}
{"id":"evt-1rw34fg20fq8","type":"node.created","subject":"iss-1bxj0nh","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Redact network identifiers from the vrek export, and define a process for sensitive information in commits","body":"Owned by the user (deferred at their request). Two parts. (1) Redaction: supersede findings fnd-j1s725e, fnd-b73cav4, fnd-msm9prc and fnd-hcnbvht with versions that replace LAN and IPv6 addresses, MACs and serials with placeholders; re-export; decide whether to scrub git history (commit 0ec434c onward) and force-push. (2) Process: define what counts as sensitive (credentials, keys and hashes always; network identifiers, serials and usernames?), when it's checked (before vrek_export, before commit, before push; possibly a pre-commit hook), and what to do on a leak (record a decision, rotate anything secret, redact, clean history). The context is in the decision recorded alongside this item.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T18:04:47.041Z","parents":["evt-vh2bjsh1fea8"],"hash":"894aacb5f4ffdeeb7b1ce925181b0a536f601c6b1ad85fa8fc21c7d9b6ea36c1"}
{"id":"evt-a4npb2w88vte","type":"edge.added","subject":"iss-1bxj0nh","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-1bxj0nh","to":"gol-6q1q5mr"},"at":"2026-09-19T18:04:47.043Z","parents":["evt-1rw34fg20fq8"],"hash":"a519313f26ea772abd6254d3ed8d38bfc6844dde653f713ee99f830e232f4e8f"}
{"id":"evt-tsmr6cd0ghvr","type":"edge.added","subject":"iss-1bxj0nh","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-1bxj0nh","to":".vrek/log.ndjson"},"at":"2026-09-19T18:04:47.044Z","parents":["evt-a4npb2w88vte"],"hash":"fe544aed510c73c22580af3bc50763e4e0880ee208913309943fee807bb369e6"}
{"id":"evt-7t03qn3fbpx6","type":"edge.added","subject":"iss-1bxj0nh","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-1bxj0nh","to":"area:security"},"at":"2026-09-19T18:04:47.045Z","parents":["evt-tsmr6cd0ghvr"],"hash":"8ad77db3813c2713b1c9c44be9c5b83c88082140d1150cd21f56d7dfb5a6e49f"}
{"id":"evt-nnnmtac6gnyt","type":"edge.added","subject":"iss-1bxj0nh","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-1bxj0nh","to":"area:process"},"at":"2026-09-19T18:04:47.046Z","parents":["evt-7t03qn3fbpx6"],"hash":"7fd17b7ecd54e87854d301a612f4310b7dca7a16302a21165ffed67dd44793c1"}
{"id":"evt-gpmjravvh5va","type":"edge.added","subject":"iss-1bxj0nh","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"discovered_from","from":"iss-1bxj0nh","to":"dec-kgm44qw"},"at":"2026-09-19T18:04:50.915Z","parents":["evt-nnnmtac6gnyt"],"hash":"da511ca2fcb79c0c13a0ad469434da57989d4c1162e9a949028151db912b2190"}
{"id":"evt-n35a8pje6x9q","type":"node.updated","subject":"iss-1bxj0nh","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"attrs":{"revisit":null,"deferred_because":"dec-kgm44qw: the user will handle redaction and define the process later; not a priority now."}}},"at":"2026-09-19T18:04:52.064Z","parents":["evt-gpmjravvh5va"],"hash":"8a29fa5aa9b10a696847c829d94324f7a9d90798d06f84917eaf4626ba5a675b"}
{"id":"evt-f2pb9y02c8s4","type":"node.status_changed","subject":"iss-1bxj0nh","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"deferred"},"at":"2026-09-19T18:04:52.066Z","parents":["evt-n35a8pje6x9q"],"hash":"9c2dae2516d21e9ffd325f0dfbb07c516c4f00b40ab237a9f9266365c9b86169"}
{"id":"evt-f70ry47s80d4","type":"node.created","subject":"dec-84ghhqr","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"decision","title":"Configuration, first slice: image and stream settings, Operator login only, per-section Edit → Save","body":"User's choices, 2026-09-19, for gol-8v1wqvp. (1) Scope of the first slice: Image (day/night IR-cut, brightness/contrast/saturation/sharpness, WDR, backlight compensation, exposure and white balance modes) and Streams (resolution, fps, bitrate, VBR/CBR, GOP, quality per stream, limited to values the camera reports as supported). Deferred: time/NTP and reboot, and network (highest risk of making a camera unreachable). (2) Privilege: keep using the stored ONVIF Operator login (least privilege). Detect per camera which writes it may make, offer only those, and explain how to grant more; don't upgrade the user or store a second admin login. (3) Editing: each section on /cameras/[id] gets Edit → Save / Cancel. Changes are validated (zod, and against the camera's reported options), disruptive changes (anything that restarts a stream) are confirmed, and after saving the settings are re-read from the camera to show what actually applied. Rejected: apply-immediately controls, and a single page-wide edit mode. (4) The user permitted a no-op permission probe (write back current imaging and encoder values; no reboot or network). It is ON HOLD: the library's setters aren't faithful no-ops (see the findings informing this goal), so the probe waits until faithful serializers exist, or the user picks a narrower probe.","status":"recorded","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T18:07:36.746Z","parents":["evt-f2pb9y02c8s4"],"hash":"11b356dae7ccaa058db017561b8b03d22221fe6830edcda849e4150bbb476444"}
{"id":"evt-s4764fcq6b76","type":"edge.added","subject":"dec-84ghhqr","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"dec-84ghhqr","to":"gol-8v1wqvp"},"at":"2026-09-19T18:07:36.749Z","parents":["evt-f70ry47s80d4"],"hash":"bd29f604a432097f6c02d04dcd3510e5455b4fda32f71a9201fe04411b7b108a"}
{"id":"evt-bgqw4c6py8yf","type":"node.created","subject":"fnd-sx2k2bg","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"finding","title":"onvif's setVideoEncoderConfiguration drops the H.264 profile when you pass back the read configuration","body":"In node_modules/onvif/lib/media.js (onvif 0.8.x): reads return the profile as H264.H264Profile, but the setter only serializes H264.profile, so passing back the read object omits <H264Profile>. ONVIF's schema requires it, so a camera may reject the write or apply a default profile. Also: it skips Quality when it is 0 (a truthiness check); it always sends a ConstantBitRate attribute (false if absent); it sends ForcePersistence=true on Media1; and after success it re-reads the configuration itself. Implication: encoder writes need our own mapping (H264Profile → profile, quality 0 allowed) or our own SOAP body; never pass the read object back as-is.","status":"current","owner":"prn-q80g8mz","attrs":{"sources":[{"node":"dec-s205dvs","note":"source reading of node_modules/onvif/lib/media.js Cam.prototype.setVideoEncoderConfiguration, 2026-09-19"}],"as_of":"2026-09-19"}},"at":"2026-09-19T18:07:40.789Z","parents":["evt-s4764fcq6b76"],"hash":"9fdf7f8982956cd64684efce5ec0368c89d8b6eeadba4f2d1ec55eb33ef04d1d"}
{"id":"evt-ff0x71pg7jv5","type":"edge.added","subject":"fnd-sx2k2bg","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-sx2k2bg","to":"gol-8v1wqvp"},"at":"2026-09-19T18:07:40.792Z","parents":["evt-bgqw4c6py8yf"],"hash":"53db260d9b47b94423ba41df1fa0cc3ca3f7a577e4e022c887303ab27b16173a"}
{"id":"evt-e25v6gt6n548","type":"node.created","subject":"fnd-jzmm3nz","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"finding","title":"onvif's setImagingSettings can't write WDR, backlight compensation, white balance or noise reduction, and skips zero values","body":"In node_modules/onvif/lib/imaging.js, SetImagingSettings serializes only brightness, colorSaturation, contrast, sharpness, irCutFilter, exposure (mode, priority, min/max exposure time, gain, iris, …) and focus. WideDynamicRange, BacklightCompensation, WhiteBalance and noise reduction (an Imaging extension) are never sent. Each field is included only if truthy, so 0 (e.g. brightness 0, minGain 0) can't be written. ONVIF leaves the handling of omitted optional elements to the device (unchanged vs. reset), so a partial write isn't guaranteed to be a no-op. Implication: covering the chosen image scope needs our own SetImagingSettings SOAP body (via the camera's authenticated request path) that sends the complete settings, including zeros.","status":"current","owner":"prn-q80g8mz","attrs":{"sources":[{"node":"dec-s205dvs","note":"source reading of node_modules/onvif/lib/imaging.js Cam.prototype.setImagingSettings, 2026-09-19"}],"as_of":"2026-09-19"}},"at":"2026-09-19T18:07:44.036Z","parents":["evt-ff0x71pg7jv5"],"hash":"d2f7a9e7c623f8bca413fdb7885744af34936b55d8fbbe9db52a32a897d568d7"}
{"id":"evt-ahvdrsqbgc0n","type":"edge.added","subject":"fnd-jzmm3nz","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-jzmm3nz","to":"gol-8v1wqvp"},"at":"2026-09-19T18:07:44.037Z","parents":["evt-e25v6gt6n548"],"hash":"7ebc45134bc20a0a254efc16154605f928463c4d3820958c325b4bc7b36243bf"}
{"id":"evt-dnw56xh92ekj","type":"node.created","subject":"dec-n4e8jah","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"decision","title":"Configuration writes: probe after our writers exist, confirm stream saves, local audit log","body":"User's choices, 2026-09-19, for gol-8v1wqvp (following dec-84ghhqr). (1) The no-op permission probe runs only after complete, tested writers exist, re-sending each camera's full current image and stream settings through them; this is a faithful no-op and also proves the writers against the real firmware. Rejected: a brightness-only probe now through the library (unknown handling of omitted fields), or skipping the probe. (2) Saving stream settings shows a confirmation dialog: 'This restarts the <main/sub> stream; recorders and viewers reconnect'. Image saves need no dialog. Rejected: confirming every save, or never confirming. (3) Every configuration change is appended to a local audit log, .data/audit.jsonl (gitignored, 0600): time, admin user, camera id, section, setting old → new, and result (applied / refused / camera applied a different value). Passwords are never logged. Rejected: server console only, or no logging. How writes are implemented is still open: the user asked for a patch that's easy to upstream (see the question about patching onvif).","status":"recorded","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T18:10:29.639Z","parents":["evt-ahvdrsqbgc0n"],"hash":"d47981efc1afdd7dc3ec8da652edfc37f7127fdf0c66bb0189d982bfed75ee44"}
{"id":"evt-qaea58xkvdyb","type":"edge.added","subject":"dec-n4e8jah","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"dec-n4e8jah","to":"gol-8v1wqvp"},"at":"2026-09-19T18:10:29.640Z","parents":["evt-dnw56xh92ekj"],"hash":"f47c634477012112b4522d6639f580b700a6f36489ac1e8d06701e09c6c19281"}
{"id":"evt-mxg0w1b0ym0x","type":"node.created","subject":"que-xbw1xgq","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"question","title":"How should we fix onvif's incomplete setters so the fixes are easy to send upstream (agsh/onvif)?","body":"Context: fnd-sx2k2bg (setVideoEncoderConfiguration drops H264Profile, skips quality 0) and fnd-jzmm3nz (setImagingSettings can't write WDR, BLC, white balance or noise reduction, and skips zeros). The user wants a patch that is easy to submit to the upstream maintainer. npm has no built-in patch command. Dependency rule: pri-mz2jxpb. Upstream: https://github.com/agsh/onvif, currently 0.8.3, modified 2026-09-09.","status":"open","owner":null,"attrs":{"options":[{"option":"Fork + git dependency (recommended)","consequence":"Fork agsh/onvif; fix the setters on a branch with tests in upstream's style; package.json points at the fork branch (commit pinned in the lockfile). The branch is the upstream PR. No new dependency; we maintain the fork until upstream releases, then switch back to npm."},{"option":"patch-package (new dev dependency)","consequence":"Edit node_modules/onvif, commit patches/onvif+0.8.3.patch, reapply on postinstall. The .patch file becomes the upstream PR. Adds patch-package 8.0.1 (MIT, 14 direct deps) as a dev dependency; requires this question to be answered per pri-mz2jxpb."},{"option":"git apply postinstall script","consequence":"Keep .patch files in the repo and apply them with git apply on postinstall. No new package, but a hand-rolled version of patch-package that's more fragile."}],"revisit":null}},"at":"2026-09-19T18:10:35.415Z","parents":["evt-qaea58xkvdyb"],"hash":"b5161adbaa22a8d837510f0e5f4c6e1cbcaa40f80b6a3753019c742cea11a420"}
{"id":"evt-03h90anmj8vv","type":"edge.added","subject":"que-xbw1xgq","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"que-xbw1xgq","to":"gol-8v1wqvp"},"at":"2026-09-19T18:10:35.416Z","parents":["evt-mxg0w1b0ym0x"],"hash":"4241a535897522bd0567d33187b5de5bbd5c070e129dd9a53b6bd913b435e8a4"}
{"id":"evt-g9aj9t0k14dn","type":"edge.added","subject":"pri-mz2jxpb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"guides","from":"pri-mz2jxpb","to":"que-xbw1xgq"},"at":"2026-09-19T18:10:35.417Z","parents":["evt-03h90anmj8vv"],"hash":"b9be9c5d5565787dea3629d55a74dbabbfcf830d248dd43e1018d34a409a36b2"}
{"id":"evt-zzceh57jjpth","type":"edge.added","subject":"pri-p9h51hx","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"guides","from":"pri-p9h51hx","to":"que-xbw1xgq"},"at":"2026-09-19T18:10:35.418Z","parents":["evt-g9aj9t0k14dn"],"hash":"a822e7578f79be01333d2f1840c5ecd7658684257a2504df1201509be638e59a"}
{"id":"evt-s0j5mh6xz49t","type":"node.created","subject":"que-ph8eak5","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"question","title":"Do the current I91ET cameras have a microphone, and do their streams carry audio?","body":"Raised by the user on 2026-09-19; the user asked to record it and do nothing yet. How to find out: (1) ONVIF GetAudioSources (is there an audio input?), GetAudioEncoderConfigurations and each profile's audioEncoderConfiguration (is audio configured: codec, bitrate, sample rate?); (2) the SDP from an RTSP DESCRIBE on /Streaming/Channels/101 and /102 (an m=audio line means audio is in that stream); (3) the camera web page under Configuration → Video/Audio (stream type 'Video' vs 'Video & Audio'; an Audio tab exists if there's a mic). All read-only.","status":"open","owner":null,"attrs":{"options":[],"revisit":null}},"at":"2026-09-19T18:10:38.186Z","parents":["evt-zzceh57jjpth"],"hash":"c4db15ad420e1c46adf9eacb7d938c92efae0fc7d8e8dd595679087af7df5eb8"}
{"id":"evt-phq90201zv32","type":"edge.added","subject":"que-ph8eak5","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"que-ph8eak5","to":"gol-2nxgqjn"},"at":"2026-09-19T18:10:38.188Z","parents":["evt-s0j5mh6xz49t"],"hash":"68edd12a1ae0012a6496a7e86df0fa40141c9d3d0b5463f9215e3a6f4afd57ca"}
{"id":"evt-ka1bgsndfcb2","type":"edge.added","subject":"que-ph8eak5","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"que-ph8eak5","to":"gol-6q1q5mr"},"at":"2026-09-19T18:10:38.189Z","parents":["evt-phq90201zv32"],"hash":"8b39dc1ed7b06ade4a4d964f24c93f154704a2783fac223d5e682ecacbbdae0e"}
{"id":"evt-pptprvh4vqdd","type":"node.created","subject":"iss-td8y1cc","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Test audio streaming on the current cameras","body":"Deferred at the user's request (2026-09-19): record only, don't start. When picked up: read-only checks per camera. (a) ONVIF: GetAudioSources, GetAudioEncoderConfigurations, and the profiles' audioEncoderConfiguration. (b) RTSP DESCRIBE on Channels/101 and /102, looking for m=audio in the SDP, with the codec (e.g. G.711/AAC) and clock rate. (c) Optionally, pull a few seconds of the stream to confirm audio packets actually flow. Record the results as findings answering the audio question. If audio exists, follow up with whether the camera page should show audio settings (read-only) and whether stream URLs should mention it. Any probe needs the user's go-ahead at the time (as with dec-s205dvs), and redaction rules from dec-kgm44qw apply to what gets recorded.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T18:10:41.975Z","parents":["evt-ka1bgsndfcb2"],"hash":"ee65a7d19090f62ac443facba2541eb63062397c7b7707cddecd51b66494a17b"}
{"id":"evt-4trzvpcfgfpp","type":"edge.added","subject":"iss-td8y1cc","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-td8y1cc","to":"gol-2nxgqjn"},"at":"2026-09-19T18:10:41.977Z","parents":["evt-pptprvh4vqdd"],"hash":"aa3c6e9cf939263053da0889503f5d37302656d7adba4d7717016b37b2f0919c"}
{"id":"evt-ghmty51vfe3m","type":"edge.added","subject":"iss-td8y1cc","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-td8y1cc","to":"area:audio"},"at":"2026-09-19T18:10:41.978Z","parents":["evt-4trzvpcfgfpp"],"hash":"e4277056f8a944ce164eae19a8b445fab84281fcebad9fd4c457d5cec5313b3f"}
{"id":"evt-vh1n0k158144","type":"edge.added","subject":"iss-td8y1cc","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-td8y1cc","to":"area:settings"},"at":"2026-09-19T18:10:41.979Z","parents":["evt-ghmty51vfe3m"],"hash":"ba2f3318728a448182774325a7fbf613a4d222dc5a6f08da87e3b087ca8495f8"}
{"id":"evt-bredmkzts5gb","type":"node.updated","subject":"iss-td8y1cc","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"attrs":{"revisit":null,"deferred_because":"User asked to record the audio test and not start it yet (2026-09-19)."}}},"at":"2026-09-19T18:10:47.915Z","parents":["evt-vh1n0k158144"],"hash":"08a2308b0e4ee9a0e8b4da810c65e9b483db585f08ec4e5a099ee40913188623"}
{"id":"evt-ntsjbkdrh9e3","type":"node.status_changed","subject":"iss-td8y1cc","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"deferred"},"at":"2026-09-19T18:10:47.917Z","parents":["evt-bredmkzts5gb"],"hash":"4dee9f0bf893504ec16e189f92e6a7049e4fe1cc89e823fa6f75e7410ba2a472"}
{"id":"evt-q8b1ec1esd2n","type":"node.updated","subject":"que-ph8eak5","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"attrs":{"revisit":null,"deferred_because":"To be answered by iss-td8y1cc when the user picks it up; recorded only for now (2026-09-19)."}}},"at":"2026-09-19T18:10:49.283Z","parents":["evt-ntsjbkdrh9e3"],"hash":"6fb5060e89189199cf32d4ed832dc30f6178ab9958ca8b12cc87a665344b35bd"}
{"id":"evt-hmt0mv1brkkw","type":"node.status_changed","subject":"que-ph8eak5","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"deferred"},"at":"2026-09-19T18:10:49.285Z","parents":["evt-q8b1ec1esd2n"],"hash":"90e8b2f33e530b7998f93228eabc5b3e2d38b8a9f0e6290a4a6cf9a369c62139"}
{"id":"evt-nanz1yr33rce","type":"edge.added","subject":"iss-td8y1cc","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"discovered_from","from":"iss-td8y1cc","to":"que-ph8eak5"},"at":"2026-09-19T18:10:50.653Z","parents":["evt-hmt0mv1brkkw"],"hash":"b26069cb622af73200089aa263f14b96ec45a5b4030a678aebc84f974de02d83"}
{"id":"evt-1fc2mpgvmk0a","type":"node.created","subject":"dec-2myt5ht","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"decision","title":"Fix onvif's setters in the user's GitHub fork (mikemainguy/onvif), consumed as a git dependency until upstream merges","body":"Chosen by the user on 2026-09-19. The fork already exists at https://github.com/mikemainguy/onvif (fork of agsh/onvif). Fixes for fnd-sx2k2bg and fnd-jzmm3nz go on a topic branch with tests in upstream's own style, so the branch can be opened as a PR against agsh/onvif unchanged. package.json points \"onvif\" at the fork by commit (github:mikemainguy/onvif#<sha>), pinned in the lockfile. When upstream releases the fixes, switch back to the npm version. No new dependency (pri-mz2jxpb). Rejected: patch-package (new dev dependency) and a hand-rolled git-apply postinstall. Pushing branches to the fork and opening the upstream PR are outward-facing, so Claude asks before each.","status":"recorded","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T18:13:28.947Z","parents":["evt-nanz1yr33rce"],"hash":"99719ba9705b8d73eb54f2bd12e28a35645147de4cf0530e695636158feac28d"}
{"id":"evt-jq8hkdpdynqg","type":"edge.added","subject":"dec-2myt5ht","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"answers","from":"dec-2myt5ht","to":"que-xbw1xgq"},"at":"2026-09-19T18:13:28.948Z","parents":["evt-1fc2mpgvmk0a"],"hash":"fa3a30b3db1ecb90f8c45d65559ed2547db697ee1d6daa14378f833271246c66"}
{"id":"evt-4mmhvv4yg0fj","type":"question.answered","subject":"que-xbw1xgq","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"answer":"dec-2myt5ht"},"at":"2026-09-19T18:13:28.949Z","parents":["evt-jq8hkdpdynqg"],"hash":"0205c6c6f2ec3e7ee54d938cc5caa57883a33f895b612be740ced1bcf1b73d8b"}
{"id":"evt-8z140shxdty5","type":"edge.added","subject":"dec-2myt5ht","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"dec-2myt5ht","to":"gol-8v1wqvp"},"at":"2026-09-19T18:13:28.950Z","parents":["evt-4mmhvv4yg0fj"],"hash":"c133e4eeab629f242c8206117447e19f22ac1111c13c29f274ab06bd9838c05a"}
{"id":"evt-yz3ptpy1wmdm","type":"node.created","subject":"fnd-0xcw70x","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"finding","title":"Upstream onvif has a v1 TypeScript rewrite (1.0.0-rc.2) whose setters build complete typed structures; the 0.8.x line lives on the v0.x branch","body":"agsh/onvif master (last commit 2026-09-14) is v1: TypeScript in src/, promise-based API split into services (cam.imaging.setImagingSettings({videoSourceToken, imagingSettings, forcePersistence}), media.setVideoEncoderConfiguration({configuration, forcePersistence})), Jest tests against a HappyTime ONVIF server, and builds to build/. Its setImagingSettings serializes the whole ImagingSettings through Imaging.imagingSettingsToBuild(), and setVideoEncoderConfiguration maps every field explicitly, so the gaps in fnd-sx2k2bg and fnd-jzmm3nz probably don't exist there (not yet verified field by field). npm dist-tags: latest 0.8.3, rc 1.0.0-rc.2, legacy 0.8.1. The 0.8.x JavaScript line (the one this app uses) is the upstream v0.x branch, whose head is exactly the v0.8.3 tag (last commit 2026-09-07), with mocha tests and a mock server. So a PR fixing the 0.8 setters would target v0.x; moving to v1 would mean migrating our camera.ts, onvif.ts and camera-settings.ts to the new API.","status":"current","owner":"prn-q80g8mz","attrs":{"sources":[{"url":"https://github.com/agsh/onvif","note":"branches master and v0.x, src/imaging.ts setImagingSettings, src/media.ts setVideoEncoderConfiguration, read 2026-09-19 via a clone of the fork"},{"url":"https://www.npmjs.com/package/onvif","note":"dist-tags, 2026-09-19"}],"as_of":"2026-09-19"}},"at":"2026-09-19T18:14:20.243Z","parents":["evt-8z140shxdty5"],"hash":"0f0a5643391d48e22518cf84009fa15a4d9b50507b9bb4ba426865b2c3ee6e29"}
{"id":"evt-8ah09j3n0thq","type":"edge.added","subject":"fnd-0xcw70x","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-0xcw70x","to":"que-xbw1xgq"},"at":"2026-09-19T18:14:20.244Z","parents":["evt-yz3ptpy1wmdm"],"hash":"e548ddb1c585d32f4e896929348cd4d1b45714d5f328778eb526e96c8e940bb9"}
{"id":"evt-mf19tnbw4xbd","type":"edge.added","subject":"fnd-0xcw70x","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-0xcw70x","to":"gol-8v1wqvp"},"at":"2026-09-19T18:14:20.245Z","parents":["evt-8ah09j3n0thq"],"hash":"093a3e027db618425e3771418592f640327d2edb2cbd2ca9ace0157bc84fe4e6"}
{"id":"evt-jv625t6fq7m6","type":"node.created","subject":"fnd-a9y3w97","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"finding","title":"onvif v1 (1.0.0-rc.2) fixes the encoder's H.264 profile and quality, and adds WDR/BLC/white balance, but still skips zero values, omits CBR/VBR and passes the imaging extension through raw","body":"Field-by-field reading of agsh/onvif master src/imaging.ts and src/media.ts (2026-09-19). Imaging.imagingSettingsToBuild: brightness, colorSaturation, contrast and sharpness use `x && {…}`, so 0 is dropped; exposureToBuild drops minGain, maxGain, gain, min/max exposure time and iris when 0 (our cameras report minGain 0 per fnd-g4z7c6d); wideDynamicRangeToBuild drops level 0; whiteBalanceToBuild drops crGain/cbGain 0; backlightCompensation level uses !== undefined (correct). Extension is passed through as read (camelCase keys such as noiseReduction/level), which likely serializes the wrong element names, so noise reduction probably isn't writable correctly. ForcePersistence is only sent when true. Media.setVideoEncoderConfiguration: maps H264Profile correctly (fixes fnd-sx2k2bg's main bug), sends Quality unconditionally (0 allowed), but RateControl omits the ConstantBitRate attribute, so CBR/VBR can't be changed through Media1 in v1. Conclusion: both v0.8.3 and v1 need upstream fixes for this app's configuration scope; migrating to v1 alone wouldn't be enough.","status":"current","owner":"prn-q80g8mz","attrs":{"sources":[{"url":"https://github.com/agsh/onvif/blob/master/src/imaging.ts","note":"imagingSettingsToBuild, exposureToBuild, wideDynamicRangeToBuild, whiteBalanceToBuild, backlightCompensationToBuild, setImagingSettings; read at master ac284ae via the fork clone"},{"url":"https://github.com/agsh/onvif/blob/master/src/media.ts","note":"setVideoEncoderConfiguration around line 1177"}],"as_of":"2026-09-19"}},"at":"2026-09-19T18:15:42.267Z","parents":["evt-mf19tnbw4xbd"],"hash":"634a9365f2e5e06ffc35ea087f26ce8a148d8c1b58a486141f1f845b197b17e6"}
{"id":"evt-cpk4rjq3dhjq","type":"edge.added","subject":"fnd-a9y3w97","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-a9y3w97","to":"que-xbw1xgq"},"at":"2026-09-19T18:15:42.270Z","parents":["evt-jv625t6fq7m6"],"hash":"97d5d1c3de41f8245179ad71b2d085cf2299b54ac1389e8a0f37ede8e706b21b"}
{"id":"evt-1nn8wxjq3s3j","type":"edge.added","subject":"fnd-a9y3w97","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-a9y3w97","to":"gol-8v1wqvp"},"at":"2026-09-19T18:15:42.271Z","parents":["evt-cpk4rjq3dhjq"],"hash":"9c9b807bc2ddf623b81d45804b34b0aa32325f11fc6515c892be91cceb218e2a"}
{"id":"evt-427ysa5scptf","type":"node.created","subject":"dec-70qga6k","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"decision","title":"Fix onvif v0.x in the fork now and consume it; send equivalent fixes to v1 master as a second PR","body":"User's choice, 2026-09-19, after verifying v1 (fnd-0xcw70x, fnd-a9y3w97). Refines dec-2myt5ht. (1) In mikemainguy/onvif, branch from upstream/v0.x; fix setVideoEncoderConfiguration (accept the read shape H264.H264Profile as well as H264.profile; send Quality 0) and setImagingSettings (send 0 values; add WideDynamicRange, BacklightCompensation, WhiteBalance, and the NoiseReduction extension with correct element names), with mocha tests in the branch's existing style. Open a PR against agsh/onvif v0.x, and point this app's package.json at the fork commit. (2) Separately, branch from upstream master and fix the same classes of bug in v1 (zero values in imaging builders, the ConstantBitRate attribute on RateControl, proper serialization of the imaging extension), with Jest tests, and open a PR against master. No migration to v1 now. Rejected: fixing v0.x only; migrating to v1 now. Pushes to the fork and opening either PR need the user's go-ahead each time; the gh CLI is logged in as 'immersiveidea', not 'mikemainguy', so push access must be confirmed.","status":"recorded","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T18:16:27.329Z","parents":["evt-1nn8wxjq3s3j"],"hash":"8410a6357cf5c6b510d6c6cad58cc4acd108f3d0c10b4274238045edf2901b9b"}
{"id":"evt-4h1zrzy0bm84","type":"edge.added","subject":"dec-70qga6k","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"dec-70qga6k","to":"gol-8v1wqvp"},"at":"2026-09-19T18:16:27.330Z","parents":["evt-427ysa5scptf"],"hash":"eae35c1ff86ff5d0db97cb83935dba5e978210c10c5743c7bf12b460f6fc0c8e"}
{"id":"evt-jg2sm72x1hfq","type":"node.created","subject":"iss-3g6m4rz","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Camera configuration, first slice: image and stream settings (Edit → Save)","body":"Parent for gol-8v1wqvp, following dec-84ghhqr, dec-n4e8jah, dec-2myt5ht and the v0.x-plus-v1 decision. Done when every child is closed, the no-op permission probe has passed on both cameras, and the user has changed an image and a stream setting on a real camera and seen them re-read correctly.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T18:16:28.961Z","parents":["evt-4h1zrzy0bm84"],"hash":"7b477be10ede1932a951f1a98f722d22c127e87fe95f74631fce75f7ace63e56"}
{"id":"evt-026n1aa67s46","type":"edge.added","subject":"iss-3g6m4rz","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-3g6m4rz","to":"gol-8v1wqvp"},"at":"2026-09-19T18:16:28.962Z","parents":["evt-jg2sm72x1hfq"],"hash":"868e8eaf530f9a0358459a6ed8937577148a6413d3bcf17070e2e61d012f2a07"}
{"id":"evt-k6h5mp2472r6","type":"edge.added","subject":"iss-3g6m4rz","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-3g6m4rz","to":"area:config"},"at":"2026-09-19T18:16:28.963Z","parents":["evt-026n1aa67s46"],"hash":"761a2f96ec50989c86e0186256ddc0fec144373fef6d06ad02a2e88250f72e11"}
{"id":"evt-20g7fxpp4900","type":"node.created","subject":"iss-tsq9zc3","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Fork (v0.x): fix setVideoEncoderConfiguration and setImagingSettings, with mocha tests","body":"In ~/WebstormProjects/onvif (clone of mikemainguy/onvif; upstream remote = agsh/onvif), branch fix/v0-complete-setters from upstream/v0.x. Encoder: accept H264.H264Profile (the read shape) as well as H264.profile; send Quality even when 0; H265 isn't in Media1, so no change there. Imaging: send numeric fields when !== undefined (0 allowed); add WideDynamicRange (Mode, Level), BacklightCompensation (Mode, Level), WhiteBalance (Mode, CrGain, CbGain), and Extension/NoiseReduction/Level with correct ONVIF element names and namespaces; keep existing fields and behaviour unchanged otherwise. Tests: mocha in the branch's test/ style against its server mockup, asserting the serialized SOAP for each fix (and that unchanged inputs produce the same XML as before). Run the branch's lint and tests. Commit with upstream-style messages. Don't push until the user says so.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T18:16:35.796Z","parents":["evt-k6h5mp2472r6"],"hash":"fd9dbeb8f069fb3e5fbcbdc8a11e04300df913f5637a2eb4e3c8b4b9d249769e"}
{"id":"evt-bsakdsecmdg0","type":"edge.added","subject":"iss-tsq9zc3","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-tsq9zc3","to":"gol-8v1wqvp"},"at":"2026-09-19T18:16:35.798Z","parents":["evt-20g7fxpp4900"],"hash":"dad84d8edbab1ed5149d175ed96104cedff341d812e51463b6151ca6adc27250"}
{"id":"evt-pggr49rmjs62","type":"edge.added","subject":"iss-tsq9zc3","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-tsq9zc3","to":"~/WebstormProjects/onvif"},"at":"2026-09-19T18:16:35.799Z","parents":["evt-bsakdsecmdg0"],"hash":"fb5bfe448ea94787ad8971423647710dbdf728a3e4bb1b541ed6242a5b530fe8"}
{"id":"evt-f1p7atpb974x","type":"edge.added","subject":"iss-3g6m4rz","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"parent_of","from":"iss-3g6m4rz","to":"iss-tsq9zc3"},"at":"2026-09-19T18:16:35.800Z","parents":["evt-pggr49rmjs62"],"hash":"5c636d9ff04b87bbb5953adb100768cc999729a423361d19b06c53c076884eb4"}
{"id":"evt-e13x6f4nk47x","type":"edge.added","subject":"iss-tsq9zc3","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-tsq9zc3","to":"area:config"},"at":"2026-09-19T18:16:35.801Z","parents":["evt-f1p7atpb974x"],"hash":"be9e666f7f34552af5d630fa3abc546ffd51e10af65151977c53a01ddff8acc6"}
{"id":"evt-qmvax8eckyx2","type":"edge.added","subject":"iss-tsq9zc3","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-tsq9zc3","to":"area:upstream"},"at":"2026-09-19T18:16:35.802Z","parents":["evt-e13x6f4nk47x"],"hash":"6c5be829e1523ed27f6c03e968141681d650da8549df7561a760e8af21a951cd"}
{"id":"evt-je73jw2y5hna","type":"node.created","subject":"iss-7p7pt6q","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Point the app at the fixed fork commit and re-verify","body":"After the v0.x fix branch is pushed (with the user's go-ahead): set \"onvif\": \"github:mikemainguy/onvif#<commit>\" in package.json, install, and confirm that serverExternalPackages still works, the full suite and build pass, and discovery, the dashboard and the camera page still work. Record the upstream PR URL in vrek once it's opened. When agsh/onvif releases a 0.8.x with the fixes, switch back to the npm version (tracked here).","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T18:16:42.367Z","parents":["evt-qmvax8eckyx2"],"hash":"bf0577776469bccdddf26c3990e0a3291db7770261108684e5f965215f139826"}
{"id":"evt-v4rm08zx9x8y","type":"edge.added","subject":"iss-7p7pt6q","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-7p7pt6q","to":"gol-8v1wqvp"},"at":"2026-09-19T18:16:42.373Z","parents":["evt-je73jw2y5hna"],"hash":"6c6c94d32c15a8ea08cf1ba5960e166bfcd56b0cc22ebddc1bea2212cfcb080e"}
{"id":"evt-hje66bwxj4s0","type":"edge.added","subject":"iss-7p7pt6q","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-7p7pt6q","to":"package.json"},"at":"2026-09-19T18:16:42.374Z","parents":["evt-v4rm08zx9x8y"],"hash":"2015b9c3207ec13e147e4a9be3559956dd87b7276f51bfeb050fb5a9658d61f4"}
{"id":"evt-hjrk78ajr8w9","type":"edge.added","subject":"iss-7p7pt6q","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-7p7pt6q","to":"package-lock.json"},"at":"2026-09-19T18:16:42.375Z","parents":["evt-hje66bwxj4s0"],"hash":"18defebc972a3e07dcf6aee62d885c3ca4614049fff5d0edcae5dc0e69446d6d"}
{"id":"evt-fdkje7h2ba0p","type":"edge.added","subject":"iss-tsq9zc3","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"blocks","from":"iss-tsq9zc3","to":"iss-7p7pt6q"},"at":"2026-09-19T18:16:42.376Z","parents":["evt-hjrk78ajr8w9"],"hash":"45de4d9f4b02a88a68e358e511f4e8b9133f5dcfa8b37dd846ee08d4e36c5773"}
{"id":"evt-0rhzmfs3hn4w","type":"edge.added","subject":"iss-3g6m4rz","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"parent_of","from":"iss-3g6m4rz","to":"iss-7p7pt6q"},"at":"2026-09-19T18:16:42.377Z","parents":["evt-fdkje7h2ba0p"],"hash":"850b049378660791db2d0d0b0c5698a8dda102979f90aaef7d600a512bccddc1"}
{"id":"evt-1bd88fydb5jx","type":"edge.added","subject":"iss-7p7pt6q","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-7p7pt6q","to":"area:config"},"at":"2026-09-19T18:16:42.378Z","parents":["evt-0rhzmfs3hn4w"],"hash":"ae3c562256bc622aee4c59c6c4eb632b975ae2a67ee9838f04843202a53bc689"}
{"id":"evt-ja6x5wbpese9","type":"edge.added","subject":"iss-7p7pt6q","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-7p7pt6q","to":"area:upstream"},"at":"2026-09-19T18:16:42.379Z","parents":["evt-1bd88fydb5jx"],"hash":"08273a651c76a12b78697261f3e408957b72dc2eeb94e6f7653087a95f1d0097"}
{"id":"evt-msp648h4d957","type":"node.created","subject":"iss-h5e97mf","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Fork (v1 master): fix zero values, ConstantBitRate and the imaging extension, with Jest tests","body":"Per fnd-a9y3w97: branch from upstream master in the fork. imagingSettingsToBuild and its sub-builders send numeric fields when !== undefined; RateControl gets the ConstantBitRate attribute when provided; the imaging extension (NoiseReduction etc.) is serialized with ONVIF element names rather than passed through raw. Jest tests in the repo's style (unit tests of the builders where possible, as the full suite needs the HappyTime server). Separate PR against agsh/onvif master. Lower priority than the v0.x fix; this app doesn't consume it.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T18:16:45.236Z","parents":["evt-ja6x5wbpese9"],"hash":"c01d75640d3fb4318d533d85d1ee023409c8a7a905077850a3fa76ca82eafacc"}
{"id":"evt-w6n6gbdend0a","type":"edge.added","subject":"iss-h5e97mf","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-h5e97mf","to":"gol-8v1wqvp"},"at":"2026-09-19T18:16:45.238Z","parents":["evt-msp648h4d957"],"hash":"dd5892daca7544b9dccd6ee84daa74dfd6a5f83e13fc30165066ef5363660c59"}
{"id":"evt-amqsddmd1tby","type":"edge.added","subject":"iss-h5e97mf","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-h5e97mf","to":"~/WebstormProjects/onvif"},"at":"2026-09-19T18:16:45.239Z","parents":["evt-w6n6gbdend0a"],"hash":"0465f92759e78b24f45ef6913f3ee979b5dffee6be8064b5f154b4a7da2ff112"}
{"id":"evt-jcvzxdqasysg","type":"edge.added","subject":"iss-3g6m4rz","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"parent_of","from":"iss-3g6m4rz","to":"iss-h5e97mf"},"at":"2026-09-19T18:16:45.240Z","parents":["evt-amqsddmd1tby"],"hash":"0a306ca16b02a444c5695463dc7852c56c30e64b85e4f04a70f6cd374fc5aa8c"}
{"id":"evt-hb3damgv2t3g","type":"edge.added","subject":"iss-h5e97mf","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-h5e97mf","to":"area:upstream"},"at":"2026-09-19T18:16:45.241Z","parents":["evt-jcvzxdqasysg"],"hash":"2a5758870ccf05e0dc43cc56da314164b27dfb28d8af1876f8f248cf8ccf88cb"}
{"id":"evt-3prg2e2pdk60","type":"node.created","subject":"iss-a6j9jg5","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Camera configuration writers: validated image and stream saves, re-read, audit log","body":"src/lib/camera-config.ts (server-only), alongside camera-settings.ts behind the same source interface (pri-p9h51hx). It reads the allowed values (Imaging GetOptions, GetVideoEncoderConfigurationOptions) so the UI offers only supported values. It writes image settings (full current settings merged with the change) and a stream's encoder configuration (current configuration merged with the change) through the fixed onvif setters. Every input is validated with zod plus the camera's options. After writing it re-reads and reports applied / camera-adjusted / refused. Permission detection: a 'not authorized' fault marks that section read-only for that camera. src/lib/audit-log.ts appends JSON lines to .data/audit.jsonl (0600, with a turbopackIgnore'd path), per dec-n4e8jah. Tests use a fake camera (pri-e14bahk).","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T18:16:49.910Z","parents":["evt-hb3damgv2t3g"],"hash":"1fb0a7328e5ab5cbc301df0976c0a4c17f2ad6ae0f74d22de3ac4e76ae81819d"}
{"id":"evt-bm8bdkfgxhvh","type":"edge.added","subject":"iss-a6j9jg5","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-a6j9jg5","to":"gol-8v1wqvp"},"at":"2026-09-19T18:16:49.913Z","parents":["evt-3prg2e2pdk60"],"hash":"141b3c0355a1bd21f3e00ebcd2e04693135c268118c1c3ef173fe587e3ed6693"}
{"id":"evt-ce32nbjsvy9r","type":"edge.added","subject":"iss-a6j9jg5","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-a6j9jg5","to":"src/lib/camera-config.ts"},"at":"2026-09-19T18:16:49.914Z","parents":["evt-bm8bdkfgxhvh"],"hash":"fa3273034716888c999cfd7c3ac46ee711b7c52630d4e1806aa8262ae596f4c7"}
{"id":"evt-5n364n7wfk1e","type":"edge.added","subject":"iss-a6j9jg5","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-a6j9jg5","to":"src/lib/audit-log.ts"},"at":"2026-09-19T18:16:49.915Z","parents":["evt-ce32nbjsvy9r"],"hash":"d5c06c6f2867a1917603a1805199fbd426f44ff38eb765b4e132fdbe88522dd9"}
{"id":"evt-kwrtye8899jt","type":"edge.added","subject":"iss-tsq9zc3","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"blocks","from":"iss-tsq9zc3","to":"iss-a6j9jg5"},"at":"2026-09-19T18:16:49.916Z","parents":["evt-5n364n7wfk1e"],"hash":"c60733eb7a95ac79125fd8bb573ec3c830e1bd62a5e449b32e8c51c05443d607"}
{"id":"evt-8fgphyztja8t","type":"edge.added","subject":"iss-3g6m4rz","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"parent_of","from":"iss-3g6m4rz","to":"iss-a6j9jg5"},"at":"2026-09-19T18:16:49.917Z","parents":["evt-kwrtye8899jt"],"hash":"f1034c2d13df9e48f10f15641742292cac06f7709ec6c9783fe91e51df797570"}
{"id":"evt-tsk3nxs2vjb7","type":"edge.added","subject":"iss-a6j9jg5","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-a6j9jg5","to":"area:config"},"at":"2026-09-19T18:16:49.918Z","parents":["evt-8fgphyztja8t"],"hash":"94a288bfba4e35f1a1214834ba1130aa2d636aaf216dfee585c0ac6c1bc2db40"}
{"id":"evt-e8az9263f7aj","type":"node.created","subject":"iss-j8zzg1r","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Camera page: Edit → Save for the image and stream sections, with stream-restart confirmation","body":"On /cameras/[id], the Image and each Stream section get Edit, with Save and Cancel. Controls are limited to the values the camera supports; Server Actions validate and call the writers. Saving a stream first shows a confirmation dialog ('This restarts the main stream; recorders and viewers reconnect'). After saving, the section shows what the camera actually applied, including values it adjusted, with a clear message. Sections the Operator login may not change show as read-only, with a note on how to grant more. Icons per pri-8ev5m89. Tests cover validation, confirmation, and the applied/adjusted/refused outcomes.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T18:16:53.981Z","parents":["evt-tsk3nxs2vjb7"],"hash":"09111be4620a27d3471fc91b1265325a6e7c613a63689b9604e6c494c8f140ae"}
{"id":"evt-z78njn7mvkyv","type":"edge.added","subject":"iss-j8zzg1r","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-j8zzg1r","to":"gol-8v1wqvp"},"at":"2026-09-19T18:16:53.983Z","parents":["evt-e8az9263f7aj"],"hash":"4dca7902481b04ba9c72323755382eab0ef7a8c7427cd446014fe25100031bd1"}
{"id":"evt-9j5dvcy3tyqj","type":"edge.added","subject":"iss-j8zzg1r","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-j8zzg1r","to":"src/app/cameras/[id]/"},"at":"2026-09-19T18:16:53.985Z","parents":["evt-z78njn7mvkyv"],"hash":"cf277acfae8103049f1dea4c61064935bb87d079e0cd28022d3c8866d0bb2f1b"}
{"id":"evt-eqbpfnyte9z7","type":"edge.added","subject":"iss-tsq9zc3","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"blocks","from":"iss-tsq9zc3","to":"iss-j8zzg1r"},"at":"2026-09-19T18:16:53.986Z","parents":["evt-9j5dvcy3tyqj"],"hash":"c8fbbb3f270159af25d68bab4708a860b53c2ea4be86fc27fe4e7624494aa786"}
{"id":"evt-0we7fpf23hc0","type":"edge.added","subject":"iss-3g6m4rz","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"parent_of","from":"iss-3g6m4rz","to":"iss-j8zzg1r"},"at":"2026-09-19T18:16:53.987Z","parents":["evt-eqbpfnyte9z7"],"hash":"6b06fd3a8658a07e79bbd89257091c53efccad134a3bc8f3c7edf81d432a9082"}
{"id":"evt-wmmgy41r4h1q","type":"edge.added","subject":"iss-j8zzg1r","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-j8zzg1r","to":"area:config"},"at":"2026-09-19T18:16:53.988Z","parents":["evt-0we7fpf23hc0"],"hash":"2c6aa91a793a2b6335b32f197d396c792808bcd6ea413d9138c34310736affff"}
{"id":"evt-0bj1hrcm6ws7","type":"edge.added","subject":"iss-j8zzg1r","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-j8zzg1r","to":"area:ui"},"at":"2026-09-19T18:16:53.989Z","parents":["evt-wmmgy41r4h1q"],"hash":"6d407bee0c98a7cbeaeb863b38091e1d7e946248adff4b94b2261414335ea463"}
{"id":"evt-h7ce8q60jzfk","type":"node.created","subject":"iss-hg50epa","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"No-op permission probe on both cameras with the finished writers","body":"Per dec-n4e8jah: with the fixed writers, re-send each camera's full current image settings and each stream's current encoder configuration, then re-read and confirm nothing changed. Record which writes the Operator login may make as findings, redacted per dec-kgm44qw. Needs the user's go-ahead at the time. Re-sending a stream's configuration may briefly restart that stream.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T18:16:56.111Z","parents":["evt-0bj1hrcm6ws7"],"hash":"7bd2ce10d89758cf8d2e1883412086a8b74be1aebdbc0452cb57e18bcd886254"}
{"id":"evt-nct8wwnarnjp","type":"edge.added","subject":"iss-hg50epa","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-hg50epa","to":"gol-8v1wqvp"},"at":"2026-09-19T18:16:56.112Z","parents":["evt-h7ce8q60jzfk"],"hash":"d08cf7dcd95e0ba9f27f3598001ded250755065e6d707c6128e0158e36973c23"}
{"id":"evt-7x22r3fmxyh1","type":"edge.added","subject":"iss-3g6m4rz","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"parent_of","from":"iss-3g6m4rz","to":"iss-hg50epa"},"at":"2026-09-19T18:16:56.113Z","parents":["evt-nct8wwnarnjp"],"hash":"39c848f0273e9f6ccf4946039682299855a10a33c97736102fa05f54d5bf20de"}
{"id":"evt-n371g6x9k4j3","type":"edge.added","subject":"iss-hg50epa","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-hg50epa","to":"area:config"},"at":"2026-09-19T18:16:56.114Z","parents":["evt-7x22r3fmxyh1"],"hash":"be6f4bb03938435753b801f3d97ee413f08af91e1f0825d3a6dde9862ba2b3d4"}
{"id":"evt-qzcb8kh8m28k","type":"edge.added","subject":"iss-a6j9jg5","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"blocks","from":"iss-a6j9jg5","to":"iss-hg50epa"},"at":"2026-09-19T18:17:00.344Z","parents":["evt-n371g6x9k4j3"],"hash":"0b037544dac3642405f81657a44eb2187d53dde8f21d7e2bc7b4ebcc4972ef82"}
{"id":"evt-7gstdrfz7sy9","type":"edge.added","subject":"iss-a6j9jg5","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"blocks","from":"iss-a6j9jg5","to":"iss-j8zzg1r"},"at":"2026-09-19T18:17:01.720Z","parents":["evt-qzcb8kh8m28k"],"hash":"1e05a711228705c8be3561a2227f819e9005938ed7b6be0c1d4e58a810398315"}
{"id":"evt-dhwh7n6qhya3","type":"node.status_changed","subject":"iss-tsq9zc3","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"in_progress"},"at":"2026-09-19T18:19:00.735Z","parents":["evt-7gstdrfz7sy9"],"hash":"f8b5e55e8c727ebe5aa7a85f0e1f43cce5b1241d835d226961d11ee54d922f14"}
{"id":"evt-vnn5sk9nw6g0","type":"node.created","subject":"ver-5a37qc7","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"On branch fix/v0-complete-setters (from upstream/v0.x) in the fork clone: setImagingSettings sends 0 values, emits elements in ImagingSettings20 order, and adds BLC/WDR/white balance and the NoiseReduction extension (read shape round-trips); setVideoEncoderConfiguration accepts H264.H264Profile and MPEG4.Mpeg4Profile (and emits the schema name Mpeg4Profile) and sends quality 0; upstream's lint and full mocha suite pass; the new tests fail against the unfixed code.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T18:30:34.353Z","parents":["evt-dhwh7n6qhya3"],"hash":"6aca83ce0f431f972592347c2dbaf22c71d783321435f12226c06b7bbd8d2d8c"}
{"id":"evt-7wzxpz1hxkr1","type":"edge.added","subject":"ver-5a37qc7","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-5a37qc7","to":"iss-tsq9zc3"},"at":"2026-09-19T18:30:34.355Z","parents":["evt-vnn5sk9nw6g0"],"hash":"fb9fed9ed42173a32c8cba192f25f1d31ca80a68b999560c2b5dee92c4b1aeea"}
{"id":"evt-sqm38spn1bps","type":"verification.recorded","subject":"ver-5a37qc7","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"~/WebstormProjects/onvif, local commits 9a5bd635 (imaging) and bfda12fd (media) on fix/v0-complete-setters, 2 ahead of upstream/v0.x (4ac697bc = v0.8.3). `npm test` (eslint + nyc mocha against the repo's serverMockup): 178 passing (168 before plus 10 new serialization tests that capture the SOAP via the rawRequest event). With lib/ stashed back to v0.8.3, 7 of the 10 new tests fail (the 3 that pass cover unchanged behaviour: omit absent fields, round-trip, legacy H264.profile). Extra fix found along the way: the MPEG4 element was misnamed MPEG4Profile (schema: Mpeg4Profile, as in v1). NOT pushed; waiting for the user's go-ahead. 2026-09-19."},"at":"2026-09-19T18:30:34.356Z","parents":["evt-7wzxpz1hxkr1"],"hash":"8fb99eacabef51f5bfd21403a24b458024f25139bdd63baac0e9d5f6aad2fdab"}
{"id":"evt-ecfvyr1056a7","type":"node.status_changed","subject":"iss-tsq9zc3","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"in_progress","to":"done"},"at":"2026-09-19T18:30:35.465Z","parents":["evt-sqm38spn1bps"],"hash":"7e8545bceb6cc61f624221d8784ec7f6366a964cf6a9a2afbc0a479956ab92aa"}
{"id":"evt-e76m90p50fh9","type":"node.updated","subject":"iss-7p7pt6q","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"body":"Interim state (2026-09-19): the app uses the local fork checkout via `npm install ../onvif --no-save`, so node_modules/onvif is a symlink to ~/WebstormProjects/onvif (branch fix/v0-complete-setters, commits 9a5bd635 and bfda12fd). package.json and package-lock.json still say onvif ^0.8.3 from npm, and a plain `npm install` or `npm ci` restores the npm version; re-run `npm install ../onvif --no-save` to relink. next.config.ts now detects a linked onvif outside the project and sets turbopack.root to the nearest common parent, as the Next 16 docs require for linked packages; with a normal install it does nothing. The user will push the branch and open the upstream PR themselves, then tell Claude to switch. Remaining: set \"onvif\": \"github:mikemainguy/onvif#<commit>\" in package.json, npm install (the lockfile pins the commit), confirm node_modules/onvif is a real install (no symlink), re-run the full suite and build, and record the upstream PR URL here. When agsh/onvif releases a 0.8.x with the fixes, switch back to the npm version."}},"at":"2026-09-19T18:35:11.871Z","parents":["evt-ecfvyr1056a7"],"hash":"3ab5191fd07505abe3526769d24597a660b7cd4e2f94fc7f8eecd2420ba98b85"}
{"id":"evt-gppppbprhqhv","type":"node.created","subject":"ver-0rqfwy8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"Interim: the app runs against the local fixed fork without changing package.json or the lockfile; the full suite, tsc, eslint and next build pass.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T18:35:14.933Z","parents":["evt-e76m90p50fh9"],"hash":"b897e7da3748e53934439629aace8d473a07429c6fb4f1d5619d5c267d8975bf"}
{"id":"evt-cvhvvpjdfgpd","type":"edge.added","subject":"ver-0rqfwy8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-0rqfwy8","to":"iss-7p7pt6q"},"at":"2026-09-19T18:35:14.934Z","parents":["evt-gppppbprhqhv"],"hash":"6e40e895ebb399a4b0729b9c2eab70927430631e958be700ec016ef0f6af5b14"}
{"id":"evt-0xvwtj5cvrk4","type":"verification.recorded","subject":"ver-0rqfwy8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"2026-09-19: `npm install ../onvif --no-save` → node_modules/onvif -> ../../onvif (require.resolve points at ~/WebstormProjects/onvif/lib/onvif.js; lib/imaging.js contains the new schemaValue helper); git status clean apart from next.config.ts. Vitest 522/522 (99.89% lines), tsc and eslint clean. next build first failed with \"Can't resolve 'onvif'\" (Turbopack doesn't resolve outside its root); fixed with the automatic turbopack.root in next.config.ts, after which the build compiles cleanly. Not yet exercised against the real cameras with the linked copy. The final switch to the GitHub commit is still open."},"at":"2026-09-19T18:35:14.935Z","parents":["evt-cvhvvpjdfgpd"],"hash":"40e2884a2ff00bd646d67bd25fbb66e7270dd810a3ca667bdef70d69dd254293"}
{"id":"evt-z00bf305k585","type":"node.status_changed","subject":"iss-a6j9jg5","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"in_progress"},"at":"2026-09-19T18:52:31.512Z","parents":["evt-0xvwtj5cvrk4"],"hash":"fe11293cda9edbcf51fb3285c8b1c5fb8bfc994b3484a6b90fd3ddc37567df15"}
{"id":"evt-1gmn972mpzv6","type":"node.updated","subject":"iss-h5e97mf","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"body":"Owned by the user, who works on the fork in a separate process. Claude must not change ~/WebstormProjects/onvif from the cameras session (user instruction, 2026-09-19). Per fnd-a9y3w97: branch from upstream master in the fork. imagingSettingsToBuild and its sub-builders send numeric fields when !== undefined; RateControl gets the ConstantBitRate attribute when provided; the imaging extension (NoiseReduction etc.) is serialized with ONVIF element names rather than passed through raw. Jest tests in the repo's style. Separate PR against agsh/onvif master. Lower priority than the v0.x fix; this app doesn't consume it.","owner":"mikemainguy"}},"at":"2026-09-19T18:55:47.316Z","parents":["evt-z00bf305k585"],"hash":"d84e61c63a82ea7c07f68e5fed2c4476cd2243a849c521447f52405e794aef86"}
{"id":"evt-rwknn9nza9n0","type":"node.created","subject":"dec-gan94zb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"decision","title":"The onvif fork is changed only by the user; the cameras session only verifies the app against it","body":"User instruction, 2026-09-19: \"Don't ever change code in the onvif repo from here, I'll do in another process. Just verify our code here against the fork.\" Applies to ~/WebstormProjects/onvif (clone of mikemainguy/onvif, linked into cameras with `npm install ../onvif --no-save`). From the cameras session: read files there, and run the cameras suite, tsc and build against it; no edits, commits, branch changes, installs or test runs inside the fork. If cameras needs a library change, describe it to the user. Context: while the user was rewriting the fix commits (now 6efbc258 and cc348ee1 on fix/v0-complete-setters) and fixing other bugs, Claude committed a typings change (8cdb8701, index.d.ts only) and ran the fork's test suite, which clashed on the mock server port. The user decides whether to keep 8cdb8701. Refines dec-70qga6k: the fork parts of iss-tsq9zc3 and iss-h5e97mf are the user's.","status":"recorded","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T18:55:50.987Z","parents":["evt-1gmn972mpzv6"],"hash":"e8fabb6b2d0497a0825bf40f1150c0df0fa01b793a63f3da08c45476ac3f9649"}
{"id":"evt-6znzd7szxckj","type":"edge.added","subject":"dec-gan94zb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"dec-gan94zb","to":"gol-8v1wqvp"},"at":"2026-09-19T18:55:50.990Z","parents":["evt-rwknn9nza9n0"],"hash":"abafd6ec2dee73dc4b8bf51a0733e991d51b68c1231014875c4d5763629e4e1a"}
{"id":"evt-tkwqn3eyh756","type":"node.created","subject":"ver-gmdmcap","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"camera-config.ts reads each camera's allowed image and stream values, validates patches (zod shape + the camera's options), merges the change into the current full settings, writes through the fork's setters, re-reads and reports each field as applied or adjusted; refusals are classified not-permitted vs failed with fixed text; stream writes drop the stale cached connection; every attempted write is appended to .data/audit.jsonl (0600, folder 0700) without secrets.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T18:59:36.493Z","parents":["evt-6znzd7szxckj"],"hash":"f058fe2e9ffe5cf1c50a12516fde82097e68442bad1714793e4b91845718a873"}
{"id":"evt-rppn517b2twa","type":"edge.added","subject":"ver-gmdmcap","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-gmdmcap","to":"iss-a6j9jg5"},"at":"2026-09-19T18:59:36.501Z","parents":["evt-tkwqn3eyh756"],"hash":"76ddf47e81d9810fb2fe1963ea944ad9c430d4f2c44421d03299a3288b1b12da"}
{"id":"evt-vevkkr666gw4","type":"verification.recorded","subject":"ver-gmdmcap","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"src/lib/camera-config.ts (readConfigOptions, updateImageSettings, updateStreamSettings, parse*/validate* helpers) and src/lib/audit-log.ts (AUDIT_LOG_FILE, default .data/audit.jsonl with the turbopackIgnore marker). Tests: camera-config.test.ts (an in-memory fake camera that stores settings; covers merge-preserves-everything incl. minGain 0 and noise reduction, nested WDR/BLC/exposure/WB, camera-adjusted values, text booleans, per-option validation for every field, shape/unknown-field/empty rejection before connecting, unsupported options, not-permitted vs failed with no 'hunter2' leak, login errors propagating, unknown or bare streams, Media2 declined, resetConnection on success and failure, options parsing incl. ONVIF Min/Max, single values and extension H264) and audit-log.test.ts (JSONL, modes 0600/0700, timestamps). camera-config.ts 99.4% lines. Full suite 564/564, coverage 99.81%, tsc, eslint and next build clean, run against the linked fork (node_modules/onvif -> ~/WebstormProjects/onvif), 2026-09-19. Not yet exercised against the real cameras: that's the no-op probe iss-hg50epa, which needs the user's go-ahead."},"at":"2026-09-19T18:59:36.502Z","parents":["evt-rppn517b2twa"],"hash":"c59d5f3c21f240452de159ff78bce8e579825c2606f103fcf425fdae72a21bf8"}
{"id":"evt-1dgapbwj4x52","type":"node.status_changed","subject":"iss-a6j9jg5","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"in_progress","to":"done"},"at":"2026-09-19T18:59:38.800Z","parents":["evt-vevkkr666gw4"],"hash":"8dd62ea375354c9b85c2e1638c72f8920ac7379c2e00667e4d05fd77c9da23d0"}
{"id":"evt-64685bn85a6x","type":"node.status_changed","subject":"iss-j8zzg1r","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"in_progress"},"at":"2026-09-19T19:04:53.623Z","parents":["evt-1dgapbwj4x52"],"hash":"a2fbc3214647959733ee6d01a574ccdc01a194a9f2c69290a5581caf5669eddb"}
{"id":"evt-zasar55qysy2","type":"node.updated","subject":"pri-8ev5m89","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"body":"Standing rule derived from dec-1vnz5fw (Lucide chosen 2026-09-19) and applied in iss-scj42jq, iss-chrdd0v, iss-egy082x, iss-s935a4n and iss-j8zzg1r.\n\n1. Source: icons come from lucide-react, imported by name (`import { KeyRound } from \"lucide-react\"`); Next 16 optimizes those imports by default. No other icon library, icon font or hand-drawn SVG icon without a new dependency question (pri-mz2jxpb). Check that a name exists in the installed version before using it.\n2. Meaning: an icon never carries meaning alone. Status, warnings and actions always have visible text (e.g. the status badge's 'Needs login'), and state is never conveyed by color or icon alone.\n3. Accessibility: icons next to text are decorative and get `aria-hidden`. A control with only an icon must have an `aria-label` that says what it does. Warnings keep role=\"alert\" and their text. Adding an icon must not change a control's accessible name.\n4. Sizing and layout: size with Tailwind classes, not the `size` prop: `size-3.5` in small text and badges, `size-4` in buttons, inline messages and section headings, `size-5` in banners, `size-6` in page headings. Lay out with `inline-flex items-center gap-1…2`, and use `shrink-0` beside wrapping text.\n5. Motion and color: spinners use LoaderCircle with `animate-spin motion-reduce:animate-none`. Colored icons and badges have `dark:` variants.\n6. Consistency: one concept, one icon across the app. Current mapping: Cctv = a camera (live status; camera page heading); LoaderCircle = in progress; KeyRound = login; ShieldAlert = unsecured or not activated; ShieldCheck = secured or signed in; WifiOff = offline; CircleAlert = error message; CircleCheck = success message; TriangleAlert = needs attention (setup needed, clock drift, stream-restart warning, read-only note); RefreshCw = retry or refresh; Radar = scan; Timer = refresh rate; Trash2 = forget; Lock = save a login, or the sign-in heading; Save = save settings; Pencil = edit; X = cancel; LogIn/LogOut = session; ExternalLink = opens another site in a new tab; ArrowLeft = back to the dashboard; Video = streams; SunMoon = image and day/night settings; Clock = time and NTP; Network = network settings; Copy → Check = copy to clipboard / copied. Reuse these; if a new concept needs an icon, add it here.\n7. Tests: assert icons by their `lucide-<name>` class with toContain, since Lucide also adds alias classes (Trash2 → 'trash' and 'trash-2'), and assert every rendered `svg.lucide` is aria-hidden (see src/app/icons.test.tsx).\n\nWhy: icons should make the dashboard faster to scan without making it less accessible or less consistent. Rules out: icon-only status, icons that change accessible names, ad-hoc SVGs, and mixing icon sets."}},"at":"2026-09-19T19:09:23.636Z","parents":["evt-64685bn85a6x"],"hash":"5bb2518e98f7a9eaab43076af5d489da30bb7b360f114c068f2fc9fc4f34ee58"}
{"id":"evt-vs08kwh1xr4y","type":"node.created","subject":"ver-c00gkf5","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"On /cameras/[id], Image and each stream with camera-reported options show Edit; forms offer only supported values (ranges in labels, browser-enforced min/max), send only changed fields, require an inline confirmation before a stream save, show applied/adjusted/invalid/not-permitted/session/camera outcomes, refresh the page on success, and turn a section read-only with guidance when the login isn't permitted; Server Actions check access, resolve the camera from the registry, record the actor, and map errors to fixed text.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T19:09:31.001Z","parents":["evt-zasar55qysy2"],"hash":"2bd201c522d4a26ee152aa23be1256cb49d132b69870f8d96402b6d35d856fcb"}
{"id":"evt-pb5q49zdvyz0","type":"edge.added","subject":"ver-c00gkf5","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-c00gkf5","to":"iss-j8zzg1r"},"at":"2026-09-19T19:09:31.003Z","parents":["evt-vs08kwh1xr4y"],"hash":"b4f53ffdbe9782ffb8df0816cb7905d079ed1164ccc935bf3aa5e7afb1bc9337"}
{"id":"evt-g4qev4pkp7n5","type":"verification.recorded","subject":"ver-c00gkf5","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"src/app/cameras/[id]/{actions.ts, save-result.ts, edit-forms.tsx}; settings-sections.tsx loads readConfigOptions alongside the settings (null means read-only). camera-settings.ts now also reads the WDR and BLC levels. Tests: actions.test.ts (actor, unsecured actor, signed-out, bad id/unknown/non-private host, fixed error text with no leak, stream token validation), edit-forms.test.tsx (supported values only, browser range blocking, changed-fields-only, no-change close, cancel reset, adjusted message, invalid issues keep the form, not-permitted → read-only, session message, hidden unsupported controls, stream confirm/back-out/no-change-no-warning, spinner, icons aria-hidden), settings-sections.test.tsx (Edit only where options exist; read-only when options fail). The confirmation is an inline role=alertdialog rather than a native <dialog>, which jsdom can't open. Full suite 603/603, coverage 99.83%, tsc, eslint and next build clean against the linked fork, 2026-09-19. Not yet used in a real browser: saving writes to a real camera."},"at":"2026-09-19T19:09:31.005Z","parents":["evt-pb5q49zdvyz0"],"hash":"d5708991b26b394ad788fe3964645e7baa4bfe77a60b62954abd778104874609"}
{"id":"evt-6qsfagkwfnp6","type":"node.status_changed","subject":"iss-j8zzg1r","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"in_progress","to":"done"},"at":"2026-09-19T19:09:33.608Z","parents":["evt-g4qev4pkp7n5"],"hash":"cd04f3d5466b3322dfc9b41217769e04de0699ce92a5cadd5ff22e30e4202bdb"}
{"id":"evt-xsbkdj3w1rfd","type":"node.created","subject":"ver-8vzn88e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"package.json pins onvif to the user's pushed fork commit; installs work without SSH keys; the installed library sends zero values, WDR/BLC/white balance/noise reduction in schema order, and the encoder H264Profile, quality 0 and ConstantBitRate; the full suite, tsc, eslint and next build pass on a real (non-symlink) install.","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T19:18:11.387Z","parents":["evt-6qsfagkwfnp6"],"hash":"b478e0dfd11de884aa4c96caf3ab9fbc51932b6b55728b1ca21f1c4cc50c8cb7"}
{"id":"evt-tzp396yw6eqv","type":"edge.added","subject":"ver-8vzn88e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-8vzn88e","to":"iss-7p7pt6q"},"at":"2026-09-19T19:18:11.391Z","parents":["evt-xsbkdj3w1rfd"],"hash":"38639974535edaf4ae76c3c26c9da88cf605b78617354aa7bcd7024fafde2e05"}
{"id":"evt-yshbbgrh4bb4","type":"verification.recorded","subject":"ver-8vzn88e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"2026-09-19: the user pushed fix/v0-complete-setters to github.com/mikemainguy/onvif as a single commit e8c340fdf97cf4a42da23ce810b4ecdd8c93dd4b (git ls-remote; matches the local fork HEAD). package.json: \"onvif\": \"github:mikemainguy/onvif#e8c340fd…\"; package-lock resolved git+ssh://…#e8c340fd…, but `GIT_SSH_COMMAND=/usr/bin/false npm pack github:…#e8c340fd` fetched https://codeload.github.com/mikemainguy/onvif/tar.gz/e8c340fd… (200), so no SSH key is needed. node_modules/onvif is a real directory. The installed library's XML, captured with a stubbed _request (no network): Brightness/ColorSaturation/Contrast/Sharpness/MinGain 0 sent; BacklightCompensation(Mode ON, Level 0), WideDynamicRange(ON, 50), WhiteBalance(MANUAL, CrGain 0, CbGain 64), NoiseReduction(Level 0.5); ImagingSettings20 order true; encoder Quality 0, H264Profile Main, GovLength 50, BitrateLimit 10240, ConstantBitRate=\"true\". Vitest 603/603 (99.83%), tsc, eslint and next build clean. The upstream PR URL isn't known yet (the user is opening it)."},"at":"2026-09-19T19:18:11.392Z","parents":["evt-tzp396yw6eqv"],"hash":"0dd271a699d3bf203622733e83131c9d9cee4e8079d958c995e32d8f3b109796"}
{"id":"evt-gzc0f3j2yxpq","type":"node.status_changed","subject":"iss-7p7pt6q","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T19:18:13.781Z","parents":["evt-yshbbgrh4bb4"],"hash":"bad4fc99981d78e094a6e64af93ad951b5d4004289964f3924287b5069236e6a"}
{"id":"evt-88ym20js8h46","type":"node.created","subject":"iss-pkp09k8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Track the upstream onvif PR and switch back to the npm release when it ships","body":"The app is pinned to github:mikemainguy/onvif#e8c340fd (the user's fork, fix/v0-complete-setters). The user opens the PR against agsh/onvif v0.x themselves. When known, record the PR URL here. When agsh/onvif publishes a 0.8.x containing the fixes, change package.json back to the npm version, reinstall, and re-verify: the XML capture of zero values, WDR/BLC/WB/NR, H264Profile and CBR; the full suite and build. The fork itself is only changed by the user (dec-gan94zb).","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T19:18:16.291Z","parents":["evt-gzc0f3j2yxpq"],"hash":"ab1a0850ef44dae1099c7c2ce0ef41690aadcb6cb0ba464cc7e36b3910671fee"}
{"id":"evt-9nxje0x317x3","type":"edge.added","subject":"iss-pkp09k8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-pkp09k8","to":"gol-8v1wqvp"},"at":"2026-09-19T19:18:16.294Z","parents":["evt-88ym20js8h46"],"hash":"a4a41806e66b45bdb1b8a9cde6b8101845aaef49b818e9655402a30a0140a096"}
{"id":"evt-88tca49p5s8x","type":"edge.added","subject":"iss-pkp09k8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-pkp09k8","to":"package.json"},"at":"2026-09-19T19:18:16.295Z","parents":["evt-9nxje0x317x3"],"hash":"643b7991ca155b00322e12f46103e590190ce900dce487a7778b14626b75e3e7"}
{"id":"evt-n8w66k6rzm32","type":"edge.added","subject":"iss-pkp09k8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-pkp09k8","to":"package-lock.json"},"at":"2026-09-19T19:18:16.298Z","parents":["evt-88tca49p5s8x"],"hash":"49e82affa4a579892df11caf9a4ef819b84446a1a393fcbe022d58a8029cfec6"}
{"id":"evt-zzaefas56b3h","type":"edge.added","subject":"iss-3g6m4rz","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"parent_of","from":"iss-3g6m4rz","to":"iss-pkp09k8"},"at":"2026-09-19T19:18:16.299Z","parents":["evt-n8w66k6rzm32"],"hash":"a5ac6518875f36afa8c8e62aec41bda85127164fcbcab70157f7e6baa767d972"}
{"id":"evt-93p86as1f298","type":"edge.added","subject":"iss-pkp09k8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-pkp09k8","to":"area:upstream"},"at":"2026-09-19T19:18:16.300Z","parents":["evt-zzaefas56b3h"],"hash":"eb42d06e9518d67c55eb37e54a1647e6dd91d7b0cdcacd02bf16861f21cf8d20"}
{"id":"evt-kb2qn15pc02p","type":"node.updated","subject":"iss-pkp09k8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"attrs":{"revisit":null,"deferred_because":"Waiting on the user's upstream PR and an agsh/onvif 0.8.x release; nothing to do until then."}}},"at":"2026-09-19T19:18:21.727Z","parents":["evt-93p86as1f298"],"hash":"33ee7d6b0d1da40ceca8ddfeb290ffa3c09c5bf2829b51bd578ed3b651857bb1"}
{"id":"evt-7t2g4efw1f8j","type":"node.status_changed","subject":"iss-pkp09k8","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"deferred"},"at":"2026-09-19T19:18:21.730Z","parents":["evt-kb2qn15pc02p"],"hash":"d9153bc187cb56b54ed43d18d489fc77c0fc18004a358dd67b3634ca2a8856aa"}
{"id":"evt-zmqdfxf58qkx","type":"node.created","subject":"fnd-cw8aw95","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"finding","title":"On the I91ET, Brightness only affects the picture in Auto exposure; in Manual exposure it has no visible effect","body":"Reported by the user on 2026-09-19 after editing camera …af54 (192.168.31.2) through the new Edit → Save UI. The audit log shows every write was applied exactly as requested (exposure AUTO↔MANUAL several times, brightness and contrast 1–90, backlight compensation OFF), so the values aren't being inverted by the app. With exposure AUTO, changing brightness visibly changes the image; with exposure MANUAL it doesn't. Consistent with ONVIF exposure semantics: in MANUAL the camera uses fixed ExposureTime/Gain (Iris) rather than aiming at a brightness target. The edit form currently offers only the exposure *mode*, not exposure time or gain, so in Manual it offers no control that changes the image. Not yet verified: which exposure time and gain the camera holds in MANUAL, and the ranges it reports in Imaging GetOptions (needs a read-only check).","status":"current","owner":"prn-q80g8mz","attrs":{"sources":[{"node":"iss-j8zzg1r","note":"user's hands-on test of the Edit UI, 2026-09-19; .data/audit.jsonl entries 19:25–19:29Z for camera …af54"}],"as_of":"2026-09-19"}},"at":"2026-09-19T19:33:22.532Z","parents":["evt-7t2g4efw1f8j"],"hash":"4984eca7f3841252aefb7f9c90ee31c080e19dadbe5a9840a34d2cfe7fe8a116"}
{"id":"evt-krfypg1ybg9n","type":"edge.added","subject":"fnd-cw8aw95","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-cw8aw95","to":"gol-8v1wqvp"},"at":"2026-09-19T19:33:22.535Z","parents":["evt-zmqdfxf58qkx"],"hash":"1985e6cdfc7f22cdd06bdb5beea2d27865f13217816358eeafa98ebf26aa1fab"}
{"id":"evt-6hf42apcp1xn","type":"node.created","subject":"dec-6hbye54","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"decision","title":"Manual exposure gets exposure time and gain controls; Brightness is noted as mainly an Auto-mode setting","body":"User's choice, 2026-09-19, after fnd-cw8aw95. When Exposure is Manual, the Image edit form shows Exposure time and Gain, limited to the ranges the camera reports in Imaging GetOptions, and notes that Brightness mainly affects the picture in Auto exposure. Rejected: a hint only; hiding Manual. The user permitted a read-only check of camera …af54's exposure settings and options to learn the real ranges and units (recorded redacted per dec-kgm44qw).","status":"recorded","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T19:35:13.650Z","parents":["evt-krfypg1ybg9n"],"hash":"aec09b8ae4b5ea049d7cceaf23a0afcebfaa4b3953cf88a532a0d06ee8d381c7"}
{"id":"evt-2m4zs2werqer","type":"edge.added","subject":"dec-6hbye54","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"dec-6hbye54","to":"gol-8v1wqvp"},"at":"2026-09-19T19:35:13.653Z","parents":["evt-6hf42apcp1xn"],"hash":"17a2b97005592e0a882b96538eada62f2cb0bea681d8babef5f7e4a289fb2809"}
{"id":"evt-528ny7rbxmf5","type":"node.created","subject":"fnd-qe3heme","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"finding","title":"Camera B (I91ET) reports manual exposure ranges of exposure time 10–1,000,000 µs and gain 0–100, but no current manual values while in Auto","body":"Read-only check, 2026-09-19, with the user's permission (dec-6hbye54), on the second I91ET (identifiers omitted per dec-kgm44qw). GetImagingSettings while in AUTO: exposure {mode AUTO, minExposureTime 10, maxExposureTime 33333, minGain 0, maxGain 100}, with no ExposureTime or Gain fields; brightness and contrast 50. Imaging GetOptions exposure: mode [MANUAL, AUTO]; minExposureTime 10–333333; maxExposureTime 33–1000000; minGain 0–0; maxGain 0–100; exposureTime 10–1000000; gain 0–100. ONVIF exposure time is in microseconds (33333 µs ≈ 1/30 s). Implication: switching to MANUAL without sending ExposureTime/Gain leaves the camera on internal manual values the app can't see; the Manual form must offer exposure time (µs) and gain within these ranges.","status":"current","owner":"prn-q80g8mz","attrs":{"sources":[{"node":"dec-6hbye54","note":"read-only GetImagingSettings + Imaging GetOptions via a scratch script using the app's credential store; nothing written"}],"as_of":"2026-09-19"}},"at":"2026-09-19T19:35:40.340Z","parents":["evt-2m4zs2werqer"],"hash":"95160f7cc1886c065cf730b8a6e3e8df324c6c4083c5e425c72b003d9c9b78b6"}
{"id":"evt-t2zp5280rn8w","type":"edge.added","subject":"fnd-qe3heme","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-qe3heme","to":"gol-8v1wqvp"},"at":"2026-09-19T19:35:40.342Z","parents":["evt-528ny7rbxmf5"],"hash":"fab4c6182eefcd737b30bb57227cef20c4835d437d115ab7b3329cf0e4906da3"}
{"id":"evt-c4ap15mmw52s","type":"node.created","subject":"iss-tgk8sgp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Manual exposure: exposure time and gain controls, plus a Brightness hint","body":"Per dec-6hbye54 and fnd-cw8aw95. Read exposureTime/gain (settings) and their ranges (options.exposure.exposureTime/gain). Patch fields exposureTime (µs) and gain, validated against the ranges and allowed only when the resulting exposure mode is MANUAL. In the Image form, when Exposure is Manual, show 'Exposure time (µs)' with an approximate shutter speed (e.g. ≈1/30 s) and 'Gain', and a note that Brightness mainly works in Auto. On the read-only page, show exposure time and gain when in Manual. Tests with a fake camera.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T19:35:43.614Z","parents":["evt-t2zp5280rn8w"],"hash":"51439638adc150cfbf507f3f4c06e5f52fcc030c4ab9e62d9ce9ff8c7f36f77b"}
{"id":"evt-qdygw3ytgt9s","type":"edge.added","subject":"iss-tgk8sgp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-tgk8sgp","to":"gol-8v1wqvp"},"at":"2026-09-19T19:35:43.615Z","parents":["evt-c4ap15mmw52s"],"hash":"f69baaa2cb46e7f9abf7b8df69aaaa79f4e0eeb29164580ff8a902f237d1bb81"}
{"id":"evt-wk1wg83thy5f","type":"edge.added","subject":"iss-tgk8sgp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-tgk8sgp","to":"src/lib/camera-config.ts"},"at":"2026-09-19T19:35:43.616Z","parents":["evt-qdygw3ytgt9s"],"hash":"dec41409c6bb98659ec85c778569b2e32ea30915838a91d3fc21a6052accceff"}
{"id":"evt-a3tazav34bqf","type":"edge.added","subject":"iss-tgk8sgp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-tgk8sgp","to":"src/lib/camera-settings.ts"},"at":"2026-09-19T19:35:43.617Z","parents":["evt-wk1wg83thy5f"],"hash":"58ffb5202a9134f3660e9781fd4f00d5efcb2523c9de48ca55ebb64a59cf8c39"}
{"id":"evt-pk564v4t2wct","type":"edge.added","subject":"iss-tgk8sgp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-tgk8sgp","to":"src/app/cameras/[id]/"},"at":"2026-09-19T19:35:43.618Z","parents":["evt-a3tazav34bqf"],"hash":"f8e29269b199d1110ce142eb6b5d503c4e60eb8e576b9d0d9b67cc6eb7538fd2"}
{"id":"evt-0xreayvmgbbx","type":"edge.added","subject":"iss-3g6m4rz","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"parent_of","from":"iss-3g6m4rz","to":"iss-tgk8sgp"},"at":"2026-09-19T19:35:43.619Z","parents":["evt-pk564v4t2wct"],"hash":"218259baf86a1197b50cc129a4a3d2452327e6ce0fcb45a6b1c3efda168f22c2"}
{"id":"evt-avy8vfpkpcq8","type":"edge.added","subject":"iss-tgk8sgp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-tgk8sgp","to":"area:config"},"at":"2026-09-19T19:35:43.620Z","parents":["evt-0xreayvmgbbx"],"hash":"6db27db0e3a90f71849a8ec76eb92ca26490fe8eeefb137bfd4f83d44f73cad8"}
{"id":"evt-9s26vvdzc2x4","type":"edge.added","subject":"iss-tgk8sgp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-tgk8sgp","to":"area:ui"},"at":"2026-09-19T19:35:43.621Z","parents":["evt-avy8vfpkpcq8"],"hash":"f440827328599a428fcaeb432d78546fc6a22fc5b0e7d9ba5ec5a71fc7204c4b"}
{"id":"evt-r6rfyn75st1t","type":"node.created","subject":"ver-558kxkj","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"Exposure time and gain are read, validated against the camera's ranges, refused outside Manual, and edited only in Manual (with the Brightness hint); the settings page shows them in Manual; suite, lint, type-check and build are clean","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T19:39:16.724Z","parents":["evt-9s26vvdzc2x4"],"hash":"309b8f8c9aebb28c9bddcd6948c1df72e5133702d68349ea3fc7506a4767545e"}
{"id":"evt-rhjtpn32v25k","type":"edge.added","subject":"ver-558kxkj","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-558kxkj","to":"iss-tgk8sgp"},"at":"2026-09-19T19:39:16.725Z","parents":["evt-r6rfyn75st1t"],"hash":"bff0ecb0dddc4a83591727605317d1546cf8226728c4bf72bd8e015bcb73346f"}
{"id":"evt-7x4acxzsv7z5","type":"verification.recorded","subject":"ver-558kxkj","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"npx vitest run --coverage → 43 files, 621 tests pass (99.83% lines); npx tsc --noEmit and npx eslint src test clean; npx next build compiles. Tests: camera-config.test.ts \"manual exposure (fnd-qe3heme)\", camera-settings.test.ts, format.test.ts shutterLabel, edit-forms.test.tsx \"manual exposure\", settings-sections.test.tsx \"manual exposure\""},"at":"2026-09-19T19:39:16.726Z","parents":["evt-rhjtpn32v25k"],"hash":"53eff9f5ff1c2df4345ac75d5ad35c6712940b1f53928f7612abbcfd20ae677f"}
{"id":"evt-0vvcqqxzqrfn","type":"node.created","subject":"ver-hvwbxtk","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"On a real camera, switching to Manual and changing exposure time or gain visibly changes image brightness, and the camera applies the values (audit log shows applied or adjusted)","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T19:39:18.254Z","parents":["evt-7x4acxzsv7z5"],"hash":"59ceccf2e4f3e3d7f4ea5f43da7615c739d0ef08beff64bcc7c1f07435e69bbc"}
{"id":"evt-s8nde3943p84","type":"edge.added","subject":"ver-hvwbxtk","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-hvwbxtk","to":"iss-tgk8sgp"},"at":"2026-09-19T19:39:18.256Z","parents":["evt-0vvcqqxzqrfn"],"hash":"1dee7ac0b11da43a088635c03e6c194a44f3527e633415c6321694c8f4d80bed"}
{"id":"evt-a68ygks3mdx4","type":"verification.recorded","subject":"ver-hvwbxtk","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pending","evidence":"User check in the browser after restarting next start; confirm with .data/audit.jsonl entries for exposureTime/gain"},"at":"2026-09-19T19:39:18.257Z","parents":["evt-s8nde3943p84"],"hash":"f17d70f87c7e602476628ac662da52f91656522530b562790587be99282b3a0b"}
{"id":"evt-rzaq1r37f1ms","type":"node.created","subject":"ver-rc4ddd9","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"On a real camera, switching to Manual and changing exposure time or gain visibly changes image brightness, and the camera applies the values","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T19:42:39.732Z","parents":["evt-a68ygks3mdx4"],"hash":"59cd5a99bfb38ca054d7383f03daa95a132d6c47df4be5494ce3ea72c8443bb1"}
{"id":"evt-4wpwv4znk8dk","type":"edge.added","subject":"ver-rc4ddd9","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-rc4ddd9","to":"iss-tgk8sgp"},"at":"2026-09-19T19:42:39.733Z","parents":["evt-rzaq1r37f1ms"],"hash":"57bd2476c8d3e43d1b3aa3044bba681fc59fca90c6b2f8e7c74755e4b45678ce"}
{"id":"evt-jyvqv0nkqwjm","type":"verification.recorded","subject":"ver-rc4ddd9","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"User confirmed in the browser on 2026-09-19 (\"works great\"); writes recorded in .data/audit.jsonl"},"at":"2026-09-19T19:42:39.734Z","parents":["evt-4wpwv4znk8dk"],"hash":"634f269dd74fbe34fa6171e2ea5eb66498187114d203d6ea6b37c8c321926ec6"}
{"id":"evt-stk5b41r6c4q","type":"node.status_changed","subject":"iss-tgk8sgp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T19:42:41.194Z","parents":["evt-jyvqv0nkqwjm"],"hash":"38edee0c3d01922a86f1169052b15d0932f80dbe34894838c539a935c1c8df8a"}
{"id":"evt-crahg3cgq3se","type":"node.created","subject":"iss-p7vdbng","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Note which image settings mostly affect Auto exposure while Exposure is Manual","body":"In the Image edit form, while Exposure is Manual, show \"Mostly affects Auto exposure\" under Brightness and Backlight compensation. The fields stay visible and editable: the camera keeps their values and uses them again in Auto, and hiding fields would make the form jump around. First test on a real camera in Manual: Brightness 0 vs 100, and BLC off vs on. If one truly changes nothing, the note says \"No effect in Manual\" instead. Contrast, saturation, sharpness, white balance and day/night still apply in Manual and get no note.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T19:43:50.854Z","parents":["evt-stk5b41r6c4q"],"hash":"921f82f1eebb2060f2f8d585ee9d1bb28f2b4c874678d88c51fc1713e52ee6ec"}
{"id":"evt-rgxyz1vn75ky","type":"edge.added","subject":"iss-p7vdbng","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-p7vdbng","to":"gol-8v1wqvp"},"at":"2026-09-19T19:43:50.856Z","parents":["evt-crahg3cgq3se"],"hash":"82b0e99152984b3f31c25e98ff01e25c8bea844775e557216d93ccbb65050fd1"}
{"id":"evt-4bnfbpmc0ey6","type":"edge.added","subject":"iss-p7vdbng","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-p7vdbng","to":"src/app/cameras/[id]/edit-forms.tsx"},"at":"2026-09-19T19:43:50.857Z","parents":["evt-rgxyz1vn75ky"],"hash":"7cb30d41bde30c172326c0ed2c9a78504227cdd4290fe14ef1251d83f712c28d"}
{"id":"evt-jgwjmsj36n2n","type":"edge.added","subject":"iss-p7vdbng","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-p7vdbng","to":"area:ui"},"at":"2026-09-19T19:43:50.858Z","parents":["evt-4bnfbpmc0ey6"],"hash":"25f02f7dc52f4bda1a43a5cdc322999d28fd2ebae314255039894301476c593e"}
{"id":"evt-a2q4cc2fn7wc","type":"edge.added","subject":"iss-p7vdbng","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"discovered_from","from":"iss-p7vdbng","to":"iss-tgk8sgp"},"at":"2026-09-19T19:43:50.859Z","parents":["evt-jgwjmsj36n2n"],"hash":"81b034aef5c9e15d27ccf4958a8c65e9c601ba248a3456a397fe983e8776a263"}
{"id":"evt-zndg9fqn2k5m","type":"node.created","subject":"iss-mt5zv64","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Pop out a camera's live snapshot into its own window","body":"A \"Pop out\" button on each dashboard card opens /cameras/[id]/live in a named popup window (window.open with name camera-<id>, so a second click reuses it). The page shows only the frame, fit to the window (object-contain, dark background), polled through the same useCameraSnapshot hook as the card (pauses when hidden, stops on error with Retry). It starts at the dashboard's refresh rate via ?refresh= and has its own rate selector. A small overlay shows the camera name and the time of the last frame, and fades when the mouse is still. While a pop-out is open, the card pauses its own polling, dims its frame and says \"Showing in pop-out\", so the camera isn't polled twice. Pop-out and card coordinate over a BroadcastChannel (the pop-out announces open and closed, and answers a ping from a card that mounts later), so a dashboard reload still knows. The page checks access and validates the id against the registry like /cameras/[id] (pri-m1csgrm). No new server routes or dependencies: the existing snapshot route already serves the main stream at full resolution. Decided with the user 2026-09-19: pause the card; fit-to-window only (no 1:1 view); name and time overlay. Out of scope: real video (RTSP to browser).","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T19:47:15.358Z","parents":["evt-a2q4cc2fn7wc"],"hash":"e8106def3ebd13135e7a905f0538b9853c536eca595daa57dc81c9e30f0094be"}
{"id":"evt-1qjzxa73h42z","type":"edge.added","subject":"iss-mt5zv64","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-mt5zv64","to":"gol-6q1q5mr"},"at":"2026-09-19T19:47:15.359Z","parents":["evt-zndg9fqn2k5m"],"hash":"a3adb272547f5320a27bb0c693aaff34cb3e4e71e6aaf468093cf99cc3213208"}
{"id":"evt-a5nfzwqnma1t","type":"edge.added","subject":"iss-mt5zv64","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-mt5zv64","to":"src/app/camera-card.tsx"},"at":"2026-09-19T19:47:15.360Z","parents":["evt-1qjzxa73h42z"],"hash":"6b409ebfb4c22448a8bdd3e31d38ca12dd81b5f1bce739afe6c19174b16b58a7"}
{"id":"evt-hksd7r1mqnmd","type":"edge.added","subject":"iss-mt5zv64","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-mt5zv64","to":"src/app/cameras/[id]/live/page.tsx"},"at":"2026-09-19T19:47:15.361Z","parents":["evt-a5nfzwqnma1t"],"hash":"713d7ef79bf2c71958c0e8480745796c7dc03008dfe4c76e3ad6ff3561828bcf"}
{"id":"evt-at4cj275af2j","type":"edge.added","subject":"iss-mt5zv64","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-mt5zv64","to":"area:ui"},"at":"2026-09-19T19:47:15.362Z","parents":["evt-hksd7r1mqnmd"],"hash":"30674fbab77e34bf702390a30c47b3889f7d46173b81055ec0b194ec5837f727"}
{"id":"evt-zkbjn3129r2j","type":"node.updated","subject":"pri-8ev5m89","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"body":"Standing rule derived from dec-1vnz5fw (Lucide chosen 2026-09-19) and applied in iss-scj42jq, iss-chrdd0v, iss-egy082x, iss-s935a4n, iss-j8zzg1r and iss-mt5zv64.\n\n1. Source: icons come from lucide-react, imported by name (`import { KeyRound } from \"lucide-react\"`); Next 16 optimizes those imports by default. No other icon library, icon font or hand-drawn SVG icon without a new dependency question (pri-mz2jxpb). Check that a name exists in the installed version before using it.\n2. Meaning: an icon never carries meaning alone. Status, warnings and actions always have visible text (e.g. the status badge's 'Needs login'), and state is never conveyed by color or icon alone.\n3. Accessibility: icons next to text are decorative and get `aria-hidden`. A control with only an icon must have an `aria-label` that says what it does. Warnings keep role=\"alert\" and their text. Adding an icon must not change a control's accessible name.\n4. Sizing and layout: size with Tailwind classes, not the `size` prop: `size-3.5` in small text and badges, `size-4` in buttons, inline messages and section headings, `size-5` in banners, `size-6` in page headings. Lay out with `inline-flex items-center gap-1…2`, and use `shrink-0` beside wrapping text.\n5. Motion and color: spinners use LoaderCircle with `animate-spin motion-reduce:animate-none`. Colored icons and badges have `dark:` variants.\n6. Consistency: one concept, one icon across the app. Current mapping: Cctv = a camera (live status; camera page heading); LoaderCircle = in progress; KeyRound = login; ShieldAlert = unsecured or not activated; ShieldCheck = secured or signed in; WifiOff = offline; CircleAlert = error message; CircleCheck = success message; TriangleAlert = needs attention (setup needed, clock drift, stream-restart warning, read-only note); RefreshCw = retry or refresh; Radar = scan; Timer = refresh rate; Trash2 = forget; Lock = save a login, or the sign-in heading; Save = save settings; Pencil = edit; X = cancel; LogIn/LogOut = session; ExternalLink = opens another site in a new tab; AppWindow = a camera's pop-out window (the Pop out link, and \"Showing in pop-out\"); ArrowLeft = back to the dashboard; Video = streams; SunMoon = image and day/night settings; Clock = time and NTP; Network = network settings; Copy → Check = copy to clipboard / copied. Reuse these; if a new concept needs an icon, add it here.\n7. Tests: assert icons by their `lucide-<name>` class with toContain, since Lucide also adds alias classes (Trash2 → 'trash' and 'trash-2'), and assert every rendered `svg.lucide` is aria-hidden (see src/app/icons.test.tsx).\n\nWhy: icons should make the dashboard faster to scan without making it less accessible or less consistent. Rules out: icon-only status, icons that change accessible names, ad-hoc SVGs, and mixing icon sets."}},"at":"2026-09-19T19:50:35.607Z","parents":["evt-at4cj275af2j"],"hash":"7ba99cb1a4c0fb1d42eae43a577770b33b158ff7338e0d0839c30a4499d52015"}
{"id":"evt-t3hqkac1y365","type":"node.created","subject":"ver-y1f3cw0","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"Pop out link opens /cameras/[id]/live in a named popup sized to the main stream (falls back to a link if blocked); the live page checks access and validates the camera; the view polls, fades its overlay, handles errors; the card pauses polling while its pop-out is open and resumes after; suite, lint, type-check and build clean","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T19:50:38.519Z","parents":["evt-zkbjn3129r2j"],"hash":"77a58abe109a255f35159a4036ea9d96d6e365f7c9056fce77f35c2df0c629f7"}
{"id":"evt-wsygcbp4wew9","type":"edge.added","subject":"ver-y1f3cw0","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-y1f3cw0","to":"iss-mt5zv64"},"at":"2026-09-19T19:50:38.520Z","parents":["evt-t3hqkac1y365"],"hash":"d39a775088f73bd03d05f89a4d970a10d29aaec61c0699b3a9d56f7d66b79ca6"}
{"id":"evt-awbc9xdfygnz","type":"verification.recorded","subject":"ver-y1f3cw0","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"npx vitest run --coverage → 46 files, 647 tests pass (99.84% lines); tsc, eslint clean; next build lists ƒ /cameras/[id]/live. Tests: src/app/popout.test.ts, camera-card.test.tsx \"pop-out (iss-mt5zv64)\", src/app/cameras/[id]/live/page.test.tsx, live-view.test.tsx"},"at":"2026-09-19T19:50:38.521Z","parents":["evt-wsygcbp4wew9"],"hash":"f52cf400fc79482e2ed255df3facb4da8a71a45cc2bde1c90f61d83a850a5626"}
{"id":"evt-bccr7jx8j9dy","type":"node.created","subject":"ver-2weq6nk","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"In a real browser: Pop out opens one window per camera at full size, the card shows \"Showing in pop-out\" and stops polling, and closing the window resumes the card","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T19:50:39.957Z","parents":["evt-awbc9xdfygnz"],"hash":"992c903df63aab8e47a57ef2fc140dea75046b1eeb53f594bf39b391ec4eb49b"}
{"id":"evt-6dx2683hh7ra","type":"edge.added","subject":"ver-2weq6nk","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-2weq6nk","to":"iss-mt5zv64"},"at":"2026-09-19T19:50:39.958Z","parents":["evt-bccr7jx8j9dy"],"hash":"67b8183d4ae87a3720d54b74aaaeb3865dbe04d2b07091a69690ebf69e0fc508"}
{"id":"evt-gj6095st959x","type":"verification.recorded","subject":"ver-2weq6nk","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pending","evidence":"User check after restarting next start"},"at":"2026-09-19T19:50:39.959Z","parents":["evt-6dx2683hh7ra"],"hash":"8f0ceb4c6300c7647d4aeb1d4ff649266f5008131d3774b7a32d886d37c88b39"}
{"id":"evt-bjgmx6g72taa","type":"node.status_changed","subject":"iss-mt5zv64","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"in_progress"},"at":"2026-09-19T19:50:44.014Z","parents":["evt-gj6095st959x"],"hash":"88a401dabb3bc34c0f4cad50d0a1dab607feb891ca232ddaef4af1e81d3850b3"}
{"id":"evt-2n1cbeknh1hw","type":"node.created","subject":"fnd-dw1pqcw","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"finding","title":"The ONVIF snapshot on camera …af54 is 1200×536 (sub-stream size) for every profile, not the 4096×1860 main stream","body":"Checked read-only on 2026-09-19 through our /api/cameras/…af54/snapshot route in the user's signed-in browser. Profile_1 (mainStream, 4096×1860) and Profile_2 (subStream, 1200×536) both return a 1200×536 baseline JPEG: about 206 KB, 4:2:0 chroma, luma quantization ≈ quality 81. At 1:1 the camera's own image already shows stair-stepped diagonals and jagged on-screen-display text, which suggests a cheap downscale inside the camera. The dashboard then shows it about 2x smaller on the card and scales it up in the pop-out (about 2x on a Retina display), which adds softness. So the odd antialiasing starts in the camera, and the pop-out makes it worse. A full-resolution still likely needs Hikvision ISAPI (/ISAPI/Streaming/channels/101/picture), which is not yet verified on this camera.","status":"current","owner":"prn-q80g8mz","attrs":{"sources":[{"url":"http://localhost:3000/api/cameras/22ec0000-8b90-11b5-845d-d03bf404af54/snapshot","note":"Local app route; decoded in the browser with createImageBitmap, JPEG markers parsed by hand; ?profile=Profile_1 and Profile_2 compared"}],"as_of":"2026-09-19"}},"at":"2026-09-19T19:57:38.977Z","parents":["evt-bjgmx6g72taa"],"hash":"259053e14b8104cbb93df275cdaba6164f28c3c6ffb15d9f46a07ac729665df5"}
{"id":"evt-kd4byxatx87j","type":"edge.added","subject":"fnd-dw1pqcw","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-dw1pqcw","to":"iss-mt5zv64"},"at":"2026-09-19T19:57:38.979Z","parents":["evt-2n1cbeknh1hw"],"hash":"4409b9e07be659e882ffeea14901c1e2dbff7bf5a4e7870d525bcadc643da55d"}
{"id":"evt-vyq8f62h333a","type":"edge.added","subject":"fnd-dw1pqcw","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-dw1pqcw","to":"gol-6q1q5mr"},"at":"2026-09-19T19:57:38.980Z","parents":["evt-kd4byxatx87j"],"hash":"23bd950e2c1d2bfae489d8cda7ae04028f32982efa3dc565885e079e68827245"}
{"id":"evt-5rfzp4kpkz0d","type":"node.created","subject":"iss-3vck3js","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Probe Hikvision ISAPI for full-resolution snapshots","body":"Per fnd-dw1pqcw, the ONVIF snapshot is 1200×536 whatever the profile, while the main stream is 4096×1860. Check read-only whether GET /ISAPI/Streaming/channels/101/picture (digest auth) returns a main-stream-size JPEG on camera …af54. Record the result as a finding: its size and quality, and which login it accepts. Hikvision keeps ONVIF users separate from web users, so the Operator ONVIF login (\"camera\") may be refused. If so, find out what a least-privilege web account needs before designing anything. If it works: add the ISAPI snapshot behind the vendor module (pri-p9h51hx), with ONVIF as the fallback, and let the pop-out use it. Needs the user's go-ahead before touching the camera, and before adding any second per-camera login.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T20:01:06.904Z","parents":["evt-vyq8f62h333a"],"hash":"e0fb3c278175eb1483fe0380921b0fbc8fc7e2a41c37f3f74fb2e7c6e8e70098"}
{"id":"evt-jarrqz4865sf","type":"edge.added","subject":"iss-3vck3js","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-3vck3js","to":"gol-6q1q5mr"},"at":"2026-09-19T20:01:06.906Z","parents":["evt-5rfzp4kpkz0d"],"hash":"6007c607d1a2eb7c5fa87a4c80f5984b5adabf70fc07f42bd0c69ecd601b74e6"}
{"id":"evt-0829k70der31","type":"edge.added","subject":"iss-3vck3js","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-3vck3js","to":"src/lib/camera.ts"},"at":"2026-09-19T20:01:06.907Z","parents":["evt-jarrqz4865sf"],"hash":"59ab8ec52dd841497404d4046aa5dde4e34431aef90fc7fe718835dde23d44f9"}
{"id":"evt-ydfbgq1g5s2n","type":"edge.added","subject":"iss-3vck3js","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-3vck3js","to":"area:device"},"at":"2026-09-19T20:01:06.908Z","parents":["evt-0829k70der31"],"hash":"6b06fe095e04a92397f9de06766ffb13f1b185da49504cb3b597cd6c3dc26641"}
{"id":"evt-867k0s2fnszr","type":"edge.added","subject":"iss-3vck3js","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"discovered_from","from":"iss-3vck3js","to":"iss-mt5zv64"},"at":"2026-09-19T20:01:06.909Z","parents":["evt-ydfbgq1g5s2n"],"hash":"d1d3d770037df84343164748ec8b359c9a8425ac28d8ef771576695e3bc9bffe"}
{"id":"evt-qjekzvjdf9yf","type":"node.updated","subject":"iss-mt5zv64","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"body":"A \"Pop out\" button on each dashboard card opens /cameras/[id]/live in a named popup window (window.open with name camera-<id>, so a second click reuses it). The page shows only the frame, centered on a dark background at no more than its own size: fnd-dw1pqcw found the ONVIF snapshot is only 1200×536, so stretching it would only blur it. The frame is polled through the same useCameraSnapshot hook as the card (pauses when hidden, stops on error with Retry). It starts at the dashboard's refresh rate via ?refresh= and has its own rate selector. A small overlay shows the camera name and the time of the last frame, and fades when the mouse is still. While a pop-out is open, the card pauses its own polling, dims its frame and says \"Showing in pop-out\", so the camera isn't polled twice. Pop-out and card coordinate over a BroadcastChannel (the pop-out announces open and closed, and answers a ping from a card that mounts later), so a dashboard reload still knows. The page checks access and validates the id against the registry like /cameras/[id] (pri-m1csgrm). No new server routes or dependencies. Decided with the user 2026-09-19: pause the card; never stretch past the frame's own size; name and time overlay. Out of scope: real video (RTSP to browser). Full-resolution stills: separate follow-up issue."}},"at":"2026-09-19T20:01:09.995Z","parents":["evt-867k0s2fnszr"],"hash":"1d977e3235b7783221f22133dd3a50fc35ae6ed977d2e4ed3234ccb2ef49e5fc"}
{"id":"evt-wdpzzhh7p6gt","type":"node.created","subject":"ver-r6hm6fx","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"In a real browser: Pop out opens one window per camera, the frame is not stretched past its own size, the card shows \"Showing in pop-out\" while it is open","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T20:02:27.847Z","parents":["evt-qjekzvjdf9yf"],"hash":"91c6a70aca65b51a9b29246210132486f1eadfa96dd6b31942b8ef63daf456b8"}
{"id":"evt-ayk36tqppyzy","type":"edge.added","subject":"ver-r6hm6fx","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-r6hm6fx","to":"iss-mt5zv64"},"at":"2026-09-19T20:02:27.849Z","parents":["evt-wdpzzhh7p6gt"],"hash":"099836c18a6b4af0731379a71ecacd0008bf92c8acc72b452c4bcd8ccf2c754c"}
{"id":"evt-kq708ggdmxjh","type":"verification.recorded","subject":"ver-r6hm6fx","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"User checked in the browser on 2026-09-19 (\"looks good\")"},"at":"2026-09-19T20:02:27.850Z","parents":["evt-ayk36tqppyzy"],"hash":"4912547d89c2ac6b46b447b9a01c89c60905982c5858b1c02f82e8ebacad0170"}
{"id":"evt-mhcr8a835jb7","type":"node.status_changed","subject":"iss-mt5zv64","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"in_progress","to":"done"},"at":"2026-09-19T20:02:29.498Z","parents":["evt-kq708ggdmxjh"],"hash":"32cbe454fa3ac9e4fdab2c334cecff996981fc67162ecbf4b5088ff06d629e89"}
{"id":"evt-dq0ez0abqfhz","type":"node.created","subject":"fnd-56ba18a","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"finding","title":"Camera …af54's ISAPI picture endpoint accepts the ONVIF \"camera\" login but answered 503 Device Busy","body":"One read-only GET of /ISAPI/Streaming/channels/101/picture on 2026-09-19. Unauthenticated: 401 with a Digest challenge. With the stored Operator ONVIF login (\"camera\"), digest auth was accepted (no 401), which suggests ISAPI accepts this login on this firmware. The reply was HTTP 503 ResponseStatus: statusCode 2, \"Device Busy\", subStatusCode deviceBusy, with the description \"Unknow DDE mode:0 ;Unknow FFC mode:0 ;Unknow AGC mode:0 ;Get iris cfg failed ;get_image_gamma_correction failed\". The description reads like imaging-config lookups that fail on this model rather than true load, but that is unconfirmed. The dashboard may have been polling the ONVIF snapshot at the same time. Not yet known: whether a retry, ?snapShotImageType=JPEG, or channel 102 behaves differently.","status":"current","owner":"prn-q80g8mz","attrs":{"sources":[{"url":"http://localhost/ISAPI/Streaming/channels/101/picture","note":"Path on camera …af54 (address redacted per dec-kgm44qw); probed from a scratch Node script using the app's credential store, one request"}],"as_of":"2026-09-19"}},"at":"2026-09-19T20:04:00.606Z","parents":["evt-mhcr8a835jb7"],"hash":"d2b5a2d4fe35f8cd453cf81aab8d5987c191c6cfbad4635fc2e922f2a38acde7"}
{"id":"evt-80ktyzfhkxc3","type":"edge.added","subject":"fnd-56ba18a","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-56ba18a","to":"iss-3vck3js"},"at":"2026-09-19T20:04:00.607Z","parents":["evt-dq0ez0abqfhz"],"hash":"6ea8f1e98600fa712115f5c8089d4abdbb56e295fb493bed9721831e9db93438"}
{"id":"evt-75k0zxbpxk5y","type":"edge.added","subject":"fnd-56ba18a","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"discovered_from","from":"fnd-56ba18a","to":"iss-3vck3js"},"at":"2026-09-19T20:04:00.608Z","parents":["evt-80ktyzfhkxc3"],"hash":"d3455c8a99b47b2a7c8623df5a4422ac218fc898c1a2d6f1fd5d2ab52cc8fc80"}
{"id":"evt-pnmbsqwrjv8r","type":"node.status_changed","subject":"iss-3vck3js","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"in_progress"},"at":"2026-09-19T20:04:02.444Z","parents":["evt-75k0zxbpxk5y"],"hash":"bce741481436200634f13696942242ca48b8fe8ccf4e200649a306d7f05744ff"}
{"id":"evt-baxhtc1rj1by","type":"node.created","subject":"fnd-k1jyt83","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"finding","title":"Camera …af54 has no full-resolution still over ISAPI: channel 101 always answers Device Busy, and channel 102 gives a 1200×536 JPEG at lower quality than ONVIF","body":"Three read-only requests on 2026-09-19, a few seconds apart, with the dashboard closed, using the stored Operator ONVIF login (\"camera\"). Digest auth was accepted every time. (1) /ISAPI/Streaming/channels/101/picture → 503 Device Busy (deviceBusy, empty description). (2) The same with ?snapShotImageType=JPEG → 503 Device Busy. (3) /ISAPI/Streaming/channels/102/picture → 200 image/jpeg, 1200×536, about 105 KB, luma quantization ≈ quality 41. That is worse than the ONVIF snapshot (1200×536, ≈ quality 81, fnd-dw1pqcw). Since it isn't a clash with polling, the refusal on 101 looks like a limit of the camera or firmware: it doesn't encode main-stream-size stills. It is not a permissions refusal, which would be a 401 or 403. Consequence: the ONVIF snapshot stays the best still on this camera; sharper full-size viewing needs the main video stream. The other camera is untested.","status":"current","owner":"prn-q80g8mz","attrs":{"sources":[{"url":"http://localhost/ISAPI/Streaming/channels/101/picture","note":"Camera …af54, address redacted per dec-kgm44qw; scratch Node script over the app's credential store; also ?snapShotImageType=JPEG and channels/102/picture"}],"as_of":"2026-09-19"}},"at":"2026-09-19T20:05:28.062Z","parents":["evt-pnmbsqwrjv8r"],"hash":"ca2758c41215058c9c162df586fa9bdf2546b43889ad5e6d5e9da81001e1c789"}
{"id":"evt-zqcxafrk8pxz","type":"edge.added","subject":"fnd-k1jyt83","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-k1jyt83","to":"iss-3vck3js"},"at":"2026-09-19T20:05:28.064Z","parents":["evt-baxhtc1rj1by"],"hash":"dc2374edb2504816e264758206c48da003af2bfa918c795469e586ae8a1b1954"}
{"id":"evt-pjz0ckg0z53k","type":"edge.added","subject":"fnd-k1jyt83","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-k1jyt83","to":"iss-mt5zv64"},"at":"2026-09-19T20:05:28.065Z","parents":["evt-zqcxafrk8pxz"],"hash":"56336db0181a83c2408e15106d2486119f28197776989320171c323096ac3214"}
{"id":"evt-tfs8wd261e62","type":"edge.added","subject":"fnd-k1jyt83","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-k1jyt83","to":"gol-6q1q5mr"},"at":"2026-09-19T20:05:28.066Z","parents":["evt-pjz0ckg0z53k"],"hash":"d5650bcbdf8e5e1f4d2593dc3156b635f8574f05d5d1aa9b5dcf7fe1a2093145"}
{"id":"evt-bsv9ac5477ks","type":"edge.added","subject":"fnd-k1jyt83","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"discovered_from","from":"fnd-k1jyt83","to":"iss-3vck3js"},"at":"2026-09-19T20:05:28.067Z","parents":["evt-tfs8wd261e62"],"hash":"728827a322190649cd18147db9e5330cca4c69a20e04ad9fae8a6984b6cac9c0"}
{"id":"evt-vx8r965hf7z1","type":"edge.added","subject":"fnd-k1jyt83","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"supersedes","from":"fnd-k1jyt83","to":"fnd-56ba18a"},"at":"2026-09-19T20:05:28.068Z","parents":["evt-bsv9ac5477ks"],"hash":"2f3215a2d55678186d11bb0b91c3130795ca59b3f3c078a6dc9c78fcbfe52c61"}
{"id":"evt-4ry45291zfz5","type":"node.status_changed","subject":"fnd-56ba18a","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"current","to":"superseded"},"at":"2026-09-19T20:05:28.069Z","parents":["evt-vx8r965hf7z1"],"hash":"07e58dcf959f3c1815740636adbd6904b04fec2cbaec896e10eb9aee5761397d"}
{"id":"evt-8nnk65a0jnq2","type":"node.created","subject":"ver-texrp4e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"Determine whether ISAPI gives a main-stream-size still on camera …af54, and with which login","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T20:05:30.679Z","parents":["evt-4ry45291zfz5"],"hash":"388f1e5cbcd82839b8968a41381c2e08f05ba71848a85860e809a88070283b57"}
{"id":"evt-3c977965vgmj","type":"edge.added","subject":"ver-texrp4e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-texrp4e","to":"iss-3vck3js"},"at":"2026-09-19T20:05:30.681Z","parents":["evt-8nnk65a0jnq2"],"hash":"d96a9075e55ceb614632f55654965812d1f51752e8994a13fcd0517d4fe8b5a6"}
{"id":"evt-s501w58t42j0","type":"verification.recorded","subject":"ver-texrp4e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"fnd-dw1pqcw and fnd-56ba18a superseded by the latest finding: 4 read-only requests on 2026-09-19. The ONVIF login is accepted; channel 101 is always 503 Device Busy; channel 102 is 1200×536 at about quality 41. Answer: no. Nothing to build; ONVIF snapshot stays."},"at":"2026-09-19T20:05:30.682Z","parents":["evt-3c977965vgmj"],"hash":"71e25b1395ed581914d43067af2f691f655ed6cd9a7cc47fdc64b6cc0f24b800"}
{"id":"evt-31wfy0e0behn","type":"node.status_changed","subject":"iss-3vck3js","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"in_progress","to":"done"},"at":"2026-09-19T20:05:32.277Z","parents":["evt-s501w58t42j0"],"hash":"ab824e99be59c7a0d026a2a4280d7728a307604e34cc44331417626f04e8cc0d"}
{"id":"evt-gp90bp7dyxp1","type":"node.status_changed","subject":"gol-2nxgqjn","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"active","to":"retired"},"at":"2026-09-19T20:06:51.785Z","parents":["evt-31wfy0e0behn"],"hash":"8d82783db5503360307b60bc9f50e2329c5e4aeb31f76d05e65ed06ed9020996"}
{"id":"evt-pejn1c73aty9","type":"node.updated","subject":"gol-wqf95dq","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"target":1,"direction":"up","unit":"observation"}},"at":"2026-09-19T20:08:10.073Z","parents":["evt-gp90bp7dyxp1"],"hash":"ccca269a5d342339b5ec7433796edc84682d9ba1a073684b84c5631622807037"}
{"id":"evt-f0gez4jpjtrg","type":"node.updated","subject":"gol-8v1wqvp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"target":1,"direction":"up","unit":"observation"}},"at":"2026-09-19T20:08:11.760Z","parents":["evt-pejn1c73aty9"],"hash":"8f64b7a4052553c54d86b6af71a9114cc1fc23e205b4ff75cc3903b32bc79438"}
{"id":"evt-5z1z2db4js40","type":"node.created","subject":"mea-t947bzm","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"measurement","title":"User observed the admin login working end to end","body":"One observation by the user, in their own browser on 2026-09-19: they went through the admin login flow (first-run setup, sign-in, access checks) and reported it working.","status":"recorded","owner":null,"attrs":{"value":1,"applies_at":"2026-09-19"}},"at":"2026-09-19T20:08:13.879Z","parents":["evt-f0gez4jpjtrg"],"hash":"733c19d29fd9b0f7c182b5ea69d157fe9ab002ceee4051d9ebe159b59b194061"}
{"id":"evt-1cj89wb280va","type":"edge.added","subject":"mea-t947bzm","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"measures","from":"mea-t947bzm","to":"gol-wqf95dq"},"at":"2026-09-19T20:08:13.880Z","parents":["evt-5z1z2db4js40"],"hash":"d74c4a5b6cfa76d441682181caff0f11437abf9f6f2008b391215261b731c6a3"}
{"id":"evt-w09sa3f2h4aa","type":"node.created","subject":"mea-gv9sb0y","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"measurement","title":"User observed camera configuration from the dashboard working","body":"One observation by the user, in their own browser on 2026-09-19: they edited image settings, including Manual exposure time and gain, on camera …af54 from the dashboard, saw the changes take effect and re-read correctly, and reported it working. The audit log (.data/audit.jsonl) records the writes.","status":"recorded","owner":null,"attrs":{"value":1,"applies_at":"2026-09-19"}},"at":"2026-09-19T20:08:15.658Z","parents":["evt-1cj89wb280va"],"hash":"fe9b573fedaf665cb134380df7a7900401d78c9cc921da02d38029df0f3c7c0e"}
{"id":"evt-dz2878nr89ab","type":"edge.added","subject":"mea-gv9sb0y","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"measures","from":"mea-gv9sb0y","to":"gol-8v1wqvp"},"at":"2026-09-19T20:08:15.659Z","parents":["evt-w09sa3f2h4aa"],"hash":"651b96a20aa6b5821228bd1dd3e453166459b0c40615ebbea40473181c504def"}
{"id":"evt-1nrbjvd6dhwk","type":"node.status_changed","subject":"gol-wqf95dq","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"active","to":"retired"},"at":"2026-09-19T20:08:17.305Z","parents":["evt-dz2878nr89ab"],"hash":"4e463236d2b2f7eebacdee0fb7ed54f32b15ff22708ab4b6da2ea4131cc0c986"}
{"id":"evt-se02cx1hnx2m","type":"node.status_changed","subject":"gol-8v1wqvp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"active","to":"retired"},"at":"2026-09-19T20:08:19.008Z","parents":["evt-1nrbjvd6dhwk"],"hash":"2e2f8e20f651351b95817371b2eebd95ba39d91001344beadd6af12baa90d6cf"}
{"id":"evt-pvn59g6yvahc","type":"node.updated","subject":"gol-2nxgqjn","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"target":1,"direction":"up","unit":"observation"}},"at":"2026-09-19T20:09:20.114Z","parents":["evt-se02cx1hnx2m"],"hash":"17f6bf3cbc5e7e08bc700192f5a0cb8093170ac3eac39e970979d31fe2348920"}
{"id":"evt-5cca5zknswf6","type":"node.created","subject":"mea-dd2b0x7","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"measurement","title":"User observed the read-only camera settings and stream URLs working","body":"One observation by the user, in their own browser on 2026-09-19: they checked the per-camera settings page (streams with copyable RTSP URLs without credentials, image, time and network sections) and the dashboard summary, and reported it working (\"checked, looks good\").","status":"recorded","owner":null,"attrs":{"value":1,"applies_at":"2026-09-19"}},"at":"2026-09-19T20:09:21.856Z","parents":["evt-pvn59g6yvahc"],"hash":"4dff0305499dac5f03120673f1f9c8137e451bc81fc3b40f6e07074e32092a0a"}
{"id":"evt-5p47w1dkzhx0","type":"edge.added","subject":"mea-dd2b0x7","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"measures","from":"mea-dd2b0x7","to":"gol-2nxgqjn"},"at":"2026-09-19T20:09:21.857Z","parents":["evt-5cca5zknswf6"],"hash":"9a13324d2983b2c6510a13b10497d65d2d6cd2247dc11d39ee3123f079ba5786"}
{"id":"evt-c6ya332v04s4","type":"node.updated","subject":"iss-p7vdbng","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"attrs":{"revisit":"2026-09-20","deferred_because":"User deferred on 2026-09-19: revisit if related work comes up, or tomorrow morning at the latest."}}},"at":"2026-09-19T20:12:41.490Z","parents":["evt-5p47w1dkzhx0"],"hash":"8320f0be3e0dbeb75282895299f1fb283edb3dc23cb3a4ab21aacb2901941c1f"}
{"id":"evt-bt77t8b5j2qx","type":"node.status_changed","subject":"iss-p7vdbng","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"deferred"},"at":"2026-09-19T20:12:41.491Z","parents":["evt-c6ya332v04s4"],"hash":"c5ccc9048ff1e9d5a1f8a6c03738424b28744124045385c39016b8ed176c21d0"}
{"id":"evt-d4nw4d55xcke","type":"node.updated","subject":"iss-hg50epa","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"attrs":{"revisit":"2026-09-20","deferred_because":"User deferred on 2026-09-19: revisit if related work comes up, or tomorrow morning at the latest."}}},"at":"2026-09-19T20:12:43.295Z","parents":["evt-bt77t8b5j2qx"],"hash":"19605fd01bbdd1e714ab151aef49994a40964e64ded39748a45595e325e7f540"}
{"id":"evt-ehjwyfy6529d","type":"node.status_changed","subject":"iss-hg50epa","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"deferred"},"at":"2026-09-19T20:12:43.297Z","parents":["evt-d4nw4d55xcke"],"hash":"f28bc24f279ed69e5c3cb5549740535a14094df673b55241f914f7bc323a9a11"}
{"id":"evt-4nwm0qzxt2n4","type":"node.updated","subject":"iss-3g6m4rz","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"attrs":{"revisit":"2026-09-20","deferred_because":"User deferred on 2026-09-19: revisit if related work comes up, or tomorrow morning at the latest."}}},"at":"2026-09-19T20:12:44.848Z","parents":["evt-ehjwyfy6529d"],"hash":"1bbf688c0f0ebe0e244f32227eb98e3db3027fc8b96737d851458eeb57458a41"}
{"id":"evt-m1t2n80t4765","type":"node.status_changed","subject":"iss-3g6m4rz","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"deferred"},"at":"2026-09-19T20:12:44.849Z","parents":["evt-4nwm0qzxt2n4"],"hash":"4f079b88730fb162146777937e75bb917b109d0c61823776ab98e4ac9682bda9"}
{"id":"evt-cmthprybhxc3","type":"node.created","subject":"gol-sxakryh","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"goal","title":"Enable secure live video streaming and recording","body":"Watch each camera's live video in the browser at full stream quality, and record video, without exposing cameras or their logins. Why now: fnd-dw1pqcw and fnd-k1jyt83 show the still snapshots top out at 1200×536 while the main stream is 4096×1860, so full-detail viewing needs the video stream itself. \"Secure\" follows the existing principles: video reaches the browser only through this app, behind the admin session (pri-m1csgrm); camera credentials never leave the server and are never embedded in a URL the browser sees (pri-tyrxdz9); cameras are addressed by id from the registry, never by a client-supplied host. Browsers can't play RTSP directly, so this needs a server-side bridge (for example remuxing RTSP H.264/H.265 to WebRTC, fMP4 over WebSocket, or HLS) or an external media server; any new package or binary goes through a dependency question first (pri-mz2jxpb). Recording raises its own questions: where files live, retention and disk limits, continuous or event-triggered, whether recording runs on this server or on the cameras' SD cards or an NVR, and who can view or delete recordings. Scope, priority (weight) and a measurable target are not decided yet. Created by the user on 2026-09-19.","status":"active","owner":null,"attrs":{},"weight":null,"target":null,"direction":"up","unit":null},"at":"2026-09-19T20:13:39.094Z","parents":["evt-m1t2n80t4765"],"hash":"d2776d775f7e834b409ecd46dbc395153fed3a194af4d564b7d75e247b027c12"}
{"id":"evt-bp1pckjmt17c","type":"edge.added","subject":"gol-sxakryh","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"gol-sxakryh","to":"gol-6q1q5mr"},"at":"2026-09-19T20:13:39.097Z","parents":["evt-cmthprybhxc3"],"hash":"289a279de58c9a7b488e7e2964a9c94ba9af4dd81de962eae3cb6dc57a0c5d71"}
{"id":"evt-xkqkhvy7q100","type":"edge.added","subject":"gol-sxakryh","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"upholds","from":"gol-sxakryh","to":"pri-tyrxdz9"},"at":"2026-09-19T20:13:39.098Z","parents":["evt-bp1pckjmt17c"],"hash":"6f59edcc571f427ed15cb90affd9ea3421671978167ae1382c1bd5733cbc98ff"}
{"id":"evt-3a720kgxps99","type":"edge.added","subject":"gol-sxakryh","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"upholds","from":"gol-sxakryh","to":"pri-m1csgrm"},"at":"2026-09-19T20:13:39.099Z","parents":["evt-xkqkhvy7q100"],"hash":"e3ef4dda938f0014d2dfcc2a006070e95fa6adb1b167c6c1b0d43b685e3d1109"}
{"id":"evt-y58jsd9yckz2","type":"edge.added","subject":"gol-sxakryh","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"upholds","from":"gol-sxakryh","to":"pri-mz2jxpb"},"at":"2026-09-19T20:13:39.100Z","parents":["evt-3a720kgxps99"],"hash":"1b922906dbcb2479a85b4d89cbaf3d10da3f41469b9bba4b587d23821e342057"}
{"id":"evt-3rcgvzeq636m","type":"node.updated","subject":"gol-sxakryh","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"weight":1}},"at":"2026-09-19T20:17:01.981Z","parents":["evt-y58jsd9yckz2"],"hash":"20edb17a83a64f10e7ea0278d249368670e4634fc7d44179ee62e3926dc61762"}
{"id":"evt-jgnx569r7qpj","type":"node.created","subject":"dec-9xrjx82","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"decision","title":"Live video: build live viewing first; design recording after it works, and when recording, record on motion/events","body":"User's answers on 2026-09-19 while analysing gol-sxakryh. Storage: live view first. Recording stays in the goal but isn't designed until live viewing works. Considered and set aside for now: recording to this server's own folder, camera SD cards, and an existing NVR. Rough sizing that informed this: the 4096×1860 main stream at about 5 Mbps is about 54 GB per camera per day continuous, and the 1200×536 sub stream at about 0.75 Mbps is about 8 GB per day, while the dev Mac has 72 GB free (92% full). What to record, once recording is designed: motion or camera events only, with pre- and post-roll, rather than continuous or manual clips. That needs the cameras' event feed (ONVIF events or ISAPI alertStream). Priority: the goal's weight is 1, the same as gol-6q1q5mr.","status":"recorded","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T20:17:05.372Z","parents":["evt-3rcgvzeq636m"],"hash":"9c2ce8e982a40f36fe1dba766e71b5647726efffbbc015a77df5501169bdcd28"}
{"id":"evt-xyed6p4p0vn7","type":"edge.added","subject":"dec-9xrjx82","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"dec-9xrjx82","to":"gol-sxakryh"},"at":"2026-09-19T20:17:05.374Z","parents":["evt-jgnx569r7qpj"],"hash":"9f68c40d0200cd96f666fcdf1b693e43b67afc1d27a48d249f81342db98549fd"}
{"id":"evt-08xye0bsyq00","type":"node.created","subject":"que-588mjxt","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"question","title":"Add MediaMTX as the video bridge, and how should it be installed and run?","body":"Dependency justification per pri-mz2jxpb, for gol-sxakryh. The user chose MediaMTX over go2rtc, ffmpeg run by the app, and a pure-JS RTSP client (2026-09-19); this question approves the dependency itself and how it's installed.\n\n(1) Problem: browsers can't play RTSP. Something has to repackage the cameras' H.264/H.265 streams, without re-encoding, into WebRTC (under 1 s latency) or HLS. Later it would also record motion clips with retention.\n\n(2) Why not the platform or our own code: Node has no RTSP/RTP or WebRTC media stack. Writing an RTSP client, RTP depacketizer and fMP4 muxer ourselves is weeks of protocol work (the rejected pure-JS option).\n\n(3) Size and reach: a single static Go binary with no runtime dependencies; the v1.21.0 darwin_arm64 archive is 26.7 MB. It runs as a separate server process and ships nothing to the browser. The browser only receives video, through our app.\n\n(4) Health, checked 2026-09-19 against GitHub and Homebrew: MIT license; latest release v1.21.0 on 2026-09-05, with releases about every 2 weeks; about 20.2k stars; code pushed 2026-09-19; Homebrew formula mediamtx 1.21.0. Eight published advisories (GHSA): 4 high DoS or argument-injection, 2 medium CORS or open redirect, 2 low. All are patched in 1.20.1 or earlier, so 1.21.0 is unaffected. That history argues for pinning to the latest release, updating promptly, and never exposing MediaMTX to the network directly.\n\n(5) Removal: MediaMTX sits behind our own route handlers and a small src/lib/video module, so replacing it (e.g. with go2rtc) touches only that module and the process config.\n\nSecurity design either way: MediaMTX listens on 127.0.0.1 only, with its control API bound to localhost and password-protected. The app gives it each camera's RTSP source, with credentials, over that API at runtime, so no camera password is written to MediaMTX's config file. The browser never talks to MediaMTX's HTTP ports. WebRTC signaling (WHEP) and any HLS go through our session-checked route handlers. With WebRTC, the media packets themselves flow browser↔MediaMTX over UDP/TCP ICE ports on the LAN, but only after an authenticated signaling exchange for that session. Those ICE ports need to be reachable from browsers on the LAN.","status":"open","owner":null,"attrs":{"options":[{"option":"Homebrew (brew install mediamtx)","consequence":"Easiest on this Mac; brew upgrade keeps it patched. The version isn't pinned by the repo, and other machines need brew or their own install. The app finds it on PATH or through a MEDIAMTX_BIN env var."},{"option":"Pinned release binary fetched by a script","consequence":"An npm script downloads a specific GitHub release for the platform, verifies its SHA-256 against a checksum committed in the repo, and unpacks it into a gitignored bin/ folder. Reproducible and pinned; upgrades are a deliberate one-line change. Slightly more to maintain."},{"option":"Docker container","consequence":"Pinned image tag; isolated from the host. On macOS, Docker's networking makes WebRTC UDP harder (host networking is limited), so HLS might be needed. Adds Docker as a runtime requirement."},{"option":"Don't add MediaMTX","consequence":"Revisit the bridge choice (go2rtc, ffmpeg, or pure JS)."}],"revisit":null}},"at":"2026-09-19T20:17:18.939Z","parents":["evt-xyed6p4p0vn7"],"hash":"693a91ef6088b1a444fa4d5cdf7dd71360e389242c91fa24126fdaf3a50b9c05"}
{"id":"evt-pvt2jey438j0","type":"edge.added","subject":"que-588mjxt","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"que-588mjxt","to":"gol-sxakryh"},"at":"2026-09-19T20:17:18.941Z","parents":["evt-08xye0bsyq00"],"hash":"b728e559e7d8faca41992447056e3ef12b628642f131877eb0e11e1a1233dc3e"}
{"id":"evt-2wvwc6gerkcq","type":"edge.added","subject":"pri-mz2jxpb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"guides","from":"pri-mz2jxpb","to":"que-588mjxt"},"at":"2026-09-19T20:17:18.942Z","parents":["evt-pvt2jey438j0"],"hash":"adbe655043680bb6daee7ed603067680d1cb460661bd1eb0e931f9fac2289f73"}
{"id":"evt-rx6e31ymn856","type":"edge.added","subject":"pri-tyrxdz9","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"guides","from":"pri-tyrxdz9","to":"que-588mjxt"},"at":"2026-09-19T20:17:18.943Z","parents":["evt-2wvwc6gerkcq"],"hash":"a21de4cfd3ea9b0ba282ec56c43b42568a87d091a6243ddda42121d19a56eb34"}
{"id":"evt-g8hcdn117p6y","type":"edge.added","subject":"pri-m1csgrm","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"guides","from":"pri-m1csgrm","to":"que-588mjxt"},"at":"2026-09-19T20:17:18.944Z","parents":["evt-rx6e31ymn856"],"hash":"a6f9ab33a0ae6b61636d3b249101c58ab46df8ebbac7d8d8ae1f9556cd35e176"}
{"id":"evt-vy7bgy5j61jv","type":"node.created","subject":"dec-xkn4z0e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"decision","title":"Add MediaMTX as a pinned, checksum-verified release binary in a gitignored bin/, started and supervised by the app","body":"User's answers on 2026-09-19. Install: an npm script downloads a specific MediaMTX GitHub release (starting at v1.21.0) for the current platform, verifies its SHA-256 against checksums committed in the repo, and unpacks it into a gitignored bin/. Upgrades are a deliberate change to the pinned version and checksums. Rejected: Homebrew, because the repo doesn't pin the version, and Docker, because WebRTC networking on macOS is awkward. Process: the app starts MediaMTX as a child process from instrumentation.ts when the server boots. It writes a generated config that binds everything to 127.0.0.1 except the WebRTC ICE port, generates a random password for the local control API, restarts MediaMTX if it crashes, and stops it on exit. Rejected: running it as a separate service. Placement: live video plays the main stream in the pop-out and on the camera page; dashboard cards keep polling snapshots. Codec: first check each camera's stream codecs read-only. If any browser-bound stream is H.265, come back with the trade-off before changing anything. The security design in que-588mjxt applies: the browser never talks to MediaMTX's HTTP ports, signaling goes through session-checked routes, and camera credentials go to MediaMTX over its localhost API at runtime and are never written to disk.","status":"recorded","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T20:19:17.982Z","parents":["evt-g8hcdn117p6y"],"hash":"24473856f1f624027e1dee13c2abc895ae1a5acf43c2a486b266b08e7d136b21"}
{"id":"evt-v2vjh2w920x8","type":"edge.added","subject":"dec-xkn4z0e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"answers","from":"dec-xkn4z0e","to":"que-588mjxt"},"at":"2026-09-19T20:19:17.984Z","parents":["evt-vy7bgy5j61jv"],"hash":"4dd4542f5633c261f82a582797dd6b056ffa2ff5cea67990d982d57a5bbf9553"}
{"id":"evt-snf3gvcc99ab","type":"question.answered","subject":"que-588mjxt","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"answer":"dec-xkn4z0e"},"at":"2026-09-19T20:19:17.985Z","parents":["evt-v2vjh2w920x8"],"hash":"2bfec6fb3bcc138288eb4a1536b213c3d6626d90a0a467140784e0924fc47cdf"}
{"id":"evt-f87v7q4sy8bj","type":"edge.added","subject":"dec-xkn4z0e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"dec-xkn4z0e","to":"gol-sxakryh"},"at":"2026-09-19T20:19:17.986Z","parents":["evt-snf3gvcc99ab"],"hash":"ac26c89fa172f4ae95b2f498ce2f4cb687aee159b6c4c271d007d6be0cf6a9a3"}
{"id":"evt-xcgtprrxfh9h","type":"node.created","subject":"iss-4atmz13","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Live video, first slice: main stream live in the pop-out and on the camera page via MediaMTX","body":"Parent for gol-sxakryh's first slice, following dec-9xrjx82 and dec-xkn4z0e. Done when every child is closed and the user has watched both cameras live in the pop-out and on the camera page, in their own browser, with the session required and no camera credentials visible to the browser (network tab, page source).","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T20:19:22.713Z","parents":["evt-f87v7q4sy8bj"],"hash":"929e42446221e5751221c11870029a6a7c39d0d296f85c874025d31be9cdf11d"}
{"id":"evt-9kenz6nax8wn","type":"edge.added","subject":"iss-4atmz13","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-4atmz13","to":"gol-sxakryh"},"at":"2026-09-19T20:19:22.714Z","parents":["evt-xcgtprrxfh9h"],"hash":"e0657c1dd18abc8bd170436ef36e323227075df17a69c14f87d8dc6b995b476c"}
{"id":"evt-01q96258sk5p","type":"edge.added","subject":"iss-4atmz13","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-4atmz13","to":"area:video"},"at":"2026-09-19T20:19:22.715Z","parents":["evt-9kenz6nax8wn"],"hash":"293a5d229b2cdeb49ef634e4bfeca70840782c784bf4abce349449c59df77f68"}
{"id":"evt-adhxfp2ph2hg","type":"node.created","subject":"iss-5xkn65m","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Check both cameras' stream codecs (read-only)","body":"Per dec-xkn4z0e: read each camera's main and sub stream codec and H.264/H.265 profile. The dashboard info route and the settings page already report this over ONVIF, so no new device access is needed. Record the result as a finding. If a main stream is H.265, bring the trade-off (switch to H.264 with the stream editor vs keep H.265 with limited browser support) to the user before changing anything.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T20:19:28.262Z","parents":["evt-01q96258sk5p"],"hash":"51bd5b4e2f66b6e0f53713bb050947529f9306f96047b111a06bae01a60d43a2"}
{"id":"evt-bq6m9abtyyth","type":"edge.added","subject":"iss-5xkn65m","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-5xkn65m","to":"gol-sxakryh"},"at":"2026-09-19T20:19:28.265Z","parents":["evt-adhxfp2ph2hg"],"hash":"d30f9e43130b5e1dc9bbec9f1b9c69c19778d15fa867aa905e833f93a941dc09"}
{"id":"evt-vsg26dcqe547","type":"edge.added","subject":"iss-4atmz13","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"parent_of","from":"iss-4atmz13","to":"iss-5xkn65m"},"at":"2026-09-19T20:19:28.266Z","parents":["evt-bq6m9abtyyth"],"hash":"22abf91ad24a4a9644bac25e5a1af562bac511b866e76428688c0567310aad97"}
{"id":"evt-2fqcjmmnxp03","type":"edge.added","subject":"iss-5xkn65m","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-5xkn65m","to":"area:video"},"at":"2026-09-19T20:19:28.267Z","parents":["evt-vsg26dcqe547"],"hash":"2c1e6de8ff08723aa79df7400543c537a154b27e1d640fbbde80ad325a329ac4"}
{"id":"evt-bzvv0nj0cepf","type":"node.created","subject":"iss-qdc4vqw","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Script to fetch the pinned MediaMTX release and verify its checksum","body":"Per dec-xkn4z0e. An npm script (e.g. npm run video:install, plain Node with no new packages) downloads MediaMTX v1.21.0 for the current OS/arch from GitHub releases, verifies the SHA-256 against checksums committed in the repo, and unpacks the binary into a gitignored bin/. It refuses a mismatch, is idempotent, and prints what it did. The README documents it and how to upgrade: bump the version and the checksums together. Tests use a local fake download and never reach the network (pri-e14bahk).","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T20:19:30.899Z","parents":["evt-2fqcjmmnxp03"],"hash":"0382af9788622f5247050bb7ee1772f92bd1139661ea7bdf5edddc299d57994c"}
{"id":"evt-r66mtwfsfcsk","type":"edge.added","subject":"iss-qdc4vqw","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-qdc4vqw","to":"gol-sxakryh"},"at":"2026-09-19T20:19:30.901Z","parents":["evt-bzvv0nj0cepf"],"hash":"f42acece43c12f698b9fd53b49ff1f77a9af96d8cae27b6df07fbf0126737c8c"}
{"id":"evt-mb41hr5spqnd","type":"edge.added","subject":"iss-qdc4vqw","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-qdc4vqw","to":"scripts/"},"at":"2026-09-19T20:19:30.902Z","parents":["evt-r66mtwfsfcsk"],"hash":"957d2997aa46379af5f615bd06377b7dd05b8893fc17d3c8b58fca6b2271a83e"}
{"id":"evt-52t6dk7zwmmc","type":"edge.added","subject":"iss-qdc4vqw","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-qdc4vqw","to":".gitignore"},"at":"2026-09-19T20:19:30.903Z","parents":["evt-mb41hr5spqnd"],"hash":"3c3547a07081fe3973eae59b949f62c5c826d63f7bd53745e8db72d0495c7110"}
{"id":"evt-9697egkf32ae","type":"edge.added","subject":"iss-qdc4vqw","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-qdc4vqw","to":"README.md"},"at":"2026-09-19T20:19:30.904Z","parents":["evt-52t6dk7zwmmc"],"hash":"a8e3bb73a987209678f1a5ff01b561ff71824864551acca559d0bc10d11597a7"}
{"id":"evt-fy7yac43gq30","type":"edge.added","subject":"iss-qdc4vqw","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-qdc4vqw","to":"package.json"},"at":"2026-09-19T20:19:30.905Z","parents":["evt-9697egkf32ae"],"hash":"d0d2e04491577f40710329e90e0642a1bee749858de0a1c251a46da106a8c20a"}
{"id":"evt-a9vd17x5p17v","type":"edge.added","subject":"iss-4atmz13","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"parent_of","from":"iss-4atmz13","to":"iss-qdc4vqw"},"at":"2026-09-19T20:19:30.906Z","parents":["evt-fy7yac43gq30"],"hash":"a4e699722f31ded9c3cbb6f11694b709f9c400e2ef0ca4aed9d08acd76475551"}
{"id":"evt-4mg4959afkqf","type":"edge.added","subject":"iss-qdc4vqw","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-qdc4vqw","to":"area:video"},"at":"2026-09-19T20:19:30.907Z","parents":["evt-a9vd17x5p17v"],"hash":"700a5496856112a77b055d98ff4ae61c1131545eb01eaa7b2d1407f67546542b"}
{"id":"evt-vzyhdhhcnzq0","type":"node.created","subject":"iss-nk6zrzv","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Start and supervise MediaMTX from the app, locked to localhost","body":"Per dec-xkn4z0e and the security design in que-588mjxt. A server-only src/lib/video module: instrumentation.ts starts bin/mediamtx (or MEDIAMTX_BIN) with a generated config written 0600 under .data/. The config binds RTSP, HLS, WebRTC HTTP and the control API to 127.0.0.1 and disables RTMP, SRT and anything else unused. Only the WebRTC ICE UDP/TCP port listens on the LAN. The control API is password-protected with a random per-boot secret. Paths are added at runtime over the API per camera and stream (source = the ONVIF RTSP URI with credentials, sourceOnDemand so a camera is only pulled while someone watches), and are never written into the config file. The module restarts MediaMTX on crash with backoff, stops it on exit, and exposes a status of running, missing binary or crashed that the UI can show. Tests replace the child process and HTTP API (pri-e14bahk).","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T20:19:39.065Z","parents":["evt-4mg4959afkqf"],"hash":"0b470693fd064b59a50a196f3ef70303f7d6c72eec514f96d578206044685ddd"}
{"id":"evt-f6yt1k8y4g24","type":"edge.added","subject":"iss-nk6zrzv","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-nk6zrzv","to":"gol-sxakryh"},"at":"2026-09-19T20:19:39.066Z","parents":["evt-vzyhdhhcnzq0"],"hash":"565ab16757cc7902df68da51dfcfa7f19b936530194b3a1dc1cb1ec373e5e56a"}
{"id":"evt-thv2y79eyt4j","type":"edge.added","subject":"iss-nk6zrzv","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-nk6zrzv","to":"src/lib/video.ts"},"at":"2026-09-19T20:19:39.067Z","parents":["evt-f6yt1k8y4g24"],"hash":"8d6eeaa35ce39c0ecea75b506426ee06a6254f879259ca82be4b0f0d943f9cc9"}
{"id":"evt-rhdqz22eny45","type":"edge.added","subject":"iss-nk6zrzv","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-nk6zrzv","to":"src/instrumentation.ts"},"at":"2026-09-19T20:19:39.068Z","parents":["evt-thv2y79eyt4j"],"hash":"c55ef5581f8cba2f28fd20efbe6a7cf093a8775973dc8cd4bb0c94d7b5da0808"}
{"id":"evt-wdnwjn7v0wa0","type":"edge.added","subject":"iss-qdc4vqw","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"blocks","from":"iss-qdc4vqw","to":"iss-nk6zrzv"},"at":"2026-09-19T20:19:39.069Z","parents":["evt-rhdqz22eny45"],"hash":"295cac86c847500d674554be4f80bdbcaf647a9eaf41bdf0987c93085710846c"}
{"id":"evt-yn2bzj0yy529","type":"edge.added","subject":"iss-4atmz13","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"parent_of","from":"iss-4atmz13","to":"iss-nk6zrzv"},"at":"2026-09-19T20:19:39.070Z","parents":["evt-wdnwjn7v0wa0"],"hash":"4fb5db76fb53ff0d4ad46d7e24cb07873f8a9c76cdcbc995556e6fddb1552f94"}
{"id":"evt-xa17ccz2sv18","type":"edge.added","subject":"iss-nk6zrzv","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-nk6zrzv","to":"area:video"},"at":"2026-09-19T20:19:39.071Z","parents":["evt-yn2bzj0yy529"],"hash":"92e737c93f9be8681425112b1061acd64fa246e715095e2ab8c429b7a3bebfe2"}
{"id":"evt-sy87hwxarh0b","type":"node.created","subject":"iss-cbx21zy","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Session-checked WebRTC (WHEP) route and a live player in the pop-out and on the camera page","body":"Per dec-xkn4z0e. POST /api/cameras/[id]/live/whep (and the matching PATCH/DELETE for ICE candidates and teardown) checks access, validates the id against the registry, ensures the camera's MediaMTX path exists, and relays the SDP offer and answer to MediaMTX on localhost. The browser never gets a MediaMTX URL or any camera credential. The client player uses the browser's own RTCPeerConnection, with no new package, and shows connecting, playing, reconnecting and failed states. On failure it falls back to the snapshot polling that exists today. The pop-out and the camera page play the main stream; dashboard cards keep snapshots. Tests cover access, id validation, relaying and that no secret is leaked.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T20:19:43.980Z","parents":["evt-xa17ccz2sv18"],"hash":"fb3b55e34e4c54ad4f70ebb42cd298cbae2ba7bd7f393494d2f28acaaab8da3c"}
{"id":"evt-09yyf3egw7nz","type":"edge.added","subject":"iss-cbx21zy","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-cbx21zy","to":"gol-sxakryh"},"at":"2026-09-19T20:19:43.982Z","parents":["evt-sy87hwxarh0b"],"hash":"44209c5bbd0dfe970928511f089a94011c2708718423255b346f5eb014ca2b9c"}
{"id":"evt-1jrpnv8s0aa4","type":"edge.added","subject":"iss-cbx21zy","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-cbx21zy","to":"src/app/api/cameras/[id]/live/"},"at":"2026-09-19T20:19:43.983Z","parents":["evt-09yyf3egw7nz"],"hash":"2df17664d77be04ac46a70b5c6150330951268830bda641b90c927db31179722"}
{"id":"evt-1sp412ncsf3f","type":"edge.added","subject":"iss-cbx21zy","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-cbx21zy","to":"src/app/cameras/[id]/live/"},"at":"2026-09-19T20:19:43.984Z","parents":["evt-1jrpnv8s0aa4"],"hash":"8ab7aa9a0695f4e59c52c4e618829d72a7079802214f6ff9aab901c7b8ab3f72"}
{"id":"evt-szskvjs5njmp","type":"edge.added","subject":"iss-cbx21zy","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-cbx21zy","to":"src/app/cameras/[id]/page.tsx"},"at":"2026-09-19T20:19:43.985Z","parents":["evt-1sp412ncsf3f"],"hash":"42964f3be66dd70fe313a2de2e115ec3449fe4874f5dcadf86f8427689d2c95d"}
{"id":"evt-7gk18akp3j4e","type":"edge.added","subject":"iss-5xkn65m","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"blocks","from":"iss-5xkn65m","to":"iss-cbx21zy"},"at":"2026-09-19T20:19:43.988Z","parents":["evt-szskvjs5njmp"],"hash":"0da8fed42bda956ba7cac9708f61221b21899aa299c51d55a1844b2a4d9d770a"}
{"id":"evt-v6hr8ddb8cbe","type":"edge.added","subject":"iss-4atmz13","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"parent_of","from":"iss-4atmz13","to":"iss-cbx21zy"},"at":"2026-09-19T20:19:43.989Z","parents":["evt-7gk18akp3j4e"],"hash":"196e239eb960add2a03357e03a34cd1056786471b9140880ebc7df4dff7aa2a8"}
{"id":"evt-pwshfmse0gv2","type":"edge.added","subject":"iss-cbx21zy","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-cbx21zy","to":"area:video"},"at":"2026-09-19T20:19:43.990Z","parents":["evt-v6hr8ddb8cbe"],"hash":"32df6057310761d988de5fe3689daf7f98bd1efdebabb515e0ea059fdf894c9f"}
{"id":"evt-f7n75vxq0qr9","type":"edge.added","subject":"iss-nk6zrzv","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"blocks","from":"iss-nk6zrzv","to":"iss-cbx21zy"},"at":"2026-09-19T20:19:47.949Z","parents":["evt-pwshfmse0gv2"],"hash":"6ebc726f787f499fc68fcec66833d143bbd6b7c028173e5edcbec4c66e5e77cd"}
{"id":"evt-t1r7rqcam1nn","type":"node.created","subject":"iss-g456j72","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Design motion-triggered recording (after live view works)","body":"Per dec-9xrjx82, deferred until live viewing works. Open questions to bring to the user then: where clips are stored (a folder or external drive; the dev Mac has 72 GB free), retention and disk cap, pre- and post-roll length, which event source (ONVIF PullPoint events or ISAPI alertStream), which stream to record (main is about 54 GB per day per camera continuous; motion-only is far less), and how clips are browsed, played and deleted behind the admin session. MediaMTX's own recording (fMP4 segments with recordDeleteAfter) is the likely mechanism.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T20:19:50.499Z","parents":["evt-f7n75vxq0qr9"],"hash":"c11853228019f865370bda76052a192920747304735be5ee2a12b77f06a2c8b3"}
{"id":"evt-fkraf55tg4y4","type":"edge.added","subject":"iss-g456j72","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-g456j72","to":"gol-sxakryh"},"at":"2026-09-19T20:19:50.500Z","parents":["evt-t1r7rqcam1nn"],"hash":"2e91abfa055caff88d35c7d9e5f11db9e94489e1c87e5bf7f8be5187053b40f8"}
{"id":"evt-mgw1r1gqy0z3","type":"edge.added","subject":"iss-4atmz13","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"blocks","from":"iss-4atmz13","to":"iss-g456j72"},"at":"2026-09-19T20:19:50.501Z","parents":["evt-fkraf55tg4y4"],"hash":"db15dffece0e7f65826e2131a768f9f9b4caa432d72f7aba5775a6e5c266913e"}
{"id":"evt-8c3bjp5jkx0r","type":"edge.added","subject":"iss-g456j72","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-g456j72","to":"area:video"},"at":"2026-09-19T20:19:50.502Z","parents":["evt-mgw1r1gqy0z3"],"hash":"857e3f60a4d06d265ab56616afd22a3aead6c4d6a385d7e83999f7a0ca9f035f"}
{"id":"evt-dk6q5ft9s9np","type":"node.updated","subject":"gol-sxakryh","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"target":6,"direction":"up","unit":"checks"}},"at":"2026-09-19T20:21:36.563Z","parents":["evt-8c3bjp5jkx0r"],"hash":"621e36aa3b5053cbaf5736fa8f86bf562814245314e9ec450d650e78ea1e427f"}
{"id":"evt-n7y3jswtnkew","type":"node.updated","subject":"gol-sxakryh","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"body":"Watch each camera's live video in the browser at full stream quality, and record video, without exposing cameras or their logins. Target (set by the user 2026-09-19): 6 checks, one per feature per camera across both cameras: (1) main stream watched live, (2) sub stream watched live, (3) recording enabled and validated (a motion clip recorded and played back). Each check is the user's own observation, recorded as a measurement. Why now: fnd-dw1pqcw and fnd-k1jyt83 show the still snapshots top out at 1200×536 while the main stream is 4096×1860, so full-detail viewing needs the video stream itself. \"Secure\" follows the existing principles: video reaches the browser only through this app, behind the admin session (pri-m1csgrm); camera credentials never leave the server and are never embedded in a URL the browser sees (pri-tyrxdz9); cameras are addressed by id from the registry, never by a client-supplied host. Bridge: MediaMTX (dec-xkn4z0e). Live view first, then motion-triggered recording (dec-9xrjx82). Priority 1. Created by the user on 2026-09-19."}},"at":"2026-09-19T20:21:38.836Z","parents":["evt-dk6q5ft9s9np"],"hash":"5c81cea5525238d092ca825fd46d3a6c7f4c9e21d483d5d6f68a4a4304169886"}
{"id":"evt-44sycckwpv9g","type":"node.updated","subject":"iss-cbx21zy","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"body":"Per dec-xkn4z0e. POST /api/cameras/[id]/live/whep (and the matching PATCH/DELETE for ICE candidates and teardown) checks access, validates the id and the stream (main or sub, from the camera's own profiles) against the registry, ensures the MediaMTX path exists, and relays the SDP offer and answer to MediaMTX on localhost. The browser never gets a MediaMTX URL or any camera credential. The client player uses the browser's own RTCPeerConnection, with no new package, and shows connecting, playing, reconnecting and failed states. On failure it falls back to the snapshot polling that exists today. The pop-out and the camera page play the main stream by default, with a Main/Sub switch, because the goal's target counts watching both streams live on each camera. Dashboard cards keep snapshots. Tests cover access, id and stream validation, relaying and that no secret is leaked."}},"at":"2026-09-19T20:21:41.658Z","parents":["evt-n7y3jswtnkew"],"hash":"e1f3c7b6638fba35f3a78fad4338252824a1f5bcf36417d357afc3da9490d4c1"}
{"id":"evt-9ng8fzbr1yn7","type":"node.created","subject":"fnd-srfdnmm","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"finding","title":"Both I91ET cameras stream H.264 on main (4096×1860 @ 20 fps) and sub (1200×536 @ 20 fps)","body":"Read on 2026-09-19 through the app's own /api/cameras/[id]/info route (ONVIF GetProfiles, the same read the dashboard makes), for cameras …af2e and …af54. Both report mainStream H264 4096×1860 at 20 fps and subStream H264 1200×536 at 20 fps. No H.265 anywhere, so both streams can go to browsers over WebRTC as they are, with no codec change needed. The H.264 profile (Main or High) wasn't in this read; WebRTC in Chrome and Safari decodes both. 4096×1860 needs H.264 level 5.1, which some older or low-power devices can't decode in hardware.","status":"current","owner":"prn-q80g8mz","attrs":{"sources":[{"url":"http://localhost:3000/api/cameras/22ec0000-8b90-11b5-845d-d03bf404af54/info","note":"and the same route for …af2e; read in the user's signed-in browser"}],"as_of":"2026-09-19"}},"at":"2026-09-19T20:22:08.820Z","parents":["evt-44sycckwpv9g"],"hash":"36ff418aedae5f689ee2449b3cfdfb55898455e52f6c5601eab143a92e01941d"}
{"id":"evt-xfxed11agevk","type":"edge.added","subject":"fnd-srfdnmm","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-srfdnmm","to":"iss-cbx21zy"},"at":"2026-09-19T20:22:08.822Z","parents":["evt-9ng8fzbr1yn7"],"hash":"015ec05f5227c691e8b0f5432ad110a88af258d28cc927b8506e70a0cc51b3ec"}
{"id":"evt-yxqd8n1sxthq","type":"edge.added","subject":"fnd-srfdnmm","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-srfdnmm","to":"gol-sxakryh"},"at":"2026-09-19T20:22:08.823Z","parents":["evt-xfxed11agevk"],"hash":"bd40bd30c6c5cacfb77f6f6c3f4efe27d43dd58e53ec39bf5ee2eeaadfb0ec66"}
{"id":"evt-m4z8jjddzhkt","type":"edge.added","subject":"fnd-srfdnmm","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"discovered_from","from":"fnd-srfdnmm","to":"iss-5xkn65m"},"at":"2026-09-19T20:22:08.824Z","parents":["evt-yxqd8n1sxthq"],"hash":"541dd6d2d09dded4b4c3b67538550a2f9a572b18923a3c3df1c84c76d2d85d6c"}
{"id":"evt-2bmbv75gmsmp","type":"node.status_changed","subject":"iss-5xkn65m","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T20:22:10.807Z","parents":["evt-m4z8jjddzhkt"],"hash":"772585e54de080aa75d6a6e7a3de7ffbe60c5805624912d4eaa338d23fde2a7c"}
{"id":"evt-yjttarpgr6jc","type":"node.created","subject":"ver-rpgs2rj","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"npm run video:install downloads the pinned MediaMTX v1.21.0 for this platform, refuses a checksum mismatch before writing, installs an executable into gitignored bin/, and is idempotent; tested without the network","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T20:24:23.115Z","parents":["evt-2bmbv75gmsmp"],"hash":"fe1d5dffbbcb9be1d412b28fd78316eb1c4d59cb4bebf59c497478c35a1c4366"}
{"id":"evt-p4qevsh6qfdg","type":"edge.added","subject":"ver-rpgs2rj","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-rpgs2rj","to":"iss-qdc4vqw"},"at":"2026-09-19T20:24:23.117Z","parents":["evt-yjttarpgr6jc"],"hash":"07e3bf7ea038c51d4b4fb8538cc458dd1100489228b74438f788f1f3439e0750"}
{"id":"evt-vh4jb5zvascn","type":"verification.recorded","subject":"ver-rpgs2rj","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"src/lib/mediamtx-install.test.ts (18 tests: fakes, a loopback HTTP server, local tar). Real run on 2026-09-19 (darwin-arm64): \"Installed MediaMTX v1.21.0 … (checksum verified)\"; ./bin/mediamtx --version → v1.21.0; a second run reports already installed; git check-ignore shows bin/ ignored. Checksums match both the release's checksums.sha256 and GitHub's per-asset digests. Full suite 665 tests pass, 99.85% lines; tsc and eslint clean."},"at":"2026-09-19T20:24:23.118Z","parents":["evt-p4qevsh6qfdg"],"hash":"963a9304ba47d6f7aad6553a13d5f7409d77539bd61af939f7e5458f3c7647f8"}
{"id":"evt-phz710emtb24","type":"node.status_changed","subject":"iss-qdc4vqw","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T20:24:25.012Z","parents":["evt-vh4jb5zvascn"],"hash":"e580599882ff85e0d67d0594163feef29bf1b0dfa44cfd117173e37dcb108197"}
{"id":"evt-ng74y2xergby","type":"node.created","subject":"ver-64pxyd3","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"The app writes a localhost-only MediaMTX config (hashed per-boot API password, only the app user, unused protocols off, only the ICE port on the LAN), starts and supervises MediaMTX from instrumentation with backoff restarts, stops it on exit, and adds camera streams at runtime without writing or leaking credentials; tested without starting a process","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T20:30:49.469Z","parents":["evt-phz710emtb24"],"hash":"dc7344c1fdf03d3d74ed2c7b94d0dad959b8fd3a4ff262c63b2c09a02465a974"}
{"id":"evt-tns4qb1bcdxm","type":"edge.added","subject":"ver-64pxyd3","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-64pxyd3","to":"iss-nk6zrzv"},"at":"2026-09-19T20:30:49.471Z","parents":["evt-ng74y2xergby"],"hash":"cf6e13ef56b529b85f63dba0ec1f5f8942eac398005860d6fb271021d5fa956f"}
{"id":"evt-t6vq74hhtncf","type":"verification.recorded","subject":"ver-64pxyd3","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"src/lib/mediamtx-config.test.ts, mediamtx-supervisor.test.ts, video.test.ts, instrumentation.test.ts and camera.test.ts (\"rtspSourceWithLogin\"). npx vitest run --coverage → 50 files, 716 tests pass (99.86% lines); tsc and eslint clean; next build compiles; pgrep confirms no MediaMTX was started. Config keys checked against MediaMTX v1.21.0 mediamtx.yml, api/openapi.yaml and internal/conf/credential.go."},"at":"2026-09-19T20:30:49.472Z","parents":["evt-tns4qb1bcdxm"],"hash":"fb87ed56562e51703dcaae9114c97379937ed51a3beadad0a4ab6b2928a9db8d"}
{"id":"evt-7crwj3hazzza","type":"node.created","subject":"ver-httak2e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"Real start by the user: next start launches MediaMTX, which accepts the generated config and logs that it's ready; the API and WebRTC signaling listen on 127.0.0.1 only and UDP 8189 on the LAN; stopping the server stops MediaMTX","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T20:30:51.951Z","parents":["evt-t6vq74hhtncf"],"hash":"9ee1f3369a9eaf97d1543261d4e85f6711d6f404530a4b0d880725d21b06b1b3"}
{"id":"evt-wm83ze5endvt","type":"edge.added","subject":"ver-httak2e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-httak2e","to":"iss-nk6zrzv"},"at":"2026-09-19T20:30:51.959Z","parents":["evt-7crwj3hazzza"],"hash":"868729b9261d5bf84ea19aa5fa9a7bd363aa83608b35a710761305f7cd8fdf99"}
{"id":"evt-484cpa2byz24","type":"verification.recorded","subject":"ver-httak2e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pending","evidence":"User test after npm run build && npm start: server log lines \"[mediamtx] …\", and lsof -nP -iTCP:9997 -iTCP:8889 -sTCP:LISTEN, plus lsof -nP -iUDP:8189"},"at":"2026-09-19T20:30:51.960Z","parents":["evt-wm83ze5endvt"],"hash":"96200c4f8f406548733af608474f3128a47b28b0601fce41e94dcb7b8b7da999"}
{"id":"evt-kk0mpms2ndrc","type":"node.status_changed","subject":"iss-nk6zrzv","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"in_progress"},"at":"2026-09-19T20:30:53.636Z","parents":["evt-484cpa2byz24"],"hash":"a0f1b9b38c90ca0fde7fb267b6843865972b4a679487ea129bb233185566c121"}
{"id":"evt-mqp5z5db3qh2","type":"node.created","subject":"ver-s2jz1b6","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"Real start by the user: MediaMTX accepts the generated config; WebRTC signaling and the API listen on 127.0.0.1 only; ICE on UDP :8189","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T20:32:43.306Z","parents":["evt-kk0mpms2ndrc"],"hash":"87b055539e9a91661a2b101d5ec0dcebd0ddbe3aade39ce320d7b6e1814709a9"}
{"id":"evt-82w4d5ax3yj1","type":"edge.added","subject":"ver-s2jz1b6","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-s2jz1b6","to":"iss-nk6zrzv"},"at":"2026-09-19T20:32:43.307Z","parents":["evt-mqp5z5db3qh2"],"hash":"70b8928d3f8e5c7b4c4bf1f6624626e29633f504c8bf7e0990fde6a5b365ed91"}
{"id":"evt-jfnjdtx7mzpj","type":"verification.recorded","subject":"ver-s2jz1b6","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"User's server log on 2026-09-19 15:32:21: \"MediaMTX v1.21.0, darwin, arm64\"; \"configuration loaded from …/.data/mediamtx/mediamtx.yml\"; \"[WebRTC] started with listeners on 127.0.0.1:8889 (TCP/HTTP), :8189 (UDP/ICE)\"; \"[API] started with listener on 127.0.0.1:9997 (TCP/HTTP)\". No RTSP, RTMP, HLS, SRT or MoQ listeners were logged. Still unchecked: that stopping the server stops MediaMTX."},"at":"2026-09-19T20:32:43.308Z","parents":["evt-82w4d5ax3yj1"],"hash":"caceccc7fb50f3510276bd02b824faaa44c473d6149362980a77cbf8f80cdcb9"}
{"id":"evt-jcapg6mxwgt5","type":"node.created","subject":"ver-sf1mzks","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"Stopping the server stops MediaMTX","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T20:33:18.727Z","parents":["evt-jfnjdtx7mzpj"],"hash":"5918d8162fc349602d897856497b1a3dcd8cda0b94b28d75b57b4884cc991e22"}
{"id":"evt-hm2by4azjzpr","type":"edge.added","subject":"ver-sf1mzks","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-sf1mzks","to":"iss-nk6zrzv"},"at":"2026-09-19T20:33:18.729Z","parents":["evt-jcapg6mxwgt5"],"hash":"4577cfb8165103ef5cb54882df743d2406588b380e9b9e035034c820db29aac7"}
{"id":"evt-seev8nnntjvq","type":"verification.recorded","subject":"ver-sf1mzks","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"User stopped the server on 2026-09-19; pgrep -fl mediamtx returned nothing."},"at":"2026-09-19T20:33:18.730Z","parents":["evt-hm2by4azjzpr"],"hash":"7e337610a33592ce39a4c01609bf0784bc530b1789ccc645a51f2de3356c7e23"}
{"id":"evt-cx4e42e42ysm","type":"node.status_changed","subject":"iss-nk6zrzv","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"in_progress","to":"done"},"at":"2026-09-19T20:33:20.659Z","parents":["evt-seev8nnntjvq"],"hash":"667eceb5e7a655d386f58f9deaf543c0c48375f877b5d88fc9508d6dd3fd44c1"}
{"id":"evt-ehjrnw0w1ajy","type":"node.created","subject":"iss-yd2sq2q","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Stop a leftover MediaMTX from an earlier server run on startup","body":"Discovered while building iss-nk6zrzv. If the server dies without a normal exit (force-quit, killed terminal, crash; more likely with npm run dev), its MediaMTX child keeps running and holds ports 8189, 8889 and 9997. The next start's MediaMTX then can't bind and crash-loops with backoff. The workaround today is pkill mediamtx. Fix: record MediaMTX's pid in .data/mediamtx/, and on startup stop a leftover process only if that pid is still our bin/mediamtx (check the command, so an unrelated reused pid is never killed). Tests use fakes, never real processes.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T20:33:23.225Z","parents":["evt-cx4e42e42ysm"],"hash":"30f32672fd964bd376b73e864e78811dc91f41e99e50a608cbf9af4e651c9348"}
{"id":"evt-x8ncj61f2m4j","type":"edge.added","subject":"iss-yd2sq2q","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-yd2sq2q","to":"gol-sxakryh"},"at":"2026-09-19T20:33:23.226Z","parents":["evt-ehjrnw0w1ajy"],"hash":"5f625694d824ba7857c31191d52d2f86f8ae62f60e5cbfcfa2378f00e5b38b49"}
{"id":"evt-kkggfhv82k8a","type":"edge.added","subject":"iss-yd2sq2q","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-yd2sq2q","to":"src/lib/mediamtx-supervisor.ts"},"at":"2026-09-19T20:33:23.227Z","parents":["evt-x8ncj61f2m4j"],"hash":"11a9c2709d03b53b712c11a81abadaa5f64e95b29ec3af3995fec3c2d0a4a7c0"}
{"id":"evt-t3hv9z68czv1","type":"edge.added","subject":"iss-yd2sq2q","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-yd2sq2q","to":"src/lib/video.ts"},"at":"2026-09-19T20:33:23.228Z","parents":["evt-kkggfhv82k8a"],"hash":"5a7eccead42a1252f5d2669492cfbe2c15d037016121c6d1fb38e1235d294233"}
{"id":"evt-r8ad4s4jhgpw","type":"edge.added","subject":"iss-4atmz13","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"parent_of","from":"iss-4atmz13","to":"iss-yd2sq2q"},"at":"2026-09-19T20:33:23.229Z","parents":["evt-t3hv9z68czv1"],"hash":"ef606c14c0ef29a9538119652ef1482d7ed8b9ec4af4966afc80cdf614ea0173"}
{"id":"evt-88zs5h5gg17f","type":"edge.added","subject":"iss-yd2sq2q","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-yd2sq2q","to":"area:video"},"at":"2026-09-19T20:33:23.230Z","parents":["evt-r8ad4s4jhgpw"],"hash":"f04eaa848b5e077d22a68283a76b3d1be03ac60ff6fd3d0a720b234d509cf572"}
{"id":"evt-69m4zgpr4mzg","type":"edge.added","subject":"iss-yd2sq2q","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"discovered_from","from":"iss-yd2sq2q","to":"iss-nk6zrzv"},"at":"2026-09-19T20:33:23.231Z","parents":["evt-88zs5h5gg17f"],"hash":"c4b77e1ae675f302e51d418c5433781a0e4300741a9dd346ddf78bec9b826e40"}
{"id":"evt-qehb93vm0ycn","type":"node.created","subject":"ver-mmjvk5b","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"On startup the bridge stops a leftover MediaMTX recorded in .data/mediamtx/mediamtx.pid (SIGTERM, then SIGKILL after about 3 s), but only when that pid still runs our binary with our config; the pid file is written on spawn and removed on exit","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T20:35:44.472Z","parents":["evt-69m4zgpr4mzg"],"hash":"cb195ffff80a349f25e0976b83c0269220a9643eee71ad0711b7d1cc52b357cd"}
{"id":"evt-k1jkcdnpvpfj","type":"edge.added","subject":"ver-mmjvk5b","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-mmjvk5b","to":"iss-yd2sq2q"},"at":"2026-09-19T20:35:44.473Z","parents":["evt-qehb93vm0ycn"],"hash":"a4325f9e3429ed46171791ff9fedafb98cf249f188410985b79cd4ffa04d930e"}
{"id":"evt-egns6xcvsxz0","type":"verification.recorded","subject":"ver-mmjvk5b","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"src/lib/mediamtx-leftover.test.ts (fake process table; never signals a reused pid), mediamtx-supervisor.test.ts (onSpawn/onExit), video.test.ts (\"leftover MediaMTX and pid file\"). Full suite 738 tests pass, 99.86% lines; tsc and eslint clean; next build compiles."},"at":"2026-09-19T20:35:44.474Z","parents":["evt-k1jkcdnpvpfj"],"hash":"3879a5cf742d0ef6bb36b7ccb4deae76e9972d197f1b71e9080cdad86e99c372"}
{"id":"evt-4rqs4x7276j6","type":"node.created","subject":"ver-r06y7ck","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"Real check by the user: after force-killing the server (kill -9 on next-server), the next start logs \"[video] stopped a MediaMTX left running by an earlier server\" and MediaMTX starts normally","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T20:35:46.362Z","parents":["evt-egns6xcvsxz0"],"hash":"2162360435f0db9b49fe200ef289849460a1d1a92317974e6930f77f933fec7c"}
{"id":"evt-4ycqh2c3rdqp","type":"edge.added","subject":"ver-r06y7ck","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-r06y7ck","to":"iss-yd2sq2q"},"at":"2026-09-19T20:35:46.364Z","parents":["evt-4rqs4x7276j6"],"hash":"1bbff09fcdc57d01100e0ea486995230a0b7b97090c29133d69ecaeca5b9f6f6"}
{"id":"evt-e0y77cepccqd","type":"verification.recorded","subject":"ver-r06y7ck","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pending","evidence":"User test with the new build"},"at":"2026-09-19T20:35:46.365Z","parents":["evt-4ycqh2c3rdqp"],"hash":"18780a1e5853ebbe44fdbe416b1ef1dc602361687279dfd17154df7c517479bf"}
{"id":"evt-zjaa2vr6m15t","type":"node.status_changed","subject":"iss-yd2sq2q","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"in_progress"},"at":"2026-09-19T20:35:48.277Z","parents":["evt-e0y77cepccqd"],"hash":"d1fc6f039f2c5c2bbbc8ef4b38676bbbe2919e0a18d74b50abc7b99d1f901a35"}
{"id":"evt-zc7rkzaets70","type":"node.created","subject":"ver-qsqa18n","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"POST/DELETE /api/cameras/[id]/live/whep check the session, validate the id and stream, relay signaling to MediaMTX on localhost with the bridge login, and never return MediaMTX's address, its error text or a camera credential; the player negotiates WebRTC in the browser, shows connecting/reconnecting/failed with Retry, and the pop-out falls back to snapshots; the camera page and pop-out both offer Main/Sub","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T21:35:04.892Z","parents":["evt-zjaa2vr6m15t"],"hash":"0a12104cdbccd44c7db496b321ed67e47f5038c3fe1851573b914cea58a33aaa"}
{"id":"evt-dqw4sny9j4j0","type":"edge.added","subject":"ver-qsqa18n","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-qsqa18n","to":"iss-cbx21zy"},"at":"2026-09-19T21:35:04.894Z","parents":["evt-zc7rkzaets70"],"hash":"53827af283759ae1b215bad43a3dd797e5f3b07e57d286f4626fb70ac0a9bf42"}
{"id":"evt-r3afr6svxe26","type":"verification.recorded","subject":"ver-qsqa18n","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"src/app/api/cameras/[id]/live/whep/route.test.ts (18 tests), live-player.test.tsx (fake RTCPeerConnection), live-view.test.tsx, live-panel.test.tsx, page.test.tsx. Full suite 769 tests pass, 99.81% lines; tsc and eslint clean; next build lists ƒ /api/cameras/[id]/live/whep. WHEP behaviour checked against MediaMTX v1.21.0 internal/servers/webrtc/http_server.go."},"at":"2026-09-19T21:35:04.895Z","parents":["evt-dqw4sny9j4j0"],"hash":"4fcd50e634ab3957602b8fd5e4399e045338f26d7acee865c08db969fef007ec"}
{"id":"evt-t8adbjc357z1","type":"node.created","subject":"ver-pgf465d","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"Real check by the user: both cameras play live in the pop-out and on the camera page, on main and sub, with the session required and no camera credentials or MediaMTX address visible in the browser","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T21:35:07.527Z","parents":["evt-r3afr6svxe26"],"hash":"5e823af787b849d1b4b3715c9cd639ff971bde25046f099dd2543d8e8437ef19"}
{"id":"evt-vcksn6nsjyhr","type":"edge.added","subject":"ver-pgf465d","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-pgf465d","to":"iss-cbx21zy"},"at":"2026-09-19T21:35:07.530Z","parents":["evt-t8adbjc357z1"],"hash":"e68a14bd5ee9d09fde7ce89631455a5d647fa53c39ff167934438ab2884c8703"}
{"id":"evt-r1qj2e2yyrwe","type":"verification.recorded","subject":"ver-pgf465d","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pending","evidence":"User test after npm run build && npm start (or npm run dev): watch each camera, switch Main/Sub, and check the browser's network tab"},"at":"2026-09-19T21:35:07.531Z","parents":["evt-vcksn6nsjyhr"],"hash":"204c81adcc35d0725e4ecf38bc659526e87a2e20297238b039c03225573a455d"}
{"id":"evt-kmm00xbmnw9k","type":"node.status_changed","subject":"iss-cbx21zy","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"in_progress"},"at":"2026-09-19T21:35:09.464Z","parents":["evt-r1qj2e2yyrwe"],"hash":"5b8d3b666a7b0807cafaae7f631bb591c504a29213af8ac479153072fa81ef28"}
{"id":"evt-j1n23nmpk1yd","type":"node.created","subject":"mea-95yr6gy","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"measurement","title":"User watched both cameras live, on main and sub, in the browser","body":"Four of the goal's six checks, observed by the user on 2026-09-19 in their own browser after the live player shipped: cameras …af2e and …af54, each on the main (4096×1860) and sub (1200×536) streams, played live over WebRTC through the app's session-checked WHEP route (\"looks good\"). No firewall prompt appeared, which fits watching from the same Mac, where the loopback candidate is used. The two remaining checks are motion recording, one per camera, which isn't designed yet (iss-g456j72).","status":"recorded","owner":null,"attrs":{"value":4,"applies_at":"2026-09-19"}},"at":"2026-09-19T21:39:58.273Z","parents":["evt-kmm00xbmnw9k"],"hash":"7b614246944c81a570fc5469993b0b4117da1c1d02aa0defe736024e8aff700b"}
{"id":"evt-3tvvjzpx0fpx","type":"edge.added","subject":"mea-95yr6gy","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"measures","from":"mea-95yr6gy","to":"gol-sxakryh"},"at":"2026-09-19T21:39:58.275Z","parents":["evt-j1n23nmpk1yd"],"hash":"85b9d4aabec09896e90b5696e1ba0f3ae5d23251bcef503d259a7823241692fe"}
{"id":"evt-f58yed61x6g9","type":"node.created","subject":"ver-jra5mms","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"Real check by the user: both cameras play live in the pop-out and on the camera page, on main and sub","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T21:40:00.026Z","parents":["evt-3tvvjzpx0fpx"],"hash":"b8b09e4b2ac8ebac9d16b7909fb23ccc5d561362c2f740aca515bb9fe2ee601f"}
{"id":"evt-2mf7gbv44fmb","type":"edge.added","subject":"ver-jra5mms","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-jra5mms","to":"iss-cbx21zy"},"at":"2026-09-19T21:40:00.027Z","parents":["evt-f58yed61x6g9"],"hash":"d162456933fd05a2e6e019e5106a569ef7635cea9018d32972b1c3b143df207f"}
{"id":"evt-w404t95c5wxk","type":"verification.recorded","subject":"ver-jra5mms","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"User on 2026-09-19: \"looks good\", both cameras, main and sub (measurement mea for gol-sxakryh, value 4 of 6)."},"at":"2026-09-19T21:40:00.028Z","parents":["evt-2mf7gbv44fmb"],"hash":"f54aa94f712f9045480d6c6a0e29d17c3c2791405805ecb3aafab5b8bd36f9d3"}
{"id":"evt-ew0n4dq70n0y","type":"node.status_changed","subject":"iss-cbx21zy","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"in_progress","to":"done"},"at":"2026-09-19T21:40:01.764Z","parents":["evt-w404t95c5wxk"],"hash":"44defac6820b215f010e288c5dd6d875b0d6a0608439728ce7231e10999f0a6f"}
{"id":"evt-w1yxzjdqq09b","type":"node.created","subject":"ver-5z3ts6x","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"Real check by the user: after force-killing the server, the next start stops the leftover MediaMTX and starts a new one normally","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T21:41:16.914Z","parents":["evt-ew0n4dq70n0y"],"hash":"336f262c4593932f7cb7d0b8fd2dedff4512d110913bd407a8104622b919a04a"}
{"id":"evt-6264hv25s2zd","type":"edge.added","subject":"ver-5z3ts6x","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-5z3ts6x","to":"iss-yd2sq2q"},"at":"2026-09-19T21:41:16.915Z","parents":["evt-w1yxzjdqq09b"],"hash":"7ac4403f3783e6dbf707c923c39fc135d60f11a95a1b71d5268c09abb9d42b8d"}
{"id":"evt-55f353kkgk7e","type":"verification.recorded","subject":"ver-5z3ts6x","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"User's dev server log on 2026-09-19 16:40:56: \"[video] stopped a MediaMTX left running by an earlier server (stopped)\", followed by MediaMTX v1.21.0 starting with its usual localhost listeners; the dashboard then served info and snapshot requests normally."},"at":"2026-09-19T21:41:16.916Z","parents":["evt-6264hv25s2zd"],"hash":"fa9da9d18144b3986d8788ba4a1a14e943bb0a375e6f4702e9f0dd195a78dadf"}
{"id":"evt-2qaq4pkj2rp2","type":"node.status_changed","subject":"iss-yd2sq2q","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"in_progress","to":"done"},"at":"2026-09-19T21:41:18.548Z","parents":["evt-55f353kkgk7e"],"hash":"a7fec7c639d5cba7229dcac3a8252b12b15a4a51ff47a216c02646a76d07ebc4"}
{"id":"evt-4vc7pdzkspy4","type":"node.created","subject":"ver-k4vv5k5","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"First slice complete: every child closed, and the user has watched both cameras live in the pop-out and on the camera page behind the admin session","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T21:41:20.308Z","parents":["evt-2qaq4pkj2rp2"],"hash":"c1352b97301f262b32584c3a94099884c9207b0da5637ea956ff49cdc54ec2a2"}
{"id":"evt-xggtmgdgjebx","type":"edge.added","subject":"ver-k4vv5k5","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-k4vv5k5","to":"iss-4atmz13"},"at":"2026-09-19T21:41:20.309Z","parents":["evt-4vc7pdzkspy4"],"hash":"8a13d91e27c50547bfa59405618b466933e348e4c2fd894c6840774004045bd5"}
{"id":"evt-n40y7wbr2hbd","type":"verification.recorded","subject":"ver-k4vv5k5","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"Children iss-5xkn65m, iss-qdc4vqw, iss-nk6zrzv, iss-cbx21zy and iss-yd2sq2q are done with their own evidence. User watched both cameras on main and sub on 2026-09-19 (mea-95yr6gy, 4 of the goal's 6 checks). Commits 5824db8 and 41021a6."},"at":"2026-09-19T21:41:20.310Z","parents":["evt-xggtmgdgjebx"],"hash":"c13f0e6985013f4fe145e0b889d62c2f47ae53f9018e718df6fa35f8bfed8c29"}
{"id":"evt-w69ds2hrjjap","type":"node.status_changed","subject":"iss-4atmz13","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T21:41:22.275Z","parents":["evt-n40y7wbr2hbd"],"hash":"9cb35d1b86150eee9f7fd4c352b36409d3d9f57417ec38648f37adccfdb7d50d"}
{"id":"evt-kwy0vjrsd4c4","type":"node.created","subject":"dec-9z8vdmj","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"decision","title":"Recording: motion-triggered clips with no pre-roll, kept 7 days, in .data (env-overridable), stream chosen by configuration","body":"User's answers on 2026-09-19 while designing iss-g456j72, under gol-sxakryh and dec-9xrjx82. (1) Storage: clips go under the .data directory, with an environment variable to override the location (e.g. RECORDINGS_DIR), so an external drive or NAS can be used without code changes. (2) Clips start when motion is reported and stop a few seconds after it ends; no pre-roll, since MediaMTX keeps no buffer of what came before and continuous recording was rejected for now. The first moment of movement may be missed because cameras report motion slightly late. (3) Which stream is recorded is a configuration option per the user, not a fixed choice; the default should be the sub stream (about 5 MB per minute of motion) with main (about 38 MB per minute) available. (4) Retention: delete clips older than 7 days, which MediaMTX does itself (recordDeleteAfter). (5) Event source: check both ONVIF events and the Hikvision alert stream read-only on one camera first, then decide with evidence, rather than assuming ONVIF works on these cameras.","status":"recorded","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T21:44:42.461Z","parents":["evt-w69ds2hrjjap"],"hash":"782b31386f891ac78649998ef231a5be2f2748b7c73f9ca77b1d3f89dc924026"}
{"id":"evt-ms7s9cx05sfw","type":"edge.added","subject":"dec-9z8vdmj","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"dec-9z8vdmj","to":"gol-sxakryh"},"at":"2026-09-19T21:44:42.464Z","parents":["evt-kwy0vjrsd4c4"],"hash":"286c93e2f28115e607ae3a77572ce7807e68268603f38eaa83b07ae511614949"}
{"id":"evt-2zg9qga5ks7x","type":"node.created","subject":"iss-hdt480y","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Check motion event feeds read-only: ONVIF events vs Hikvision alert stream","body":"Per dec-9z8vdmj, before any recording work. On one camera, read-only: (1) ONVIF — GetEventProperties for the topics offered, then a PullPointSubscription for about a minute, waving at the camera, and record which topics arrive (RuleEngine/CellMotionDetector, VideoSource/MotionAlarm, …) and how quickly. (2) Hikvision — GET /ISAPI/Event/notification/alertStream (digest, multipart) for about a minute, recording the event types and timing; today's probe showed the ONVIF login is accepted by ISAPI but the picture endpoint answered Device Busy, so this may not work either. Record both as findings (no IPs or serials, per dec-kgm44qw), then decide the event source. Needs the user's go-ahead: it subscribes to the camera rather than only reading.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T21:44:52.191Z","parents":["evt-ms7s9cx05sfw"],"hash":"9031d55bf3fc939168cdd41b3a1c9f28213b06a47f4dfed0a9cd52e31970ab51"}
{"id":"evt-79hvbpc2j14f","type":"edge.added","subject":"iss-hdt480y","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-hdt480y","to":"gol-sxakryh"},"at":"2026-09-19T21:44:52.192Z","parents":["evt-2zg9qga5ks7x"],"hash":"e067593bd1e1143992ffc2b5516e68e5f2c2ec6747394879c1ce4e15290d6da6"}
{"id":"evt-3da1a83twh35","type":"edge.added","subject":"iss-g456j72","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"parent_of","from":"iss-g456j72","to":"iss-hdt480y"},"at":"2026-09-19T21:44:52.193Z","parents":["evt-79hvbpc2j14f"],"hash":"98a35912a4ccac2085a9292195286973fa15ab20bbbd267824f99a9ebd4c4127"}
{"id":"evt-v5pmc08pksx6","type":"edge.added","subject":"iss-hdt480y","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-hdt480y","to":"area:video"},"at":"2026-09-19T21:44:52.194Z","parents":["evt-3da1a83twh35"],"hash":"75c71922afa40442cd9437caaae4e36012ad32899a41a45c44824bcd598d0289"}
{"id":"evt-pdttwz00qz5b","type":"edge.added","subject":"iss-hdt480y","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-hdt480y","to":"area:device"},"at":"2026-09-19T21:44:52.195Z","parents":["evt-v5pmc08pksx6"],"hash":"b3a52bd4efb187d1aa1d901a7874697849f50118903e28a88f3151ff20be1cca"}
{"id":"evt-x11eb0bntkw9","type":"node.updated","subject":"iss-g456j72","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"body":"Motion-triggered recording, designed per dec-9z8vdmj (answers given 2026-09-19). Clips start when motion is reported and stop a few seconds after it ends: no pre-roll, because MediaMTX holds no buffer of what came before. Storage under .data, overridable with an environment variable (RECORDINGS_DIR) so an external drive or NAS can be used. Which stream is recorded is a configuration option, defaulting to the sub stream (about 5 MB per minute of motion; main is about 38 MB). Retention: MediaMTX deletes clips older than 7 days (recordDeleteAfter: 7d, recordFormat fmp4). Recording is switched on and off per path over MediaMTX's localhost API as events arrive. The event source is decided by the read-only check in the child issue. Still to design after that: how clips are listed, played and deleted behind the admin session, and what the dashboard shows when a camera is recording. The goal's last 2 checks are one recorded and played-back motion clip per camera."}},"at":"2026-09-19T21:44:56.541Z","parents":["evt-pdttwz00qz5b"],"hash":"42fb6fc8aa29c864e63d486bea7b4718e684781b5a136614c8d0049a3f099964"}
{"id":"evt-s71rh42bj7hk","type":"node.created","subject":"fnd-3kp0rwy","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"finding","title":"Camera …af54 reports motion over ONVIF within seconds; Hikvision's alert stream refuses the ONVIF login with 403","body":"Read-only probe on 2026-09-19 with the stored Operator ONVIF login, about 100 seconds, while the user walked in front of the camera. ONVIF PullPoint delivered events immediately and repeated the full state about once a second. Topics seen: tns1:VideoSource/MotionAlarm (State), tns1:RuleEngine/CellMotionDetector/Motion (IsMotion), tns1:RuleEngine/TamperDetector/Tamper, tns1:VideoSource/ImageTooDark/ImagingService, tns1:Monitoring/OperatingTime/{LastReset,LastReboot,LastClockSynchronization}, tns1:Monitoring/ProcessorUsage, tns1:Device/HardwareFailure/StorageFailure. MotionAlarm tracked the walking: false at start, true at 8.8 s, false at 36.9 s, true at 57.7 s, false at 75.7 s. CellMotionDetector/Motion went true at 8.9 s but stayed true until 75.8 s, so it is much slower to clear and is the worse trigger. GetEventProperties returned a shape this probe's parser didn't read, so the offered topic list wasn't captured; the live events are the evidence. Hikvision /ISAPI/Event/notification/alertStream answered HTTP 403 application/xml for this login, matching the ISAPI picture refusal (fnd-k1jyt83). Caveat: the onvif library logged \"Parse Error: Data after `Connection: close`\" several times during the run; it recovered each time, but a recording controller must tolerate dropped pulls and re-subscribe.","status":"current","owner":"prn-q80g8mz","attrs":{"sources":[{"url":"http://localhost/onvif/device_service","note":"Camera …af54 (address redacted per dec-kgm44qw): ONVIF PullPoint via the onvif library, plus /ISAPI/Event/notification/alertStream; scratch script run from .data and deleted afterwards"}],"as_of":"2026-09-19"}},"at":"2026-09-19T21:50:06.223Z","parents":["evt-x11eb0bntkw9"],"hash":"7e423b20dc4113cf291605f7deb7406b5d5379a0d337f54357fa3a50aeac9a35"}
{"id":"evt-5yg9w820fp5f","type":"edge.added","subject":"fnd-3kp0rwy","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-3kp0rwy","to":"iss-hdt480y"},"at":"2026-09-19T21:50:06.224Z","parents":["evt-s71rh42bj7hk"],"hash":"5ff8a879774111421fa9cf9d5d25a1f216101d82b3239cfb12c1c951df5474f8"}
{"id":"evt-7x7h9eh0t7d8","type":"edge.added","subject":"fnd-3kp0rwy","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-3kp0rwy","to":"iss-g456j72"},"at":"2026-09-19T21:50:06.225Z","parents":["evt-5yg9w820fp5f"],"hash":"0505ae3c1b968677bb8cc84f9455c9948856482e92d5220fc4208f2aaefe4498"}
{"id":"evt-kdnkm81nr5nk","type":"edge.added","subject":"fnd-3kp0rwy","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-3kp0rwy","to":"gol-sxakryh"},"at":"2026-09-19T21:50:06.226Z","parents":["evt-7x7h9eh0t7d8"],"hash":"d8c2d5f5241b411201b192cadac5ec79a4b7b9ef38b5c6ed545560b95b665113"}
{"id":"evt-r81bw4h3c3yb","type":"edge.added","subject":"fnd-3kp0rwy","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"discovered_from","from":"fnd-3kp0rwy","to":"iss-hdt480y"},"at":"2026-09-19T21:50:06.227Z","parents":["evt-kdnkm81nr5nk"],"hash":"c34c51f0850ab2e5de1d50646eec51b55464ed237ba2d7376defcb26c00b6126"}
{"id":"evt-v6w9rsx8erzq","type":"node.created","subject":"dec-d50j69a","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"decision","title":"Motion comes from ONVIF events, triggering on VideoSource/MotionAlarm","body":"Per fnd-srfdnmm and the probe in fnd-dw1pqcw's successor: ONVIF PullPoint works on camera …af54 with the Operator login and reports motion within seconds, while Hikvision's alert stream refuses that login with 403. So recording triggers on ONVIF, which also keeps the app vendor-neutral (pri-p9h51hx). Use tns1:VideoSource/MotionAlarm, whose State follows movement closely (true about 9 s after walking started, false about 20 s after stopping). Do not use RuleEngine/CellMotionDetector/Motion: it went true at the same moment but stayed true for over a minute after movement ended. The camera repeats its whole state about once a second, so the controller acts on changes, not on every message, and it must survive dropped pulls: the onvif library logged repeated parse errors during the probe and recovered, so re-subscribing has to be automatic. ISAPI remains unused; revisit only if a web-account login is ever added.","status":"recorded","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T21:50:12.326Z","parents":["evt-r81bw4h3c3yb"],"hash":"8d1a17d054f2733c3c4f9a4fc56de296ebf793cca7a6dcc7d1df92c37da9da78"}
{"id":"evt-90ck7pe92yzb","type":"edge.added","subject":"dec-d50j69a","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"answers","from":"dec-d50j69a","to":"iss-hdt480y"},"at":"2026-09-19T21:50:12.327Z","parents":["evt-v6w9rsx8erzq"],"hash":"86675848af2322f42bf9c4453467e1633bb947ebc64f7b5e70a63ad8c842b9a6"}
{"id":"evt-hq3cwd9sq2kb","type":"edge.added","subject":"dec-d50j69a","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"dec-d50j69a","to":"gol-sxakryh"},"at":"2026-09-19T21:50:12.328Z","parents":["evt-90ck7pe92yzb"],"hash":"75118130559649fcbe4d30f7eab54e5734dad48b2954fb7be7b4898204899bcd"}
{"id":"evt-3w4qpcc7n1qv","type":"node.status_changed","subject":"iss-hdt480y","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T21:50:16.490Z","parents":["evt-hq3cwd9sq2kb"],"hash":"0930689ca703fb569f1f4a20b5e04888fde27ca44db69abd4f9234fe5fb18218"}
{"id":"evt-y6y3j2xmc9sn","type":"node.created","subject":"iss-ws9nb88","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Record motion clips: ONVIF motion watcher driving MediaMTX recording, with retention","body":"Per dec-9z8vdmj and dec-d50j69a. A server-only motion module subscribes to each registry camera's ONVIF events through the existing cached connection and acts on changes to tns1:VideoSource/MotionAlarm (the camera repeats its state about once a second). On motion: configure that camera's recording path in MediaMTX (source pulled continuously, record on, fMP4) so a clip is written. After motion ends plus a post-roll of a few seconds: recording off and the source back to on-demand, so the camera isn't pulled while nothing happens. Re-subscribes by itself after dropped pulls (fnd-3kp0rwy). Configuration: RECORDINGS_DIR (default .data/recordings), the stream to record (default sub), retention days (default 7), and an off switch. Retention is our own hourly sweep of the recordings folder, not MediaMTX's, so it runs whether or not a path is currently recording. Started from instrumentation after the video bridge. Tests use a fake camera event source and a stubbed MediaMTX API (pri-e14bahk).","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T21:51:50.582Z","parents":["evt-3w4qpcc7n1qv"],"hash":"1c9ffab6dbf55b0e7378668efb2796895e0e178dba6268713c1b9d6caaa7d332"}
{"id":"evt-szpfkcnf9pfh","type":"edge.added","subject":"iss-ws9nb88","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-ws9nb88","to":"gol-sxakryh"},"at":"2026-09-19T21:51:50.586Z","parents":["evt-y6y3j2xmc9sn"],"hash":"b6e87de7222ea3ecf49e852c05e8e2e522eb11e7dfae5524b9daa6ee853b8811"}
{"id":"evt-25cqdt058rtc","type":"edge.added","subject":"iss-ws9nb88","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-ws9nb88","to":"src/lib/motion.ts"},"at":"2026-09-19T21:51:50.587Z","parents":["evt-szpfkcnf9pfh"],"hash":"543d1203161566d5c92a6f813b8e9dfcf0aee0e2e10aa9b11c48cc3448b69e7a"}
{"id":"evt-q7xfcmjd2xae","type":"edge.added","subject":"iss-ws9nb88","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-ws9nb88","to":"src/lib/recordings.ts"},"at":"2026-09-19T21:51:50.588Z","parents":["evt-25cqdt058rtc"],"hash":"2b4efbd480aee8da4bb7f9f93458a07de072f77e8d58d0535874f44837c6da29"}
{"id":"evt-d8tmqkk62qav","type":"edge.added","subject":"iss-ws9nb88","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-ws9nb88","to":"src/instrumentation.ts"},"at":"2026-09-19T21:51:50.589Z","parents":["evt-q7xfcmjd2xae"],"hash":"3c999d385f7d772f281eb527909b6f8d2e4c48dc5cbb09fc9fa3fa647e136805"}
{"id":"evt-gr4z8fx1vtc4","type":"edge.added","subject":"iss-g456j72","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"parent_of","from":"iss-g456j72","to":"iss-ws9nb88"},"at":"2026-09-19T21:51:50.590Z","parents":["evt-d8tmqkk62qav"],"hash":"89ad14104b481e6113e02c52c8c44af16f8b9e43372eccda4def83b9e72b2e03"}
{"id":"evt-jhr3base57e1","type":"edge.added","subject":"iss-ws9nb88","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-ws9nb88","to":"area:video"},"at":"2026-09-19T21:51:50.591Z","parents":["evt-gr4z8fx1vtc4"],"hash":"d9f9a94c108d33d2c18a42ff99a1c37260c9ff1a15f4f0e51e4722f915fc1678"}
{"id":"evt-m054t5j3mmg3","type":"node.created","subject":"iss-v677bbd","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Browse, play and delete motion clips behind the admin session","body":"Per dec-9z8vdmj, after the recorder. A Recordings page lists clips per camera, newest first, with the time, length and size, and plays them in the browser (fMP4 over a route handler that supports Range requests, so seeking works). Deleting a clip asks first. Every route checks the session and resolves the camera from the registry; file paths are never taken from the request (pri-m1csgrm), so a request can only name a clip that the recordings folder actually holds. The camera page links to that camera's clips, and the dashboard card shows when a camera is recording.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T21:51:54.615Z","parents":["evt-jhr3base57e1"],"hash":"0022d4f96766890597bdcc76de7bfd26ded16f68a257cfcb41db06739f8c8037"}
{"id":"evt-jq2phdnh4mwe","type":"edge.added","subject":"iss-v677bbd","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-v677bbd","to":"gol-sxakryh"},"at":"2026-09-19T21:51:54.616Z","parents":["evt-m054t5j3mmg3"],"hash":"3d24bbd7d7ff768d8b58404b72bb46ece9fe7580d97acd5e16daf2688f6b99b5"}
{"id":"evt-xdybt29kct1m","type":"edge.added","subject":"iss-v677bbd","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-v677bbd","to":"src/app/recordings/"},"at":"2026-09-19T21:51:54.617Z","parents":["evt-jq2phdnh4mwe"],"hash":"e9600b1b50a90c4de03d2b33d3a039df88021686a78195d60b616f8c4e703b7f"}
{"id":"evt-6nhmt9n3mndx","type":"edge.added","subject":"iss-v677bbd","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-v677bbd","to":"src/app/api/recordings/"},"at":"2026-09-19T21:51:54.618Z","parents":["evt-xdybt29kct1m"],"hash":"e8c58ba4bd94c8adf5fc0dbb39d8b15ceadc4dae4d9027f68b1eebe0ac8a26f3"}
{"id":"evt-3y54bg2z49vh","type":"edge.added","subject":"iss-g456j72","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"parent_of","from":"iss-g456j72","to":"iss-v677bbd"},"at":"2026-09-19T21:51:54.619Z","parents":["evt-6nhmt9n3mndx"],"hash":"fb9eb814fc5d2be18133483339475228409f8956277189fff635e0f3cecc5c9a"}
{"id":"evt-hftcfaxfxzjx","type":"edge.added","subject":"iss-v677bbd","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-v677bbd","to":"area:video"},"at":"2026-09-19T21:51:54.620Z","parents":["evt-3y54bg2z49vh"],"hash":"fd1a704c07e6be61533dbf1b6146a34309999f6897ea08b46fda86e801269b31"}
{"id":"evt-wgae0f19mnfz","type":"edge.added","subject":"iss-v677bbd","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-v677bbd","to":"area:ui"},"at":"2026-09-19T21:51:54.621Z","parents":["evt-hftcfaxfxzjx"],"hash":"f8b0be12bbc80e39d47b3a2c9e9731104f58770103ba7586718d7d0489efea35"}
{"id":"evt-dy4wxfhczrsa","type":"node.created","subject":"ver-5gs8ng9","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"Motion from ONVIF VideoSource/MotionAlarm starts recording through a per-camera MediaMTX path and stops after a 10 s post-roll; only changes act, changes are applied one at a time, failures are logged and retried; clips land in RECORDINGS_DIR (default .data/recordings) per camera, the configured stream is used, and an hourly sweep deletes clips older than the retention window","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T21:56:20.284Z","parents":["evt-wgae0f19mnfz"],"hash":"ddfa12b57430e0d6f3697bc2c646f00a5f02d5e18a72a96a4451b2075aedc448"}
{"id":"evt-x78nqh6vvynk","type":"edge.added","subject":"ver-5gs8ng9","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-5gs8ng9","to":"iss-ws9nb88"},"at":"2026-09-19T21:56:20.288Z","parents":["evt-dy4wxfhczrsa"],"hash":"bed3d140e09f5299788b86929ac335388b989d0d16dd8c1f4f9db43e2f108e4a"}
{"id":"evt-9en2g7vhe8x5","type":"verification.recorded","subject":"ver-5gs8ng9","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"src/lib/motion.test.ts, motion-recorder.test.ts, recordings.test.ts, mediamtx-config.test.ts (\"recording paths\"), video.test.ts (\"setRecording\"), instrumentation.test.ts. Full suite 813 tests pass, 99.66% lines; tsc and eslint clean; next build compiles. Nothing was recorded from a real camera in these tests."},"at":"2026-09-19T21:56:20.289Z","parents":["evt-x78nqh6vvynk"],"hash":"82b5cdf4d205ae6979fb7b63009f6cb490417551f4c1e63936b507720643cc4f"}
{"id":"evt-tb8r7cvwjxbj","type":"node.created","subject":"ver-4yj6bh2","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"Real check by the user: walking in front of a camera produces a clip in .data/recordings/&lt;camera id&gt;/ that plays, and recording stops afterwards","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T21:56:22.369Z","parents":["evt-9en2g7vhe8x5"],"hash":"02b3d4562dc1b8035ec7948138169e979ce5c75457bdef5591f7102edf3fae27"}
{"id":"evt-bmf0a3nw9n8t","type":"edge.added","subject":"ver-4yj6bh2","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-4yj6bh2","to":"iss-ws9nb88"},"at":"2026-09-19T21:56:22.371Z","parents":["evt-tb8r7cvwjxbj"],"hash":"19854dc60349872fc2a05b84f01f53b70128da14f4c058c2aaecd0fc0451d36d"}
{"id":"evt-9ykcspgn1s0h","type":"verification.recorded","subject":"ver-4yj6bh2","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pending","evidence":"User test after restarting the server: watch the log for \"[motion] recording …\" and \"[motion] stopped recording …\", then open the newest file under .data/recordings"},"at":"2026-09-19T21:56:22.372Z","parents":["evt-bmf0a3nw9n8t"],"hash":"14fa7604ce2b8a79e1c62160c4bb33a62b5961533130cf9e3e8d31ae41f6b908"}
{"id":"evt-tjemq34879n9","type":"node.status_changed","subject":"iss-ws9nb88","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"in_progress"},"at":"2026-09-19T21:56:24.082Z","parents":["evt-9ykcspgn1s0h"],"hash":"21858a059abeb41c7cb30f9c8e71c30eebf0d533e3b78fd2a4e86bebe67bcd20"}
{"id":"evt-4dvvscpkf0wy","type":"node.created","subject":"fnd-vbq0qrp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"finding","title":"Next 16's connection() never resolves outside a request, so startup code must not call registry functions that use it","body":"Found on 2026-09-19 while testing motion recording. src/lib/camera-registry.ts listCameras() awaits connection() from next/server to opt out of prerendering. Called from instrumentation.ts at server start, that await simply never settles: no error, no log, and the recorder silently never started (no \"[motion] watching …\" line and no recordings folder), while the dev server and MediaMTX ran normally. Fix: allCameraRecords() reads the same registry without connection(), for code that runs outside a request; listCameras() stays for pages and routes. Also, register() in instrumentation holds up the server until it returns, so background work like watching cameras is started without awaiting it. Applies to any future startup or background code in this app.","status":"current","owner":"prn-q80g8mz","attrs":{"sources":[{"node":"iss-ws9nb88","note":"Observed on the user's dev server: MediaMTX started, pages served, no [motion] logs, .data/recordings missing"},{"url":"https://nextjs.org/docs/app/api-reference/functions/connection","note":"connection() is documented for request-time opt-out of prerendering"}],"as_of":"2026-09-19"}},"at":"2026-09-19T22:02:07.530Z","parents":["evt-tjemq34879n9"],"hash":"c34b08ba03f7b70419c6d6de4feaacf08c9a8d98d3617a22ea004b6ae65e1ec1"}
{"id":"evt-fpkhbrq3j0xe","type":"edge.added","subject":"fnd-vbq0qrp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-vbq0qrp","to":"iss-ws9nb88"},"at":"2026-09-19T22:02:07.532Z","parents":["evt-4dvvscpkf0wy"],"hash":"82217109b924ad2d39f077c1614a070f9c3e4df686df182523be96810bf222e8"}
{"id":"evt-e45rzbqc5085","type":"edge.added","subject":"fnd-vbq0qrp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-vbq0qrp","to":"pri-e0qjyy0"},"at":"2026-09-19T22:02:07.533Z","parents":["evt-fpkhbrq3j0xe"],"hash":"0c612ba5aaaa78fae0ec12039910c3455f4757625d53026ba91c676320e5192d"}
{"id":"evt-7h76c4xfe8fv","type":"edge.added","subject":"fnd-vbq0qrp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"discovered_from","from":"fnd-vbq0qrp","to":"iss-ws9nb88"},"at":"2026-09-19T22:02:07.534Z","parents":["evt-e45rzbqc5085"],"hash":"b60ff41e988497367ee4872418e7f77db2aa2ed45ea0103a7de359679558dd4e"}
{"id":"evt-fn09kn6wekz6","type":"node.created","subject":"fnd-y7h6pyf","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"finding","title":"Camera …af54's sub stream carries G711 audio; camera …af2e's does not","body":"Observed on 2026-09-19 in MediaMTX's own log while recording motion clips from both cameras' sub streams: \"[path cam-…af54-sub-rec] [recorder] recording 2 tracks (H264, G711)\" and \"[path cam-…af2e-sub-rec] [recorder] recording 1 track (H264)\". So at least one I91ET has a working microphone and publishes audio over RTSP; the other does not (no microphone, audio disabled, or stream type set to video only in its web page). Consequences: recorded clips from …af54 contain a G711 audio track, which browsers generally cannot decode inside MP4, so playback may be silent even though the video plays; and live WebRTC can carry G711 (Opus and G711 are both allowed), which is worth checking when audio in the live player is wanted. The main streams were not checked, nor was the other camera's configuration.","status":"current","owner":"prn-q80g8mz","attrs":{"sources":[{"node":"iss-ws9nb88","note":"MediaMTX v1.21.0 recorder log lines during the user's motion-recording test"}],"as_of":"2026-09-19"}},"at":"2026-09-19T22:07:36.606Z","parents":["evt-7h76c4xfe8fv"],"hash":"3cc4250476e4d9153f098cf67ec1ee94e32ff42c94f895c689dd487d5f7bd7f9"}
{"id":"evt-w6mdsh4dhs11","type":"edge.added","subject":"fnd-y7h6pyf","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-y7h6pyf","to":"que-ph8eak5"},"at":"2026-09-19T22:07:36.609Z","parents":["evt-fn09kn6wekz6"],"hash":"ca45632818099a645ed3442a1a7be7dac2b502862646a87c2d87bd76c4fa7e1d"}
{"id":"evt-hkzghm8k50gr","type":"edge.added","subject":"fnd-y7h6pyf","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-y7h6pyf","to":"iss-td8y1cc"},"at":"2026-09-19T22:07:36.610Z","parents":["evt-w6mdsh4dhs11"],"hash":"b710073f65c4c80bffeb993fa99b126aee10304a58910ece5a7318e3c7738b62"}
{"id":"evt-ek2ad9wj3s41","type":"edge.added","subject":"fnd-y7h6pyf","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-y7h6pyf","to":"gol-sxakryh"},"at":"2026-09-19T22:07:36.614Z","parents":["evt-hkzghm8k50gr"],"hash":"1ab59d4494576780c1dda07f1271c0ea4cb39e6013e5684603cad42e9b6a1b42"}
{"id":"evt-pts2yq477k34","type":"node.created","subject":"ver-wbk0v4t","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"Real check by the user: walking in front of the cameras starts and stops recording, and clips land on disk","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T22:07:39.759Z","parents":["evt-ek2ad9wj3s41"],"hash":"b560ba8edba8e4065da74b4704ab2d841f6218e939285114eb778cbc7e5d1f46"}
{"id":"evt-fqxcxhh97c1x","type":"edge.added","subject":"ver-wbk0v4t","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-wbk0v4t","to":"iss-ws9nb88"},"at":"2026-09-19T22:07:39.760Z","parents":["evt-pts2yq477k34"],"hash":"7f61fcd22e5c71adf48faee46fab3f7f23f551e669c17a95472868216792ea7b"}
{"id":"evt-gr6gmbpx3d72","type":"verification.recorded","subject":"ver-wbk0v4t","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"User's dev server log on 2026-09-19 17:06: \"[motion] watching 2 camera(s), recording the sub stream\", then for both cameras \"[motion] recording <id>\", MediaMTX \"[recorder] recording … tracks\", and \"[motion] stopped recording <id>\" after the post-roll. Files on disk: .data/recordings/cam-<id>-sub-rec/2026-09-19_17-06-*.mp4, 1.2–2.5 MB each; box inspection shows valid fMP4 (ftyp, moov, moof/mdat fragments, avc1)."},"at":"2026-09-19T22:07:39.761Z","parents":["evt-fqxcxhh97c1x"],"hash":"82786785d155aa9499945f9306118813533d1c9bb409d50dc905c034faf65869"}
{"id":"evt-gsf60sb8qp1v","type":"node.status_changed","subject":"iss-ws9nb88","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"in_progress","to":"done"},"at":"2026-09-19T22:07:43.955Z","parents":["evt-gr6gmbpx3d72"],"hash":"5f7bb9c68f09bb12cc86d905793efc99f10974c05845cdbfb112429ff33e1094"}
{"id":"evt-s5abwkxw1wm1","type":"node.created","subject":"ver-ncyy24g","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"A session-checked Recordings page lists clips per camera newest first with time and size, plays them with seeking (Range requests), and deletes after a confirmation; clip ids are matched against fixed patterns and resolved inside the recordings folder, so no request can reach another file; the camera page links to its own clips","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T22:13:30.531Z","parents":["evt-gsf60sb8qp1v"],"hash":"cabdfc8bd5463c8acb6c382e14e8e1a7d26f15a68a13504cc9069374ce55cd0c"}
{"id":"evt-n01s9wvxbbp7","type":"edge.added","subject":"ver-ncyy24g","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-ncyy24g","to":"iss-v677bbd"},"at":"2026-09-19T22:13:30.533Z","parents":["evt-s5abwkxw1wm1"],"hash":"58a3ffe785a9456e3d9773a387a4cb9f1855e601c43608c790c3cbb9e12898b6"}
{"id":"evt-1hgpgz7w83rh","type":"verification.recorded","subject":"ver-ncyy24g","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"src/lib/recordings.test.ts (listClips, clipFile traversal refusals, deleteClip), src/app/api/recordings/route.test.ts, src/app/api/recordings/clip/route.test.ts (full, range, suffix, 416, traversal), src/app/recordings/clip-list.test.tsx, src/app/recordings/page.test.tsx. Full suite 861 tests pass, 99.67% lines; tsc and eslint clean; next build lists ƒ /recordings, /api/recordings and /api/recordings/clip."},"at":"2026-09-19T22:13:30.534Z","parents":["evt-n01s9wvxbbp7"],"hash":"fbc9a2e411cb1de6b54f186f1a27a332ef6d651adf4eef1f72f7df69ff93a51e"}
{"id":"evt-m27ze50m7ew2","type":"node.created","subject":"ver-aexbfgn","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"Real check by the user: a motion clip from each camera plays in the browser on /recordings, and deleting one works","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T22:13:32.598Z","parents":["evt-1hgpgz7w83rh"],"hash":"c5ef657ba791dd92636ecf742dd6248d77f79c49d20ebb65e8a8715f2432f861"}
{"id":"evt-rgavza6239gj","type":"edge.added","subject":"ver-aexbfgn","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-aexbfgn","to":"iss-v677bbd"},"at":"2026-09-19T22:13:32.599Z","parents":["evt-m27ze50m7ew2"],"hash":"9a6583fb04bbebc781101bc5a5d3ac538e45048ea2d0c39234968880eddd51ff"}
{"id":"evt-8jf435p6dswr","type":"verification.recorded","subject":"ver-aexbfgn","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pending","evidence":"User test after restarting the server: open /recordings, play a clip from each camera, delete one"},"at":"2026-09-19T22:13:32.600Z","parents":["evt-rgavza6239gj"],"hash":"4abc418fc138f427492f611f43968dcbbfa9a74f93bc4148c7aacb4abcfca654"}
{"id":"evt-bmcdhjathg9p","type":"node.status_changed","subject":"iss-v677bbd","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"in_progress"},"at":"2026-09-19T22:13:34.671Z","parents":["evt-8jf435p6dswr"],"hash":"035e51380b988013c77356b04ed50ea7bf55555138e45d969fccaca0e15ca6df"}
{"id":"evt-cyh6g3kt46hq","type":"node.created","subject":"fnd-0qyrzm9","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"finding","title":"Recorded G711 audio from camera …af54 does play in the browser, alongside H.264 video","body":"Corrects the caveat in fnd-y7h6pyf, which assumed browsers could not decode G711 inside MP4. On 2026-09-19 the user played motion clips on /recordings and reported that audio worked. So MediaMTX's fMP4 recordings of a G711 (A-law) track are playable in the user's browser as written, with no transcoding, and no audio conversion is needed for playback. Camera …af54 publishes H264 plus G711 on its sub stream; …af2e publishes video only. Untested: whether other browsers behave the same, and whether audio also arrives over the live WebRTC player.","status":"current","owner":"prn-q80g8mz","attrs":{"sources":[{"node":"iss-v677bbd","note":"User's own playback on /recordings: \"this works well and audio seems to work too\""}],"as_of":"2026-09-19"}},"at":"2026-09-19T22:15:57.638Z","parents":["evt-bmcdhjathg9p"],"hash":"2334e4b4fe34f75664847d726b4a3c702108b6ca6d1fdcc9f5ae51c32e5167c4"}
{"id":"evt-pvzz20apprz9","type":"edge.added","subject":"fnd-0qyrzm9","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-0qyrzm9","to":"que-ph8eak5"},"at":"2026-09-19T22:15:57.639Z","parents":["evt-cyh6g3kt46hq"],"hash":"6624e96f367c3a826ba21949c49f3814cc8ae61e22b7110fe04f5efc055d4971"}
{"id":"evt-8nwppxpaj613","type":"edge.added","subject":"fnd-0qyrzm9","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-0qyrzm9","to":"iss-td8y1cc"},"at":"2026-09-19T22:15:57.640Z","parents":["evt-pvzz20apprz9"],"hash":"c4741584dd29cd005f2c0ecf7f3cdcfe61426abf482ef3664d29d70657b68877"}
{"id":"evt-k5wqxg4b8t29","type":"edge.added","subject":"fnd-0qyrzm9","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-0qyrzm9","to":"gol-sxakryh"},"at":"2026-09-19T22:15:57.641Z","parents":["evt-8nwppxpaj613"],"hash":"3165e4ebf290ddec762097f256959ee4b0b5b6393808d69d2e532d86925b9e54"}
{"id":"evt-kgedg91yjbht","type":"edge.added","subject":"fnd-0qyrzm9","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"supersedes","from":"fnd-0qyrzm9","to":"fnd-y7h6pyf"},"at":"2026-09-19T22:15:57.642Z","parents":["evt-k5wqxg4b8t29"],"hash":"f9a55af6f43f68b49eac03d7b09fe302480032fcd3f66ab9429da2204b9d95d5"}
{"id":"evt-kxff6phn2yet","type":"node.status_changed","subject":"fnd-y7h6pyf","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"current","to":"superseded"},"at":"2026-09-19T22:15:57.643Z","parents":["evt-kgedg91yjbht"],"hash":"bba33d5a64fcf42c2a472d188960e9fc659f97d2a1cae7b53ba1748632607756"}
{"id":"evt-q6y6kzp0x3rt","type":"node.created","subject":"ver-60m8ttp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"Real check by the user: motion clips play in the browser on /recordings","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T22:16:00.004Z","parents":["evt-kxff6phn2yet"],"hash":"bacbde6fdba262183500c21401a8349287c68fb122a0ed4e2b075b4f0142bb47"}
{"id":"evt-xah5c4gbg51b","type":"edge.added","subject":"ver-60m8ttp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-60m8ttp","to":"iss-v677bbd"},"at":"2026-09-19T22:16:00.005Z","parents":["evt-q6y6kzp0x3rt"],"hash":"5fc6c47a5ee1452b99e37a9e03b0535035c0b7d7501c54a831a3abc321bdeab5"}
{"id":"evt-tvjs688dbkvd","type":"verification.recorded","subject":"ver-60m8ttp","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"User on 2026-09-19: \"this works well and audio seems to work too\" after opening /recordings and playing clips."},"at":"2026-09-19T22:16:00.006Z","parents":["evt-xah5c4gbg51b"],"hash":"3d1b87655feb999a73e54d75a528a625c905420e09c088f434a4034416ccfd63"}
{"id":"evt-rd8yca1xcevk","type":"node.status_changed","subject":"iss-v677bbd","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"in_progress","to":"done"},"at":"2026-09-19T22:16:01.993Z","parents":["evt-tvjs688dbkvd"],"hash":"7a448702e6738eb96202a1e82f57a913a64dfb18b154a98b2fcd0246879c0f68"}
{"id":"evt-ap6xjp24dfr8","type":"node.created","subject":"mea-a0g012a","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"measurement","title":"User recorded and played back motion clips from both cameras","body":"The goal's remaining 2 checks, observed by the user on 2026-09-19, bringing the total to 6 of 6. Walking in front of each camera started and stopped recording (server log, plus clips on disk under .data/recordings), and the user then played two clips from each camera on /recordings: \"this works well and audio seems to work too\". Audio from …af54's G711 track played in the browser (fnd-0qyrzm9). Together with mea-95yr6gy (both cameras watched live on main and sub), every feature in the target is confirmed on both cameras.","status":"recorded","owner":null,"attrs":{"value":6,"applies_at":"2026-09-19"}},"at":"2026-09-19T22:16:44.984Z","parents":["evt-rd8yca1xcevk"],"hash":"91369d403de77d0bfb5d2c88c2d732d8fe3397991fe3fb207bfbd5605b434c6b"}
{"id":"evt-zv1z537kfmc3","type":"edge.added","subject":"mea-a0g012a","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"measures","from":"mea-a0g012a","to":"gol-sxakryh"},"at":"2026-09-19T22:16:44.985Z","parents":["evt-ap6xjp24dfr8"],"hash":"a7865a4c35bec87c11aca5a9b536ec46fcb4b7f2c759e6d559930a2944350cf6"}
{"id":"evt-hrxw69g3nvke","type":"node.created","subject":"ver-5kvnk31","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"Motion recording works end to end: clips are recorded on motion from both cameras, stored under .data with retention, and played back in the browser","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T22:16:48.182Z","parents":["evt-zv1z537kfmc3"],"hash":"72a49cd162ce9b36eafd0b3499b954491008119bd95c8ea6756f3b6c9a94ad36"}
{"id":"evt-y6pdt76849w4","type":"edge.added","subject":"ver-5kvnk31","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-5kvnk31","to":"iss-g456j72"},"at":"2026-09-19T22:16:48.183Z","parents":["evt-hrxw69g3nvke"],"hash":"13977feeaf64f0c0088f40f40d89ad084f7975a55bf1429cad1f8eacef3b5465"}
{"id":"evt-5t6zbc00w86p","type":"verification.recorded","subject":"ver-5kvnk31","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"Children iss-hdt480y, iss-ws9nb88 and iss-v677bbd are done with their own evidence. User played two clips from each camera on /recordings on 2026-09-19 (measurement 6 of 6 for gol-sxakryh). Commits 4a07905 and 4fc68ca. Not yet observed in reality: the 7-day retention sweep actually deleting an aged clip."},"at":"2026-09-19T22:16:48.184Z","parents":["evt-y6pdt76849w4"],"hash":"5ff23bcbe67b4e243b189486bbb301fc7c381717bd9c2e8126d838a3afaef2e9"}
{"id":"evt-3hey0db4tj74","type":"node.status_changed","subject":"iss-g456j72","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T22:16:50.958Z","parents":["evt-5t6zbc00w86p"],"hash":"e0251a2668737729586dea3707a557d222f2d0517a2e03b7061a537379b8c637"}
{"id":"evt-xgsp0cxgeqsc","type":"node.created","subject":"iss-xmk9w3e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Confirm the 7-day clip retention actually deleted old clips","body":"The only part of motion recording never observed in reality (ver-5kvnk31). Today's clips (2026-09-19) pass the 7-day window on 2026-09-26, so a check on 2026-09-27 should find them gone. How to check, with the server having run at some point that day: (1) `find .data/recordings -type f -mtime +7` prints nothing; (2) the server log shows a line like \"[motion] deleted N clip(s) older than 7 days\"; (3) /recordings no longer lists clips from more than a week ago. Note the sweep only runs while the server runs: once at startup, then hourly, so a machine that was off for days sweeps on its next start. If clips are still there, look first at sweepOldRecordings in src/lib/recordings.ts and whether the recorder started at all (\"[motion] watching N camera(s)\").","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T22:19:19.302Z","parents":["evt-3hey0db4tj74"],"hash":"ee5a24e465b7a47807d5add833960ea60d87285b06a824dd74f7b09582236359"}
{"id":"evt-22ssq7q8rgjx","type":"edge.added","subject":"iss-xmk9w3e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-xmk9w3e","to":"gol-sxakryh"},"at":"2026-09-19T22:19:19.303Z","parents":["evt-xgsp0cxgeqsc"],"hash":"a2789a5541eb2eab23e3124d279bd175e7b416f636f807b99285301a7f63da82"}
{"id":"evt-mv5zfwfrvz6f","type":"edge.added","subject":"iss-xmk9w3e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-xmk9w3e","to":"src/lib/recordings.ts"},"at":"2026-09-19T22:19:19.304Z","parents":["evt-22ssq7q8rgjx"],"hash":"c3840944b05ccb3b394342b05baa4c2c8ad65cfaee4f5b613b2958a8d1d452ba"}
{"id":"evt-grb705n504fn","type":"edge.added","subject":"iss-xmk9w3e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-xmk9w3e","to":"src/lib/motion-recorder.ts"},"at":"2026-09-19T22:19:19.305Z","parents":["evt-mv5zfwfrvz6f"],"hash":"0ce19d0231a63e682cea1a02dcb99acfd2401d474f6f3e162c7c4defccf6aabe"}
{"id":"evt-k2y32pmzgsav","type":"edge.added","subject":"iss-xmk9w3e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-xmk9w3e","to":"area:video"},"at":"2026-09-19T22:19:19.306Z","parents":["evt-grb705n504fn"],"hash":"2e89be635bb5fe8dbbd45c1acd6b1f503d0009ba67ee3f123333a19461756a4d"}
{"id":"evt-tzf54n4r5mav","type":"node.updated","subject":"iss-xmk9w3e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"attrs":{"revisit":"2026-09-27","deferred_because":"Nothing to check until today's clips pass the 7-day window on 2026-09-26; the user asked to check back in 8 days (2026-09-19)."}}},"at":"2026-09-19T22:19:23.949Z","parents":["evt-k2y32pmzgsav"],"hash":"1cbd9f706af6f551855d96434554a8398d7f507301b3e0bcd40249852c24bb86"}
{"id":"evt-ka14nmmw0r5k","type":"node.status_changed","subject":"iss-xmk9w3e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"deferred"},"at":"2026-09-19T22:19:23.952Z","parents":["evt-tzf54n4r5mav"],"hash":"b314788a825399e06e4f501d945609763c0f3822cecf548e769635949ddcb39f"}
{"id":"evt-njvgvjp6ax6t","type":"node.status_changed","subject":"gol-sxakryh","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"active","to":"retired"},"at":"2026-09-19T22:19:26.100Z","parents":["evt-ka14nmmw0r5k"],"hash":"edaf006242d2513feaf339710709c42081314e2b1e85c5f253f4fcfeaa6f837c"}
{"id":"evt-nhft0k949tqn","type":"node.created","subject":"gol-7ajfexk","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"goal","title":"Research the cameras' own Hikvision/Annke web interface and what it would take to use or replace it","body":"Understand what each camera's own web page offers and how it works, then decide between two routes (user's answer 2026-09-19: research both, recommend after). Route A, embed it: proxy or wrap the camera's web page inside this app so nobody has to leave it. Known obstacles: the cameras send X-Frame-Options: SAMEORIGIN, so today the app only links out in a new tab; the page has its own login, separate from ONVIF users (fnd-k1jyt83, fnd-3kp0rwy show ISAPI refusing the ONVIF Operator login for some endpoints); parts may need plugins or WebSocket streams; proxying someone else's app means rewriting URLs and carrying its session, and it would put a camera's admin surface behind our own session, which is a security decision in itself. Route B, learn from it: catalogue what the web page and ISAPI expose (settings we can't reach over ONVIF, events, SD-card playback, PTZ, firmware, user management) and record, feature by feature, what we could rebuild natively here. Output is evidence, not code: findings per feature with how it's reached, which login it needs and roughly what it would cost, plus a recommendation. Read-only against the user's own cameras; anything that would change a camera or add a second per-camera login needs the user's go-ahead first. Priority 1. Redaction of addresses and serials follows dec-kgm44qw.","status":"active","owner":null,"attrs":{},"weight":1,"target":null,"direction":"up","unit":null},"at":"2026-09-19T22:21:54.828Z","parents":["evt-njvgvjp6ax6t"],"hash":"47152d5dde06a5864aaeca9aa24f4d8c3f7924c7d8a12bc79cb324062097f87e"}
{"id":"evt-h6mj1v3hv46v","type":"edge.added","subject":"gol-7ajfexk","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"gol-7ajfexk","to":"gol-6q1q5mr"},"at":"2026-09-19T22:21:54.829Z","parents":["evt-nhft0k949tqn"],"hash":"6cc1ea35564f62dbc9e2b765d31ee616997766b4275e0ab13786b2d1ff8aeecc"}
{"id":"evt-fasqkg4wxjyb","type":"edge.added","subject":"gol-7ajfexk","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"upholds","from":"gol-7ajfexk","to":"pri-p9h51hx"},"at":"2026-09-19T22:21:54.830Z","parents":["evt-h6mj1v3hv46v"],"hash":"a6245da0084df156ca73fca91f89d3413ffc17f1ae56fb8c9265bc5fed140f29"}
{"id":"evt-26j6b8g7v7y8","type":"edge.added","subject":"gol-7ajfexk","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"upholds","from":"gol-7ajfexk","to":"pri-m1csgrm"},"at":"2026-09-19T22:21:54.831Z","parents":["evt-fasqkg4wxjyb"],"hash":"04a45db7c3ac4d477a02d37229ad1318d27744eebcf8c184be2e8b61e6fbe8cd"}
{"id":"evt-bxm1abqc01ft","type":"edge.added","subject":"gol-7ajfexk","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"upholds","from":"gol-7ajfexk","to":"pri-tyrxdz9"},"at":"2026-09-19T22:21:54.832Z","parents":["evt-26j6b8g7v7y8"],"hash":"92cb17aa2b59c4ae13f3a46d2a5636974e345bfdd75a8f2cb7c707f6625133c2"}
{"id":"evt-803bjn9bqfe6","type":"node.created","subject":"iss-6f346cm","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Survey online sources and forums for deeper Hikvision/Annke integration without flashing","body":"Desk research, no camera access needed, for gol-7ajfexk. Find what other people have got working on stock firmware: which ISAPI endpoints exist beyond what the camera's own web page shows, which user level each needs (our Operator ONVIF login was refused by the picture endpoint with Device Busy and by alertStream with 403: fnd-k1jyt83, fnd-3kp0rwy), how the web page's own login and session work, whether live view and SD-card playback can be reached without plugins, what the event/alert feeds carry, and how Annke's OEM firmware differs from Hikvision's. Worth looking at: Hikvision's published ISAPI specifications, the IPCamTalk and Hikvision community forums, Home Assistant's integrations and their issue trackers, open-source clients (pyhik, hikvisionapi, python-hikvision, node clients), MediaMTX and go2rtc issues mentioning Hikvision, and ONVIF conformance notes. Out of scope, and not to be pursued: flashing or patching firmware, telnet or SSH backdoors, default or leaked credentials, authentication bypasses and known CVEs. Only documented or openly discussed interfaces on stock firmware, used with our own login. Record each useful thing as its own finding with its source, the date, which firmware it was seen on and how sure it looks; contradictory claims are common in forums, so mark what is confirmed versus reported.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T22:24:05.068Z","parents":["evt-bxm1abqc01ft"],"hash":"470f890960a9aee26231bf6ac73d2a3b88234db2d7772777c6d112dc543cb769"}
{"id":"evt-hfd5ezpc5mxm","type":"edge.added","subject":"iss-6f346cm","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-6f346cm","to":"gol-7ajfexk"},"at":"2026-09-19T22:24:05.069Z","parents":["evt-803bjn9bqfe6"],"hash":"49a80ecbfe6b8abc6ec08147a1b29acd85907b3d4d1c10eb64f08ce31f911806"}
{"id":"evt-zbvncm69dec9","type":"edge.added","subject":"iss-6f346cm","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-6f346cm","to":"area:research"},"at":"2026-09-19T22:24:05.070Z","parents":["evt-hfd5ezpc5mxm"],"hash":"0d8922a9a8d8305d9310ed1ab3e936452b757d7e43c33f2375d386ba7bc40721"}
{"id":"evt-bkz4xcsdhpg3","type":"edge.added","subject":"iss-6f346cm","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-6f346cm","to":"area:device"},"at":"2026-09-19T22:24:05.071Z","parents":["evt-zbvncm69dec9"],"hash":"2351f1d644739699d3f13f014cc18599214b441a0c24c8cfff3961c529c189b6"}
{"id":"evt-cdb3qf9z1qq2","type":"node.created","subject":"fnd-5jk0ghz","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"finding","title":"ISAPI works with an activated Operator account, but the alert stream needs more rights and main-stream stills are refused on both cameras","body":"Tested on 2026-09-19 on camera …af2e with a purpose-made Operator account, created under the camera's own User Management and activated. Results: /ISAPI/System/deviceInfo 200; /ISAPI/System/capabilities 200 (12 KB); /ISAPI/Security/userCheck 200; /ISAPI/Streaming/channels/101/picture 503 \"Device Busy\"; /ISAPI/Event/notification/alertStream 403 Forbidden (statusCode 4). Two lessons. (1) 403 on alertStream is an authorization limit, not a login problem, so the alert stream needs a higher level than Operator on this firmware; ONVIF PullPoint already gives us motion with an Operator login (dec-d50j69a), so there is no reason to raise the level for that. (2) 503 on the main-stream still appears with every account tried, on both cameras, so it is a camera or firmware limit, confirming fnd-k1jyt83. Before activation the same account returned 401 everywhere: an account that has not been activated cannot authenticate at all, which is easy to mistake for a rights problem. Also: on camera …af54 the ONVIF user \"camera\" is accepted by ISAPI (deviceInfo 200), while on …af2e it is not, so accounts and their reach differ between two cameras of the same model; the ONVIF user list and the web user list are separate, and an entry in one does not imply the other. Capabilities advertised by …af2e: 91 isSupport flags, 46 true, including MotionDetection, TamperDetection, ROI, Snapshot, PanoramaCalibration, DeviceAudioCapture, VoiceTalkEvent, ConfigFileExport/Import, OnlineUpgradeTask, UsersPermissionConfig and ONVIFUserManagement; sections cover Audio, Device, Event, IO, Network, Racm (storage/playback), Security, Serial, Smart, Test, Video and WLAlarm. That document is the map for what ISAPI could add beyond ONVIF.","status":"current","owner":"prn-q80g8mz","attrs":{"sources":[{"url":"http://localhost/ISAPI/System/capabilities","note":"Cameras …af2e and …af54, addresses redacted per dec-kgm44qw; read-only curl with digest auth, no settings changed"}],"as_of":"2026-09-19"}},"at":"2026-09-19T22:32:38.646Z","parents":["evt-bkz4xcsdhpg3"],"hash":"b8fba9dcd5772018e4ad46e9f7f6ae167040a8113f6372d2009586b3f96c0925"}
{"id":"evt-zx81hkmwz8tw","type":"edge.added","subject":"fnd-5jk0ghz","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-5jk0ghz","to":"gol-7ajfexk"},"at":"2026-09-19T22:32:38.648Z","parents":["evt-cdb3qf9z1qq2"],"hash":"4e4b6e5e99bfc8e81e4a4410506702098ecf0a0ce5dd4c06324a84975e3a8a8a"}
{"id":"evt-vena3jmyy9t4","type":"edge.added","subject":"fnd-5jk0ghz","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-5jk0ghz","to":"iss-6f346cm"},"at":"2026-09-19T22:32:38.649Z","parents":["evt-zx81hkmwz8tw"],"hash":"d0c1d800b312001d80c2aca49ab1a6d902a5f752077a9310cd6c8d08b9317b8c"}
{"id":"evt-6ch89xwctq4h","type":"node.created","subject":"fnd-qa5n7s5","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"finding","title":"ISAPI offers far more than ONVIF on these cameras, and an Operator account can read almost all of it","body":"Read-only survey of camera …af2e on 2026-09-19 with an activated Operator account; nothing was changed. Endpoints that answered 200: /ISAPI/System/deviceInfo, /System/capabilities, /System/time, /Security/users, /Security/userCheck, /ContentMgmt/Storage, /ContentMgmt/record/tracks, /Streaming/channels (and /101 plus its capabilities), /Image/channels/1 (plus capabilities), /System/Video/inputs/channels/1 (plus overlays), /Event/notification/httpHosts. Refused: /Event/triggers (500 Device Error), /Event/notification/alertStream (403, needs more than Operator), /Streaming/channels/101/picture (503 Device Busy, a camera limit).\n\nImaging is the big gain over ONVIF. /Image/channels/1 exposes, with ranges from its capabilities document: WDR level 0-100, sharpness 0-100, gain level and a separate gain limit 0-100, shutter level with explicit min and max shutter limits, brightness, contrast and saturation 0-100, HLC 0-100, two noise-reduction levels 0-100, white balance style plus red and blue 0-255 with fine tuning 0-100, IR-cut filter day/night/auto/schedule with a night-to-day threshold 0-7 and delay 5-120 s, backlight compensation with a direction (up, down, left, right, centre), exposure type manual or auto, power line frequency 50/60 Hz, lens distortion correction, digital zoom, overexposure suppression, palettes, and supplement lighting: IR and white-light brightness 0-100 with modes. Several of these have no ONVIF equivalent at all, in particular the shutter limits, gain limit, HLC, lens distortion correction and the lighting controls.\n\nStreaming channel 101 reports what ONVIF also shows (H.264 Main, 4096x1860, VBR 10240 kbps, GOP 50, 20 fps) plus audio G.711 ulaw on the main stream, and carries SmartCodec and SVC elements, which ONVIF does not describe. Storage: hddList is empty, so this camera has no SD card, and on-camera recording or playback is not available on it. /Security/users returns only the requesting Operator account, so an Operator cannot enumerate other users. /Event/notification/httpHosts exists, which means the camera can push events to an HTTP endpoint of ours rather than us polling, worth weighing against ONVIF PullPoint.","status":"current","owner":"prn-q80g8mz","attrs":{"sources":[{"url":"http://localhost/ISAPI/Image/channels/1/capabilities","note":"Camera …af2e, address redacted per dec-kgm44qw; read-only digest GETs, no settings changed"}],"as_of":"2026-09-19"}},"at":"2026-09-19T22:37:28.286Z","parents":["evt-vena3jmyy9t4"],"hash":"071fc1436e2e1682ca2e13d9b73a68c84204bee69520699f6176fb1a8752398e"}
{"id":"evt-97ygefxzcenz","type":"edge.added","subject":"fnd-qa5n7s5","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-qa5n7s5","to":"gol-7ajfexk"},"at":"2026-09-19T22:37:28.288Z","parents":["evt-6ch89xwctq4h"],"hash":"3aee392e63232f6a583e4001bc9fb7214b83d99f123decf76370b8446beebed3"}
{"id":"evt-tv30h3fn3mca","type":"edge.added","subject":"fnd-qa5n7s5","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-qa5n7s5","to":"iss-6f346cm"},"at":"2026-09-19T22:37:28.289Z","parents":["evt-97ygefxzcenz"],"hash":"66e93ac460855284be275c9639d773dcae7acc096ac5ec56ad05b5615114151c"}
{"id":"evt-ewra0hxv3d2k","type":"edge.added","subject":"fnd-qa5n7s5","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-qa5n7s5","to":"gol-6q1q5mr"},"at":"2026-09-19T22:37:28.290Z","parents":["evt-tv30h3fn3mca"],"hash":"6390cfea6c3fa5a16677dbbdca607ec0b7a9de008094b51eb60b1803c6ea1b96"}
{"id":"evt-nbwjp2wfcy1m","type":"node.created","subject":"fnd-9c9dshr","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"finding","title":"Both cameras expose the same 34 ISAPI imaging settings; their own web page is a modern bundled app that blocks framing","body":"Read-only checks on 2026-09-19; nothing changed on either camera. (1) /ISAPI/Image/channels/1/capabilities is byte-for-byte identical on …af2e and …af54: 4352 bytes, the same 34 settings with the same ranges and choices. So one ISAPI imaging module would serve both, and by extension other cameras of this model; only the account differs (…af2e needs the purpose-made web Operator account, …af54 accepts its ONVIF user). (2) The camera's own web interface: GET / returns 334 bytes that redirect by script to /doc/index.html, which is a single-page app, 7 KB of HTML pulling 19 scripts and 12 stylesheets with webpack-style hashed names (root-application, vendors~root-application), so a recent rewrite rather than the old frames-and-ActiveX design, though its text still mentions plugin and ActiveX, presumably for older browsers or for features like two-way audio. Every response carries X-Frame-Options: SAMEORIGIN and no Content-Security-Policy, so the page cannot be embedded in ours directly; a proxy that strips that header could embed it, at the cost of rewriting asset URLs and carrying its session, and it would put the camera's full admin surface behind our own login. No login was needed to fetch the shell pages; authentication happens inside the app, so how its session works is still unknown and is the next thing to look at for the embedding route.","status":"current","owner":"prn-q80g8mz","attrs":{"sources":[{"url":"http://localhost/doc/index.html","note":"Cameras …af2e and …af54, addresses redacted per dec-kgm44qw; read-only GETs of the web shell and the ISAPI imaging capabilities"}],"as_of":"2026-09-19"}},"at":"2026-09-19T23:04:53.155Z","parents":["evt-ewra0hxv3d2k"],"hash":"fe986ccb8d73aa629d2292e4d6240a6a0132998712bccb5db72103450f6bf23f"}
{"id":"evt-3p8sd93kx37m","type":"edge.added","subject":"fnd-9c9dshr","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-9c9dshr","to":"gol-7ajfexk"},"at":"2026-09-19T23:04:53.157Z","parents":["evt-nbwjp2wfcy1m"],"hash":"935e7e791334551a5e90382bdb2c96a18ccecc7637e45c95834f5c455110c37d"}
{"id":"evt-w45wpzxtqc16","type":"edge.added","subject":"fnd-9c9dshr","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-9c9dshr","to":"iss-6f346cm"},"at":"2026-09-19T23:04:53.158Z","parents":["evt-3p8sd93kx37m"],"hash":"f755d1bee0fb323e2137d4716667447ba98c44ca7bbc2bc7c613ed4afadf8d5e"}
{"id":"evt-ems2fv4qefyf","type":"node.created","subject":"iss-b41wnpb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Read the ISAPI-only imaging settings and show them on the camera page","body":"Per fnd-qa5n7s5 and fnd-9c9dshr: /ISAPI/Image/channels/1 exposes 34 settings, identical on both cameras, many with no ONVIF equivalent. Read-only first slice: a vendor module (pri-p9h51hx) that does digest GETs of /ISAPI/Image/channels/1 and its capabilities, parses them, and adds a section to the camera page for what ONVIF can't show: shutter level with its minimum and maximum limits, gain limit, HLC level, WDR level, backlight direction, noise reduction (frame and inter-frame), white balance style with red/blue and fine tuning, IR-cut mode including schedule with its threshold and delay, lens distortion correction, digital zoom, and supplement lighting (IR and white-light brightness and modes). Credentials: try the camera's stored login; it works for ISAPI on …af54 but not on …af2e, which needs the camera's own web account, so when ISAPI answers 401 the section says the extra settings need a camera web account rather than failing the page. Everything stays read-only here; editing is a separate issue. Tests use a fake HTTP camera, never a real one (pri-e14bahk).","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T23:07:02.158Z","parents":["evt-w45wpzxtqc16"],"hash":"ba578f692b0cd9744894ff3a08030345f6ab024d1e1d21d935ececb9fc0077a8"}
{"id":"evt-nqc3ea89pfh2","type":"edge.added","subject":"iss-b41wnpb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-b41wnpb","to":"gol-7ajfexk"},"at":"2026-09-19T23:07:02.159Z","parents":["evt-ems2fv4qefyf"],"hash":"74e1b93806a33c5598a4afb4836acdc8fea7b0f40d188d6e12442d9b9e04bb3b"}
{"id":"evt-rrkkb0kgq8gf","type":"edge.added","subject":"iss-b41wnpb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-b41wnpb","to":"gol-6q1q5mr"},"at":"2026-09-19T23:07:02.160Z","parents":["evt-nqc3ea89pfh2"],"hash":"e17c53c33e6da4bb21415affb0bf5e690e54d94fc49225f848e49abc4e3088ef"}
{"id":"evt-6edtkebcqj1h","type":"edge.added","subject":"iss-b41wnpb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-b41wnpb","to":"src/lib/isapi.ts"},"at":"2026-09-19T23:07:02.161Z","parents":["evt-rrkkb0kgq8gf"],"hash":"b1c3987ed2006d2332b5d8a87b49773c6cb60a2ebac94b95c3a3a147cfb8f256"}
{"id":"evt-gqenyev52kej","type":"edge.added","subject":"iss-b41wnpb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-b41wnpb","to":"src/app/cameras/[id]/settings-sections.tsx"},"at":"2026-09-19T23:07:02.162Z","parents":["evt-6edtkebcqj1h"],"hash":"ce83015a4ee5401ecd380d01537217702a4023f0424a4dd248533cd657041f79"}
{"id":"evt-qvsddppypdwg","type":"edge.added","subject":"iss-b41wnpb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-b41wnpb","to":"area:device"},"at":"2026-09-19T23:07:02.163Z","parents":["evt-gqenyev52kej"],"hash":"c3da8d11deee06a3e9aab6010eae7e6c783f8767e63d4c7929657085529421d5"}
{"id":"evt-38exeed0tfza","type":"node.created","subject":"iss-n7aa43e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Edit the ISAPI-only imaging settings from the camera page","body":"Follows the read-only slice. Add Edit and Save for the settings ONVIF can't reach: shutter minimum and maximum limits, gain limit, HLC, WDR level, backlight direction, noise reduction, white balance fine tuning, IR-cut schedule, lens distortion correction and supplement lighting. Each write is a PUT of the full /ISAPI/Image/channels/1 document with the change merged in, validated with zod against the ranges in the camera's own capabilities, re-read afterwards to report what actually applied, and appended to the audit log, exactly as the ONVIF writers do (dec-84ghhqr). First question to settle with a no-op write on one camera: whether an Operator account may write imaging at all, or whether this needs a higher level. Needs the user's go-ahead, because it changes a camera.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T23:07:07.379Z","parents":["evt-qvsddppypdwg"],"hash":"2cf15697e40fbf3c25e63f16ad2969b618644a7e20b0c89c85419e6a00e7e990"}
{"id":"evt-hpdpv36jn3bx","type":"edge.added","subject":"iss-n7aa43e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-n7aa43e","to":"gol-7ajfexk"},"at":"2026-09-19T23:07:07.380Z","parents":["evt-38exeed0tfza"],"hash":"f97d45bcf58b1f38e81d9f7f6c74540fbd1fe0ecf42b469f4766b264c1e27e4a"}
{"id":"evt-kk3dxgacc2md","type":"edge.added","subject":"iss-n7aa43e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-n7aa43e","to":"gol-6q1q5mr"},"at":"2026-09-19T23:07:07.381Z","parents":["evt-hpdpv36jn3bx"],"hash":"172636b8f3ffdc5971374c56c15e945266fdccc32da71e2c596f1b377944bba2"}
{"id":"evt-n89m4zh1qsr1","type":"edge.added","subject":"iss-n7aa43e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-n7aa43e","to":"src/lib/isapi.ts"},"at":"2026-09-19T23:07:07.382Z","parents":["evt-kk3dxgacc2md"],"hash":"b01b63fd6dfd378cf917a6d27e4d2314c4e2317a446aacb66cd3b370edcbaeff"}
{"id":"evt-s2h2p4zt2sf1","type":"edge.added","subject":"iss-n7aa43e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-n7aa43e","to":"src/app/cameras/[id]/edit-forms.tsx"},"at":"2026-09-19T23:07:07.383Z","parents":["evt-n89m4zh1qsr1"],"hash":"909ab5f64cdc25759350536c126693ccc2bd80ea2c803753abacf8a9591a3ad1"}
{"id":"evt-jhj7bq8jpb2r","type":"edge.added","subject":"iss-n7aa43e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-n7aa43e","to":"area:device"},"at":"2026-09-19T23:07:07.384Z","parents":["evt-s2h2p4zt2sf1"],"hash":"c436abf46b2d1f79a3e7857a8964d8ac63177092d36f1aa959a274b6f4e19b2d"}
{"id":"evt-hwa4d6p2kgkt","type":"edge.added","subject":"iss-n7aa43e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-n7aa43e","to":"area:config"},"at":"2026-09-19T23:07:07.385Z","parents":["evt-jhj7bq8jpb2r"],"hash":"1a962fa106bdf4b4bcbbbd09a911f12219a1d2a666c1500865954e8e13197f27"}
{"id":"evt-tpv317k0x84m","type":"node.created","subject":"iss-f6n6ejt","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Explore making first-time camera setup easier, perhaps through ISAPI","body":"Setting a camera up by hand today means: activate it, create an ONVIF user under Integration Protocol, enable ONVIF, and for ISAPI also create and activate a web user under User Management. Today's research showed why this is fiddly: the two user lists are separate, an unactivated account fails with 401 as if its rights were wrong, and the two cameras ended up configured differently (fnd-5jk0ghz, fnd-qa5n7s5). Worth exploring: the capabilities document advertises UsersConfig, UsersPermissionConfig and ONVIFUserManagement, which suggests that, given one administrator login, the app could do the whole thing itself: activate the camera, create its own least-privilege accounts and switch on the integration protocol, so the user only ever types the admin password once, at discovery. Read-only exploration first: which endpoints exist, what an Operator may and may not do, what activation needs, and whether Annke firmware behaves the same. Then a recommendation, including whether an admin password should ever be held, even briefly, and whether it should be kept or discarded after setup. Also worth weighing against embedding the camera's own page for setup only, which would avoid holding an admin login at all. Nothing that changes a camera without the user's go-ahead.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T23:07:18.400Z","parents":["evt-hwa4d6p2kgkt"],"hash":"2270cb7e2d4329a597199e06f20bc8fcc4fcd6d2da9a3b24a3863bd468fcd5eb"}
{"id":"evt-dqmrk8xg9ee9","type":"edge.added","subject":"iss-f6n6ejt","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-f6n6ejt","to":"gol-7ajfexk"},"at":"2026-09-19T23:07:18.401Z","parents":["evt-tpv317k0x84m"],"hash":"8d945e0a7cb42aabbd33e13f3991eb60adc51889f770c6fd98c2ce3d9eab0c76"}
{"id":"evt-5yw8dey82b9v","type":"edge.added","subject":"iss-f6n6ejt","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-f6n6ejt","to":"gol-6q1q5mr"},"at":"2026-09-19T23:07:18.402Z","parents":["evt-dqmrk8xg9ee9"],"hash":"75a922df93d1fc9f9bca66d685da24474f9d5a1b3d7d164256da8927e59d7dfa"}
{"id":"evt-5p6zr8bhfhps","type":"edge.added","subject":"iss-f6n6ejt","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-f6n6ejt","to":"area:device"},"at":"2026-09-19T23:07:18.403Z","parents":["evt-5yw8dey82b9v"],"hash":"f4039c58208761f4b58e5b874d4be31558e643c60f16c6447a57f39455fa3bf9"}
{"id":"evt-56jbxp9hq2pq","type":"edge.added","subject":"iss-f6n6ejt","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-f6n6ejt","to":"area:research"},"at":"2026-09-19T23:07:18.404Z","parents":["evt-5p6zr8bhfhps"],"hash":"f395536d44d7293f7fbe2e699ac33a64dc31d73bcb51eea4cd33100b9cd8c72a"}
{"id":"evt-8ygxcqng452a","type":"node.updated","subject":"iss-f6n6ejt","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"attrs":{"revisit":"2026-09-26","deferred_because":"User asked on 2026-09-19 to add the easy capabilities first and come back to whether a different approach would make setup easier."}}},"at":"2026-09-19T23:07:23.610Z","parents":["evt-56jbxp9hq2pq"],"hash":"78607d324de93e5d49399af6029670c429d62247fd2a9869dcf1f26630812860"}
{"id":"evt-zrf1vnhpxqs3","type":"node.status_changed","subject":"iss-f6n6ejt","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"deferred"},"at":"2026-09-19T23:07:23.611Z","parents":["evt-8ygxcqng452a"],"hash":"dbb66ba145f23655cdfb7e55d825a4eb987cf1cc3d02cb8c6cd5ffde7c9f312b"}
{"id":"evt-td3txgjw832m","type":"node.created","subject":"ver-h1tv32h","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"The camera page shows an Advanced image panel with the settings ONVIF can't reach, read over ISAPI with the camera's stored login; a refused login says a camera web account is needed, any other failure says unavailable, and neither breaks the page","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T23:13:24.343Z","parents":["evt-zrf1vnhpxqs3"],"hash":"145931cce2152a78e2d7864f9344cabe96e570524523c3b016c34c8c1573c1b6"}
{"id":"evt-b8axg0rewnke","type":"edge.added","subject":"ver-h1tv32h","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-h1tv32h","to":"iss-b41wnpb"},"at":"2026-09-19T23:13:24.345Z","parents":["evt-td3txgjw832m"],"hash":"a70c7504512c48544f01e2645297a2402960a418e1daec0d55cfd1d1a25a759f"}
{"id":"evt-174yn4p50c71","type":"verification.recorded","subject":"ver-h1tv32h","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"src/lib/isapi.test.ts (digest against a loopback server, 401/403 as a login refusal, timeouts), src/lib/isapi-image.test.ts (parser checked against test/fixtures/isapi-image.xml, a real document from one of the cameras), settings-sections.test.tsx (\"Advanced image\"). Full suite 882 tests pass, 99.68% lines; tsc and eslint clean; next build compiles. Not yet seen in a browser."},"at":"2026-09-19T23:13:24.346Z","parents":["evt-b8axg0rewnke"],"hash":"ff287bea2ad34526649fc42f70a8e5af6c95e81c3baca30f3b53538d82c1822f"}
{"id":"evt-1rv9vc2tgz5r","type":"node.created","subject":"ver-0xsz79j","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"Real check by the user: the Advanced image panel shows values on …af2e, and says a camera web account is needed on …af54 (whose stored ONVIF login ISAPI accepts, so it may show values there too)","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T23:13:26.554Z","parents":["evt-174yn4p50c71"],"hash":"5bbfcc0ed8c8da48fe1376e04c43ea6f8e688194a00ea1bbfaa99457ba89b6b1"}
{"id":"evt-3g11g9z250b7","type":"edge.added","subject":"ver-0xsz79j","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-0xsz79j","to":"iss-b41wnpb"},"at":"2026-09-19T23:13:26.558Z","parents":["evt-1rv9vc2tgz5r"],"hash":"25106802b69828eb5320fa36a22ea5ce4093c438158b57f2f8c35255015d44e7"}
{"id":"evt-q4cdsp40wwzq","type":"verification.recorded","subject":"ver-0xsz79j","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pending","evidence":"User opens each camera's page after restarting the server"},"at":"2026-09-19T23:13:26.562Z","parents":["evt-3g11g9z250b7"],"hash":"dfe738086af6bec856bc90f63e0dff710ee406471d1fd1081d62dc7d3c0674dc"}
{"id":"evt-9f61zmymr0rj","type":"node.status_changed","subject":"iss-b41wnpb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"in_progress"},"at":"2026-09-19T23:13:28.509Z","parents":["evt-q4cdsp40wwzq"],"hash":"e877c2759c2406079f56228f3a283635516b486c17cdabde31ec65aec5547226"}
{"id":"evt-gazexh8y4pwg","type":"node.created","subject":"ver-7t5zt61","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"Availability is probed from the camera's own capabilities rather than inferred: the panel lists only settings the camera advertises, with a line saying how many were found, and the answer (including a refusal) is cached per camera and path for five minutes","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T23:17:59.576Z","parents":["evt-9f61zmymr0rj"],"hash":"f52843abc56f17622eeb74b62c8bc005f3eaf64e838dbac841458b50b34a032b"}
{"id":"evt-wfexe355p48g","type":"edge.added","subject":"ver-7t5zt61","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-7t5zt61","to":"iss-b41wnpb"},"at":"2026-09-19T23:17:59.579Z","parents":["evt-gazexh8y4pwg"],"hash":"f60a32704101b91a37263e11285de25682f10c4a380d3b86047c23f17344459c"}
{"id":"evt-rrnm0qtbwbn5","type":"verification.recorded","subject":"ver-7t5zt61","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"src/lib/isapi-image.test.ts parseImageOptions against test/fixtures/isapi-image-capabilities.xml (a real 4352-byte document from camera …af2e) and readAdvancedImage; src/lib/isapi.test.ts isapiGetCached (one request then memory, expiry, per-camera and per-path separation, refusals cached, resetIsapiCache); settings-sections.test.tsx \"leaves out settings this camera doesn't advertise\". Full suite 890 tests pass, 99.69% lines; tsc, eslint and next build clean."},"at":"2026-09-19T23:17:59.580Z","parents":["evt-wfexe355p48g"],"hash":"cbf1597d8c40c593ef1696c092b095707476214fbeedf59f514db1d168b9fc5d"}
{"id":"evt-rrh6q5yw9axg","type":"node.created","subject":"fnd-vfrgk0m","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"finding","title":"ISAPI imaging is read-only for an Operator account: writes are refused with lowPrivilege","body":"No-op write probe on camera …af2e on 2026-09-19, with the user's go-ahead, using the purpose-made Operator account. Read /ISAPI/Image/channels/1 (4057 bytes), sent exactly those bytes back with PUT, then read again. The PUT answered HTTP 403 ResponseStatus statusCode 4 \"Invalid Operation\", subStatusCode lowPrivilege; the document afterwards was byte-for-byte identical, so nothing on the camera changed. Conclusion: reading ISAPI imaging works at Operator level (fnd-qa5n7s5), writing does not, and a higher level would be needed. Worth noting the asymmetry: the same class of account over ONVIF does write imaging successfully, which is how the app's existing image editing works, so ONVIF remains the way to change the settings both protocols expose. The settings only ISAPI exposes (shutter limits, gain limit, HLC, lens distortion correction, the camera's lights) cannot be changed without raising the account's level. Not tested: whether an Administrator account writes successfully, or whether some ISAPI sub-paths are writable at Operator level.","status":"current","owner":"prn-q80g8mz","attrs":{"sources":[{"node":"iss-n7aa43e","note":"Camera …af2e, address redacted per dec-kgm44qw; read, identical PUT, read again"}],"as_of":"2026-09-19"}},"at":"2026-09-19T23:20:45.984Z","parents":["evt-rrnm0qtbwbn5"],"hash":"dcf60d57b810697a41a890c1c779ac7bb4583f9d81d92f0a7b63543adaf4b390"}
{"id":"evt-pwqpt5ch5xd9","type":"edge.added","subject":"fnd-vfrgk0m","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-vfrgk0m","to":"iss-n7aa43e"},"at":"2026-09-19T23:20:45.987Z","parents":["evt-rrh6q5yw9axg"],"hash":"9e04d088acf220d8574e88ad32c28f77c89f207564ac7a2fe456949308ccffe0"}
{"id":"evt-d9e5m0081d5v","type":"edge.added","subject":"fnd-vfrgk0m","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-vfrgk0m","to":"gol-7ajfexk"},"at":"2026-09-19T23:20:45.988Z","parents":["evt-pwqpt5ch5xd9"],"hash":"7bf080fd2ce3053442a8b978a6ae9e479263b3a4090d21c291f9e2783e78308d"}
{"id":"evt-vt9fn6dyyz8q","type":"node.updated","subject":"iss-n7aa43e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"body":"Blocked on a decision, not on code: fnd-vfrgk0m shows an Operator account cannot write ISAPI imaging (403, lowPrivilege), though it can read it. So editing the settings only ISAPI exposes (shutter minimum and maximum limits, gain limit, HLC, lens distortion correction, the camera's own lights) needs an account with a higher level, which means this app would hold a more powerful camera login than it does today. The settings both protocols expose keep being written over ONVIF, where an Operator is enough.\n\nOptions for the user: (a) leave ISAPI read-only and accept that these extras are view-only; (b) give the app's camera account Administrator level, gaining the writes and the risk; (c) keep a separate, rarely used Administrator login for changes, kept apart from the everyday Operator one. Once decided, the writer itself is straightforward: PUT the full /ISAPI/Image/channels/1 document with the change merged in, validated with zod against the camera's own capabilities, re-read afterwards, and appended to the audit log, exactly as the ONVIF writers do (dec-84ghhqr). Not yet tested: whether an Administrator account actually succeeds, and whether any sub-path is writable at Operator level."}},"at":"2026-09-19T23:20:55.963Z","parents":["evt-d9e5m0081d5v"],"hash":"fc00811353614cd2a270b3b1dc48ff7cfbeb583b135a9b762db1d4ec2bc4a2c5"}
{"id":"evt-tezhd9kctmce","type":"node.created","subject":"dec-ysy9emf","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"decision","title":"ISAPI stays read-only; changes keep going through ONVIF","body":"User's decision on 2026-09-19, after fnd-vfrgk0m showed an Operator account can read ISAPI imaging but not write it (403, lowPrivilege). The app keeps its Operator-level camera logins and does not gain an Administrator one, so the settings only ISAPI exposes (shutter minimum and maximum limits, gain limit, HLC, lens distortion correction, the camera's own lights) are shown but not editable. Settings both protocols expose continue to be written over ONVIF, where Operator is enough. Rejected for now: raising the app's account to Administrator, and keeping a separate Administrator login for changes; both would give this server a camera login that can change users and firmware, for a modest gain on settings that are rarely changed. Revisit if a need appears, or if a way is found to write these at Operator level.","status":"recorded","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T23:21:50.775Z","parents":["evt-vt9fn6dyyz8q"],"hash":"8a2d5348675319b655af2349ee1f972329454c6d5ec027f2f36009c7359f1b9e"}
{"id":"evt-deqh44zgj97r","type":"edge.added","subject":"dec-ysy9emf","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"dec-ysy9emf","to":"gol-7ajfexk"},"at":"2026-09-19T23:21:50.777Z","parents":["evt-tezhd9kctmce"],"hash":"b20bc55d1eac7c2de308d965620200e5d1b2f3c9400210ca752539411f2f760a"}
{"id":"evt-s0bqn6fcxn51","type":"node.updated","subject":"iss-n7aa43e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"attrs":{"revisit":null,"deferred_because":"Deferred by the user on 2026-09-19 per dec: ISAPI stays read-only rather than giving the app an Administrator camera login. Revisit if these settings are needed, or if Operator-level writes turn out to be possible."}}},"at":"2026-09-19T23:21:52.847Z","parents":["evt-deqh44zgj97r"],"hash":"9cdcfea6ec9ee00c55e0e9d0976ed619fc9695147ee1761fbf1a5ba9b23d7158"}
{"id":"evt-gk8y7966m8ar","type":"node.status_changed","subject":"iss-n7aa43e","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"deferred"},"at":"2026-09-19T23:21:52.848Z","parents":["evt-s0bqn6fcxn51"],"hash":"b8bb33ba609edb205ddb492873e23d907ab7e57bb968b58d1c1e9164bb23c486"}
{"id":"evt-a7am7j38qanb","type":"node.created","subject":"iss-wdpf95s","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Show on the dashboard which cameras have advanced settings","body":"Idea from 2026-09-19, not started. Each camera's page now says whether the camera offers settings beyond ONVIF (iss-b41wnpb), but the dashboard doesn't, so you have to open a camera to find out. Add a small marker on the card: advanced settings available, needs a camera web login, or nothing when the camera doesn't offer them. The card is a client component, so this needs the status from the server: either a field on /api/cameras/[id]/info or a small endpoint of its own, backed by the five-minute cache in src/lib/isapi.ts so it costs nothing per render. Keep it text, not colour alone (pri-8ev5m89).","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T23:23:29.414Z","parents":["evt-gk8y7966m8ar"],"hash":"019607c371b56cd9d2e7f03a43180033a5ec8a067e6098e7fe88df42639febfd"}
{"id":"evt-6b4cn3hfaghs","type":"edge.added","subject":"iss-wdpf95s","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-wdpf95s","to":"gol-7ajfexk"},"at":"2026-09-19T23:23:29.417Z","parents":["evt-a7am7j38qanb"],"hash":"e2915666112b99b5332437f98b7b6812df7405adc8bc757fe3fd8f9c9c3d94bb"}
{"id":"evt-6sz3w9a8phgq","type":"edge.added","subject":"iss-wdpf95s","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-wdpf95s","to":"gol-6q1q5mr"},"at":"2026-09-19T23:23:29.418Z","parents":["evt-6b4cn3hfaghs"],"hash":"80671404679ff42f0a6ed3f7cfe25400e57f24827b5110c4cfef7c8a83e58bb9"}
{"id":"evt-8ztfmbpq590p","type":"edge.added","subject":"iss-wdpf95s","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-wdpf95s","to":"src/app/camera-card.tsx"},"at":"2026-09-19T23:23:29.419Z","parents":["evt-6sz3w9a8phgq"],"hash":"5f080b0c6ad40454600fb51e7276e233aaac6c97a7ee423ef1074a7395639473"}
{"id":"evt-2shea26bknef","type":"edge.added","subject":"iss-wdpf95s","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-wdpf95s","to":"src/app/api/cameras/[id]/info/route.ts"},"at":"2026-09-19T23:23:29.420Z","parents":["evt-8ztfmbpq590p"],"hash":"8106a61c2b1cb3be71d0b7d8badc4d5f058902533805995df8e173ab12fdd0ea"}
{"id":"evt-cy48ze03vv1t","type":"edge.added","subject":"iss-wdpf95s","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-wdpf95s","to":"area:ui"},"at":"2026-09-19T23:23:29.421Z","parents":["evt-2shea26bknef"],"hash":"060731d8420724e095570a4949d3d0d4ef115b9d95a73c5f581ec96528975259"}
{"id":"evt-hpgxjted58jq","type":"node.updated","subject":"iss-6f346cm","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"attrs":{"revisit":null,"deferred_because":"Deferred indefinitely by the user on 2026-09-19. Direct probing of the cameras answered the questions that mattered (fnd-5jk0ghz, fnd-qa5n7s5, fnd-vfrgk0m), so the desk research is no longer urgent; pick it up if a capability is wanted that the cameras don't explain themselves."}}},"at":"2026-09-19T23:23:36.380Z","parents":["evt-cy48ze03vv1t"],"hash":"91c3674302adc273894c8aaad10d12bb59e142b93cd0ea15e6c621efade6c49b"}
{"id":"evt-2rjmf1et51v9","type":"node.status_changed","subject":"iss-6f346cm","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"deferred"},"at":"2026-09-19T23:23:36.381Z","parents":["evt-hpgxjted58jq"],"hash":"eaa4110035104fffad97589f444babb35938750837bb4a33935b8e532029d8d1"}
{"id":"evt-hwmtgatb4tav","type":"node.updated","subject":"iss-wdpf95s","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"attrs":{"revisit":null,"deferred_because":"Deferred indefinitely by the user on 2026-09-19: nice to have, and the camera page already says it."}}},"at":"2026-09-19T23:23:37.862Z","parents":["evt-2rjmf1et51v9"],"hash":"6e1c438de637b1bfe6901751bde7101a57e94316ef3eac6d0f4c119f0ab84e0d"}
{"id":"evt-1qqv3a71m62r","type":"node.status_changed","subject":"iss-wdpf95s","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"deferred"},"at":"2026-09-19T23:23:37.866Z","parents":["evt-hwmtgatb4tav"],"hash":"08753d7ee6314709c527835159e5da04a4cecd3b2be69a7f3d5b8b529caf2f18"}
{"id":"evt-hgj83ejnr95s","type":"node.created","subject":"fnd-vygp293","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"finding","title":"Every call the app makes to a camera is plain HTTP or RTSP; no password is sent in the clear, but everything else is","body":"Read from the code on 2026-09-19. ONVIF goes over http:// on the camera's port (src/lib/camera.ts), with the onvif library's WS-Security, so the password travels as a SHA-1 digest with a nonce rather than in the clear. Snapshots follow the camera's own snapshot URI, which is http, with digest or basic auth (basic only if the camera offers no digest challenge, which would put the password on the wire). ISAPI uses node:http explicitly with digest auth (src/lib/isapi.ts). Video is pulled by MediaMTX over rtsp:// on TCP, and that source URL carries the username and password, so those credentials do cross the network in a recoverable form. Everything else is readable by anyone on that network segment: snapshots, video, settings documents, the camera's serial and MAC. Browser to app is plain HTTP too, which is why the session cookie is only marked secure behind a proxy that terminates TLS. The cameras themselves advertise isSupportHttps, certificate import and a self-signed certificate they can generate (fnd-qa5n7s5's capabilities), and MediaMTX supports RTSPS, so encrypting these paths is possible; the obstacle is trusting a self-signed certificate properly rather than turning verification off, which would be worse than plain HTTP because it would look safe.","status":"current","owner":"prn-q80g8mz","attrs":{"sources":[{"node":"iss-b41wnpb","note":"src/lib/camera.ts, src/lib/isapi.ts, src/lib/video.ts and the camera capabilities read on 2026-09-19"}],"as_of":"2026-09-19"}},"at":"2026-09-19T23:27:10.082Z","parents":["evt-1qqv3a71m62r"],"hash":"57977d8f7953218fc84456c28774de28a48a90cc31f862c014c16d10368ab5be"}
{"id":"evt-24kehq6822j3","type":"edge.added","subject":"fnd-vygp293","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-vygp293","to":"gol-6q1q5mr"},"at":"2026-09-19T23:27:10.083Z","parents":["evt-hgj83ejnr95s"],"hash":"45deac0d79eeba1a18e756e83f54d7a9c82a85b05eb56ee627634f1a315ce752"}
{"id":"evt-vvpz099x707j","type":"edge.added","subject":"fnd-vygp293","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-vygp293","to":"pri-tyrxdz9"},"at":"2026-09-19T23:27:10.084Z","parents":["evt-24kehq6822j3"],"hash":"aa44393003c1c55ad808595c98d9d1793bdb512543ee6d9dfbdad4031990ebe7"}
{"id":"evt-fmre2fmnz1aw","type":"node.created","subject":"iss-aret59y","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Investigate encrypting the app's traffic to the cameras","body":"Per fnd-vygp293: ONVIF, snapshots and ISAPI all go over plain HTTP, and MediaMTX pulls video over RTSP with the login in the source URL. Investigate, read-only first, what it would take to encrypt each path and whether it is worth it on a LAN. Questions to answer with evidence: does the camera answer ONVIF, ISAPI and snapshots over HTTPS on port 443, and with what certificate (self-signed by default, so what is its subject and lifetime); can it be given a certificate we trust (the capabilities advertise certificate import, self-signed export and CSR-style handling); does it support RTSPS, and does MediaMTX pull from it; what breaks, since the snapshot and stream URIs the camera advertises are http/rtsp, so the app would have to rewrite them; how certificate trust would work in Node without disabling verification, for example pinning the camera's certificate per camera in the registry. The trap to avoid: turning verification off, which looks safe and is not. Then a recommendation, including doing nothing on a trusted LAN, and what changes if the app is ever reached from outside. Also worth noting separately: the app itself is served over plain HTTP, so the admin session cookie is not marked secure.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T23:27:21.170Z","parents":["evt-vvpz099x707j"],"hash":"b178e7cac2f5ae3f4b5b7daf1fccbd5283b5d5d9dd1b2eac3fd542a0c67cc1e9"}
{"id":"evt-nmecd3aw36na","type":"edge.added","subject":"iss-aret59y","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-aret59y","to":"gol-6q1q5mr"},"at":"2026-09-19T23:27:21.172Z","parents":["evt-fmre2fmnz1aw"],"hash":"b6bf26de53bceebaa3c9a2cac158a5b0e1059de1643f7409aab69f927db11076"}
{"id":"evt-4sqzd8t0080q","type":"edge.added","subject":"iss-aret59y","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-aret59y","to":"src/lib/camera.ts"},"at":"2026-09-19T23:27:21.173Z","parents":["evt-nmecd3aw36na"],"hash":"7f5f35f0197c948073bc8b88a261ffc9352456c2e2a9a13380bf288a25cad44a"}
{"id":"evt-trfkcaw0fmsp","type":"edge.added","subject":"iss-aret59y","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-aret59y","to":"src/lib/isapi.ts"},"at":"2026-09-19T23:27:21.174Z","parents":["evt-4sqzd8t0080q"],"hash":"6c81347a69bd920ff90a9e0c41e2d11e6b2ef012c072966c6a27ace6b31ef547"}
{"id":"evt-8wn4cqhcdq7f","type":"edge.added","subject":"iss-aret59y","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-aret59y","to":"src/lib/video.ts"},"at":"2026-09-19T23:27:21.175Z","parents":["evt-trfkcaw0fmsp"],"hash":"47424b1053490d3f79b972e53dfcaa8de84213d92d902f7898ae289c7165e288"}
{"id":"evt-tentp8y9yec4","type":"edge.added","subject":"iss-aret59y","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-aret59y","to":"area:security"},"at":"2026-09-19T23:27:21.176Z","parents":["evt-8wn4cqhcdq7f"],"hash":"d80909538d6f3e55c909d3b22dcd422e8fd01338510ec1e972a97a2ebb74c8f1"}
{"id":"evt-4hkcq1zg97js","type":"edge.added","subject":"iss-aret59y","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-aret59y","to":"area:device"},"at":"2026-09-19T23:27:21.177Z","parents":["evt-tentp8y9yec4"],"hash":"5c23dd196a9edd65fd180541dadab43a4e7f701205ed97ed2112dd589bd22f84"}
{"id":"evt-9yndg25pyww2","type":"node.created","subject":"fnd-0b0q7xw","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"finding","title":"Both cameras answer HTTPS with a self-signed certificate, but digest that works on port 80 is refused on 443; repeated attempts locked the account","body":"Read-only probing on 2026-09-19. Ports open on both cameras: 80, 443, 554 (RTSP) and 8000 (Hikvision SDK); 322 (RTSPS) is closed, so encrypted video would need enabling on the camera, if the firmware offers it. HTTPS on 443 answers and challenges for credentials. The certificate is self-signed: subject and issuer are the same random hex name (per camera), valid 2024-01-01 to 2033-12-29, so nothing in a normal trust store signs it, and curl without -k, like Node, refuses it. The digest challenge is character-for-character the same scheme and realm on both ports, yet the account that authenticates over HTTP was refused (401) over HTTPS on deviceInfo and userCheck, with plain digest, with anyauth, with basic, with Connection: close and with TLS pinned to 1.2. Hypothesis, untested: this firmware expects the session flow its own web app uses (/ISAPI/Security/sessionLogin, a challenge-and-hash exchange) on HTTPS, rather than HTTP digest. Caution learned the hard way: after those failed attempts the camera also began refusing the credentials over HTTP, which matches Hikvision's illegal-login lock (about five failures, roughly 30 minutes), and that also blocks the app's own reads while it lasts. Any further TLS testing should be one attempt at a time, with the lock given time to clear in between.","status":"current","owner":"prn-q80g8mz","attrs":{"sources":[{"node":"iss-aret59y","note":"Cameras …af2e and …af54, addresses redacted per dec-kgm44qw; nc port checks, openssl s_client, curl against ISAPI over both ports"}],"as_of":"2026-09-19"}},"at":"2026-09-19T23:30:53.721Z","parents":["evt-4hkcq1zg97js"],"hash":"b815e17838bf24ff32db3df8f2a5fdb3633111d7105ef44e07745ed0fcee8c67"}
{"id":"evt-bwt21kmnwevq","type":"edge.added","subject":"fnd-0b0q7xw","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-0b0q7xw","to":"iss-aret59y"},"at":"2026-09-19T23:30:53.722Z","parents":["evt-9yndg25pyww2"],"hash":"c0086bd3560a4d62196790b080ced77ab65927c243ded6063dd0b8dd00973329"}
{"id":"evt-7e1estxqp6zv","type":"edge.added","subject":"fnd-0b0q7xw","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-0b0q7xw","to":"gol-6q1q5mr"},"at":"2026-09-19T23:30:53.723Z","parents":["evt-bwt21kmnwevq"],"hash":"d677fee3803b51c9db624bf64fd12b6aaf02652d29c8b7d195c67ae731dbab12"}
{"id":"evt-mpvp081sd401","type":"node.updated","subject":"iss-aret59y","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"attrs":{"revisit":"2026-09-20","deferred_because":"User asked on 2026-09-19 for a reminder tomorrow: the camera's login lock from today's failed HTTPS attempts needs to expire before testing again, one attempt at a time, starting with the sessionLogin theory."}}},"at":"2026-09-19T23:30:56.101Z","parents":["evt-7e1estxqp6zv"],"hash":"6f8e43c0e73d650e648c2182ade62f998d8371a75bdeb24bbf6d98c29fe0a1e8"}
{"id":"evt-jjb91xaqzfh8","type":"node.status_changed","subject":"iss-aret59y","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"deferred"},"at":"2026-09-19T23:30:56.103Z","parents":["evt-mpvp081sd401"],"hash":"91475111219c80eca4cd67e727da928b0925436019ef3349566a492331613ad3"}
{"id":"evt-ss3akqh6yz7p","type":"node.created","subject":"fnd-5etg00n","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"finding","title":"The web account was deleted, so the later 401s were expected; the HTTPS refusal seen while it existed still stands unexplained","body":"Correction to fnd-0b0q7xw, recorded 2026-09-19. The user deleted the purpose-made web Operator account on camera …af2e, so requests with it now fail with 401 whatever the protocol, and the \"illegal login lock\" hypothesis for those later failures is unsupported. What still stands, because it was observed while the account existed and worked over HTTP: HTTPS on 443 answers with the same digest scheme and realm as port 80, yet the same credentials were refused there, across plain digest, anyauth, basic, Connection: close and TLS 1.2. What is now unknown: whether a lock ever occurred at all. The app was never affected, since it authenticates with each camera's separate ONVIF account, which is also why the dashboard kept working throughout. To resume the TLS investigation (iss-aret59y) a web account has to exist again on a camera; alternatively camera …af54, whose ONVIF account ISAPI does accept, can be used without creating anything new. Also worth noting for hygiene: the deleted account's password had been shared in conversation, so deleting it removed that exposure.","status":"current","owner":"prn-q80g8mz","attrs":{"sources":[{"node":"iss-aret59y","note":"User's statement that they deleted the account, plus one confirming request that returned 401"}],"as_of":"2026-09-19"}},"at":"2026-09-19T23:32:17.241Z","parents":["evt-jjb91xaqzfh8"],"hash":"5dd57916c789d1ecc66af5ba2b9066109ff49f3f46c498a5872ad84c39785bc5"}
{"id":"evt-71gb7p0p0g38","type":"edge.added","subject":"fnd-5etg00n","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-5etg00n","to":"iss-aret59y"},"at":"2026-09-19T23:32:17.242Z","parents":["evt-ss3akqh6yz7p"],"hash":"bcf63df26c2780a4be305880583f3bf3813fe1c0a1091dc45aa20a8f02aaae48"}
{"id":"evt-0ghcbqs4gg45","type":"edge.added","subject":"fnd-5etg00n","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-5etg00n","to":"gol-7ajfexk"},"at":"2026-09-19T23:32:17.243Z","parents":["evt-71gb7p0p0g38"],"hash":"17a09c89781685cdfb9206cbf5728e0fbcb9a82f1f5836a7b9dd6b16117f267e"}
{"id":"evt-6bb4da2h9bp0","type":"edge.added","subject":"fnd-5etg00n","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"supersedes","from":"fnd-5etg00n","to":"fnd-0b0q7xw"},"at":"2026-09-19T23:32:17.244Z","parents":["evt-0ghcbqs4gg45"],"hash":"a317e3cb4ceb1b662a373db747d6eaa8156b589294b75638aafd34ff2b792b8b"}
{"id":"evt-ek78k490qg8w","type":"node.status_changed","subject":"fnd-0b0q7xw","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"current","to":"superseded"},"at":"2026-09-19T23:32:17.245Z","parents":["evt-6bb4da2h9bp0"],"hash":"a5692d573786b1316bf02bf1d6f85f8d2b4c002bceae2f7d418525ebd6406ec9"}
{"id":"evt-n4mh8xy8tcf2","type":"node.created","subject":"fnd-5k90a9q","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"finding","title":"Both cameras' existing ONVIF accounts reach ISAPI; the earlier claim that they were configured differently was wrong","body":"Checked on 2026-09-19 with one request per camera: /ISAPI/System/deviceInfo answered HTTP 200 on both …af2e and …af54 using the account each camera already has saved in this app (\"camera\"). This corrects fnd-5jk0ghz and the parts of fnd-qa5n7s5, fnd-9c9dshr and fnd-5etg00n that said …af2e refused its stored account for ISAPI and therefore needed a separate web account. That claim came from a single failed request earlier the same evening, while a second account was being created and activated on that camera; what changed in between is unknown. Consequences: the Advanced image panel works on both cameras with the logins already stored, no extra account is needed, deleting the purpose-made account cost nothing, and the TLS investigation (iss-aret59y) can use either camera's existing login. Lesson for future probes: one failed authentication is not evidence of a configuration difference, particularly while something about the camera's accounts is being changed.","status":"current","owner":"prn-q80g8mz","attrs":{"sources":[{"node":"iss-b41wnpb","note":"One digest GET per camera with the app's stored credentials; addresses redacted per dec-kgm44qw"}],"as_of":"2026-09-19"}},"at":"2026-09-19T23:33:43.891Z","parents":["evt-ek78k490qg8w"],"hash":"d2d6978b1c4f35ddd713ba5c12bfeb590bcfd0ceb84a7f96ae02d53d5807e15e"}
{"id":"evt-mprxakksgsd4","type":"edge.added","subject":"fnd-5k90a9q","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-5k90a9q","to":"iss-aret59y"},"at":"2026-09-19T23:33:43.893Z","parents":["evt-n4mh8xy8tcf2"],"hash":"29ee0edc4c5694dcc21a35ebf10799283aa47800d49c705799119c99ee973c26"}
{"id":"evt-x34mv9vygd2r","type":"edge.added","subject":"fnd-5k90a9q","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-5k90a9q","to":"iss-b41wnpb"},"at":"2026-09-19T23:33:43.894Z","parents":["evt-mprxakksgsd4"],"hash":"d5b64456d42b18400a63c0ce49e246b67d0c0dbea1b3bccc30142a0a606a8f1a"}
{"id":"evt-dvzqjpxs1enm","type":"edge.added","subject":"fnd-5k90a9q","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-5k90a9q","to":"gol-7ajfexk"},"at":"2026-09-19T23:33:43.895Z","parents":["evt-x34mv9vygd2r"],"hash":"a66c23ac67911f46228c68544e1b18a085b6331fcf1bbb3748cadb03b48148a3"}
{"id":"evt-4mrgj8k8894b","type":"edge.added","subject":"fnd-5k90a9q","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"supersedes","from":"fnd-5k90a9q","to":"fnd-5jk0ghz"},"at":"2026-09-19T23:33:43.896Z","parents":["evt-dvzqjpxs1enm"],"hash":"6faa4af81959a223052b6728964969c138f6373427d0d7a370b1e982ee1a9741"}
{"id":"evt-7cjgw25qnz44","type":"node.status_changed","subject":"fnd-5jk0ghz","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"current","to":"superseded"},"at":"2026-09-19T23:33:43.897Z","parents":["evt-4mrgj8k8894b"],"hash":"366e6dd70ea6f0c7e502c6a14057c6900038d4b41d8d9790a1a06e1a6c98ed42"}
{"id":"evt-rphskmnjaxnj","type":"edge.added","subject":"fnd-5k90a9q","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"supersedes","from":"fnd-5k90a9q","to":"fnd-5etg00n"},"at":"2026-09-19T23:33:43.898Z","parents":["evt-7cjgw25qnz44"],"hash":"c4524fb9b1f08e522061671f1a8856480fa587e9beee6782c61531247c6865b9"}
{"id":"evt-bd2bm62hw7rp","type":"node.status_changed","subject":"fnd-5etg00n","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"current","to":"superseded"},"at":"2026-09-19T23:33:43.899Z","parents":["evt-rphskmnjaxnj"],"hash":"71d90d7e3140a7262ad54762071d52b4cc5625fe413e6c1a3d800f3733e4686b"}
{"id":"evt-vewwcjw74faq","type":"node.created","subject":"ver-9nygdrt","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"Real check by the user: the Advanced image panel shows the camera's extra settings in the browser","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T23:34:21.684Z","parents":["evt-bd2bm62hw7rp"],"hash":"c1a099f498874676a898da0065dacb404446d61e9430ff7eb3b049d56667f82b"}
{"id":"evt-6vpnyjtx2s5k","type":"edge.added","subject":"ver-9nygdrt","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-9nygdrt","to":"iss-b41wnpb"},"at":"2026-09-19T23:34:21.685Z","parents":["evt-vewwcjw74faq"],"hash":"a756e931d822bd6b52929fde5c171b189ef614db1301e79da3242c2a2ffc815e"}
{"id":"evt-wdbs4yjm11vw","type":"verification.recorded","subject":"ver-9nygdrt","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"User on 2026-09-19: \"advanced image looks good\", after opening the camera page with both cameras reachable over ISAPI with their stored logins (fnd-5k90a9q)."},"at":"2026-09-19T23:34:21.686Z","parents":["evt-6vpnyjtx2s5k"],"hash":"4077f2450f8ca0a4b734ce6860342962297086df038ad1b53f6909756b55aab3"}
{"id":"evt-t7adaj2dq3yb","type":"node.status_changed","subject":"iss-b41wnpb","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"in_progress","to":"done"},"at":"2026-09-19T23:34:23.676Z","parents":["evt-wdbs4yjm11vw"],"hash":"5ea833b6da7a91c273e897024c2477631cffc9e24c1084692eafe26a9172ac8a"}
{"id":"evt-fq07jr5jmbmw","type":"node.created","subject":"fnd-b1vhqcm","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"finding","title":"The onvif v0.x fixes are upstream as agsh/onvif pull request 500, open and unreviewed","body":"Checked on 2026-09-19 with the GitHub CLI: pull request #500 on agsh/onvif, \"fix: setImagingSettings and setVideoEncoderConfiguration send zero va…\", opened 2026-09-19 by the user, state OPEN, not a draft, 5 files changed, +323/-29, no comments and no reviews yet. This is the upstream home of the fixes this app currently consumes from the user's fork (dec-2myt5ht, pinned in package.json). It answers the \"record the PR URL\" part of iss-pkp09k8: https://github.com/agsh/onvif/pull/500. Switching back to the published onvif package waits on this being merged and released.","status":"current","owner":"prn-q80g8mz","attrs":{"sources":[{"url":"https://github.com/agsh/onvif/pull/500","note":"Read with gh pr view on 2026-09-19"}],"as_of":"2026-09-19"}},"at":"2026-09-19T23:35:54.295Z","parents":["evt-t7adaj2dq3yb"],"hash":"78a6464c4e1041c1612ba238a68911d13b1aa7467a88cb873b7f5b1925c94c81"}
{"id":"evt-kygkrwnr6r5y","type":"edge.added","subject":"fnd-b1vhqcm","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-b1vhqcm","to":"iss-pkp09k8"},"at":"2026-09-19T23:35:54.296Z","parents":["evt-fq07jr5jmbmw"],"hash":"e9500e31068e570d03a10c1508c9dd51551c0716b47cc552f8e326b9a4d947ea"}
{"id":"evt-ytp73k1yv0tz","type":"edge.added","subject":"fnd-b1vhqcm","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-b1vhqcm","to":"iss-h5e97mf"},"at":"2026-09-19T23:35:54.297Z","parents":["evt-kygkrwnr6r5y"],"hash":"c05ad1424d1d45156956e585b0db4128a1971bfb6d18b6ff258581118c5b8234"}
{"id":"evt-fkmmptfdrdtr","type":"node.updated","subject":"iss-h5e97mf","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"attrs":{"revisit":"2026-09-26","deferred_because":"Deferred by the user on 2026-09-19 until next week: they are waiting on a response to their upstream pull request agsh/onvif#500 (open, no comments or reviews yet), and the v1 work follows from how that goes."}}},"at":"2026-09-19T23:35:57.888Z","parents":["evt-ytp73k1yv0tz"],"hash":"a6395ce8d1e66bb6ef229f12e6ac368ed0a7c11cdc28e92af3c1d38bf479beb5"}
{"id":"evt-wfye1bgxxs2x","type":"node.status_changed","subject":"iss-h5e97mf","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"deferred"},"at":"2026-09-19T23:35:57.890Z","parents":["evt-fkmmptfdrdtr"],"hash":"b3e05d49a8ab841ad1e7491f1e725f676975e0f54901bac7ec8380b461256e9e"}
{"id":"evt-0wph5ghfxkgg","type":"node.status_changed","subject":"gol-6q1q5mr","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"active","to":"retired"},"at":"2026-09-19T23:37:37.842Z","parents":["evt-wfye1bgxxs2x"],"hash":"ac46fa3043913113974cbe1e117c139f0335919b26cec19c81c8fc79dc573bb9"}
{"id":"evt-ahbchh780qzb","type":"node.created","subject":"gol-k56hc69","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"goal","title":"Publish the app to npm so anyone can run it without cloning the repo","body":"Someone with Node installed can run this dashboard with a single command, e.g. npx cameras or a global install with a cameras start entry point, and get discovery, live video, recording and settings without git or a build step. User's answer 2026-09-19: the whole runnable app, not a library; a library split can come later if the lib layer's shape settles. Priority 1.\n\nWhat this forces us to settle: what ships (a built .next output plus server, rather than sources); the package name, licence and whether it is scoped; how the app finds its data, since .data and recordings currently sit in the working directory and a globally installed tool needs a sensible location and environment overrides; how MediaMTX is obtained, since the installer downloads a pinned binary and npm install scripts are often disabled, so it likely belongs in a first-run step with a clear message rather than postinstall; the admin setup flow for someone who has never seen the repo; Node version support; and how upgrades work. Nothing here changes what the app does, so the risk is in packaging decisions rather than features.","status":"active","owner":null,"attrs":{},"weight":1,"target":null,"direction":"up","unit":null},"at":"2026-09-19T23:40:55.218Z","parents":["evt-0wph5ghfxkgg"],"hash":"3d05b00347b2ce8f4fafa2b832b7cd376a2fd1d546f683bb524befb1071a7729"}
{"id":"evt-p1911nv9h5nq","type":"edge.added","subject":"gol-k56hc69","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"upholds","from":"gol-k56hc69","to":"pri-mz2jxpb"},"at":"2026-09-19T23:40:55.219Z","parents":["evt-ahbchh780qzb"],"hash":"6d280ad9498aa4d0e3c3f7e06bbda9a3bf6f7fde8a940dacd73c10a423294579"}
{"id":"evt-rrzrjptdf3j5","type":"node.created","subject":"gol-9jctv92","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"goal","title":"Publish a Debian package so the app installs and runs as a service","body":"apt install puts this app on a Debian or Ubuntu machine as a proper service: it starts at boot, runs as its own unprivileged user, keeps its data under /var/lib, its configuration under /etc and its logs in the journal. Priority 1. User's answer 2026-09-19: whether the package also carries MediaMTX, or leaves it to the existing first-run download, is to be decided during the work; carrying it means shipping someone else's binary and the licence and update questions that brings, while leaving it out means an install needs network access before video works.\n\nTo settle while doing it: which distributions and architectures; how Node itself is required or bundled; the systemd unit and its hardening; where the admin file, credential key and recordings live, and how the first-run setup is presented on a headless box; how the package is built and signed, and whether it is hosted in our own apt repository or only as a downloadable .deb; and upgrade and removal behaviour, in particular never deleting recordings or credentials on purge without asking.","status":"active","owner":null,"attrs":{},"weight":1,"target":null,"direction":"up","unit":null},"at":"2026-09-19T23:41:01.632Z","parents":["evt-p1911nv9h5nq"],"hash":"a9d0b5605659f5265496f45774ec7b7e5776a9e29c735cf369c45ec5fec0b2a5"}
{"id":"evt-5b1wcbrf4411","type":"edge.added","subject":"gol-9jctv92","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"upholds","from":"gol-9jctv92","to":"pri-mz2jxpb"},"at":"2026-09-19T23:41:01.633Z","parents":["evt-rrzrjptdf3j5"],"hash":"c53d7214e3e7d2a70bd86fab75b51369d756926190d52240b7516abe37483244"}
{"id":"evt-zessfejcj4fd","type":"edge.added","subject":"gol-9jctv92","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"upholds","from":"gol-9jctv92","to":"pri-tyrxdz9"},"at":"2026-09-19T23:41:01.634Z","parents":["evt-5b1wcbrf4411"],"hash":"364c55ab00f531f860f4d30fd5a2cd3ccbc7bc1e76caf607318a9370f57deb1c"}
{"id":"evt-hfez48y4kc4k","type":"node.updated","subject":"gol-k56hc69","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"target":1,"direction":"up","unit":"check"}},"at":"2026-09-19T23:42:23.463Z","parents":["evt-zessfejcj4fd"],"hash":"504f66dc11fa0441ad3aa7f8670d369fa36821026d6ac493bd4e6a27a0565221"}
{"id":"evt-3e3qc35ee2ck","type":"node.updated","subject":"gol-9jctv92","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"target":1,"direction":"up","unit":"check"}},"at":"2026-09-19T23:42:25.670Z","parents":["evt-hfez48y4kc4k"],"hash":"0a4effde1d59c92963de889b3caf29aa71c7be5a1be526b3a223be0b74dce146"}
{"id":"evt-v71kqfm3x5ex","type":"node.updated","subject":"gol-k56hc69","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"body":"Someone with Node installed can run this dashboard with a single command, e.g. npx cameras or a global install with a cameras start entry point, and get discovery, live video, recording and settings without git or a build step. User's answer 2026-09-19: the whole runnable app, not a library; a library split can come later if the lib layer's shape settles. Priority 1.\n\nTarget: 1 check — the published package installed and run on a machine that has never held this repo, reaching a working dashboard with a camera visible. Recorded as a measurement once the user has seen it.\n\nWhat this forces us to settle: what ships (a built .next output plus server, rather than sources); the package name, licence and whether it is scoped; how the app finds its data, since .data and recordings currently sit in the working directory and a globally installed tool needs a sensible location and environment overrides; how MediaMTX is obtained, since the installer downloads a pinned binary and npm install scripts are often disabled, so it likely belongs in a first-run step with a clear message rather than postinstall; the admin setup flow for someone who has never seen the repo; Node version support; and how upgrades work. Nothing here changes what the app does, so the risk is in packaging decisions rather than features."}},"at":"2026-09-19T23:42:34.303Z","parents":["evt-3e3qc35ee2ck"],"hash":"11e8cb932f0c54497592dad92186852433cfccb4a7054a7268c99ac1309320bd"}
{"id":"evt-64tn42sjy299","type":"node.updated","subject":"gol-9jctv92","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"body":"apt install puts this app on a Debian or Ubuntu machine as a proper service: it starts at boot, runs as its own unprivileged user, keeps its data under /var/lib, its configuration under /etc and its logs in the journal. Priority 1. User's answer 2026-09-19: whether the package also carries MediaMTX, or leaves it to the existing first-run download, is to be decided during the work; carrying it means shipping someone else's binary and the licence and update questions that brings, while leaving it out means an install needs network access before video works.\n\nTarget: 1 check — the .deb installed on a clean Debian or Ubuntu machine, the service running after a reboot, and the dashboard reachable with a camera visible. Recorded as a measurement once the user has seen it.\n\nTo settle while doing it: which distributions and architectures; how Node itself is required or bundled; the systemd unit and its hardening; where the admin file, credential key and recordings live, and how the first-run setup is presented on a headless box; how the package is built and signed, and whether it is hosted in our own apt repository or only as a downloadable .deb; and upgrade and removal behaviour, in particular never deleting recordings or credentials on purge without asking."}},"at":"2026-09-19T23:42:38.048Z","parents":["evt-v71kqfm3x5ex"],"hash":"712092551f720619100d0aba39c57c0d42b5e39574d34106eb26be204dba4bad"}
{"id":"evt-yg0fn3rkmp78","type":"node.created","subject":"dec-wx1j9gj","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"decision","title":"Publish as onvif-dashboard under MIT; data location prompted on first run, defaulting to the working directory","body":"User's answers on 2026-09-19 for gol-k56hc69. Package name: onvif-dashboard (free on npm; \"cameras\" is taken). Licence: MIT, which needs a LICENSE file and the field in package.json, and package.json must stop being private. Data location: rather than inventing a home for the app's files, ask on first run when the environment doesn't say, offering the current working directory as the default, so today's behaviour stays the default answer. All paths move behind one module instead of each file calling process.cwd(), keeping every existing environment override (ADMIN_AUTH_FILE, CAMERA_REGISTRY_FILE, AUDIT_LOG_FILE, RECORDINGS_DIR, MEDIAMTX_BIN). A prompt must never block a service, so when there is no terminal the default is taken and the choice is logged. First run also has to produce CAMERA_CREDENTIALS_KEY, explain the admin setup code, and fetch MediaMTX with a clear message rather than a postinstall script. What ships is Next's standalone output plus a bin entry, so installing does not drag in React and Next as dependencies.","status":"recorded","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T23:50:01.566Z","parents":["evt-64tn42sjy299"],"hash":"d7b5f79ee7f0670979a74d00417074070ef435aca954262eb40b1cb6799fa447"}
{"id":"evt-ynpkdxtwyn35","type":"edge.added","subject":"dec-wx1j9gj","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"dec-wx1j9gj","to":"gol-k56hc69"},"at":"2026-09-19T23:50:01.568Z","parents":["evt-yg0fn3rkmp78"],"hash":"ee973df5a8b9e6521f49deb4217d8e2b6577a1349d6c53559b67d1ef823364e8"}
{"id":"evt-tyfj1a5xedjc","type":"edge.added","subject":"dec-wx1j9gj","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"dec-wx1j9gj","to":"gol-9jctv92"},"at":"2026-09-19T23:50:01.569Z","parents":["evt-ynpkdxtwyn35"],"hash":"73b2539563d22014b21dc4eb3efa58884c8ecb563a893e8bb9d5a77f56ba6d85"}
{"id":"evt-mg2g11gexc9e","type":"node.created","subject":"iss-57eqhn2","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Package identity: name, MIT licence and publishable package.json","body":"Per dec-wx1j9gj. Rename the package to onvif-dashboard, drop \"private\": true, add \"license\": \"MIT\" and a LICENSE file with the user's copyright line, plus description, keywords, repository, homepage, bugs and an engines range for the Node version the app needs. Add a files list so only what is needed is published. Nothing is published in this issue: it ends with npm pack --dry-run showing a sensible file list.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T23:50:09.491Z","parents":["evt-tyfj1a5xedjc"],"hash":"2a72bc7c4f6f06fa0580e72425d72d466686a9361839e566e5147802e6c27a6d"}
{"id":"evt-0yvc5yzghh68","type":"edge.added","subject":"iss-57eqhn2","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-57eqhn2","to":"gol-k56hc69"},"at":"2026-09-19T23:50:09.492Z","parents":["evt-mg2g11gexc9e"],"hash":"e91a6dedf327e4f16c40fbbf165dfed893da2222c6fb1ddf73036ab9666ce04b"}
{"id":"evt-ff5h16pvzhk6","type":"edge.added","subject":"iss-57eqhn2","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-57eqhn2","to":"package.json"},"at":"2026-09-19T23:50:09.493Z","parents":["evt-0yvc5yzghh68"],"hash":"69a7b3cedfd0876e1ef750fa8f5ec92025fd2b94be85b6d44fbc25329c10d245"}
{"id":"evt-yj82na47szsv","type":"edge.added","subject":"iss-57eqhn2","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-57eqhn2","to":"LICENSE"},"at":"2026-09-19T23:50:09.494Z","parents":["evt-ff5h16pvzhk6"],"hash":"e2e1d4afe88fc4a256e3d3284f1e4fd6b9decfd44e68d7824874a87b49139bde"}
{"id":"evt-42paven1jcjy","type":"edge.added","subject":"iss-57eqhn2","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-57eqhn2","to":"area:packaging"},"at":"2026-09-19T23:50:09.495Z","parents":["evt-yj82na47szsv"],"hash":"f0fe03300a6eb620d0e68dc84b69e55b701151d330bb4fccba3ccfb89290ba30"}
{"id":"evt-8g8059zambxt","type":"node.created","subject":"iss-rab8hnz","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Put every data path behind one module","body":"Per dec-wx1j9gj. Today admin-file.ts, camera-registry.ts, credential-store.ts, audit-log.ts, recordings.ts, video.ts and mediamtx-install.ts each build paths from process.cwd(), so running the app from anywhere else scatters .data and bin. Introduce src/lib/paths.ts as the one place that answers where things live: a data directory (CAMERAS_DATA_DIR, else the answer chosen on first run, else ./.data) with the admin file, registry, credentials, audit log and recordings under it, and the MediaMTX binary beside it. Every existing override keeps working unchanged (ADMIN_AUTH_FILE, CAMERA_REGISTRY_FILE, AUDIT_LOG_FILE, RECORDINGS_DIR, MEDIAMTX_BIN). No behaviour change for someone running from the repo: the defaults must resolve exactly as they do now, and the tests prove it.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T23:50:14.957Z","parents":["evt-42paven1jcjy"],"hash":"73c7eacb75ea78bd60da2a52040aa6c313814e24f12a10d082b7f15b7ba81472"}
{"id":"evt-41n33hsjkd2v","type":"edge.added","subject":"iss-rab8hnz","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-rab8hnz","to":"gol-k56hc69"},"at":"2026-09-19T23:50:14.959Z","parents":["evt-8g8059zambxt"],"hash":"270fdc6d17498bd15144be673f0d985162c037371c126e226b52d93a989c8a4f"}
{"id":"evt-3enmv7b4h1hh","type":"edge.added","subject":"iss-rab8hnz","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-rab8hnz","to":"gol-9jctv92"},"at":"2026-09-19T23:50:14.960Z","parents":["evt-41n33hsjkd2v"],"hash":"ac5fc884af5111ac4657186f3b31bceaaff7d8cb800980191b1509e431dcf74a"}
{"id":"evt-jrrks4c86bk5","type":"edge.added","subject":"iss-rab8hnz","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-rab8hnz","to":"src/lib/paths.ts"},"at":"2026-09-19T23:50:14.961Z","parents":["evt-3enmv7b4h1hh"],"hash":"51b837d7ccd06e4082a1be0324c16d992f70929471980887007e76175633cded"}
{"id":"evt-dpj0qxn5ebqr","type":"edge.added","subject":"iss-rab8hnz","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-rab8hnz","to":"src/lib/admin-file.ts"},"at":"2026-09-19T23:50:14.962Z","parents":["evt-jrrks4c86bk5"],"hash":"689d8f95deb2767e8836c52b83fdfba67fe7d8219530e4cf8180c24e8909395a"}
{"id":"evt-s51taers3878","type":"edge.added","subject":"iss-rab8hnz","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-rab8hnz","to":"src/lib/camera-registry.ts"},"at":"2026-09-19T23:50:14.963Z","parents":["evt-dpj0qxn5ebqr"],"hash":"c9bfe8163ae2060f01b1a5390e1bdc859e8638a63415a87e51ebf2868eb06b57"}
{"id":"evt-9zn4v968y7xe","type":"edge.added","subject":"iss-rab8hnz","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-rab8hnz","to":"src/lib/credential-store.ts"},"at":"2026-09-19T23:50:14.964Z","parents":["evt-s51taers3878"],"hash":"7354876b457bf23614ac08df45ea8c31c5c9e1e6f37329e18ce0096f878041ff"}
{"id":"evt-8rm5bcasemv3","type":"edge.added","subject":"iss-rab8hnz","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-rab8hnz","to":"src/lib/audit-log.ts"},"at":"2026-09-19T23:50:14.965Z","parents":["evt-9zn4v968y7xe"],"hash":"fc8f8d1f6a487a9b5cb58a709e360ebab06c2c7d17fb2df3ba3ba6bc4423b5cc"}
{"id":"evt-mjvgzfsjk1sc","type":"edge.added","subject":"iss-rab8hnz","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-rab8hnz","to":"src/lib/recordings.ts"},"at":"2026-09-19T23:50:14.966Z","parents":["evt-8rm5bcasemv3"],"hash":"dd1dbccd1982e6d4dd2f032d98ac956edd97f9eb4e0542e2700c175a528008a5"}
{"id":"evt-64306h5xbzrx","type":"edge.added","subject":"iss-rab8hnz","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-rab8hnz","to":"src/lib/video.ts"},"at":"2026-09-19T23:50:14.967Z","parents":["evt-mjvgzfsjk1sc"],"hash":"cea77695ca6a254a1709bad2d4b9d1d5b266288d1e88ab33bc0853b165caf530"}
{"id":"evt-wat14dts3e43","type":"edge.added","subject":"iss-rab8hnz","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-rab8hnz","to":"area:packaging"},"at":"2026-09-19T23:50:14.968Z","parents":["evt-64306h5xbzrx"],"hash":"82034e7f1fe1c5265dcb3a814b64d10c11b958bbc361830a7bf146bd0545da79"}
{"id":"evt-1gd70vahnb38","type":"node.created","subject":"iss-kz5c88a","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"First-run experience: choose where data lives, make the keys, fetch MediaMTX","body":"Per dec-wx1j9gj, the part a stranger meets. On first start, when the environment doesn't already say: ask where data should live, offering the current working directory as the default, and remember the answer; generate CAMERA_CREDENTIALS_KEY and write it, owner-only, so saved camera logins survive restarts; say plainly whether MediaMTX is installed and offer to fetch it, since live video and recording need it; print the admin setup code and the URL to open, with the existing severe warning if setup is skipped. With no terminal, as in a service, nothing prompts: defaults are taken, the choices are logged, and the app still starts. Running it again must be harmless: nothing is regenerated or overwritten. Tests cover the prompted path, the non-interactive path and the second run.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T23:50:29.896Z","parents":["evt-wat14dts3e43"],"hash":"3a83a6356efc08f04b1c4d6b9317fb078ab4e91c765764ba2a94191910c8b3dd"}
{"id":"evt-ex7fmsbaq6wr","type":"edge.added","subject":"iss-kz5c88a","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-kz5c88a","to":"gol-k56hc69"},"at":"2026-09-19T23:50:29.897Z","parents":["evt-1gd70vahnb38"],"hash":"c2f5d6bb9ef297eedbedef62d073b1e3f45c2ce46a1344155f50491dd1e8cf34"}
{"id":"evt-py560xd57scq","type":"edge.added","subject":"iss-kz5c88a","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-kz5c88a","to":"gol-9jctv92"},"at":"2026-09-19T23:50:29.898Z","parents":["evt-ex7fmsbaq6wr"],"hash":"d33200906981fa653d46e613417c2b95e2af873a115327d5fb642c84f6d9dcda"}
{"id":"evt-ks55d7y3vrdc","type":"edge.added","subject":"iss-kz5c88a","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-kz5c88a","to":"scripts/"},"at":"2026-09-19T23:50:29.899Z","parents":["evt-py560xd57scq"],"hash":"a90c35d99715b2c7035c6ae411e8724ae7b7100691a2f0cc9270268f1dae1871"}
{"id":"evt-zwhgn2svavp6","type":"edge.added","subject":"iss-kz5c88a","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-kz5c88a","to":"src/lib/paths.ts"},"at":"2026-09-19T23:50:29.900Z","parents":["evt-ks55d7y3vrdc"],"hash":"e306307293a54e9483cbda88eecec2093f6257173f9652e61db4627c5213d5bf"}
{"id":"evt-avmwrdmh7sfa","type":"edge.added","subject":"iss-rab8hnz","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"blocks","from":"iss-rab8hnz","to":"iss-kz5c88a"},"at":"2026-09-19T23:50:29.901Z","parents":["evt-zwhgn2svavp6"],"hash":"b41ef43cd368cc8a50d50a18364929c978893613bcaad075412c63f4b508aff9"}
{"id":"evt-bsny1nb9hxce","type":"edge.added","subject":"iss-kz5c88a","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-kz5c88a","to":"area:packaging"},"at":"2026-09-19T23:50:29.902Z","parents":["evt-avmwrdmh7sfa"],"hash":"a5d537b9904ef3e91143f478a4f0f5bab7f95b5d33038306cf427aa2df529477"}
{"id":"evt-0jt4xkt2jzqb","type":"node.created","subject":"iss-ej4ahga","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Standalone build and a command to start it","body":"Per dec-wx1j9gj. Switch the build to Next's standalone output, so what ships is a minimal server plus only the dependencies it needs, and copy .next/static and public beside it as the Next docs require. Add a bin entry, onvif-dashboard, with subcommands: start (the server, honouring PORT and HOST), setup (the first-run experience), and the existing admin and MediaMTX helpers. Running it from any directory must work. Ends with the packed tarball started from a temporary directory on this machine, serving the dashboard.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T23:50:33.776Z","parents":["evt-bsny1nb9hxce"],"hash":"5644f269dee369f096373958d306bb4a2d8c539401a507c82c8222e941e3d12f"}
{"id":"evt-qg0xd0chcdqd","type":"edge.added","subject":"iss-ej4ahga","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-ej4ahga","to":"gol-k56hc69"},"at":"2026-09-19T23:50:33.778Z","parents":["evt-0jt4xkt2jzqb"],"hash":"5d4b4ad1b00df87f6b06730bced0b1c6ae2348c0c66a4dffe22d042f6878a659"}
{"id":"evt-pbgznnjzttcb","type":"edge.added","subject":"iss-ej4ahga","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-ej4ahga","to":"next.config.ts"},"at":"2026-09-19T23:50:33.779Z","parents":["evt-qg0xd0chcdqd"],"hash":"a36b7a0309a87b7cc30c836d6fa2fe4091022f544f6d0a48ed80c81ffda9967f"}
{"id":"evt-hs14sc8qsgzm","type":"edge.added","subject":"iss-ej4ahga","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-ej4ahga","to":"package.json"},"at":"2026-09-19T23:50:33.780Z","parents":["evt-pbgznnjzttcb"],"hash":"219269e8d61e671f819ddce02c6fd2239f85325cb9d94a1e0d1be47fba899065"}
{"id":"evt-x6r595dgyr6p","type":"edge.added","subject":"iss-ej4ahga","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-ej4ahga","to":"bin/"},"at":"2026-09-19T23:50:33.781Z","parents":["evt-hs14sc8qsgzm"],"hash":"8a211c361ed324b44aa25987909c9132b0b0555c6677ec9375c79de9a170fea5"}
{"id":"evt-y0wr9esz7599","type":"edge.added","subject":"iss-57eqhn2","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"blocks","from":"iss-57eqhn2","to":"iss-ej4ahga"},"at":"2026-09-19T23:50:33.782Z","parents":["evt-x6r595dgyr6p"],"hash":"3199b7ba5d30d1a95685ea4ef29105d850aafed97f4d122248874814d2409ad5"}
{"id":"evt-amdszktyzpt5","type":"edge.added","subject":"iss-rab8hnz","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"blocks","from":"iss-rab8hnz","to":"iss-ej4ahga"},"at":"2026-09-19T23:50:33.783Z","parents":["evt-y0wr9esz7599"],"hash":"96aed3d18c9c33f14852bb76fd17dc6c5dde2a8cb14663bd07f2e448466382b4"}
{"id":"evt-z2019vtp5hf7","type":"edge.added","subject":"iss-ej4ahga","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-ej4ahga","to":"area:packaging"},"at":"2026-09-19T23:50:33.784Z","parents":["evt-amdszktyzpt5"],"hash":"1a2a8ba820a90bc9639e43c14d6af7c65f128951a4906db66c858596cb77c541"}
{"id":"evt-xx39xb46aj62","type":"node.created","subject":"iss-9zcawn7","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Publish onvif-dashboard and install it on a clean machine","body":"The goal's final check. Publish a release candidate to npm under the user's account (the user publishes; the app has no credentials for that), then install it somewhere that has never held this repo, run setup, add a camera and watch it. Also check what the published tarball contains, that the fork dependency installs from a clean cache, and that uninstalling leaves the data directory alone. If the onvif pull request (agsh/onvif#500) has been released by then, depend on the published package instead of the fork (iss-pkp09k8). Records the measurement that meets gol-k56hc69.","status":"open","owner":null,"attrs":{}},"at":"2026-09-19T23:50:42.192Z","parents":["evt-z2019vtp5hf7"],"hash":"c38f1745987d9a343373557822e128b25fe023c3410c38dd6ac2886678133964"}
{"id":"evt-fveqg37yn6mj","type":"edge.added","subject":"iss-9zcawn7","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-9zcawn7","to":"gol-k56hc69"},"at":"2026-09-19T23:50:42.193Z","parents":["evt-xx39xb46aj62"],"hash":"3b738160a1fd2727d1ad5c2b63d415539a97f3605c1d48a6a152cd1e7ac4066a"}
{"id":"evt-prdywnascbd6","type":"edge.added","subject":"iss-ej4ahga","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"blocks","from":"iss-ej4ahga","to":"iss-9zcawn7"},"at":"2026-09-19T23:50:42.194Z","parents":["evt-fveqg37yn6mj"],"hash":"f8a75c643518fc63bd07cc862ca0cb2fb6be504f58dfe93dc628ff3d9cefcc55"}
{"id":"evt-6yyzx2d4dh1v","type":"edge.added","subject":"iss-kz5c88a","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"blocks","from":"iss-kz5c88a","to":"iss-9zcawn7"},"at":"2026-09-19T23:50:42.195Z","parents":["evt-prdywnascbd6"],"hash":"a3e8f768078f33e460137a03ad02e0c1af1d352a65687f971a39281ea9571c1f"}
{"id":"evt-d4acaz9qsx2c","type":"edge.added","subject":"iss-9zcawn7","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-9zcawn7","to":"area:packaging"},"at":"2026-09-19T23:50:42.196Z","parents":["evt-6yyzx2d4dh1v"],"hash":"e483e96f8348cce4c57b00894e5dda4ad996ad519123291c1929119cd1baa71d"}
{"id":"evt-g8gkqtcrk7h9","type":"node.updated","subject":"gol-k56hc69","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"target":5,"direction":"up","unit":"issues done"}},"at":"2026-09-19T23:50:50.445Z","parents":["evt-d4acaz9qsx2c"],"hash":"3fa12e83fcd919155898ae84018b46a3e5890cdebe94a62b5ae9b57177f13346"}
{"id":"evt-dh6y5bhh7vj1","type":"node.updated","subject":"gol-k56hc69","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"patch":{"body":"Someone with Node installed can run this dashboard with a single command, e.g. npx onvif-dashboard, and get discovery, live video, recording and settings without git or a build step. User's answers 2026-09-19 (dec-wx1j9gj): the whole runnable app rather than a library; package name onvif-dashboard; MIT licence; where data lives is asked on first run, defaulting to the working directory. Priority 1.\n\nTarget: 5 issues done, the work this breaks into as of 2026-09-19: iss-57eqhn2 package identity (name, MIT, publishable package.json), iss-rab8hnz every data path behind one module, iss-kz5c88a the first-run experience, iss-ej4ahga standalone build and a start command, and iss-9zcawn7 publish and install on a machine that has never held this repo. The last one carries the real proof: a working dashboard with a camera visible, on a clean machine. If the work turns out to need more issues, the target moves with it and the change is recorded.\n\nWhat this forces us to settle, and where each is handled: what ships (standalone output, iss-ej4ahga); name, licence and metadata (iss-57eqhn2); where data lives now that it can't assume the working directory (iss-rab8hnz); how MediaMTX is obtained, since npm install scripts are often disabled (iss-kz5c88a); the credential key and admin setup for someone who has never seen the repo (iss-kz5c88a); and the dependency on the onvif fork, which should become the published package once agsh/onvif#500 is released (iss-9zcawn7, iss-pkp09k8)."}},"at":"2026-09-19T23:50:56.866Z","parents":["evt-g8gkqtcrk7h9"],"hash":"793c7f4fe3d3ecd84a3549bddd1777641eb56954b7cd6e0d540407213ff57f73"}
{"id":"evt-0wzcts22k31h","type":"node.created","subject":"mea-9a31xkj","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"measurement","title":"npm packaging: 0 of 5 issues done","body":"Starting point on 2026-09-19, when the work was broken down: none of the five issues under gol-k56hc69 is closed. Counted from vrek, not estimated.","status":"recorded","owner":null,"attrs":{"value":0,"applies_at":"2026-09-19"}},"at":"2026-09-19T23:50:58.813Z","parents":["evt-dh6y5bhh7vj1"],"hash":"7f02887d8d4232b14a7f64f960c621a028b55c2771f7125fe6b0d83c7ff5f0e9"}
{"id":"evt-9pgxq502sxr1","type":"edge.added","subject":"mea-9a31xkj","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"measures","from":"mea-9a31xkj","to":"gol-k56hc69"},"at":"2026-09-19T23:50:58.814Z","parents":["evt-0wzcts22k31h"],"hash":"eabf48665cd96e15fd1b7bfcb0dd25ac15e6c39793da56777a1d3ddff493d93d"}
{"id":"evt-jg5cpmw6yjxk","type":"node.created","subject":"ver-9gbabvx","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"The package is publishable as onvif-dashboard under MIT, with a LICENSE file and metadata, and npm pack ships only what is needed","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T23:54:41.146Z","parents":["evt-9pgxq502sxr1"],"hash":"9de770f442db086e0b6d545dfbf99d198d52114f91445482ea7fdca8a34cc098"}
{"id":"evt-j8nqeh6f2zp8","type":"edge.added","subject":"ver-9gbabvx","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-9gbabvx","to":"iss-57eqhn2"},"at":"2026-09-19T23:54:41.148Z","parents":["evt-jg5cpmw6yjxk"],"hash":"d395a0f0522de00c7d523cce7738ad615e4bafadfe3c104f4c61decf3e5f519a"}
{"id":"evt-n4zmd4aqm5vq","type":"verification.recorded","subject":"ver-9gbabvx","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"package.json renamed to onvif-dashboard, \"private\" removed, license MIT with description, keywords, author, repository, engines (node >=24) and a files list; LICENSE added (MIT, Michael Mainguy, 2026). npm pack --dry-run: 43 files, 472.6 kB packed, 1.3 MB unpacked. The first attempt was 27.4 MB because the files list swept in the downloaded MediaMTX binary; bin/ is now excluded and the CLI will live in cli/."},"at":"2026-09-19T23:54:41.149Z","parents":["evt-j8nqeh6f2zp8"],"hash":"6003d5ddaa1147699f85f37ee8d3410c819ee96190d8bc5e740159e9daf1d473"}
{"id":"evt-b7vfg9e1fgzc","type":"node.created","subject":"ver-4596467","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"One module decides where the app's files live; CAMERAS_DATA_DIR moves them all, every per-file override still wins, and the defaults are exactly what running from the repo meant before","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T23:54:43.653Z","parents":["evt-n4zmd4aqm5vq"],"hash":"3336dcbad6d08e8283606625225ee6c760a79da062051d6d2fa28005c77ec142"}
{"id":"evt-tbx082w480gd","type":"edge.added","subject":"ver-4596467","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-4596467","to":"iss-rab8hnz"},"at":"2026-09-19T23:54:43.656Z","parents":["evt-b7vfg9e1fgzc"],"hash":"162c9243975b26a18f60a8bf9984f885a9fd02b113a1ed0b4ec1566e352d868a"}
{"id":"evt-928phbmmmsz8","type":"verification.recorded","subject":"ver-4596467","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"src/lib/paths.ts with admin-file, camera-registry, credential-store, audit-log, recordings, video and mediamtx-install all reading from it. src/lib/paths.test.ts pins the defaults (./.data/*, ./bin/mediamtx), CAMERAS_DATA_DIR moving everything the app writes, and each of ADMIN_AUTH_FILE, CAMERA_REGISTRY_FILE, CAMERA_CREDENTIALS_FILE, AUDIT_LOG_FILE, RECORDINGS_DIR and MEDIAMTX_BIN still winning. Full suite 912 tests pass, 99.69% lines; tsc, eslint and next build clean; npm run video:install still finds the existing binary and .data is untouched."},"at":"2026-09-19T23:54:43.657Z","parents":["evt-tbx082w480gd"],"hash":"a65c047e4e87aa4e01e0442e4c0e1fafccdca00457d3bda04d85a5c52692dccc"}
{"id":"evt-zy1a4sk0tc37","type":"node.status_changed","subject":"iss-57eqhn2","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T23:54:45.770Z","parents":["evt-928phbmmmsz8"],"hash":"5862722129d2a58d4c33aa39b4691621b1c839cd2daf1b791bddee4f9ab493a0"}
{"id":"evt-xvzexr3d4fyh","type":"node.status_changed","subject":"iss-rab8hnz","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-19T23:54:47.875Z","parents":["evt-zy1a4sk0tc37"],"hash":"07560237cef3dc19f3dc9c905f46306c4a2371c023cf778ab9b74e7c6792a215"}
{"id":"evt-2x3g58fk7cwy","type":"node.created","subject":"mea-kha11x6","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"measurement","title":"npm packaging: 2 of 5 issues done","body":"Counted from vrek on 2026-09-19: iss-57eqhn2 (package identity, MIT, publishable package.json) and iss-rab8hnz (every data path behind src/lib/paths.ts) are closed with evidence. Left: iss-kz5c88a first-run experience, iss-ej4ahga standalone build and start command, iss-9zcawn7 publish and install on a clean machine.","status":"recorded","owner":null,"attrs":{"value":2,"applies_at":"2026-09-19"}},"at":"2026-09-19T23:54:54.082Z","parents":["evt-xvzexr3d4fyh"],"hash":"b99544debb7dfefe2f317b7b3a1c96afe1c6567dfe3847ed952f1de8fe798668"}
{"id":"evt-7t57v5scysa9","type":"edge.added","subject":"mea-kha11x6","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"measures","from":"mea-kha11x6","to":"gol-k56hc69"},"at":"2026-09-19T23:54:54.083Z","parents":["evt-2x3g58fk7cwy"],"hash":"8efd00dee34eac7287d7ff89cd51d3d1b03014d1baec7a0049e254587885439d"}
{"id":"evt-e6hdherdeq9k","type":"node.created","subject":"ver-cct55bk","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"First run asks where data should live with the working directory as the default and remembers the answer, creates an owner-only credential key, reports whether MediaMTX is installed and offers to fetch it, explains the admin setup, never prompts without a terminal, and changes nothing when run again","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-19T23:59:59.063Z","parents":["evt-7t57v5scysa9"],"hash":"30a888695e091d1c0cbceacde079815fdfafd36fd8dad9040b4a1ed3cd04c305"}
{"id":"evt-1n36vqvx16jk","type":"edge.added","subject":"ver-cct55bk","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-cct55bk","to":"iss-kz5c88a"},"at":"2026-09-19T23:59:59.065Z","parents":["evt-e6hdherdeq9k"],"hash":"ea7adcf5e6de830baf1fe4fd833487aec1d3429e65d898d46c237a36e7659b66"}
{"id":"evt-eqrerenwyntb","type":"verification.recorded","subject":"ver-cct55bk","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"src/lib/first-run.ts with src/lib/first-run.test.ts (21 tests: the pointer file under XDG_CONFIG_HOME, env over remembered over asked over default, empty and relative answers, owner-only secrets 0600, nothing regenerated on a second run, the non-interactive path, applyEnvironment never overriding the environment). cli/onvif-dashboard.mjs with cli/onvif-dashboard.test.ts running the real command in a temporary directory. A real non-interactive run in /tmp created only .data/secrets.env and the config pointer, and touched nothing in the repo. Full suite 936 tests pass, 99.7% lines; tsc, eslint and next build clean."},"at":"2026-09-19T23:59:59.066Z","parents":["evt-1n36vqvx16jk"],"hash":"d9a6bae5f50ec8ab81e59da07a8af67a4895a503a6d1c6850b7eddb10914c2bd"}
{"id":"evt-5rfrprcqxrt0","type":"node.status_changed","subject":"iss-kz5c88a","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-20T00:00:01.419Z","parents":["evt-eqrerenwyntb"],"hash":"90695a74ba466023c8958bbc69904a2662e1dc9e0d76bca4eb07846891987a17"}
{"id":"evt-nwrj4gzcx3kr","type":"node.created","subject":"mea-t76mxwf","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"measurement","title":"npm packaging: 3 of 5 issues done","body":"Counted from vrek on 2026-09-19: package identity, the paths module and the first-run experience are closed with evidence. Left: iss-ej4ahga standalone build and start command, iss-9zcawn7 publish and install on a clean machine.","status":"recorded","owner":null,"attrs":{"value":3,"applies_at":"2026-09-19"}},"at":"2026-09-20T00:00:03.766Z","parents":["evt-5rfrprcqxrt0"],"hash":"cc01c984fca3860f31f63b81ea476c93a524742e2731b49ab30f14ef7c31ca5a"}
{"id":"evt-nnmw5y70jh2v","type":"edge.added","subject":"mea-t76mxwf","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"measures","from":"mea-t76mxwf","to":"gol-k56hc69"},"at":"2026-09-20T00:00:03.767Z","parents":["evt-nwrj4gzcx3kr"],"hash":"ced3f88a47aab1cf0e574138436e828205b800cd02c7ea30fe6a9b21f497d5f9"}
{"id":"evt-hhvg5v4mr4fe","type":"node.created","subject":"ver-0gx8s86","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"The build produces a standalone server with its static files, pruned of what the app never runs, and onvif-dashboard start runs it from any directory","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-20T00:08:53.630Z","parents":["evt-nnmw5y70jh2v"],"hash":"b259000d6ac7367bd7f22e69abd2889955d14fddf9d35e78438be96425de6e9b"}
{"id":"evt-se7t2mq52hda","type":"edge.added","subject":"ver-0gx8s86","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-0gx8s86","to":"iss-ej4ahga"},"at":"2026-09-20T00:08:53.633Z","parents":["evt-hhvg5v4mr4fe"],"hash":"36da77eea43d6980bbd08665f6c10700ab5117832efb9ba8fce2540b8a777fb7"}
{"id":"evt-qxcfkcrdwhsr","type":"verification.recorded","subject":"ver-0gx8s86","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"next.config.ts sets output: \"standalone\" and images.unoptimized; scripts/bundle-standalone.mjs copies .next/static and public beside the server and drops the traced-but-unused MediaMTX binary and sharp's platform binaries (79 MB), leaving .next/standalone at 27 MB and npm pack at 4.9 MB. cli/onvif-dashboard.mjs gained start (PORT/HOSTNAME, signals forwarded, quiet first run when nothing is set up) and admin. Checked for real: started from /tmp with its own data folder, /login answered HTTP 307 to setup, the console printed the one-time setup code, and it reported MediaMTX missing. Messages that said \"npm run …\" now name the installed command. Full suite 937 tests pass, 99.7% lines; tsc and eslint clean."},"at":"2026-09-20T00:08:53.634Z","parents":["evt-se7t2mq52hda"],"hash":"60c1fe2b8f8358174aad5b495257ba9908ac5ba4186e5f8345a58b638e36f7ce"}
{"id":"evt-svss3f90kf71","type":"node.status_changed","subject":"iss-ej4ahga","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-20T00:08:56.449Z","parents":["evt-qxcfkcrdwhsr"],"hash":"86783b1bae9e8acbdeed1191d27e175226cba155011fad66fd9a4befa53edc9d"}
{"id":"evt-s93xtm5nv20z","type":"node.created","subject":"mea-mtq4xsm","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"measurement","title":"npm packaging: 4 of 5 issues done","body":"Counted from vrek on 2026-09-19: package identity, the paths module, the first-run experience and the standalone build with a start command are closed with evidence. Left: iss-9zcawn7, publishing and installing on a machine that has never held this repo, which the user has to do since publishing needs their npm account.","status":"recorded","owner":null,"attrs":{"value":4,"applies_at":"2026-09-19"}},"at":"2026-09-20T00:08:58.895Z","parents":["evt-svss3f90kf71"],"hash":"b7a4892d809e426538d084c9da3c2af3d0e91b6f21cd064b1d2b772f2fcffa52"}
{"id":"evt-w855xpb3jfwm","type":"edge.added","subject":"mea-mtq4xsm","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"measures","from":"mea-mtq4xsm","to":"gol-k56hc69"},"at":"2026-09-20T00:08:58.896Z","parents":["evt-s93xtm5nv20z"],"hash":"c5fa46275ff117df73f142897cbfb085a3fefd06e939b5809d64447ea56486d1"}
{"id":"evt-5qz5x14089hy","type":"node.created","subject":"fnd-53mh3ad","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"finding","title":"A published npm package can't ship TypeScript for the CLI, and externalizing a git-URL dependency breaks the built server","body":"Both found by the user installing onvif-dashboard 0.1.0 on a real machine on 2026-09-19, and both were my errors. (1) Node refuses to strip types under node_modules: ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING for src/lib/first-run.ts. Running .ts directly works from a checkout and never works from an installed package, so the CLI's modules are now compiled to dist/ by tsconfig.cli.json (ES2022 modules, with dist/package.json marking them as ESM), and the helper scripts became plain .mjs. (2) With serverExternalPackages: [\"onvif\"], Turbopack emitted require(\"onvif-29d15b1bafe0601e\") in the server chunks, a name that resolves nowhere, because the dependency comes from a git URL rather than the registry; the installed app then failed with \"Failed to load external module\". Bundling onvif instead fixes it, and was checked from an installed tarball: /api/discover answered 200 (so dgram works bundled) and a real camera's info and snapshot came back through the packaged server. Also worth knowing for the Debian goal: npm does publish nested node_modules inside .next/standalone, so the packed app carries its own dependencies.","status":"current","owner":"prn-q80g8mz","attrs":{"sources":[{"node":"iss-9zcawn7","note":"User's install output on their server, then local reproduction with npm pack plus install into a temporary project"}],"as_of":"2026-09-20"}},"at":"2026-09-20T00:21:38.491Z","parents":["evt-w855xpb3jfwm"],"hash":"c28dc8d61823f0c69e3e12cdc6802b28f2c45fcb5e6d42035385ed66abf15f71"}
{"id":"evt-dqc3aswb5n83","type":"edge.added","subject":"fnd-53mh3ad","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-53mh3ad","to":"iss-9zcawn7"},"at":"2026-09-20T00:21:38.493Z","parents":["evt-5qz5x14089hy"],"hash":"7827762965307b09d8be5fe9d83a7720c2efa1998bd66a62020ced96c5e2e081"}
{"id":"evt-9yg21vwz860k","type":"edge.added","subject":"fnd-53mh3ad","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-53mh3ad","to":"gol-k56hc69"},"at":"2026-09-20T00:21:38.494Z","parents":["evt-dqc3aswb5n83"],"hash":"7398dd055f23e646e167823526004382e417186b7464ca8b984739848aace69c"}
{"id":"evt-6zk4dghxrwx7","type":"edge.added","subject":"fnd-53mh3ad","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"informs","from":"fnd-53mh3ad","to":"gol-9jctv92"},"at":"2026-09-20T00:21:38.495Z","parents":["evt-9yg21vwz860k"],"hash":"69f9257312d727fc3f3681910a8da37cb69f451ae65e190595510652f8180337"}
{"id":"evt-zgchgcs8qbz9","type":"node.created","subject":"iss-jwdt0pw","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Show what a network scan is doing while it runs","body":"Reported by the user on 2026-09-20 from the published package: the first scan looks like it has hung. It eventually works, but nothing says what is happening. Today the button shows a spinner and \"Scanning…\", and that is all; the scan is a multicast probe and a unicast sweep of the subnet running together (both bounded by the timeout), and afterwards router.refresh() re-renders the dashboard and every card then fetches its own info and snapshot, which is more waiting with no explanation. Make it legible: count the seconds while scanning against the timeout the user chose, say in words what is happening (listening for cameras, and checking every address on this subnet), keep a \"Updating the list…\" state while the page refreshes (useTransition), and afterwards report what was done, e.g. how many addresses were checked, how long it took and how many cameras were found. The discovery layer already knows the host count, so the route can return it (a small addition to the response the client already reads).","status":"open","owner":null,"attrs":{}},"at":"2026-09-20T00:54:20.498Z","parents":["evt-6zk4dghxrwx7"],"hash":"6239057b6d1e8df41a5cf4a399606adaa532e871b6ed5a522f18e12648e47bbc"}
{"id":"evt-09x228gd0ptb","type":"edge.added","subject":"iss-jwdt0pw","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-jwdt0pw","to":"gol-k56hc69"},"at":"2026-09-20T00:54:20.502Z","parents":["evt-zgchgcs8qbz9"],"hash":"a443be68d300ba64aa27d35d1346c4cbec729503faca1e293180958183641223"}
{"id":"evt-gpbe57gp6v00","type":"edge.added","subject":"iss-jwdt0pw","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-jwdt0pw","to":"src/app/camera-scanner.tsx"},"at":"2026-09-20T00:54:20.503Z","parents":["evt-09x228gd0ptb"],"hash":"9940073415d6ca35ce9e41f7e85bbfc57baf5cb173eeaddc6afaf4a350713c0e"}
{"id":"evt-v1bthmjhe3v8","type":"edge.added","subject":"iss-jwdt0pw","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-jwdt0pw","to":"src/app/api/discover/route.ts"},"at":"2026-09-20T00:54:20.504Z","parents":["evt-gpbe57gp6v00"],"hash":"0144b20bd1f367c42202ab75fbd16cc2019211c97761e8bfff4b92e72698a483"}
{"id":"evt-myb3dwzvdbg4","type":"edge.added","subject":"iss-jwdt0pw","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-jwdt0pw","to":"src/lib/onvif.ts"},"at":"2026-09-20T00:54:20.505Z","parents":["evt-v1bthmjhe3v8"],"hash":"b9b38dfc09596499c2a8a25c42d492477eca716a4cf1c2e4d85712e43216379c"}
{"id":"evt-chx9hkeb0sp9","type":"edge.added","subject":"iss-jwdt0pw","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-jwdt0pw","to":"area:ui"},"at":"2026-09-20T00:54:20.506Z","parents":["evt-myb3dwzvdbg4"],"hash":"9b915eef0997644a5af4fcfe9e5b8ad06be1e0e6c79203123f8bef07e5864960"}
{"id":"evt-yjrqfm80ndyh","type":"node.created","subject":"iss-n8yz5cy","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Remember the binary folder like the data folder, so install-video and start can't disagree","body":"Found by the user on 2026-09-20: onvif-dashboard install-video put MediaMTX in ./bin relative to the directory it was run in, and starting from a different directory looked elsewhere and reported it missing. Commit 731206b stopped the server from resolving it against its own directory, but the folder still follows whoever runs the command. Fix: keep it in the same config pointer that already remembers the data folder, written by install-video and setup and read by start, with CAMERAS_BIN_DIR still winning. Tests cover installing from one directory and starting from another.","status":"open","owner":null,"attrs":{}},"at":"2026-09-20T00:55:11.103Z","parents":["evt-chx9hkeb0sp9"],"hash":"2716518e0ebb924e1c49a413f630f86230cc737415bfea562c2460473070e7fc"}
{"id":"evt-pygtmv7cqz1n","type":"edge.added","subject":"iss-n8yz5cy","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-n8yz5cy","to":"gol-k56hc69"},"at":"2026-09-20T00:55:11.116Z","parents":["evt-yjrqfm80ndyh"],"hash":"c6c91d60203783516728aecae5e493d2d1bf316a40b4c530fece09a679cdffe0"}
{"id":"evt-ak71wvdrjn34","type":"edge.added","subject":"iss-n8yz5cy","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-n8yz5cy","to":"gol-9jctv92"},"at":"2026-09-20T00:55:11.117Z","parents":["evt-pygtmv7cqz1n"],"hash":"2b9fcfe6e8ecdb335da0a9f4f18976f02b580ba3da9e53288c6df0ac20d9c0bc"}
{"id":"evt-zsct72dx114b","type":"edge.added","subject":"iss-n8yz5cy","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-n8yz5cy","to":"src/lib/first-run.ts"},"at":"2026-09-20T00:55:11.118Z","parents":["evt-ak71wvdrjn34"],"hash":"c52f88638842fb1ee33fe4c4df0e154bf78d8a63f4b95331a0aa6f5acaaf8c3f"}
{"id":"evt-1gdd72r0sq08","type":"edge.added","subject":"iss-n8yz5cy","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-n8yz5cy","to":"cli/onvif-dashboard.mjs"},"at":"2026-09-20T00:55:11.119Z","parents":["evt-zsct72dx114b"],"hash":"de26356dcf8dc5f709abc6af8206f4c9694c6b3f30cd4f0fcbcbd85c65ac74de"}
{"id":"evt-45fchc2hna2c","type":"edge.added","subject":"iss-n8yz5cy","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-n8yz5cy","to":"area:packaging"},"at":"2026-09-20T00:55:11.120Z","parents":["evt-1gdd72r0sq08"],"hash":"0117efc99bf9614015110e3cf827e77aea6b49e2decdadba24568869adaf47b0"}
{"id":"evt-mm3wj70pzwa5","type":"node.created","subject":"ver-nh5yt09","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"A running scan says what it is doing: seconds counted against the chosen timeout, the phases in plain words, a state while the list updates, and a summary of what was found and how many addresses were checked","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-20T01:00:12.246Z","parents":["evt-45fchc2hna2c"],"hash":"c757f0c934bf4f8e16f87844e3160d2b3bfd486a16567dfecec48faf4966fead"}
{"id":"evt-qch17sfhy9w8","type":"edge.added","subject":"ver-nh5yt09","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-nh5yt09","to":"iss-jwdt0pw"},"at":"2026-09-20T01:00:12.250Z","parents":["evt-mm3wj70pzwa5"],"hash":"9b63c90170cd91a5b19b576770338a79da6a52001ef45deabb8c00ba692aae38"}
{"id":"evt-kqt47cssb3nw","type":"verification.recorded","subject":"ver-nh5yt09","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"src/lib/onvif.ts gained scanNetwork() returning cameras, addressesProbed and durationMs (discoverCameras still exists on top of it); the route returns that report; camera-scanner.tsx counts elapsed seconds, describes both phases, uses useTransition for the refresh and prints a summary. Tests: camera-scanner.test.tsx (summary wording, singular, phase text with and without the sweep, controls disabled while busy), discover route tests, icons test. Checked through an installed 0.1.3 tarball against the real network: 2 cameras found, 4,093 addresses probed, 3,005 ms. Full suite 944 tests pass, 99.65% lines; tsc and eslint clean."},"at":"2026-09-20T01:00:12.251Z","parents":["evt-qch17sfhy9w8"],"hash":"e0401cbfb81e2499d61d3ccc466dbd32aac728f93e3d51223623276249f094de"}
{"id":"evt-vrrs8ncbcghg","type":"node.created","subject":"ver-nah7fyr","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"install-video and start agree about where MediaMTX lives, whichever directory each is run from","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-20T01:00:15.663Z","parents":["evt-kqt47cssb3nw"],"hash":"6c3cdeef9833268401ca110910115fc5b2d88edcc50d1ca16e0beb1cfe89da51"}
{"id":"evt-ek7eke0zxksj","type":"edge.added","subject":"ver-nah7fyr","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-nah7fyr","to":"iss-n8yz5cy"},"at":"2026-09-20T01:00:15.664Z","parents":["evt-vrrs8ncbcghg"],"hash":"f748f78bee2f96fd15c0449c89ac0f8a09c353b2a4ad8753df783eeb4ee4a794"}
{"id":"evt-teap5q282n28","type":"verification.recorded","subject":"ver-nah7fyr","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"The config pointer now remembers binDir alongside dataDir, merging rather than overwriting; install-video calls rememberBinDir() and installs there; applyEnvironment feeds it back to later commands, with CAMERAS_BIN_DIR still winning. src/lib/first-run.test.ts covers remembering, keeping the data folder, and environment-over-remembered. Checked by hand: installed from /tmp/od5/here, then read back from /tmp/od5/elsewhere as /private/tmp/od5/here/bin."},"at":"2026-09-20T01:00:15.665Z","parents":["evt-ek7eke0zxksj"],"hash":"40a4b4122f6b2c48ed3ee8bf93ba9b5f4dda7e9c10fdf8526633ef1a4aef8e72"}
{"id":"evt-fj77a4q216h5","type":"node.status_changed","subject":"iss-jwdt0pw","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-20T01:00:16.813Z","parents":["evt-teap5q282n28"],"hash":"73a72c267bdf0504d269e6380884aadd3d121f82ebd05365680e9f12d9001515"}
{"id":"evt-954awzf35t82","type":"node.status_changed","subject":"iss-n8yz5cy","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"done"},"at":"2026-09-20T01:00:17.828Z","parents":["evt-fj77a4q216h5"],"hash":"98b76ddba2a716012a86e7b526e82dadd9f7b3af6f0b0351b0c60965af4a24c2"}
{"id":"evt-rz1v74g035zs","type":"node.created","subject":"dec-s4pec5x","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"decision","title":"Remote access is over the user's VPN; no internet-facing video","body":"Asked and decided on 2026-09-20. Watching from outside the house goes over the VPN the user already runs, so the app stays a LAN service: no ports opened, no public URL, and nothing to change in the code. What was considered and rejected for now: forwarding UDP 8189 with webrtcAdditionalHosts set to a public name (lowest latency, but a second public entrance and it breaks where UDP is blocked); ICE over TCP through a stream proxy; serving HLS through our own session-checked routes, which needs no extra ports and would be the only option keeping video behind the admin login, at two to six seconds of latency; and a TURN server, which works nearly everywhere but is another service to run and secure. Worth remembering if this is ever revisited: with WebRTC as built, only the signaling is behind the admin session, while the media itself is reachable by anyone holding a session description, which is acceptable on a private network and would not be on a public one.","status":"recorded","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-20T01:21:16.094Z","parents":["evt-954awzf35t82"],"hash":"a9a3f359844d787da4eff3069660c1d3e196bdebf3d1a8dde62358f686fa1089"}
{"id":"evt-xsejqxb5hpfy","type":"edge.added","subject":"dec-s4pec5x","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"dec-s4pec5x","to":"gol-9jctv92"},"at":"2026-09-20T01:21:16.097Z","parents":["evt-rz1v74g035zs"],"hash":"bd47d1b2e8ac5dce3947f39bdd1bd11cbf03889c9e9af87f06e2ed4dbf6fc934"}
{"id":"evt-rbb76bh618ed","type":"node.created","subject":"iss-gq9faq4","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"issue","title":"Play recordings in a pop-out window, like the live view","body":"Asked by the user on 2026-09-20. Clips used to play inline on the Recordings page; now Play opens /recordings/play?clip=… in a window of its own, the same shape as the live pop-out: the video fills the window on black, with a fading overlay showing the camera and the time, and the window title naming both. One window is reused for whichever clip is opened next, rather than one per clip. It stays a plain link with a target, so a blocked popup still opens a tab. The page checks the session and resolves the clip inside the recordings folder, so a request can only name a clip that folder holds.","status":"open","owner":null,"attrs":{}},"at":"2026-09-20T10:38:29.425Z","parents":["evt-xsejqxb5hpfy"],"hash":"a22aeaad4819a20d38451f7d18c81f7a6c04390a804bdbce496b8f033b5761b1"}
{"id":"evt-9nb9mq2j0w3b","type":"edge.added","subject":"iss-gq9faq4","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"serves","from":"iss-gq9faq4","to":"gol-k56hc69"},"at":"2026-09-20T10:38:29.430Z","parents":["evt-rbb76bh618ed"],"hash":"06688954d7c59cfbc2283ba0231aa85e46cc9d1ed80fd7e56e7be918c9c9e448"}
{"id":"evt-800hfqagdn2p","type":"edge.added","subject":"iss-gq9faq4","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-gq9faq4","to":"src/app/recordings/play/"},"at":"2026-09-20T10:38:29.432Z","parents":["evt-9nb9mq2j0w3b"],"hash":"25d2521eb48e4b798a3760c22484020f8a5f0a5ec552d1955565ef56ef5548b1"}
{"id":"evt-k65y96se0rgc","type":"edge.added","subject":"iss-gq9faq4","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-gq9faq4","to":"src/app/recordings/clip-list.tsx"},"at":"2026-09-20T10:38:29.433Z","parents":["evt-800hfqagdn2p"],"hash":"1cf40030985ca1bf0110fe4dc73df817ecd4bb2f034b2a66fc9fe84b944b7576"}
{"id":"evt-sgx0vnhx7na9","type":"edge.added","subject":"iss-gq9faq4","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"touches","from":"iss-gq9faq4","to":"src/app/popout.ts"},"at":"2026-09-20T10:38:29.434Z","parents":["evt-k65y96se0rgc"],"hash":"518450444fd3557c1d4e267a5d4c91bba55ad78efbcc28da910decb4f8350d45"}
{"id":"evt-efxy52npt9s4","type":"edge.added","subject":"iss-gq9faq4","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-gq9faq4","to":"area:ui"},"at":"2026-09-20T10:38:29.435Z","parents":["evt-sgx0vnhx7na9"],"hash":"5376696122c3778f78299768608c2a6539ed1ad481b74cde37af0686a8bd6c5d"}
{"id":"evt-2ebzn1n59az4","type":"edge.added","subject":"iss-gq9faq4","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"tagged","from":"iss-gq9faq4","to":"area:video"},"at":"2026-09-20T10:38:29.436Z","parents":["evt-efxy52npt9s4"],"hash":"e36fb32a4f03907a7e8736a6409f82aef539e3987da18ebab1e361b1957b86b6"}
{"id":"evt-w9hb2my68r0b","type":"node.created","subject":"ver-b8c9754","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"Play opens a clip in its own window, reused for the next clip, with the camera and time over the video and a link fallback when popups are blocked; the page checks the session and refuses anything that isn't a clip on disk","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-20T10:38:37.807Z","parents":["evt-2ebzn1n59az4"],"hash":"0d38543b31237f16519fed02d1c2b53bea1c68b0742ca689283906094b6eba4e"}
{"id":"evt-vkgakfas0f53","type":"edge.added","subject":"ver-b8c9754","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-b8c9754","to":"iss-gq9faq4"},"at":"2026-09-20T10:38:37.810Z","parents":["evt-w9hb2my68r0b"],"hash":"ab36d9c45c1912884211a7c8d2f2a4733a889bd342a9c3a588a1c1e16904f078"}
{"id":"evt-nrrkd6tappeg","type":"verification.recorded","subject":"ver-b8c9754","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"src/app/recordings/play/page.tsx and clip-player.tsx, with openClipPopout/CLIP_WINDOW_NAME added to src/app/popout.ts (openPopoutWindow now shared with the camera pop-out). Tests: clip-list.test.tsx (window reused, default prevented, link fallback, nothing plays inline), play/page.test.tsx (access first, traversal and missing clips are not found, unnamed and unknown cameras, window title), clip-player.test.tsx (source, controls, overlay fade). Also fixed three test files that never cleaned up the DOM between renders, which is what made the new test fail depending on order. Full suite 955 tests pass, 99.66% lines; tsc, eslint and next build clean, which lists ƒ /recordings/play. Not yet seen in a browser."},"at":"2026-09-20T10:38:37.811Z","parents":["evt-vkgakfas0f53"],"hash":"532171a6aff75b5059af1ab2f06b8bd069c53c15ba117c308be5403b8530e3f0"}
{"id":"evt-sm4xsqhdqc2j","type":"node.status_changed","subject":"iss-gq9faq4","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"from":"open","to":"in_progress"},"at":"2026-09-20T10:38:38.921Z","parents":["evt-nrrkd6tappeg"],"hash":"24720443df5d9bca04ec60a36ad497b9664fb671488d46f1538cd340373cf0af"}
{"id":"evt-kfg5xdejm9e7","type":"node.created","subject":"ver-4v63yax","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"verification","title":"The clip pop-out ships in a version that can be published","body":"","status":"pending","owner":"prn-q80g8mz","attrs":{}},"at":"2026-09-20T10:47:04.520Z","parents":["evt-sm4xsqhdqc2j"],"hash":"7bbab7aca5415bcfd85391d6950562c8f39d26878ff227f0908569db39f695a2"}
{"id":"evt-jwkz9h3ws2ht","type":"edge.added","subject":"ver-4v63yax","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"kind":"evidence_for","from":"ver-4v63yax","to":"iss-gq9faq4"},"at":"2026-09-20T10:47:04.522Z","parents":["evt-kfg5xdejm9e7"],"hash":"4bda8a34d90deae146a6b1921b8e2c2309460c19d6cba748db1e62f6e3337c64"}
{"id":"evt-5jzp8zsscjr4","type":"verification.recorded","subject":"ver-4v63yax","actor":"prn-q80g8mz","actor_kind":"agent","session":null,"payload":{"result":"pass","evidence":"Version 0.1.4 in package.json; npm run build succeeds and npm pack --dry-run gives 4.9 MB across 1,382 files; 955 tests pass. Publishing and the browser check are the user's."},"at":"2026-09-20T10:47:04.523Z","parents":["evt-jwkz9h3ws2ht"],"hash":"7a588607197c53be77caab4bbf37e79397301a226bd9c73a6b2c5e487c41cc18"}