Files
ducoterra d0077e0db4
Build and Push Containers / test (push) Successful in 45s
Build and Push Containers / build-and-push-app (push) Successful in 13s
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.
2026-11-03 23:50:43 -05:00

84 lines
3.9 KiB
Bash
Executable File

#!/bin/sh
# ~/.local/bin/gnome-kiosk-script — the mirror kiosk application.
#
# Installed by docs/kiosk-silverblue/install-kiosk.sh; see that runbook.
#
# `gnome-kiosk-script-session` runs this file as the kiosk user inside the
# kiosk session, and this script re-execs itself forever (the upstream example
# does the same). That has two consequences worth designing around:
#
# 1. Never launch unconditionally. The loop comes back every few seconds, so
# an unguarded launch opens a new window every few seconds.
# 2. Never exit. `org.gnome.Kiosk.Script.service` has
# `OnSuccess=gnome-session-shutdown.target`, so leaving this script logs
# the kiosk out and the screen goes back to the GDM greeter.
#
# So: poll, (re)launch only when firefox is gone, and keep the loop alive.
# Source the per-client config FIRST, then derive everything from it, so the
# payload file can stay byte-identical on every client and the kiosk.conf under
# ~/.config/mirror is the only thing that differs. Deriving before the source
# is a real trap: the script silently runs on the built-in default URL forever.
CONF="${MIRROR_KIOSK_CONF:-$HOME/.config/mirror/kiosk.conf}"
if [ -r "$CONF" ]; then
# shellcheck source=/dev/null
. "$CONF"
fi
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}"
log() {
[ -n "$LOG" ] || return 0
# Cap the log: this script runs forever on a machine nobody logs into.
if [ -f "$LOG" ] && [ "$(stat -c %s "$LOG" 2>/dev/null || echo 0)" -gt 1048576 ]; then
tail -c 65536 "$LOG" >"$LOG.tmp" 2>/dev/null && mv -f "$LOG.tmp" "$LOG" 2>/dev/null
fi
printf '%s %s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z' 2>/dev/null || echo -)" "$*" >>"$LOG" 2>/dev/null || :
}
# Keep the screen on. This has to happen here, inside the session, and not at
# install time: gsettings only commits when it can reach a D-Bus session bus,
# so `sudo -u kiosk gsettings set ...` over SSH warns "Cannot autolaunch D-Bus
# without X11" and silently leaves the default. It also cannot go in
# /etc/dconf/db/local.d, because /usr/bin/gnome-kiosk sets
# DCONF_PROFILE=gnomekiosk and that profile is `user-db:user` plus the shipped
# compiled defaults — it never reads a system db.
#
# $XDG_RUNTIME_DIR is per-login, so this runs once per kiosk start and still
# re-applies if someone changes the setting by hand.
GSETTINGS_STAMP="${XDG_RUNTIME_DIR:-/tmp}/mirror-kiosk-gsettings.done"
if [ ! -f "$GSETTINGS_STAMP" ]; then
gsettings set org.gnome.desktop.session idle-delay 0 >/dev/null 2>&1
gsettings set org.gnome.desktop.screensaver lock-enabled false >/dev/null 2>&1
gsettings set org.gnome.desktop.interface enable-animations false >/dev/null 2>&1
log "applied idle/lock gsettings"
: >"$GSETTINGS_STAMP" 2>/dev/null || :
fi
if pgrep -u "$(id -u)" -x firefox >/dev/null 2>&1; then
# Up already. Nothing to do but check again in a moment.
:
elif command -v curl >/dev/null 2>&1 && ! curl -fsSL -m 10 -o /dev/null "$URL" 2>/dev/null; then
# Don't park a "server not found" page on a wall display: keep polling and
# load the page the moment the mirror server answers again. This is what
# covers a reboot where the network has not come up yet.
log "target not reachable yet: $URL"
else
mkdir -p "$PROFILE" 2>/dev/null
log "launching: firefox --kiosk $URL"
# --kiosk : fullscreen, no chrome, no exit affordances
# --profile: our own dir, so the kiosk never collides with another profile
# lock and never inherits anyone's history
# `pgrep -x firefox` above can only be false here if a launch failed
# outright, so a foreground run cannot wedge the loop.
/usr/bin/firefox --kiosk --no-remote --profile "$PROFILE" "$URL" >>"$LOG" 2>&1
rc=$?
log "firefox exited rc=$rc"
fi
sleep "$POLL"
exec "$0" "$@"