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
|
|
@ -10,23 +10,32 @@ For the full list of settings and their values, see
|
|||
https://docs.djangoproject.com/en/6.0/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def _env(key, default=""):
|
||||
return os.environ.get(key, default)
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'django-insecure-it#u2wp)_qjx@cq61o*thw#*4_1*hk!%%lz#d+fww6^*l3j+zj'
|
||||
# Real key from the environment in prod; the insecure literal is a dev fallback
|
||||
# so `runserver` works out of the box.
|
||||
SECRET_KEY = _env("DJANGO_SECRET_KEY") or 'django-insecure-it#u2wp)_qjx@cq61o*thw#*4_1*hk!%%lz#d+fww6^*l3j+zj'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
# DEBUG off in prod (DJANGO_DEBUG=0); on by default for local dev.
|
||||
DEBUG = _env("DJANGO_DEBUG", "1") == "1"
|
||||
|
||||
# '.ts.net' matches any Tailscale MagicDNS name; the literal is the tailnet IP.
|
||||
# DJANGO_ALLOWED_HOSTS (comma-separated, e.g. ".fly.dev") adds prod hosts.
|
||||
ALLOWED_HOSTS = ['localhost', '127.0.0.1', 'testserver', '.ts.net', '100.82.84.50']
|
||||
ALLOWED_HOSTS += [h for h in _env("DJANGO_ALLOWED_HOSTS").split(",") if h]
|
||||
|
||||
|
||||
# Application definition
|
||||
|
|
@ -47,6 +56,7 @@ INSTALLED_APPS = [
|
|||
MIDDLEWARE = [
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'whitenoise.middleware.WhiteNoiseMiddleware', # serves static + the SPA in prod
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
|
|
@ -86,7 +96,8 @@ WSGI_APPLICATION = 'server.wsgi.application'
|
|||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'db.sqlite3',
|
||||
# DJANGO_DB_PATH points at the Fly volume (/data/db.sqlite3) in prod.
|
||||
'NAME': _env("DJANGO_DB_PATH") or (BASE_DIR / 'db.sqlite3'),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -131,6 +142,38 @@ STATIC_URL = 'static/'
|
|||
MEDIA_URL = 'media/'
|
||||
MEDIA_ROOT = BASE_DIR / 'media'
|
||||
|
||||
# Django's own static (admin) is collected here; WhiteNoise serves it and also
|
||||
# serves the compiled frontend (resources/public — index.html, app.js, css,
|
||||
# fonts, icons) at the site root. It's a hash-routed SPA, so '/' is all we need.
|
||||
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||||
WHITENOISE_ROOT = BASE_DIR / 'tl' / 'resources' / 'public'
|
||||
WHITENOISE_INDEX_FILE = True
|
||||
|
||||
STORAGES = {
|
||||
'default': {'BACKEND': 'django.core.files.storage.FileSystemStorage'},
|
||||
'staticfiles': {'BACKEND': 'whitenoise.storage.CompressedManifestStaticFilesStorage'},
|
||||
}
|
||||
|
||||
# Production media (clips/otio/thumbnails) → Cloudflare R2 (S3-compatible, free
|
||||
# egress). FileField.url then returns the public CDN URL automatically. Set the
|
||||
# R2_* env vars to switch it on; without them dev stays on local disk.
|
||||
# R2_PUBLIC_DOMAIN = a Cloudflare-fronted custom domain on the bucket (no scheme).
|
||||
if _env("R2_BUCKET"):
|
||||
STORAGES['default'] = {
|
||||
'BACKEND': 'storages.backends.s3.S3Storage',
|
||||
'OPTIONS': {
|
||||
'bucket_name': _env("R2_BUCKET"),
|
||||
'endpoint_url': _env("R2_ENDPOINT"),
|
||||
'access_key': _env("R2_ACCESS_KEY_ID"),
|
||||
'secret_key': _env("R2_SECRET_ACCESS_KEY"),
|
||||
'region_name': 'auto',
|
||||
'signature_version': 's3v4',
|
||||
'custom_domain': _env("R2_PUBLIC_DOMAIN") or None,
|
||||
'querystring_auth': not bool(_env("R2_PUBLIC_DOMAIN")),
|
||||
'file_overwrite': False,
|
||||
},
|
||||
}
|
||||
|
||||
# Allow clip uploads up to ~100 MB (files stream to a temp file past 2.5 MB).
|
||||
DATA_UPLOAD_MAX_MEMORY_SIZE = 100 * 1024 * 1024
|
||||
|
||||
|
|
@ -148,5 +191,13 @@ CORS_ALLOW_CREDENTIALS = True
|
|||
CSRF_TRUSTED_ORIGINS = ['http://localhost:8280', 'http://127.0.0.1:8280',
|
||||
'https://*.ts.net', 'http://*.ts.net',
|
||||
'http://100.*']
|
||||
# DJANGO_CSRF_TRUSTED (comma-separated, e.g. "https://my-app.fly.dev") for prod.
|
||||
CSRF_TRUSTED_ORIGINS += [o for o in _env("DJANGO_CSRF_TRUSTED").split(",") if o]
|
||||
|
||||
# Behind Fly's edge TLS: trust the forwarded-proto header and mark cookies secure.
|
||||
if not DEBUG:
|
||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||
SESSION_COOKIE_SECURE = True
|
||||
CSRF_COOKIE_SECURE = True
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue