BetterFrame/deploy
Mitchell R c5068615ee
feat(remote-debug): journal streaming + secure terminal via WebSocket
Kiosk side (remote_debug.rs + ws_client.rs refactor):
  - Journal streaming: server sends journal-start → kiosk spawns
    journalctl -f, pipes lines back as journal-line messages via WS.
    journal-stop kills the process. On-demand, not always-on.
  - Terminal: server sends terminal-request → kiosk checks lockout +
    firmware_channel == "dev" → generates 8-char code displayed on
    screen as fullscreen overlay (NOT logged) → server relays admin's
    code via terminal-auth → kiosk validates with constant-time compare
    → on success spawns bash, relays I/O as base64 terminal-data.
  - Lockout: 3 failed codes per boot → lockout_count++. 3 lockouts
    (9 total failures) → permanent (reflash only). Reboot resets
    attempt counter, not lockout counter. Successful pairing resets all.
  - ws_client.rs rewritten with split reader/writer + tokio::select!
    for multiplexing incoming WS messages with outbound journal/terminal
    data from sync threads.

Server side (coordinator-ws + routes-admin):
  - New admin debug WS endpoint: /ws/admin/debug/:kioskId. Authenticated
    via admin API key (query param) or session cookie. Relays messages
    bidirectionally between admin browser ↔ kiosk.
  - Admin pages: /admin/kiosks/:id/logs (journal viewer with start/
    stop/clear) and /admin/kiosks/:id/terminal (code entry + terminal
    area). Both open in new tabs from the kiosk detail page.
  - Angie proxy config updated with /ws/admin/debug/ location block.

Security:
  - Terminal only on dev channel
  - Code displayed physically on screen, never logged or stored server-side
  - Lockout: 3/boot, 3 lockouts = permanent, pairing resets
  - Kiosk responds "locked" without specifying which lockout triggered
2026-05-22 20:13:39 +02:00
..
angie feat(remote-debug): journal streaming + secure terminal via WebSocket 2026-05-22 20:13:39 +02:00
docker fix(docker): remove COPY .git — Coolify excludes it from build context 2026-05-22 19:30:18 +02:00
nftables feat(harden): nftables default-drop firewall + first-boot password rotation 2026-05-21 11:18:28 +02:00
pam.d feat(deploy): Pi kiosk bring-up via cage + low-priv bfkiosk user 2026-05-13 03:11:06 +02:00
pi-gen/stage-betterframe-client fix(firmware): grant bfkiosk write access to binary dir + align marker path 2026-05-21 16:03:42 +02:00
plymouth/betterframe feat(deploy): BetterFrame plymouth boot splash 2026-05-13 03:21:37 +02:00
rauc fix(rauc): use CA cert for bundle verify + don't fail build on verify error 2026-05-21 16:22:36 +02:00
scripts fix(firmware): grant bfkiosk write access to binary dir + align marker path 2026-05-21 16:03:42 +02:00
systemd feat(harden): nftables default-drop firewall + first-boot password rotation 2026-05-21 11:18:28 +02:00
tmpfiles fix(firmware): grant bfkiosk write access to binary dir + align marker path 2026-05-21 16:03:42 +02:00
udev feat(kiosk): harden field image defaults 2026-05-20 05:18:18 +02:00
README.md fix(deploy): move docker-compose.yml to repo root 2026-05-18 12:05:09 +02:00

BetterFrame deployment

Deployment shapes

BetterFrame ships as two artifacts, deployable in any combination:

Variant What it runs Where it runs
bf-server Docker compose (server + Angie + Node-RED) Coolify / VM / on-prem box
bf-client Rust kiosk binary + cage + plymouth Pi 5 (LAN-attached)
bf-aio Both, single Pi Demo / single-site only

The bf-aio mode (server + kiosk colocated on one Pi) is the simplest install but couples failure domains — when the Pi dies, you lose both the displays it drives AND the management plane for any other kiosks. Use for demos or a single-display site. For anything else, run bf-server separately and have bf-client Pis point at it.

