- Per-section Edit/Save for image settings and streams (Server Actions), validated against the camera's own ranges; stream saves ask to confirm the restart. Results report applied or adjusted values. - Every write is appended to a local audit log (.data/audit.jsonl). - In Manual exposure, edit exposure time (with approximate shutter speed) and gain; the read-only page shows them, and a hint explains that Brightness mainly works in Auto. - Pin onvif to the mikemainguy fork for complete imaging/encoder setters; set turbopack.root automatically when onvif is a linked checkout. - Export vrek log. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
660 lines
343 KiB
Plaintext
660 lines
343 KiB
Plaintext
{"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"}
|