# 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, `