phase: 08_kiosk_page
Build and Push Containers / test (push) Successful in 42s
Build and Push Containers / build-and-push-app (push) Successful in 9s

**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:
2026-11-03 22:38:22 -05:00
parent 736da1c1ef
commit cd29156a7d
45 changed files with 2389 additions and 750 deletions
@@ -0,0 +1,22 @@
**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`.
@@ -0,0 +1,36 @@
........................................................................ [ 20%]
......................................sssss............................. [ 41%]
........................................................................ [ 62%]
........................................................................ [ 83%]
.......................................................... [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.15/site-packages/fastapi/testclient.py:1
/home/ducoterra/Projects/Personal/mirror/.venv/lib64/python3.15/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.15.0-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------
src/mirror/__init__.py 3 0 100%
src/mirror/__main__.py 72 3 96% 129, 145, 156
src/mirror/access.py 60 0 100%
src/mirror/app.py 194 10 95% 108, 113, 117, 182-183, 212-216
src/mirror/config.py 68 5 93% 110-111, 118-120
src/mirror/debug.py 15 10 33% 19-28
src/mirror/protocol.py 66 0 100%
src/mirror/registry.py 228 3 99% 208, 226, 460
------------------------------------------------------
TOTAL 706 31 96%
=========================== short test summary info ============================
SKIPPED [1] tests/e2e/test_x11_capture.py:281: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:321: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:364: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:446: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:545: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
341 passed, 5 skipped, 1 warning in 96.44s (0:01:36)
All checks passed!
33 files already formatted
0 errors, 0 warnings, 0 informations
@@ -0,0 +1,16 @@
Task 01 complete — all gates green.
**Implemented**
- New `src/mirror/static/wall.js`: `createWall({grid, stageEl, template, send, rtc, onChange, stats})` owning tiles, peers, stage, selection, mute policy, autofade, fullscreen-exit race, stats ticker, `closePeers()`; returns the task's surface plus `selectedTile()`, `revealStageControls()`, `handleFullscreenChange()`. Zero document queries; socket via page's `send` (guarded on `state.ws` in the page closure, so the wall builds before the socket opens).
- `watch.js` now a thin page: socket, `renderCounts` as the `onChange` callback, `fullscreenchange`/pointer/key bindings, help, refusal, share link. Dead `ui.unstage` branch deleted with a comment pointing at the per-tile `.exit` button.
- `app.css`: `#watch-stage` → `.stage` (all 10 rules, comments updated to class-scoped/source-order rationale); `watch.html` stage gains `class="stage"`.
- **Collision found & fixed:** the share page already used `class="stage"` — the literal re-key would have made `#preview-stage` inherit `position: fixed; inset: 0; display: none` (same specificity, later rule wins). Renamed the share preview to `.preview-stage` (matching its id) so `.stage` is wall-only; `test_share_layout`/`test_live_mirror` pass unedited, proving the share page is unchanged.
- `test_app.py`: `wall.js` in parse/asset lists; new `test_wall_never_looks_up_elements_in_the_document` (cites the id-collision test as its reason); `test_the_two_pages_use_the_media_apis_they_claim` re-pointed at `wall.js` (RTCPeerConnection moved); the two TestStylesheet tests mechanically re-pointed to `.preview-stage`.
**Gates** — `ruff check` + `format --check` clean; `pyright` 0 errors; targeted e2e (`-k "keyboard or live_mirror or audio or stage or share_layout"`) 92 passed **unedited**; full e2e 103 passed; full `uv run pytest` 322 passed / 5 skipped (realdisplay-marked); `MIRROR_REALDISPLAY=1 pytest -m realdisplay` 4 passed / 1 platform-documented skip. No e2e file touched.
**Docs** — no doc impact (pure refactor; README/AGENTS.md state nothing this changed).
**Decisions** — `.preview-stage` rename (above); `rtc` added to the wall options (peers moved into the wall, config stays page-owned); redundant `renderCounts()` in the `welcome` case dropped (per-`absorb` `onChange` covers it, identical final state).
**Next pending task** — `02_extract_wall_key_router.md` (phase 08).
@@ -0,0 +1,36 @@
........................................................................ [ 22%]
...............................sssss.................................... [ 44%]
........................................................................ [ 66%]
........................................................................ [ 88%]
....................................... [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/home/ducoterra/Projects/Personal/mirror/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.8-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------
src/mirror/__init__.py 3 0 100%
src/mirror/__main__.py 72 3 96% 129, 145, 156
src/mirror/access.py 60 0 100%
src/mirror/app.py 191 11 94% 108, 113, 116-117, 182-183, 212-216
src/mirror/config.py 68 5 93% 110-111, 118-120
src/mirror/debug.py 15 10 33% 19-28
src/mirror/protocol.py 66 0 100%
src/mirror/registry.py 228 3 99% 208, 226, 460
------------------------------------------------------
TOTAL 703 32 95%
=========================== short test summary info ============================
SKIPPED [1] tests/e2e/test_x11_capture.py:281: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:321: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:364: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:446: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:545: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
322 passed, 5 skipped, 1 warning in 98.55s (0:01:38)
All checks passed!
32 files already formatted
0 errors, 0 warnings, 0 informations
@@ -0,0 +1,23 @@
All done. Final state verified — here's the report:
## Task 02 report — `installWallKeys()` in `wall.js`
**Implemented**
- `wall.js`: new `installWallKeys({ wall, onCommand, root? })` — the one `keydown` listener: case-insensitive key→command switch (`next`, `previous`, `stage`, `fullscreen`, `wall`, `step_next`, `step_previous`, `mute`, `jump` w/ 0-based `index`, `help`, `escape`), chord/text-field bail-outs, staged-reveal before dispatch (never preventDefaults), and `preventDefault()` on exactly the keys that changed something. Wall commands carry module defaults (moved verbatim from `watch.js`, with their "why" comments); `help`/`escape` have no default (page policy).
- `wall.js`: `createWall` surface gains `reRequestFullscreen()` so the `f`-when-staged default can re-request on the stage element the wall owns (same element `/watch` used via `ui.stage`).
- `watch.js`: deleted `onWallKey`, `stageSelectedOrFirst`, `stepStage`, `toggleHelp` and the `document.addEventListener("keydown", …)` line; now `installWallKeys({ wall, onCommand })` with `onCommand` answering only `help` (toggle `#wall-help`, always handled) and `escape` (overlay first, then `wall.unstage()`, handled iff either happened) — the `?` focus comment moved with the behaviour.
- `test_app.py`: new guard — `installWallKeys` defined in `wall.js`, `wall.js` holds exactly one keydown listener, and no wall-page module (watch.js, future kiosk/remote) adds its own.
**Results (all gates)**
- `uv run ruff check . && uv run ruff format --check .` → clean
- `uv run pyright` → 0 errors
- `uv run pytest tests/e2e/test_keyboard_wall.py` **unedited** → 35/35 passed
- `uv run pytest -m e2e` → 103 passed, 5 skipped (realdisplay-gated)
- `MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay` → 4 passed, 1 skipped (no audio source on host — pre-existing env skip)
- `uv run pytest` (full) → **323 passed, 5 skipped**, 0 failed; coverage ~95% (JS router covered by browser assertions, per AGENTS.md §4)
**Docs:** no doc impact (pure refactor, no user-visible change; README/AGENTS.md unaffected).
**Notable decision:** `grep 'addEventListener("keydown"' static/*.js` shows the one `wall.js` hit *plus* `share.js`'s pre-existing `/mirror` s/x/Esc listener (share-page policy, not a wall page — untouched, out of scope); the test guard encodes the "one listener per wall page" rule exactly. `fullscreen` got a module default (not listed in the task's default list, but required for item 5's thin-observer `/watch` and for `/kiosk` to publish while the module performs).
**Next pending task:** `08_kiosk_page/03_kiosk_page_and_route.md`
@@ -0,0 +1,36 @@
........................................................................ [ 21%]
...............................sssss.................................... [ 43%]
........................................................................ [ 65%]
........................................................................ [ 87%]
........................................ [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/home/ducoterra/Projects/Personal/mirror/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.8-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------
src/mirror/__init__.py 3 0 100%
src/mirror/__main__.py 72 3 96% 129, 145, 156
src/mirror/access.py 60 0 100%
src/mirror/app.py 191 11 94% 108, 113, 116-117, 182-183, 212-216
src/mirror/config.py 68 5 93% 110-111, 118-120
src/mirror/debug.py 15 10 33% 19-28
src/mirror/protocol.py 66 0 100%
src/mirror/registry.py 228 3 99% 208, 226, 460
------------------------------------------------------
TOTAL 703 32 95%
=========================== short test summary info ============================
SKIPPED [1] tests/e2e/test_x11_capture.py:281: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:321: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:364: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:446: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:545: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
323 passed, 5 skipped, 1 warning in 92.90s (0:01:32)
All checks passed!
32 files already formatted
0 errors, 0 warnings, 0 informations
@@ -0,0 +1,19 @@
All done. Final state is 100% green across all four gates.
**Report — phase 08, task 03 (`GET /kiosk` + page skeleton)**
**Implemented**
- `GET /kiosk` route in `app.py` (beside `/watch`), `/kiosk` added to `PAGES` (no-store); fixed the stale "Two pages" module docstring
- `static/kiosk.html`: chrome-free wall — `body.kiosk[data-layout="wall"][data-fullscreen="false"]`, `#kiosk-grid`, `#kiosk-empty` + `#kiosk-share-url`, `#kiosk-stage` with mandatory `class="stage"`, `#kiosk-hud`, `#kiosk-tile-template`, shared `net`/`notice`/`toast` only, no header, `<title>Wall · mirror</title>` (the check-kiosk.sh contract)
- Placeholder `static/kiosk.js` (serves, fills the share URL, shows refusal notice; wall/layout are task 04's)
- `app.css`: `.kiosk .grid` full-bleed, `.kiosk .net` fixed top-right (z 60, above stage), `.hud` fixed bottom-left (mono, dim, pointer-events none)
- `index.html`: third card "GET /kiosk — An unattended wall"
- `test_app.py` guards extended to the third page (render, no-store×5, asset, `node --check`, element-reaches-for, stylesheet+module — renamed, video muted/playsinline, id-collision incl. `kiosk` with `#kiosk-*` prefix, new stage-class + structural-element guard, address-gate refused + admitted 200); `test_live_mirror.py` refused-address list + `#kiosk-grid`/`#kiosk-stage` selectors
**Gates** — `ruff check` + `ruff format --check` ✅ · `pyright` 0 errors ✅ · `uv run pytest` **334 passed**, 5 skipped (realdisplay marker) ✅ · `MIRROR_REALDISPLAY=1 pytest -m realdisplay` 4 passed, 1 skipped (host has no audio source — the test's own environmental skip, pre-existing) ✅ · curl: `GET /kiosk` 200 + `no-store`; browser check: empty-state wall, share URL filled, no header, net pill fixed top-right, zero console errors
**Docs** — README: `/kiosk` in the page table, quickstart, and the shared-state paragraph (replaced "arrives in a later release"); AGENTS.md: "Three pages" → "Four pages" (stale count)
**Decisions** — placeholder also imports `showNotice` (the only display path in `net.js`; "show a refusal notice" is impossible with only the two named imports); `kiosk ∩ index` id assertion is `<= shared` (index.html has no `notice`/`toast` by structure — same shape as the existing index assertion); id-guard test kept its original name (AGENTS.md and `wall.js` reference it)
**Next task** — `08_kiosk_page/04_kiosk_layout_applier.md`
@@ -0,0 +1,36 @@
........................................................................ [ 21%]
................................sssss................................... [ 42%]
........................................................................ [ 63%]
........................................................................ [ 84%]
................................................... [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1
/home/ducoterra/Projects/Personal/mirror/.venv/lib64/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.8-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------
src/mirror/__init__.py 3 0 100%
src/mirror/__main__.py 72 3 96% 129, 145, 156
src/mirror/access.py 60 0 100%
src/mirror/app.py 194 11 94% 108, 113, 116-117, 182-183, 212-216
src/mirror/config.py 68 5 93% 110-111, 118-120
src/mirror/debug.py 15 10 33% 19-28
src/mirror/protocol.py 66 0 100%
src/mirror/registry.py 228 3 99% 208, 226, 460
------------------------------------------------------
TOTAL 706 32 95%
=========================== short test summary info ============================
SKIPPED [1] tests/e2e/test_x11_capture.py:281: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:321: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:364: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:446: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:545: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
334 passed, 5 skipped, 1 warning in 98.11s (0:01:38)
All checks passed!
32 files already formatted
0 errors, 0 warnings, 0 informations
@@ -0,0 +1,11 @@
All done. Final report:
**Phase 08, Task 04 — `kiosk.js`: obey the layout, publish the keystrokes — complete**
- Verified the prior partial run's `kiosk.js` against the task spec (module shape, `hello{kiosk:true}`, idempotent `applyLayout()`, remembered-intent retry, best-effort never-awaited fullscreen with the required comment, body `data-layout`/`data-fullscreen` + title mirroring, per-command `arrange` publishing, `m` local-only with the SECURITY.md §4 comment, no `fullscreenchange` policy) — spec-compliant; fixed one broken indentation block in the `onMessage` switch
- Added the missing `test_app.py` source-level guard: `test_kiosk_never_publishes_an_audio_field` — brace-matches every `type:"arrange"` object literal in `kiosk.js` (member-order independent), asserts none carries `muted:`/`audio:`, and asserts at least one `arrange` frame exists; negative controls verified to be caught
- Verified every completion criterion with a throwaway real-browser/real-server script (deleted after): arrive-after-arrange shows the staged mirror with no wall flash (grid never paints a tile) and moving pixels on the stage; raw-socket (websocat) `arrange` re-arranges an open tab; `Enter`/`g`/`n`/`p`/`1`/`f` change `GET /api/kiosk`; `j`/`k` publish nothing; `m` flips `video.muted` with `/api/kiosk` byte-identical; a second kiosk display follows the first and teardown returns both to the wall; `followers` counts both kiosks only; zero page errors
- Gates: `uv run ruff check . && uv run ruff format --check .` → all checks passed, 32 files formatted; `uv run pyright` → 0 errors; `uv run pytest` → 335 passed, 5 skipped (realdisplay-gated), 96% coverage; `MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay` → 4 passed, 1 skipped (documented host audio-source limitation)
- README.md: one sentence added — kiosk keypresses publish the same `arrange` intent (controller/display converge) and `m` is local-only; no AGENTS.md changes needed for this task (the "four pages" edit was task 03's and is correct)
- Deviation note: task 04's kiosk.js was already written by the aborted prior run; my work was full spec verification, the indentation fix, and the missing guard — no behavior code was needed
- Next pending task: `.agents/phases/todo/08_kiosk_page/05_kiosk_layout_e2e_and_docs.md` (the permanent `test_kiosk_layout.py` suite + docs)
@@ -0,0 +1,36 @@
........................................................................ [ 21%]
................................sssss................................... [ 42%]
........................................................................ [ 63%]
........................................................................ [ 84%]
.................................................... [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.15/site-packages/fastapi/testclient.py:1
/home/ducoterra/Projects/Personal/mirror/.venv/lib64/python3.15/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.15.0-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------
src/mirror/__init__.py 3 0 100%
src/mirror/__main__.py 72 3 96% 129, 145, 156
src/mirror/access.py 60 0 100%
src/mirror/app.py 194 10 95% 108, 113, 117, 182-183, 212-216
src/mirror/config.py 68 5 93% 110-111, 118-120
src/mirror/debug.py 15 10 33% 19-28
src/mirror/protocol.py 66 0 100%
src/mirror/registry.py 228 3 99% 208, 226, 460
------------------------------------------------------
TOTAL 706 31 96%
=========================== short test summary info ============================
SKIPPED [1] tests/e2e/test_x11_capture.py:281: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:321: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:364: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:446: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:545: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
335 passed, 5 skipped, 1 warning in 92.04s (0:01:32)
All checks passed!
32 files already formatted
0 errors, 0 warnings, 0 informations
@@ -0,0 +1,21 @@
Everything is in place and verified. All gates were run after the final code/doc state (the last edit was a cosmetic link-label fix in the docs).
## Report
**Implemented**
- `tests/e2e/test_kiosk_layout.py`: 5 behavioural tests + meta-guard — arrival-is-arrangement (driver `arrange` over a raw `page.evaluate` WebSocket *before* `/kiosk` opens; asserts `body[data-layout="stage"]`, `data-fullscreen="true"`, staged tile, decoded moving frames on `#kiosk-stage video`), one keypress on one display re-arranging a second pointer-free display (asserted on the second's decoded pixels + `/api/kiosk`), `n`/`p` stepping with wrap, teardown returning both displays to `data-layout="wall"` with no dead tiles, and `followers` counting displays only over a real socket lifecycle. Established `device()` shape (isolated contexts, finalizers, `#net` online wait, pageerror+console from the first line); the self-guard (same style as `test_keyboard_wall.py`, literals built from parts) bans pointer calls and explicitly allows the wire-level driver; the `document.fullscreenElement` non-assertion is commented as required.
- `docs/kiosk-silverblue.md`: new "Two endpoints: `/watch` and `/kiosk`" section (operator vs. display, keys still work on `/kiosk`, controller arrives in phase 09 `/remote`, honest `MIRROR_KIOSK_URL` migration rule with re-point + `gdm` restart; no changes to `install-kiosk.sh`/`kiosk.conf`/`gnome-kiosk-script`/`check-kiosk.sh`).
- `README.md`: one sentence in the Keys section (wall keys work on `/kiosk`; a change made there is what every other display follows). The `/kiosk` page-table row already existed from task 03.
**Gates (all green)**
- `uv run ruff check . && uv run ruff format --check .` — clean; `uv run pyright` — 0 errors
- `uv run pytest tests/e2e/test_kiosk_layout.py -x` — 6/6 passed
- `uv run pytest -m e2e` — 110 passed, 5 skipped (realdisplay-only)
- `MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay` — 4 passed, 1 skipped (documented host-has-no-audio-source)
- `uv run pytest` — 341 passed, 5 skipped, 0 failed; coverage 95% (unchanged; task adds no product code)
**Docs** — `docs/kiosk-silverblue.md` + `README.md` updated as above; `AGENTS.md` no stale facts → untouched.
**Decisions** — driver closes its socket after the server's `layout` ack (state is process-level, so no `window` global / no leaked peer); mid-test context close in the followers test is guarded so the finalizer can't double-close.
**Next pending task** — none in `todo/` (phase 08 is complete; phase 09 `/remote` is not yet authored).
@@ -0,0 +1,36 @@
........................................................................ [ 20%]
......................................sssss............................. [ 41%]
........................................................................ [ 62%]
........................................................................ [ 83%]
.......................................................... [100%]
=============================== warnings summary ===============================
.venv/lib64/python3.15/site-packages/fastapi/testclient.py:1
/home/ducoterra/Projects/Personal/mirror/.venv/lib64/python3.15/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.15.0-final-0 ________________
Name Stmts Miss Cover Missing
------------------------------------------------------
src/mirror/__init__.py 3 0 100%
src/mirror/__main__.py 72 3 96% 129, 145, 156
src/mirror/access.py 60 0 100%
src/mirror/app.py 194 11 94% 108, 113, 116-117, 182-183, 212-216
src/mirror/config.py 68 5 93% 110-111, 118-120
src/mirror/debug.py 15 10 33% 19-28
src/mirror/protocol.py 66 0 100%
src/mirror/registry.py 228 3 99% 208, 226, 460
------------------------------------------------------
TOTAL 706 32 95%
=========================== short test summary info ============================
SKIPPED [1] tests/e2e/test_x11_capture.py:281: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:321: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:364: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:446: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
SKIPPED [1] tests/e2e/test_x11_capture.py:545: set MIRROR_REALDISPLAY=1 to exercise the real X11 capture backend
341 passed, 5 skipped, 1 warning in 96.45s (0:01:36)
All checks passed!
33 files already formatted
0 errors, 0 warnings, 0 informations