festifun/deploy/DEPLOY.md
m3ultra 36bacf67f0 Live capture: USB webcams, laptop cams, and phones -> data/raw
Records rather than streams: each device records a clip WITH AUDIO and uploads
it; the normal offline pipeline takes over. Fits the design almost for free —
MediaRecorder's webm/mp4 are already ingestible, and audio-based sync means
devices need no clock sync and needn't start together.

- capture.py: POST /api/capture/upload (streamed, sanitized name, size cap,
  partial cleanup), heartbeat/devices registry, GET /capture page.
- static/capture.html: standalone page (no build step) — device picker, live
  preview, record->upload, 1fps snapshot heartbeat, 'who's shooting what'
  monitor, and secure-context detection that tells you to use
  (getUserMedia needs HTTPS — the #1 gotcha for phones).
- OPT-IN via FESTIVAL4D_CAPTURE=1; manifest reports has_capture and the viewer
  only then shows a Capture link. Public deploy leaves it off (open upload
  endpoint would be unsafe) — noted in DEPLOY.md.
- Adds python-multipart dep. Suite 103 -> 119.

Also fixes test_manifest_shape, which was already RED on main: f12b6e0 added
has_splat to the manifest without updating the exact-set lock. Updated to the
true set (now also has_capture); the lock is what caught both drifts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 15:43:51 +10:00

119 lines
4.9 KiB
Markdown

# Deploying Festival 4D to digalot.fyi/festifun
Target: `dealgod@100.94.195.115` (tailscale), served under `https://digalot.fyi/festifun` by the
existing web server that already terminates TLS for the domain.
The app runs as two pieces behind that web server:
- **Static SPA** — the built frontend (`frontend/dist`), served under `/festifun/`.
- **API backend** — uvicorn on `127.0.0.1:8000` (localhost-only), reached via `/festifun/api`
and `/festifun/media`.
This topology was validated locally end-to-end against a proxy that mirrors the nginx config in
this directory (static + prefix-stripping API/media proxy with Range) — the app loads, plays
video, and renders the 3D scene correctly under the `/festifun` prefix.
---
## Deploy mode: fully open
Per the owner's decision, **all features are public and unauthenticated** — playback, 3D, and the
mutating endpoints (annotate, correct event type, add/delete anchors). Be aware this means anyone
who finds the URL can edit or delete the project's annotations/anchors/events. If that becomes a
problem, add HTTP basic-auth on the `/festifun/` location, or run the backend read-only.
### AI classification & API keys — deliberately OFF on the public box
`POST /api/events/detect` runs ffmpeg and, if a classifier key is present, a paid AI call. To
avoid an unauthenticated public endpoint that spends real credits, **do not put `GEMINI_API_KEY`
or the OpenRouter creds on the server.** Instead bake the moment labels in *before* deploy:
```bash
# locally, with keys loaded (set -a; . ./.env; set +a):
python -m festival4d events # writes AI labels into data/project.db
```
Then ship that DB. On the server, `detect` still works but degrades to candidate-only (no spend),
exactly as designed. If you *want* live classification on the public site, set the key in the
systemd unit — but add rate-limiting first (nginx `limit_req`), or it's a bill-run-up vector.
---
## One-time server setup
```bash
# on dealgod@100.94.195.115
sudo mkdir -p /var/www/festifun
sudo chown dealgod:dealgod /var/www/festifun
# Python venv for the backend
python3 -m venv /var/www/festifun/.venv
```
## Build + push (run locally)
```bash
# 1. Build the frontend for the /festifun prefix + same-origin API
cd frontend
FESTIVAL4D_BASE=/festifun/ VITE_API_BASE=/festifun npm run build
# 2. Generate the project to ship (synthetic demo, or your real footage pipeline first),
# with labels baked in if you loaded keys:
cd ..
python -m festival4d synthetic # or: ingest -> sync -> reconstruct -> events
# 3. Push build + backend + data to the server
rsync -az --delete frontend/dist/ dealgod@100.94.195.115:/var/www/festifun/dist/
rsync -az --delete backend/ dealgod@100.94.195.115:/var/www/festifun/backend/
rsync -az pyproject.toml dealgod@100.94.195.115:/var/www/festifun/
rsync -az --delete data/ dealgod@100.94.195.115:/var/www/festifun/data/
```
## Install backend deps + service (on the server)
```bash
cd /var/www/festifun
.venv/bin/pip install -e ".[dev]" # or a runtime-only extra if defined
sudo cp backend/../deploy/festifun-api.service /etc/systemd/system/ # adjust path to the repo copy
sudo systemctl daemon-reload
sudo systemctl enable --now festifun-api
curl -s localhost:8000/api/health # -> {"status":"ok"}
```
## Wire up the web server
Append the blocks from `deploy/festifun.nginx.conf` into the existing `server { }` for
digalot.fyi, adjusting `alias` paths to `/var/www/festifun/dist/`. Then:
```bash
sudo nginx -t && sudo systemctl reload nginx
```
Visit `https://digalot.fyi/festifun/`.
> Using Caddy instead of nginx? The equivalent is a `handle_path /festifun/*` block: `file_server`
> for the SPA, `reverse_proxy 127.0.0.1:8000` for `/festifun/api/*` and `/festifun/media/*`
> (Caddy strips the matched prefix with `handle_path`). Ask and I'll write the Caddyfile once I
> can see which server is actually running there.
---
## Updating later
Re-run the build + rsync steps, then `sudo systemctl restart festifun-api` (only needed if the
backend or data changed; a frontend-only change just needs the `dist` rsync).
## Notes / caveats
- **CORS** is irrelevant in this topology — everything is same-origin under `/festifun`. The
dev-only wide CORS in `config.py` stays as-is; it doesn't affect the hosted site.
- **Single project.** The app assumes one project at a time (SQLite at `data/project.db`).
- The backend binds `127.0.0.1` only; nginx is the sole public entry point.
---
## Live capture on the public box: leave it OFF
`FESTIVAL4D_CAPTURE` is **not** set in `deploy/festifun-api.service`, so `/capture` and
`/api/capture/*` are never mounted publicly — an open upload endpoint on a public host would let
anyone write files to the server. Capture is for your local/tailnet machine (see the README's
Capture section: `FESTIVAL4D_CAPTURE=1` + `tailscale serve`). Record locally, run the pipeline,
then deploy the resulting project.