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).
105 lines
4.1 KiB
Markdown
105 lines
4.1 KiB
Markdown
# 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:
|
|
|
|
```bash
|
|
# 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).
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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.
|