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`.
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user