- Wipe "×" is now an opaque dark control-scrim with an always-light glyph, so it stays legible over bright thumbnails bleeding through AND in both themes (the glyph no longer follows --fg-*, which flipped dark-on-dark in light mode). - "Wipe now" is a red-outline danger button (bright-red text/border, fills red on hover) instead of muted gray-on-transparent that read as illegible. - Destructive hover text is white on red (was dark-on-red, low contrast). - Nudge expiry sub-text --fg-muted -> --fg-3 for a bit more contrast.
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
- images (
- Captions: a
<file>.txtor same-stem<stem>.txtsidecar 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.