**Note** from Bead: Firewatch Camera Grid · [canonical source](https://redfish.acequia.io/guerin/.agents/65783732-7907-4a36-983a-3b015e75e80b/2026-06-24/notes/05-grid-layout-v11-sketch.md) · session 2026-06-24 · discussion: Talk: Firewatch Camera Grid
Sketched by Debbie (senior UX/UI). **v11 reworks v10's submit/identity and adds a live polling dashboard — and reuses the [v10 engine](04-grid-layout-v10-sketch.md) wholesale** (same single-scene `lerp(mapPos, gridPos, t)` morph, position-preserving placement, MAP ◀▶ GRID scrubber, onboarding overlay, 2D/3D drag-pan — all intact). Two **coupled** changes from Stephen. **Rendered sketch:** [https://redfish.acequia.io/guerin/.agents/65783732-7907-4a36-983a-3b015e75e80b/2026-06-24/artifacts/sketches/grid-layout-transitions-v11.html](https://redfish.acequia.io/guerin/.agents/65783732-7907-4a36-983a-3b015e75e80b/2026-06-24/artifacts/sketches/grid-layout-transitions-v11.html) *(top bar reads `v11` + `build 2026-06-24` — freshness marker, house convention.)*
## The two coupled changes ### (1) Decentralize the protocol — no background process, no drainer v10 PUT a `sketch-answer` "request" (`.md` + `.meta.json`) to the bead's `request/` dock, expecting a drainer to correlate it through `response/`. **There is no drainer.** v11 drops the `request/`+`response/` correlation plumbing entirely. Instead: - **Each page writes its vote as its own JSON file** into a shared **ballot box** — the `votes/` dir in the bead. One file per voter: `grid-layout-v11__<voterKey>.json`, where `voterKey` = the sanitized acequia `userId` (`[a-z0-9_-]`). **Last-write-wins** — re-voting overwrites the voter's own file, so tallies stay clean. - **The dashboard (in the page) becomes the processor.** It reads the ballot box and re-tallies — there is no server-side step. It **polls every 5 seconds** (`setInterval`, plus a manual ↻), tracks seen files by name + last-modified (only GETs new/changed files), and **dedups by `voter.userId`**. A live status line reads `live · polling every 5s · updated HH:MM:SS` (pauses with the tab / when the panel closes). - The `votes/` dir carries `.acequia-access.json` `{read:anonymous,recursive:true}`, so **any teammate's dashboard reads the box with no auth** — the dashboard's read path works anonymously. Public ballot box: [https://redfish.acequia.io/guerin/.agents/65783732-7907-4a36-983a-3b015e75e80b/votes/](https://redfish.acequia.io/guerin/.agents/65783732-7907-4a36-983a-3b015e75e80b/votes/) ### (2) Fix auth + identity — use the acequia API, not a cookie Stephen tested v10 in incognito and hit the 401 modal. **Diagnosis (from his DevTools):** acequia.js does **not** set an ambient *cookie* that authorizes a WebDAV write. The identity + keys live in **IndexedDB** (`acequia` DB → `keyvaluepairs`: `deviceId`, `deviceName` e.g. `"moss-lark"`, `userId`, `userKeys` CryptoKey pair); tokens live in `acequia-peer-tokens`. So v10's `credentials:'include'` carried nothing usable → 401. v11 uses the **acequia API**: - **Readiness:** `await acequia.acequiaReady()` before reading identity or writing (defensive: it may be a function returning a promise, or a promise — guarded with `typeof`; never blocks the UI). - **Identity (what Stephen wants recorded):** `getDeviceName()` ("moss-lark"), `getUserId()` (the stable dedup key), `getDeviceId()`. `tokenId` = `tok:<shortHash(userId)>` (a sha-256 fingerprint over the userId — **never a raw token**). If acequia isn't ready, a stable **local** pseudonymous id keeps the page working. - **The write (the fix):** a **pre-authenticated** WebDAV client — ```js const client = await acequia.webdav.createAcequiaClient(); await client.putFileContents(votePath, json, { overwrite: true, contentType: 'application/json' }); ``` Defensive fallback if `createAcequiaClient` is absent: mint a token (`acequia.tokens.createUserToken({}, 3600)`) and `PUT` with `Authorization: Bearer`. ### Submit flow (`sendAnswer` → `submitVote`) 1. `await acequiaReady()`; build the vote object with the acequia identity. 2. Write it via the authenticated client (with the minted-token fallback). 3. **Token modal only on an auth error (401/403)** — paste a bearer, persist it, retry with `Authorization: Bearer`. **Network/5xx → friendly error + retry, no modal.** Most voters never see the modal. 4. On success: "vote recorded as <name>", open the dashboard, refresh.
## Vote JSON schema (NEVER includes a raw token) ```json { "sketch":"grid-layout-v11", "choice":"aspect-follows-layout|fixed-3col", "comment":"…", "voter":{ "name":"moss-lark", "userId":"…", "deviceId":"…", "tokenId":"tok:<shortHash(userId)>", "source":"acequia|local" }, "ts":"<iso8601>", "version":"v11" } ``` Plus a small non-secret `payload` (fork, crossings, effective cam ids, t) for team context — no token. The raw bearer rides **only** the `Authorization` header on the write/retry; it is never in the file. The dashboard shows the `tokenId` (transparent), never the token.
## The fork (unchanged) — grid shape - **A — aspect-follows-layout (FIRST-CUT).** rows × cols match the cameras' spatial bounding-box aspect. - **B — fixed (≤3 cols, reading order).** Predictable, dense. The live fork keys (`aspect-follows` / `fixed-cols`) map to the schema's choice values (`aspect-follows-layout` / `fixed-3col`); the tally normalizes both so any legacy file still counts.
## Kept from v8–v10 (not regressed) Single-scene `lerp(mapPos,gridPos,t)` morph (zero create/destroy); position-preserving placement (NAIVE↔POSITION, crossings → 0); dim-ghost basemap; straight-lerp; MAP ◀▶ GRID scrubber; viewport-implicit selection + live filtering; 2D/3D drag-pan; per-card un-pin/re-flow; OrientedMap glyphs + tie-point ray; 3D oblique; TIME cursor; identity color; onboarding overlay + Buxton link; "Redfish Team" label. The 180° axis stays removed. Only the old `request/`+`response/` `sketch-answer` plumbing was removed (replaced by the ballot box). `node --check` passes on the extracted inline script.
## Onboarding copy (updated) "Your vote is saved as its own file in a shared **ballot box**; everyone's dashboard tallies the box live (polls every 5s) — there's no server-side step. Anonymous/incognito works (acequia gives you an identity automatically)." Buxton link kept.
## Status First-cut, team-shareable. **Decentralized:** one JSON file per voter in the `votes/` ballot box; the in-page dashboard is the processor, polling every 5s and deduping by `userId`. **Auth fixed:** writes use `acequia.webdav.createAcequiaClient` (minted-token fallback); identity is read from the acequia API (IndexedDB-backed `getUserId`/`getDeviceName`/`getDeviceId`) — not a cookie. No raw token in any vote file. Awaiting the team's grid-shape fork choice in the ballot box.