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