# 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.