feat: production deploy (Fly + R2) + frame controls and picker glow
Deploy: - Multi-stage Dockerfile (shadow-cljs release → Python+ffmpeg runtime), fly.toml (one machine, scale-to-zero, SQLite on a volume), .dockerignore, DEPLOY.md - Same-origin release build: api-port defaults "9001" for dev, "" in release (no CORS in prod) - env-driven settings (SECRET_KEY/DEBUG/ALLOWED_HOSTS/DB_PATH/CSRF), WhiteNoise serving static + the SPA, R2 (S3-compatible) media storage when R2_* is set - thumbnails reworked storage-agnostic: download clip, ffmpeg select-pass, upload tiles via default_storage (R2 in prod, disk in dev) - pin channels/daphne; add whitenoise/django-storages/boto3 UI: - frame step buttons flanking play (disabled at timeline bounds) - Add-annotation button moved into the toolbar, made prominent - picker mode pulses a 1-bit glow on clips / frame readout / (while linking) annotation bars; linking an annotation bar inserts a timeline link Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
e00a056209
commit
e6d150c375
12 changed files with 322 additions and 87 deletions
59
DEPLOY.md
Normal file
59
DEPLOY.md
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# Deploy (Fly.io + Cloudflare R2)
|
||||
|
||||
One Fly machine (scale-to-zero) runs Django ASGI (daphne) + ffmpeg, serving the
|
||||
API, the WebSocket, and the compiled SPA. SQLite lives on a Fly volume. **All
|
||||
media — clips, OTIO, thumbnails — lives in Cloudflare R2** (S3-compatible, free
|
||||
egress), served through Cloudflare's CDN. The frontend is a same-origin release
|
||||
build, so there's no CORS in prod.
|
||||
|
||||
## 1. Cloudflare R2 (do this in the dashboard)
|
||||
|
||||
1. **Create a bucket** (R2 → Create bucket), e.g. `lumet-media`.
|
||||
2. **Public access via a custom domain**: bucket → Settings → Public access →
|
||||
*Connect a domain*, e.g. `media.yourdomain.com` (Cloudflare proxied). That host
|
||||
is `R2_PUBLIC_DOMAIN`. (This is what makes egress free + cached.)
|
||||
3. **CORS policy** on the bucket (Settings → CORS) — the app `fetch()`es the OTIO
|
||||
JSON cross-origin, so allow your app origin:
|
||||
```json
|
||||
[{"AllowedOrigins":["https://<your-app>.fly.dev"],
|
||||
"AllowedMethods":["GET"],"AllowedHeaders":["*"],"MaxAgeSeconds":3600}]
|
||||
```
|
||||
4. **API token**: R2 → Manage API Tokens → Create (Object Read & Write, this
|
||||
bucket). Note the **Access Key ID**, **Secret Access Key**, and your account's
|
||||
**S3 endpoint** `https://<accountid>.r2.cloudflarestorage.com`.
|
||||
|
||||
## 2. Fly secrets
|
||||
|
||||
```bash
|
||||
fly secrets set \
|
||||
DJANGO_SECRET_KEY="$(python -c 'import secrets;print(secrets.token_urlsafe(64))')" \
|
||||
DJANGO_CSRF_TRUSTED="https://<your-app>.fly.dev" \
|
||||
R2_BUCKET="lumet-media" \
|
||||
R2_ENDPOINT="https://<accountid>.r2.cloudflarestorage.com" \
|
||||
R2_ACCESS_KEY_ID="..." \
|
||||
R2_SECRET_ACCESS_KEY="..." \
|
||||
R2_PUBLIC_DOMAIN="media.yourdomain.com"
|
||||
```
|
||||
|
||||
## 3. Deploy
|
||||
|
||||
```bash
|
||||
fly volumes create data -r iad -n 1 -s 1 # 1 GB for SQLite (once)
|
||||
fly deploy
|
||||
fly ssh console -C "python manage.py createsuperuser" # first admin
|
||||
```
|
||||
|
||||
`fly.toml` already sets `DJANGO_DEBUG=0`, `DJANGO_ALLOWED_HOSTS=.fly.dev`,
|
||||
`DJANGO_DB_PATH=/data/db.sqlite3`, and scale-to-zero.
|
||||
|
||||
## Notes / caveats
|
||||
|
||||
- **Thumbnails run in a background thread.** With scale-to-zero the machine can
|
||||
stop right after an upload returns and kill that thread before it finishes — so
|
||||
thumbnails may not generate on the very first idle deploy. Options: bump
|
||||
`min_machines_running = 1`, or move thumbnailing to a proper queue/worker later.
|
||||
Core app (video, annotations, sync) is unaffected.
|
||||
- **Single machine only** while on SQLite + the in-memory Channels layer. To scale
|
||||
out: LiteFS (SQLite replication) + `channels_redis` (Upstash).
|
||||
- **Dev is unchanged**: `npm run dev` still serves UI on :8280 / API on :9001 with
|
||||
local-disk media. R2 only kicks in when the `R2_*` env vars are set.
|
||||
Loading…
Add table
Add a link
Reference in a new issue