diff --git a/deploy/DEPLOY.md b/deploy/DEPLOY.md new file mode 100644 index 0000000..488be8f --- /dev/null +++ b/deploy/DEPLOY.md @@ -0,0 +1,108 @@ +# 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. diff --git a/deploy/festifun-api.service b/deploy/festifun-api.service new file mode 100644 index 0000000..e47ee97 --- /dev/null +++ b/deploy/festifun-api.service @@ -0,0 +1,28 @@ +# Festival 4D backend — systemd unit (uvicorn on 127.0.0.1:8000, localhost-only) +# +# Install: sudo cp deploy/festifun-api.service /etc/systemd/system/ +# sudo systemctl daemon-reload && sudo systemctl enable --now festifun-api +# Logs: journalctl -u festifun-api -f +# +# The service binds 127.0.0.1 only — nginx is the sole public entry point. Adjust User, +# paths, and the venv location to match the server. + +[Unit] +Description=Festival 4D API (uvicorn) +After=network.target + +[Service] +Type=simple +User=dealgod +WorkingDirectory=/var/www/festifun/backend +# The app reads FESTIVAL4D_DATA_DIR for the project (db + media + point cloud). +Environment=FESTIVAL4D_DATA_DIR=/var/www/festifun/data +# NOTE: no GEMINI_API_KEY / OPENROUTER creds here on purpose — see DEPLOY.md "AI classification". +# Moment labels are baked into the shipped DB by running `events` locally before deploy, so the +# public box needs no keys and POST /api/events/detect degrades to candidates-only (no spend). +ExecStart=/var/www/festifun/.venv/bin/python -m uvicorn festival4d.api:app --host 127.0.0.1 --port 8000 +Restart=on-failure +RestartSec=3 + +[Install] +WantedBy=multi-user.target diff --git a/deploy/festifun.nginx.conf b/deploy/festifun.nginx.conf new file mode 100644 index 0000000..ec0d320 --- /dev/null +++ b/deploy/festifun.nginx.conf @@ -0,0 +1,46 @@ +# Festival 4D — nginx location block for hosting under digalot.fyi/festifun +# +# Drop these `location` blocks inside the existing `server { }` for digalot.fyi (the one that +# already terminates TLS for the domain). The app is served entirely under the /festifun prefix: +# /festifun/ -> static SPA build (frontend/dist) +# /festifun/api/... -> uvicorn backend on 127.0.0.1:8000 (prefix stripped) +# /festifun/media/... -> uvicorn backend (video files, Range-capable) +# +# Adjust FESTIFUN_ROOT to wherever you rsync the build (see deploy/DEPLOY.md). + +# --- static SPA (built with FESTIVAL4D_BASE=/festifun/) --- +location /festifun/ { + alias /var/www/festifun/dist/; + try_files $uri $uri/ /festifun/index.html; # SPA fallback +} + +# bare /festifun -> /festifun/ +location = /festifun { + return 301 /festifun/; +} + +# --- API: strip the /festifun prefix, proxy to uvicorn --- +# The trailing slash on proxy_pass performs the prefix strip: +# /festifun/api/manifest -> http://127.0.0.1:8000/api/manifest +location /festifun/api/ { + proxy_pass http://127.0.0.1:8000/api/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 300s; # POST /api/events/detect runs ffmpeg + (optional) an AI call +} + +# --- media: video files, Range-capable (required for