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).
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user