# Togen signage kiosk agent

Device-side half of Togen signage displays (DVI-1201 / DVI-1191 P4b). Turns a
stock Debian/Ubuntu box with a display attached into a fullscreen Togen
dashboard kiosk. Server-side (enrollment, Admin -> Agents -> Display, the
`/display` kiosk page, heartbeat receiver) shipped in DVI-1199/DVI-1198
(`c0120a4`) and is deployed by DVI-1200.

This agent is deliberately **thin**: it does not know about dashboards,
tiles, or reassignment. It only does three things:

1. One-time registration (exchanges an admin-minted enrollment token for a
   permanent `agent_token`).
2. Launches Chromium in kiosk mode at `/display?token=<agent_token>` -- that
   page itself polls `/display/config` every ~15s, so dashboard edits,
   reassignment, and Present all go live with zero agent-side logic.
3. Heartbeats `POST /signage/checkin` every ~30s so Admin -> Agents ->
   Display shows the device as online.

## Prerequisites

- A stock Debian or Ubuntu machine with a monitor attached (HDMI/DP), network
  access to the Togen host, and root/sudo access.
- The device does not need a desktop environment or display manager
  pre-installed -- `install.sh` installs the minimal X stack + Chromium.

## Enrollment (do this first, in Togen)

1. Togen Admin -> Agents -> Display -> **Generate enrollment token**.
2. This returns a one-time `enrollment_token` (expires in 1h) plus the
   absolute `register_url` / `checkin_url` / `display_url` for that Togen
   host. You only need the host (e.g. `https://togen.icastinc.com`) and the
   token for the installer -- it builds the same URLs itself.

## Install

The **easiest** way (and what Togen Admin -> Agents -> Display shows you) is the
one-line bootstrap -- run it in a root shell on the device. It downloads a
self-contained installer from Togen (which bundles every file in this
directory), so the device needs no repo checkout:

```sh
curl -fsSL https://togen.icastinc.com/signage/install.sh | sudo bash -s -- \
  --host https://togen.icastinc.com --token <enrollment_token>
```

Or, if you already have this `signage_agent/` directory on the device (git
clone, scp, USB), run the installer directly:

```sh
sudo ./install.sh --host https://togen.icastinc.com --token <enrollment_token>
```

Either way the installer:
- installs `chromium` (or `chromium-browser`), `curl`, `unclutter`, and -- only
  on a headless box -- `xserver-xorg`, `xinit`, `x11-xserver-utils`;
- POSTs `{enrollment_token, hostname}` to `<host>/signage/agents/register`
  and stores the returned `agent_token` at `/etc/togen-signage/agent.token`
  and the host at `/etc/togen-signage/agent.conf`;
- installs `togen-signage-checkin.{service,timer}` (heartbeat, every 30s);
- sets up the **kiosk session** in one of two modes, auto-detected:
  - **Wayland desktop** (a display manager is present -- e.g. **Raspberry Pi OS**
    with lightdm + labwc autologin): a labwc autostart launches Chromium kiosk
    *inside* the existing autologin session. This is the normal case for a Pi.
  - **Headless** (no display manager): `togen-signage-kiosk.service` starts its
    own Xorg + Chromium on tty1, running as root.

Re-running the installer on an already-enrolled device refuses to proceed
(an enrollment token is single-use, and re-registering abandons the old
agent record) unless you pass `--force`, after revoking the old agent in
Admin -> Agents -> Display.

On a Wayland-desktop box the installer runs the kiosk as the desktop autologin
user (from `lightdm.conf` `autologin-user`, else the `sudo` caller, else
`--kiosk-user <name>`). Apply the kiosk after install with
`sudo systemctl restart display-manager` (or just reboot).

## Files installed

| Path | Purpose |
|---|---|
| `/etc/togen-signage/agent.token` | Permanent `agent_token`. Headless: root-only (600). Wayland desktop: `640 root:<kiosk-group>` (dir `750`) so the desktop kiosk user can read it |
| `/etc/togen-signage/agent.conf` | `TOGEN_HOST`/`TOGEN_AGENT_ID`, read by both the heartbeat and the kiosk session |
| `/usr/local/bin/togen-signage-checkin` | Heartbeat script |
| `/etc/systemd/system/togen-signage-checkin.{service,timer}` | Runs the heartbeat every ~30s |
| **Wayland desktop only** | |
| `/usr/local/bin/togen-signage-kiosk-launch` | Respawn loop that launches Chromium kiosk in the existing Wayland session |
| `~<kiosk-user>/.config/labwc/autostart` | labwc autostart that runs the launcher (previous autostart backed up as `autostart.togen-bak`) |
| **Headless only** | |
| `/etc/togen-signage/xsession` | X session script Chromium is launched from |
| `/etc/systemd/system/togen-signage-kiosk.service` | Starts X + Chromium kiosk on tty1, `Restart=always` |

## How the kiosk session starts

