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:
parent
7c13f519d2
commit
931cf81a6c
108
deploy/DEPLOY.md
Normal file
108
deploy/DEPLOY.md
Normal 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.
|
||||
28
deploy/festifun-api.service
Normal file
28
deploy/festifun-api.service
Normal 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
|
||||
46
deploy/festifun.nginx.conf
Normal file
46
deploy/festifun.nginx.conf
Normal 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/; }
|
||||
@ -3,7 +3,11 @@
|
||||
// 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.
|
||||
|
||||
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 = {
|
||||
apiBase: API_BASE,
|
||||
|
||||
9
frontend/vite.config.js
Normal file
9
frontend/vite.config.js
Normal 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
38
plan/status/deploy.md
Normal 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/`.
|
||||
Loading…
Reference in New Issue
Block a user