phase: 09_wall_remote_page
**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:
+19
-16
@@ -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` |
|
||||
|
||||
@@ -64,9 +64,9 @@ if [ -n "$HOME_DIR" ]; then
|
||||
"$HOME_DIR/.config/mirror/kiosk.conf" 2>/dev/null | head -1)
|
||||
ok "kiosk URL: ${URL:-<empty>}"
|
||||
case "$URL" in
|
||||
*/watch) : ;;
|
||||
*/watch|*/kiosk) : ;;
|
||||
"") bad "kiosk URL is empty" ;;
|
||||
*) warn "kiosk URL is not the /watch wall: $URL" ;;
|
||||
*) warn "kiosk URL is neither wall (/watch or /kiosk): $URL — check DNS and the nginx vhost" ;;
|
||||
esac
|
||||
else
|
||||
bad "kiosk config $HOME_DIR/.config/mirror/kiosk.conf is missing"
|
||||
@@ -213,8 +213,10 @@ if [ -n "$URL" ]; then
|
||||
CODE=$(curl -s -o /dev/null -w '%{http_code}' -m 8 "$URL" 2>/dev/null)
|
||||
if printf '%s' "$BODY" | grep -q "Watch · mirror"; then
|
||||
ok "$URL serves the mirror watch page (HTTP $CODE)"
|
||||
elif printf '%s' "$BODY" | grep -q "Wall · mirror"; then
|
||||
ok "$URL serves the mirror kiosk page (HTTP $CODE)"
|
||||
else
|
||||
warn "$URL answered HTTP $CODE but it is NOT the mirror watch page — the kiosk"
|
||||
warn "$URL answered HTTP $CODE but it is NOT a mirror wall — the kiosk"
|
||||
warn "will happily display whatever this is. Check DNS and the nginx vhost."
|
||||
info "title: $(printf '%s' "$BODY" | sed -n 's/.*<title>\(.*\)<\/title>.*/\1/p' | head -1)"
|
||||
fi
|
||||
|
||||
@@ -25,7 +25,7 @@ if [ -r "$CONF" ]; then
|
||||
. "$CONF"
|
||||
fi
|
||||
|
||||
URL="${MIRROR_KIOSK_URL:-https://mirror.reeseapps.com/watch}"
|
||||
URL="${MIRROR_KIOSK_URL:-https://mirror.reeseapps.com/kiosk}"
|
||||
PROFILE="${MIRROR_KIOSK_PROFILE:-$HOME/.mirror-kiosk}"
|
||||
LOG="${MIRROR_KIOSK_LOG:-${XDG_STATE_HOME:-$HOME/.local/state}/mirror-kiosk.log}"
|
||||
POLL="${MIRROR_KIOSK_POLL:-5}"
|
||||
|
||||
@@ -17,7 +17,10 @@ set -eu
|
||||
KIOSK_USER="${KIOSK_USER:-kiosk}"
|
||||
# The default is for a NEW client only. An existing client keeps its own URL
|
||||
# unless the operator names one — see "the URL is the one per-client value" below.
|
||||
KIOSK_URL_DEFAULT="https://mirror.reeseapps.com/watch"
|
||||
# The default is the DISPLAY endpoint: /kiosk obeys the shared wall state, so
|
||||
# a device with a keyboard (the /remote page) can arrange a box that has none.
|
||||
# /watch is the operator's page — a wall a person stands at.
|
||||
KIOSK_URL_DEFAULT="https://mirror.reeseapps.com/kiosk"
|
||||
KIOSK_URL="${KIOSK_URL:-}"
|
||||
KIOSK_SESSION="gnome-kiosk-script-wayland"
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
|
||||
@@ -7,9 +7,12 @@
|
||||
# After editing: `sudo systemctl restart gdm` (or log the kiosk user out) to
|
||||
# re-read it. The browser is restarted by the kiosk script's own loop.
|
||||
|
||||
# The wall of every live mirror. `/` is the Share/Watch chooser and needs a
|
||||
# tap to get anywhere, which is wrong for a display nobody is standing at.
|
||||
MIRROR_KIOSK_URL="https://mirror.reeseapps.com/watch"
|
||||
# The controllable wall. `/kiosk` is the display endpoint: it obeys the shared
|
||||
# layout state, so a device with a keyboard (the /remote page, or any
|
||||
# `arrange` frame on /ws) decides what is on stage and whether it is
|
||||
# fullscreen — a box that has no keyboard can still be arranged. `/watch` is
|
||||
# the operator's page: a wall a person stands at.
|
||||
MIRROR_KIOSK_URL="https://mirror.reeseapps.com/kiosk"
|
||||
|
||||
# Optional overrides (uncomment to change):
|
||||
|
||||
|
||||
Reference in New Issue
Block a user