Files
esh-pfi-infrastructure/services/booth
vh f4a5ba7c31 feat(booth): add The Booth — ephemeral media drop board for CC sessions
A standing user-level web server (nh3-dev :8090) that renders drop-folders
under ~/booth-data as ephemeral media "booths" so Claude Code sessions can
surface A/B renders and smoke results to the operator, then let them self-wipe.

- Scan-and-serve model, no database, no upload API — a booth is just a folder.
  A folder's own index.html is served verbatim; otherwise an auto-gallery of
  images / webm+mp4 video / audio is rendered, with <file>.txt caption sidecars
  folded in (labels A/B pairs).
- 24h TTL from newest mtime in the tree; background sweeper wipes stale booths.
- Path-traversal + symlink-escape guarded; delete via UI button or DELETE API.
- FastAPI + Jinja2, runs from the checkout under systemctl --user (booth.service),
  alongside the other nh3-dev fleet sidecars. 15 tests, all green.
- Homepage tile added (Apps -> The Booth, siteMonitor /healthz).
- Harden the homepage rsync doc: exclude *.bak* and logs/ so --delete can't
  wipe the host's dated services.yaml backups (footgun found deploying this).
2026-07-20 10:17:40 -07:00
..

The Booth

A dead-simple standing web server for surfacing ephemeral media to the operator — A/B renders, smoke-test screenshots, audio/video samples. A Claude Code session drops a folder of files somewhere on disk; the Booth renders it as a browsable "booth" and wipes it 24h after the last activity. No database, no upload API — the filesystem is the state.

  • Live: http://10.100.10.50:8090/ (nh3-dev) · linked from Homepage → Apps → The Booth
  • Data dir: ~/booth-data/ on nh3-dev (one subfolder per booth)
  • TTL: 24h, measured from the newest mtime in a booth's tree (it lives while you're touching it, self-destructs 24h after you stop)

How a session posts

A booth is just a folder under the data dir. Three ways, cheapest first:

# 1. On nh3-dev — the helper (services/booth/scripts/booth):
booth add my-run  out/a.png out/b.png       # creates booth + copies, prints URL
booth new  my-run                            # empty booth, then cp/mv into ~/booth-data/my-run/
booth url  my-run                            # just print the URL
booth ls                                     # list booths
booth rm   my-run                            # wipe now (TTL would anyway)

# 2. On nh3-dev — raw, no helper:
mkdir -p ~/booth-data/my-run && cp out/*.png ~/booth-data/my-run/
#   -> http://10.100.10.50:8090/b/my-run/

# 3. From another host — rsync into the data dir:
rsync -a ./out/  nh3-dev:booth-data/my-run/

Then hand the operator http://10.100.10.50:8090/b/my-run/.

What a booth renders

  • Has its own index.html? → served verbatim (its relative assets — chart.png, report.css — resolve out of the same folder). Build whatever page you want.
  • No index.html?auto-gallery of the folder's media:
    • images (png jpg jpeg gif webp avif svg bmp) → <img>
    • video (webm mp4 ogv m4v mov) → <video controls>
    • audio (mp3 wav ogg flac m4a opus aac) → <audio controls>
    • anything else → a download link
  • Captions: a <file>.txt or same-stem <stem>.txt sidecar is folded in as that item's caption — the natural way to label an A/B pair:
    a.png              b.png
    a.txt  "baseline"  b.png.txt  "cudaMallocAsync (winner)"
    

Routes

Route Purpose
GET / Index — one card per booth (newest first), with expiry countdown
GET /b/<name>/ A booth (its index.html, else auto-gallery)
GET /b/<name>/<file> Serve a file out of the booth
POST /b/<name>/delete Wipe a booth (the UI's "Wipe now" button)
DELETE /b/<name> Wipe a booth (curl/API)
GET /healthz {ok, ttl_hours, booths} — Homepage siteMonitor target

Ops

Runs as a user-level systemd service on nh3-dev (no root, no Docker), alongside the other fleet sidecars (herald, zellij-web, ttyd).

systemctl --user status booth.service
systemctl --user restart booth.service
journalctl --user -u booth.service -f          # sweeper logs "[booth] swept …"

Config is env in the unit (booth.service): BOOTH_DATA_DIR, BOOTH_TTL_HOURS, BOOTH_HOST_LABEL, BOOTH_SWEEP_INTERVAL_MIN.

Install / update

cd services/booth
uv venv && uv pip install fastapi "uvicorn[standard]" jinja2   # runtime deps
cp booth.service ~/.config/systemd/user/booth.service
systemctl --user daemon-reload && systemctl --user enable --now booth.service

Code runs straight from this checkout (the unit's WorkingDirectory / ExecStart point here), so "deploy an update" = edit + systemctl --user restart booth.service.

Tests

cd services/booth && uv pip install pytest httpx && .venv/bin/python -m pytest -q

Notes / non-goals

  • No auth. LAN/WG-internal only, ephemeral content — don't drop secrets in a booth. (The data dir is world-readable-on-LAN via the server.)
  • No upload API by design. Sessions have filesystem access to nh3-dev; a folder drop is simpler and more debuggable than an HTTP upload path.
  • Booth names with /, .., or a leading . are rejected; file serving is guarded against path traversal and symlink escape.