phase: 09_wall_remote_page
Build and Push Containers / test (push) Successful in 45s
Build and Push Containers / build-and-push-app (push) Successful in 13s

**Phase 09 — final verification pass (all 3 tasks already in `complete/`; phase work was in the working tree, verified, nothing needed fixing)**

**Gates** — `uv run ruff check . && uv run ruff format --check .` → clean; `uv run pyright` → 0 errors; `uv run pytest` → **360 passed, 5 skipped** (only the realdisplay rung), coverage 96% total / 95% app.py; `MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay` → 4 passed, 1 skipped (documented host limitation: no audio source on the real X11 path); `uv run pytest tests/e2e/test_remote_control.py` → 8/8 passed; `node --check` on every `static/*.js` → all pass.

**Completion criteria** — all met: (1) "Show on wall" stages on the display, asserted on display DOM + `/api/kiosk` ✓; (2) Back/Fullscreen + untouched second controller converge (TestConvergence) ✓; (3) `/api/streams` reports `viewers: 0` / `watching: 0` with only `/remote` open (TestACostsNothing) ✓; (4) followers count displays only — 0/1/1 with a second controller (TestFollowers) ✓; (5) controller works with no display: `followers: 0`, state stored (protocol test + e2e) ✓; (6) e2e + full `-m e2e` + realdisplay all green ✓; (7) README + SECURITY.md updated, Silverblue default now `/kiosk` (installer, conf, gnome script, checker) ✓; (8) suite 100% green, coverage >90% on new code ✓; (9) PLAN.md untouched; AGENTS.md changed only for a stale statement ✓.

**Documentation** — README.md reviewed in full against code (page table, quickstart, env-var table vs `config.py`/`debug.py`, presets, keys, deployment, kiosk section): no inconsistencies found. AGENTS.md: fixed the one stale statement introduced by this phase ("Four pages" → "Five pages"); no other staleness, nothing added.

**Decisions** — no repo changes were required; the one realdisplay skip is a pre-documented environment fact, not a failure.

**Next pending phase** — none; `todo/` held only phase 09.
This commit is contained in:
2026-11-03 23:50:43 -05:00
parent cd29156a7d
commit d0077e0db4
36 changed files with 1354 additions and 43 deletions
+19 -16
View File
@@ -1,7 +1,7 @@
# Kiosk client — Fedora Silverblue
The target client for a wall display is **Fedora Silverblue running GNOME Kiosk,
auto-logged in as `kiosk`, showing `https://mirror.reeseapps.com/watch`
auto-logged in as `kiosk`, showing `https://mirror.reeseapps.com/kiosk`
fullscreen in Firefox.** One box takes about five minutes over SSH and needs no
keyboard, no monitor access, and no console.
@@ -56,7 +56,7 @@ be skipped when the packages are already layered. To point a client somewhere
other than the production wall, pass the URL:
```bash
sudo KIOSK_URL=https://mirror.reeseapps.com/watch sh install-kiosk.sh
sudo KIOSK_URL=https://mirror.reeseapps.com/kiosk sh install-kiosk.sh
```
Only `kiosk.conf` differs between clients; every other file stays byte-identical
@@ -87,17 +87,20 @@ for the moment a keyboard is plugged in (see [the key table](#operating-the-wall
and a keypress there performs the change *and* publishes the same `arrange`
intent, so a controller and a display converge on one state instead of
disagreeing (`m` is local-only — a display never becomes audible because
another machine asked). The **controller side** — a page a person on another
machine drives the wall from — arrives in phase 09 (`/remote`); until then a
controller is any client that can send an `arrange` frame on `/ws`, `websocat`
included. `GET /api/kiosk` is the state's read surface: what is on stage,
whether it is fullscreen, and how many displays (`followers`) are obeying it.
another machine asked). The **controller side** is `/remote`: a page a person
on another machine (a phone) opens to list the live mirrors, read the wall's
state, and send the decisions — show this, back to the wall, full screen. Any
client that can send an `arrange` frame on `/ws` is a controller too,
`websocat` included. `GET /api/kiosk` is the state's read surface: what is on
stage, whether it is fullscreen, and how many displays (`followers`) are
obeying it.
**The migration rule, stated honestly.** Existing clients keep their own
`MIRROR_KIOSK_URL`: an existing `kiosk.conf` URL wins over the built-in
default, and the installer's default still points at `/watch` in this phase.
So **nothing changes under a box until its `kiosk.conf` is re-pointed and
`gdm` restarted**:
`MIRROR_KIOSK_URL` until an operator re-points it and restarts `gdm`, because
the installer deliberately preserves a client's URL: an existing `kiosk.conf`
URL wins over the built-in default (which now points at `/kiosk`). So
**nothing changes under a box until its `kiosk.conf` is re-pointed and `gdm`
restarted**:
```bash
sudo sed -i 's|^MIRROR_KIOSK_URL=.*|MIRROR_KIOSK_URL="https://…/kiosk"|' \
@@ -107,10 +110,10 @@ sudo systemctl restart gdm
(`pkill -u kiosk firefox` is the shorter path — the loop re-sources
`kiosk.conf` on every relaunch, see [the section below](#the-two-facts-that-make-this-work) —
`gdm` is the clean restart when you want the whole session back.) The kiosk
page serves `<title>Wall · mirror`; `check-kiosk.sh`, untouched in this
phase, still greps for `<title>Watch · mirror`, and the checker is widened to
accept the kiosk title in phase 09, when the default URL moves with it.
`gdm` is the clean restart when you want the whole session back.) Both walls
are served by the same tree; `check-kiosk.sh` accepts both titles
(`<title>Watch · mirror</title>` and `<title>Wall · mirror</title>`) and
warns for anything else.
---
@@ -347,7 +350,7 @@ The console is not an interface. Every claim below is checkable over SSH.
| The page loaded | the **server's** `GET /api/streams` shows the kiosk in `viewers` and its socket in `ss -tn` |
| Media really flows | the sharer's own page renders each viewer's `RTCPeerConnection.connectionState`; `connected` = ICE+DTLS+SRTP up |
| It is truly fullscreen | `dim-probe.py`: `viewport == window == screen == available` |
| The URL is serving *mirror* | `check-kiosk.sh` greps the page for `<title>Watch · mirror` |
| The URL is serving *mirror* | `check-kiosk.sh` greps the page for the wall title — `<title>Watch · mirror` or `<title>Wall · mirror` |
| The kiosk holds the screen | `/sys/class/tty/tty0/active` equals the kiosk session's TTY and the session is `Active` — the one check that catches a covered wall |
| No greeter is fighting it | `journalctl -b \| grep -c "New session.*gdm-greeter"` is `0`; any non-zero number is a login screen on your wall |
| It survives a power cut | `sudo systemctl reboot`, then re-run `check-kiosk.sh` |