Deploy-ready: path-prefix hosting (digalot.fyi/festifun) + deploy artifacts

Frontend: API_BASE env-driven (VITE_API_BASE, default localhost:8000 so dev is
unchanged); vite.config.js base from FESTIVAL4D_BASE. Prod build
'FESTIVAL4D_BASE=/festifun/ VITE_API_BASE=/festifun' serves same-origin under
the prefix. Validated end-to-end via a local proxy mirroring the nginx config:
app boots, media Range 206, 3D + overlays + timeline render under /festifun.

deploy/: nginx location blocks (prefix strip + Range), systemd unit
(uvicorn 127.0.0.1:8000, no keys on the public box), DEPLOY.md. Labels baked
into the shipped DB locally so detect can't spend credits from an anonymous
public endpoint. Backend suite 103 passed; default build unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
m3ultra 2026-07-16 13:20:36 +10:00
parent 7c13f519d2
commit 931cf81a6c
6 changed files with 234 additions and 1 deletions

108
deploy/DEPLOY.md Normal file
View File

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

View File

@ -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

View File

@ -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 <video> seeking) ---
location /festifun/media/ {
proxy_pass http://127.0.0.1:8000/media/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Range $http_range; # forward Range for seeking
proxy_set_header If-Range $http_if_range;
proxy_force_ranges on;
}
# Optional, faster alternative for /festifun/media/: serve the files directly instead of
# proxying (uvicorn StaticFiles already does Range, but nginx is faster for large media):
# location /festifun/media/ { alias /var/www/festifun/data/raw/; }

View File

@ -3,7 +3,11 @@
// of truth: the transport writes `tGlobal` every animation frame; every module reads it. // of truth: the transport writes `tGlobal` every animation frame; every module reads it.
// Discrete changes (play/pause/seek/selection) go through the tiny pub/sub below. // Discrete changes (play/pause/seek/selection) go through the tiny pub/sub below.
export const API_BASE = "http://localhost:8000"; // API origin/prefix. Local dev talks to the uvicorn server cross-origin on :8000 (default).
// A hosted build sets VITE_API_BASE to a same-origin path prefix (e.g. "/festifun") so every
// request — fetch, <video> src, PLY loader — goes through the reverse proxy under that path.
// Empty string = same-origin at root. See deploy/DEPLOY.md.
export const API_BASE = import.meta.env.VITE_API_BASE ?? "http://localhost:8000";
export const state = { export const state = {
apiBase: API_BASE, apiBase: API_BASE,

9
frontend/vite.config.js Normal file
View File

@ -0,0 +1,9 @@
import { defineConfig } from "vite";
// `base` is the public path the built assets are served from.
// - local dev / root hosting: "/" (default)
// - hosted under a path prefix: "/festifun/" via FESTIVAL4D_BASE
// Pair with VITE_API_BASE (see src/state.js) so API + media requests use the same prefix.
export default defineConfig({
base: process.env.FESTIVAL4D_BASE || "/",
});

38
plan/status/deploy.md Normal file
View File

@ -0,0 +1,38 @@
# Status — deploy (digalot.fyi/festifun)
## Round 1 — 2026-07-16 — STATUS: app deploy-ready; BLOCKED on server access
**Ask:** host the app live at `digalot.fyi/festifun` on `dealgod@100.94.195.115` (tailscale).
**Owner decision:** fully open (all features public + unauthenticated). Risk flagged + accepted.
### Done — the app is now deployable behind a path prefix
- **Frontend deploy-ready.** `API_BASE` is env-driven (`VITE_API_BASE`, default `localhost:8000`
so dev is unchanged); added `vite.config.js` with `base` from `FESTIVAL4D_BASE`. A prod build
`FESTIVAL4D_BASE=/festifun/ VITE_API_BASE=/festifun npm run build` emits assets under
`/festifun/` with the API base baked as `/festifun` (localhost:8000 absent from the bundle).
- **Validated end-to-end locally.** Built the prod bundle, ran a Python reverse proxy mirroring
the nginx config (static + prefix-stripping `/festifun/api` & `/festifun/media` with Range),
loaded it in a browser: **app boots, manifest/pointcloud proxy OK, media Range = 206, videos
play, 3D scene + overlays + timeline all render under the `/festifun` prefix.**
- **Deploy artifacts written** in `deploy/`: `festifun.nginx.conf` (location blocks, prefix
strip, Range), `festifun-api.service` (uvicorn on 127.0.0.1:8000, localhost-only, no keys),
`DEPLOY.md` (build → rsync → service → nginx, plus Caddy note).
- Default build unchanged (root asset paths); backend suite still **103 passed**.
### Deliberate deploy choice — keys OFF the public box
`POST /api/events/detect` spends real AI credits. Rather than expose that on an unauthenticated
public endpoint, moment labels are baked into the shipped DB by running `events` locally before
deploy; the server carries no `GEMINI_API_KEY`/OpenRouter creds and `detect` degrades to
candidate-only (no spend). Owner can opt into live classification, but that needs nginx
rate-limiting first (documented in DEPLOY.md).
### BLOCKED — cannot finish without server access
- My SSH key is **not authorized** on `dealgod@100.94.195.115` (rejected directly and via the
johnking box — both `Permission denied (publickey)`). Need the key added to that host's
`~/.ssh/authorized_keys`:
`ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPL3EE4wE6nv0V0BNMbJ0Gl+px3wOM8K+V6LvCJZa7PM monsterrobotparty@gmail.com`
- Also need to know the server's web stack (nginx vs caddy, where digalot.fyi's server block
lives). Recon command in the chat; I'll adapt the config once I can see it.
**Next (once access granted):** recon the web stack → adapt `deploy/festifun.nginx.conf` (or write
the Caddy equivalent) → build+rsync+service+reload per DEPLOY.md → verify `https://digalot.fyi/festifun/`.