bf-server (Docker compose, Coolify-friendly)

Pull the repo on the host. Configure via env (overrides sec-config.yaml):

BF_DATA_DIR=/var/lib/betterframe
BF_SQLITE_PATH=/var/lib/betterframe/betterframe.db
BF_NODERED_URL=http://nodered:1880
BF_SELF_URL=http://server:18080
BF_FIRMWARE_SIGNING_KEY=        # paste Ed25519 PEM for stable signing key
BF_MQTT_URL=                     # optional MQTT telemetry export

In Coolify: create a Docker compose stack pointing at the repo's docker-compose.yml (repo root), inject the env vars, set a domain on the angie service. Backups via the admin UI (/admin/backup) — Coolify's S3 hook can pull these on a schedule.

bf-client (kiosk Pi)

sudo apt install -y git
git clone https://github.com/BetterCorp/BetterFrame.git ~/betterframe
sudo ~/betterframe/deploy/scripts/setup-pi-kiosk.sh client

Pairs with whichever bf-server is set in /etc/default/betterframe-kiosk (BETTERFRAME_SERVER=http://<server-host>).

Run server, Angie/nginx, and Node-RED in Docker Compose. Only Angie publishes a host port. The BetterFrame backend ports and Node-RED are internal to the Docker network, which forces /nrdp/, /in/kiosk/, and admin traffic through the proxy auth rules.

cd /opt/betterframe
docker compose up -d --build      # from repo root

Published:

  • 80 -> Angie/nginx public edge

Internal only:

  • 18080 -> admin service
  • 18081 -> kiosk API service
  • 18082 -> kiosk WebSocket service
  • 1880 -> Node-RED

Access first-run setup at:

http://<pi-ip>/setup

Node-RED editor is reachable only through:

http://<pi-ip>/nrdp/

The proxy has four route surfaces:

  • BetterFrame web/API: /, /setup, /admin/*, /auth/*, /static/*, /api/admin/*, /api/kiosk/*, /api/pair/*, /ws/kiosk
  • Kiosk-only Node-RED ingress: /in/kiosk/<node-red-path>
  • Kiosk-only Node-RED dashboards: /dash/*
  • Public Node-RED HTTP-in URLs: any otherwise-unmatched root path, plus /in/public/<node-red-path>

For example, a Node-RED http in node at /test1 is public at http://<pi-ip>/test1 and also available at http://<pi-ip>/in/public/test1. Kiosk-authenticated traffic to that same Node-RED path uses http://<pi-ip>/in/kiosk/test1.

Do not publish 18080, 18081, 18082, or 1880 on the host.

If migrating from an older native install, stop the old host daemons first:

sudo systemctl disable --now betterframe-server betterframe-nodered angie nginx 2>/dev/null || true

Kiosk

The kiosk still runs natively on the Pi because it needs Wayland/HDMI, GTK, GStreamer, display power control, and local hardware access.

sudo apt install -y libgtk-4-dev libgstreamer1.0-dev \
    libgstreamer-plugins-base1.0-dev gstreamer1.0-plugins-good \
    gstreamer1.0-plugins-bad gstreamer1.0-libav \
    gstreamer1.0-gtk4 libwebkitgtk-6.0-dev libssl-dev

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/env

cd /opt/betterframe/kiosk
cargo build --release
sudo install -Dm755 target/release/betterframe-kiosk /opt/betterframe/kiosk/betterframe-kiosk

mkdir -p ~/.config/systemd/user
cp /opt/betterframe/deploy/systemd/betterframe-kiosk.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now betterframe-kiosk

Kiosks should point at the proxy URL, not direct backend ports:

BETTERFRAME_SERVER=http://<pi-ip> /opt/betterframe/kiosk/betterframe-kiosk

Native server mode

Native server mode is for development only. Run it manually when debugging; do not install host daemons for BetterFrame server, Angie, or Node-RED in production. The Docker stack owns those services.