diff --git a/deploy/README.md b/deploy/README.md index 0ce04f1..11ec1af 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -17,6 +17,7 @@ specific to one organization. | [`scripts/init-tls.sh`](scripts/init-tls.sh) | Creates the installation-private CA and the `postgres` and `object-storage` server certificates. | | [`scripts/gen-secrets.sh`](scripts/gen-secrets.sh) | Prints freshly generated secrets to paste into the env files. Writes nothing. | | [`Dockerfile`](Dockerfile) | The one production image, for the API and every operator command ([The production image](#the-production-image)). | +| [`display-kiosk.md`](display-kiosk.md) | Setting up an unattended display screen: launch, pairing, and recovery. | ## The reference deployment diff --git a/deploy/display-kiosk.md b/deploy/display-kiosk.md new file mode 100644 index 0000000..eb21adb --- /dev/null +++ b/deploy/display-kiosk.md @@ -0,0 +1,129 @@ +# Setting up a display kiosk + +A display is a screen — a television with a small computer, or a mini PC on a wall — that opens one +module's display page full screen and keeps it open unattended. This guide sets one up so it starts +on boot, stays paired, recovers from network and server outages on its own, and never opens a second +copy of itself. + +Pairing and display identity belong to the platform (ADR 0036). Today an administrator approves a +screen in the Dashboard sidebar's **Display devices** panel; a later release moves this to +**Settings → Displays**. Nothing below changes when that happens. + +## What the screen needs + +- Chromium or Google Chrome, current stable. The display page is tested against Chromium. +- A **dedicated, persistent browser profile directory**. The device credential is an HttpOnly cookie + in that profile; it is what keeps the screen paired across reboots. Never use a guest, incognito, or + "clear data on exit" profile, and never share the profile with a person's browsing. +- Network access to the installation's **API hostname** (the display page, its realtime stream, and + its module requests) and to the storage hostname that serves images. +- A clock that is roughly right. Playback follows the server's clock, but TLS does not tolerate a + wildly wrong one. + +## The address + +Every display-capable module has one display page on the API hostname: + +```text +https:///display/ +``` + +For Signage that is `https:///display/signage`. The Signage **Displays** tab shows the +exact address under **Copy setup address**. Open it on the screen itself; opening it on an +administrator's own computer only previews the display (and that browser appears in the roster as a +*Signed-in browser*, which can be removed once it is offline). + +## Launching the browser + +Run exactly **one** launcher, and launch the page as an app window with a fixed profile: + +```bash +chromium \ + --user-data-dir=/var/lib/display-kiosk/profile \ + --app=https:///display/signage \ + --kiosk \ + --no-first-run \ + --noerrdialogs \ + --disable-session-crashed-bubble \ + --hide-crash-restore-bubble \ + --disable-features=Translate \ + --autoplay-policy=no-user-gesture-required \ + --check-for-update-interval=31536000 +``` + +- `--app` opens the page with no tab strip and no address bar. `--kiosk` makes it full screen. +- `--user-data-dir` is the persistent profile that holds the pairing. Keep it on persistent storage + and back it up with nothing: if it is lost, the screen simply shows a new pairing code. +- Do **not** add `--restore-last-session`, and do not also start the browser from a desktop autostart + entry. Chromium allows one process per profile; a second launch hands its URL to the running + process, which opens a **second window or tab** of the display. That is the usual cause of + "duplicate" displays. One launcher, one profile, one window. + +### Start on boot and restart on exit (systemd) + +Run the browser as an unprivileged user in that user's graphical session, from a single user unit: + +```ini +# ~/.config/systemd/user/display-kiosk.service +[Unit] +Description=Display kiosk +After=graphical-session.target network-online.target +Wants=network-online.target +PartOf=graphical-session.target + +[Service] +ExecStart=/usr/bin/chromium --user-data-dir=%h/.local/share/display-kiosk --app=https:///display/signage --kiosk --no-first-run --noerrdialogs --disable-session-crashed-bubble --hide-crash-restore-bubble --disable-features=Translate --autoplay-policy=no-user-gesture-required --check-for-update-interval=31536000 +Restart=always +RestartSec=5 + +[Install] +WantedBy=graphical-session.target +``` + +```bash +systemctl --user daemon-reload +systemctl --user enable --now display-kiosk.service +loginctl enable-linger # with the desktop set to log this user in automatically +``` + +`systemctl --user stop display-kiosk` closes the browser cleanly, which is what keeps its profile — +and the pairing cookie — intact. Avoid cutting power to a running kiosk when you can: a clean shutdown +is always safer for a browser profile. + +## Pairing + +1. Start the kiosk. With no credential it shows a **six-digit pairing code**. +2. In the Dashboard, enter the code under **Display devices**, give the screen a name, and approve. +3. Within a few seconds the screen collects its credential and starts showing content. +4. In Signage → **Displays**, name the display and choose its group and audience. + +A paired screen stays paired across browser restarts and reboots. The credential lasts **180 days** +from pairing and is not renewed automatically (ADR 0036); after that the screen returns to its pairing +code and is paired again the same way. Note the pairing date of each screen. + +## What happens when things go wrong + +| Event | What the screen does | +| --- | --- | +| Network drops for seconds | Nothing visible. The display keeps running; the realtime stream reconnects. | +| Network or API down for longer | Keeps playing what it has cached, with a small corner note; recovers by itself when the server is back. No remount, no reload. | +| API restarted or upgraded | Reconnects by itself. A new module version is picked up within five minutes and swapped in. | +| Browser crashes or is restarted | systemd restarts it; the same profile is still paired. | +| Machine reboots | Starts on boot and resumes. If the server is not up yet, the screen retries on its own (every few seconds, backing off to once a minute) until it is. | +| Display revoked in the Dashboard | Stops showing content within seconds and returns to its pairing code. | +| Display moved to another group or audience | Switches content within about one heartbeat (≈12 s), no reload. | + +**Offline cold start is not supported yet.** A screen that is already showing content survives an +outage. A screen that *boots* while the server is completely unreachable can load its cached page but +cannot mount the module until the server answers, because the authorization it needs is never cached +(ADR 0043, Signage ADR 0006). It shows a short message and retries on its own until the server is +reachable — nobody needs to touch it. + +## Checklist for a new kiosk + +- [ ] One systemd unit, no desktop autostart entry, no second launcher. +- [ ] `--user-data-dir` on persistent storage, not shared, not cleared on exit. +- [ ] Automatic login of the kiosk user, and screen blanking/power saving disabled. +- [ ] Paired and named; assigned in Signage; pairing date recorded. +- [ ] Unplug the network for a minute: content keeps playing, then the note clears on reconnect. +- [ ] Reboot: the screen comes back paired without anyone touching it.