**Wayland desktop (Raspberry Pi OS etc.):** the box already boots into a display
manager (lightdm) that autologins the desktop user into a Wayland compositor
(labwc), which owns the display. Starting our own Xorg here fails with *"X
server already running"*. So instead, the installer drops a kiosk-only labwc
autostart (`~<user>/.config/labwc/autostart`) that runs
`/usr/local/bin/togen-signage-kiosk-launch`. That launcher runs *inside* the
autologin session (so `WAYLAND_DISPLAY`/`XDG_RUNTIME_DIR` are already set --
no server to start, no root needed) and respawns Chromium
(`--kiosk --ozone-platform=wayland ...`) pointed at
`$TOGEN_HOST/display?token=$(cat agent.token)`. Screen blanking is turned off
best-effort via `raspi-config nonint do_blanking 1`.

**Headless (no display manager):** `togen-signage-kiosk.service` runs
`startx /etc/togen-signage/xsession -- :0 vt1 -keeptty` directly as a systemd
service. `PAMName=login` + `TTYPath=/dev/tty1` register a login-like session so
Xorg gets tty/DRM permissions; `Conflicts=getty@tty1.service` stops the login
prompt fighting Xorg for tty1. `xsession` disables DPMS/screen-blanking then
execs Chromium `--kiosk` (no `--ozone-platform`, X11 default).

## Security notes

- **Wayland desktop mode runs the kiosk as the desktop autologin user** (not
  root, no `--no-sandbox`). The `agent_token` is `640 root:<group>` so only root
  and that user can read it -- no weaker than the running kiosk, which puts the
  token in the `/display?token=` URL anyway.
- **Headless mode runs the X session as root**, which is why `xsession` passes
  Chromium `--no-sandbox` (Chromium refuses to start as root without it) and the
  token stays root-only (600). That's a deliberate simplification for a
  single-purpose appliance with no other local users. If unacceptable, run the
  Xorg units as a dedicated non-root user with
  `/etc/X11/Xwrapper.config allowed_users=anybody` and drop `--no-sandbox`.
- The heartbeat also calls `https://api.ipify.org` to report `public_ip` --
  the only outbound-to-a-third-party call the agent makes. It fails soft
  (empty string) if that's blocked or unreachable; it does not block or
  fail the heartbeat.

## Revocation

Once an admin revokes the display in Admin -> Agents -> Display, `agent_token`
stops working immediately: `/signage/checkin` and `/display/config` both
return 401. The heartbeat service will show as failed in
`systemctl status togen-signage-checkin.service` / journal (a clear signal
the device needs re-enrollment), and the kiosk page itself renders an
"unauthorized" splash (server/browser-side, no agent-side handling needed).
To bring the device back: revoke cleanup is already done server-side, mint a
new enrollment token, and re-run `install.sh --force`.

## Uninstall

```sh
# common
sudo systemctl disable --now togen-signage-checkin.timer
sudo rm -f /etc/systemd/system/togen-signage-checkin.{service,timer}
# headless kiosk
sudo systemctl disable --now togen-signage-kiosk.service 2>/dev/null
sudo rm -f /etc/systemd/system/togen-signage-kiosk.service
# Wayland desktop kiosk
sudo rm -f /usr/local/bin/togen-signage-kiosk-launch
# restore any pre-Togen labwc autostart, else remove ours:
#   mv ~<user>/.config/labwc/autostart.togen-bak ~<user>/.config/labwc/autostart
sudo systemctl daemon-reload
sudo rm -rf /etc/togen-signage /usr/local/bin/togen-signage-checkin
```
Also revoke or delete the device in Admin -> Agents -> Display so the
`agent_token` can't be reused.

## Troubleshooting

- **Blank screen / monitor sleeps anyway:** some monitors ignore DPMS-off
  from the host and blank on their own timeout -- check the monitor's own
  OSD power-saving setting.
- **(Wayland) kiosk never appears:** confirm `pgrep -af togen-signage-kiosk-launch`
  shows the launcher after `systemctl restart display-manager`; if not, check
  the autostart file exists at `~<kiosk-user>/.config/labwc/autostart` and that
  the kiosk user can read the token (`sudo -u <user> cat /etc/togen-signage/agent.token`).
- **(Headless) Chromium never appears, tty1 shows a login prompt:** `getty@tty1`
  won -- check `systemctl status togen-signage-kiosk.service` and
  `journalctl -u togen-signage-kiosk -b`. *"Cannot establish any listening
  sockets / server already running"* means a display manager owns the display
  -- this box should be in Wayland-desktop mode; re-run the installer (it
  auto-detects the DM) or pass `--kiosk-user`.
- **Heartbeat failing:** `journalctl -u togen-signage-checkin -f` shows the
  HTTP status + response body on every failed run (401 almost always means
  the display was revoked or deleted).

## Verification status

**Verified end-to-end on real Raspberry Pi hardware (DVI-1210):** Raspberry Pi
OS (Debian 13 trixie, aarch64, lightdm + labwc autologin). One-command install
against live prod `togen.icastinc.com` -> registered, heartbeat `200` (online in
Admin -> Agents -> Display), `/display/config` `200` (authorized). The labwc
autostart brings up the Chromium kiosk **automatically after a real reboot**,
rendering the assigned signage dashboard full-screen (confirmed by on-device
`grim` screen capture). The **headless Xorg path** is unchanged from DVI-1201
and remains verified only by reading the routes -- not re-tested on a
DM-less box in this pass.
