Files
mirror/.agents/phases/complete/08_kiosk_page/00_phase.md
T
ducoterra cd29156a7d
Build and Push Containers / test (push) Successful in 42s
Build and Push Containers / build-and-push-app (push) Successful in 9s
phase: 08_kiosk_page
**Phase 08 final verification pass — complete, all green.**

**Verified (all 5 tasks already implemented):**
- `static/wall.js` (`createWall` + `installWallKeys`), `kiosk.html`/`kiosk.js`, `GET /kiosk` route — guards: no `$("#…")` in wall.js, one keydown listener, kiosk ids no-collision across 3 pages, no audio field in `arrange`, no header/share link in markup, address gate covers `/kiosk`
- Cross-page e2e: arrival-arrangement, keypress-on-one-display moves the other (asserted on the second display's decoded moving pixels), dead mirror → `data-layout="wall"` on both, `followers` count, pointer-free meta-test

**Gates (exact outcomes):**
- `uv run ruff check . && uv run ruff format --check .` — passed
- `uv run pyright` — 0 errors
- `uv run pytest` — 341 passed, 5 skipped (realdisplay-gated), 96s
- `MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay` — 4 passed, 1 skipped (documented: host has no audio source for real capture)
- Coverage 96% (>90%); only warning is a starlette/httpx third-party deprecation

**Completion criteria:** all 9 ✅ — e2e green incl. `test_kiosk_layout.py` (the one e2e diff is the phase-mandated address-gate extension to `/kiosk`; assertion logic untouched), full suite 100% passing, README documents `/kiosk`, PLAN.md untouched.

**Documentation:**
- README: fixed the Kiosk-client section that implied the display box is permanently pointed at `/watch` — now states the display page is `/kiosk` with the runbook's one-line re-point (installer default still `/watch` this phase, per docs). Rest of README verified against code (routes, config table, CLI flag, presets, key table) — consistent.
- AGENTS.md: fixed 2 stale statements — CDN test moved to `TestPages` (was `TestClientScripts`), suite runtime ~50s → ~95s. No content added.

**Notable:** PLAN.md §6 says "per-leg `maxBitrate` cap (default 12 Mbit/s)" but code/README default is 25 Mbit/s — pre-existing, left untouched (locked file; flagged for a user decision).

**Next pending phase:** `09_wall_remote_page`.
2026-11-03 22:38:22 -05:00

7.2 KiB
Raw Blame History

Phase 08 — The /kiosk page: a wall that arranges itself

Source: TODO.md L3 — "Add a /kiosk endpoint … so I can just navigate to that endpoint on the kiosk and it will automatically arrange itself to the client's preferences." (the display-client half of that item) Story: n/a Context: .agents/PLAN.md §2 A9 (hand-written ES modules, no framework/bundler), A10 (nothing from outside the origin), A13 (Playwright, channel="chromium"); AGENTS.md §6 (reuse net.js rather than duplicating socket logic; ids must not collide across pages); phase 01 (01_keyboard_kiosk_control — the keyboard router, the stage mechanics, the mouseless rule); phase 07_kiosk_layout_state (the arrange/layout contract, hello.kiosk, GET /api/kiosk); src/mirror/static/watch.js, watch.html, app.css, net.js; docs/kiosk-silverblue.md (the target box: Firefox --kiosk, no keyboard, no mouse, no console)

Objective

GET /kiosk: a chrome-free wall for an unattended display that subscribes to every mirror like /watch does and then does what the server state says — stages the mirror the state names, fullscreen or not — on arrival and on every change, with no input from anyone standing at the box.

The phase is deliberately two halves:

  1. Make the wall reusable (tasks 01–02). /kiosk must not fork 400 lines of tile/WebRTC/stage code (AGENTS.md §6: reuse, don't duplicate), so the wall's core moves out of watch.js into static/wall.js and both pages consume it. Those two tasks change no behaviour — phases 01–06's suites are the gate.
  2. Build the page on it (tasks 03–05): the route, the markup, the layout applier, and the browser proof.

Rules this page lives by

  • Arrival is arrangement. welcome.layout is applied before the first roster tile finishes painting. A person who points a kiosk at /kiosk must not have to touch it.
  • The state is the boss, locally too. A keypress on the kiosk performs the action and publishes it (arrange), so the controller and the display cannot disagree — same as every other shared state: one writer path, everyone re-reads.
  • Fullscreen is best-effort, the stage is the guarantee. A remote command carries no user activation, so requestFullscreen() may be refused; the in-page stage fills the viewport anyway and the Silverblue window manager already forces the window fullscreen (docs/kiosk-silverblue/window-config.ini). The page asks and never blocks on the answer — exactly the stance phase 01 documented for f.
  • Never invent state. No auto-staging of the newest mirror, no remember-last across a server restart. An empty state means the wall.
  • No audio commands. The layout carries no sound field (phase 07, SECURITY.md §4); a display never becomes audible because a stranger asked.
  • The chrome stays off the screen. No header bar, no "Share a screen" button, no counts in a title bar — a display shows mirrors. The three shared ids (net, notice, toast) are the only elements /kiosk may share with another page, and #net stays because the connection state is the one thing that must be legible in a screenshot of a wall nobody is looking at.

Dependencies

  • 07_kiosk_layout_state (todo) — the arrange/layout frames, hello.kiosk, welcome.layout, the followers count, GET /api/kiosk. Task 01 of this phase has no dependency on it and may run first, but the phase is not complete until 07 exists.
  • 01_keyboard_kiosk_control (complete) — the router, stage()/unstage(), the mouseless rule and the pointer-free suite this phase extracts and extends.
  • 06_sharer_quality_presets (complete) — the current behaviour of the wall.

Tasks

  1. 01_extract_wall_tiles_and_stage.md — the tile/peer/stage/mute core of watch.js becomes static/wall.js (createWall()); app.css re-keys the stage rules from #watch-stage to a .stage class. No behaviour change.
  2. 02_extract_wall_key_router.md — the single keydown switch becomes installWallKeys() in wall.js, dispatching named commands to the page; /watch's key map is byte-identical in effect.
  3. 03_kiosk_page_and_route.md — static/kiosk.html, GET /kiosk in app.py (and in PAGES for no-store), and every structural guard in test_app.py extended to a third page.
  4. 04_kiosk_layout_applier.md — static/kiosk.js: hello{kiosk:true}, the wall from createWall(), applyLayout() on welcome/layout, and local keys published back with arrange.
  5. 05_kiosk_layout_e2e_and_docs.md — tests/e2e/test_kiosk_layout.py: a real sharer, two kiosk displays (isolated contexts, hence separate sockets and peer ids), and the assertion that one display's keypress re-arranges the other, proved on the second display's decoded pixels; plus the /kiosk docs.

Testing & Quality

  • Refactor gate (tasks 01–02): tests/e2e/test_keyboard_wall.py, test_live_mirror.py, test_audio_share.py, test_stage_autofade.py, test_share_layout.py pass unedited. A changed assertion in one of them during a "behaviour-preserving" extraction is a bug report, not a fix.
  • Structure gate (task 03): test_app.py's guards extended — page renders, no-store, node --check, every $("#id") exists, ids do not collide across three pages, <video> is muted playsinline, nothing from a CDN, the address gate refuses /kiosk like the rest of the app. A new guard: static/wall.js contains no $("#…") — one module serving two pages takes its elements as arguments or it will look them up in the wrong document.
  • Behaviour gate (tasks 04–05): a dedicated Playwright suite, pointer-free like phase 01's (a mouse on the kiosk would defeat the point), asserting on the second display's <video> (videoWidth, distinct frames via the conftest frames_matching() helper) and on data-layout/data-fullscreen — never on whether a handler ran. Fullscreen is asserted on the stage class, not on document.fullscreenElement, which the browser may legitimately refuse.
  • Coverage: >90% on new/modified code. The Python surface added here is one route; the JS is covered by browser assertions, per AGENTS.md §4.

Completion Criteria

  • uv run pytest -m e2e green, including the new test_kiosk_layout.py, with no existing e2e assertion edited
  • pointing a browser at /kiosk and sharing a mirror from a second browser process shows that mirror on the wall with no interaction on the first
  • a keypress on one /kiosk page re-arranges a second /kiosk page (the shared-state property, asserted cross-page, not in one page's DOM)
  • the staged mirror going away returns both displays to the wall (data-layout="wall")
  • /kiosk renders no header, no share link, and refuses a blocked address with the same explanation page as /watch
  • uv run pytest tests/test_app.py green with the guards extended to three pages, and wall.js proven free of document-scoped lookups
  • all four gates from AGENTS.md §4 green, MIRROR_REALDISPLAY=1 -m realdisplay included
  • full test suite 100% passing, coverage >90% on new code
  • README.md documents /kiosk; PLAN.md untouched