Files
BenStullsBets 950c1e6c8c feat(deploy): move app URL to /human-machine-strata + 301 from the legacy path
Build default app-path → /human-machine-strata; deploy.sh APP_PATH default too.
_write_redirects now emits a permanent redirect /human-experience-simulator/* →
/human-machine-strata/:splat so old links still land. Media (R2 bucket) + Pages
project names are unchanged (not in the benstull.art URL). Updated the build
test (asserts the legacy redirect) + static-build e2e path + README paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 15:00:40 -07:00
..

Deploying to benstull.art — Cloudflare Pages + R2

Publishes the simulator as a fully-static site: the frontend on Cloudflare Pages (benstull.art, app at /human-machine-strata, apex redirects to it) and the video/audio on R2 behind static.benstull.art. No origin server.

Design: docs/superpowers/specs/2026-06-30-cloudflare-static-publish-design.md. Plan: docs/superpowers/plans/2026-06-30-cloudflare-static-publish.md.

This project is exempt from the flotilla/PPE/§9 pipeline (operator-set) — it deploys directly to Cloudflare. Safety/hygiene rules still hold.

Repeatable deploy (after the one-time setup below)

Once the bucket/CORS/custom-domains + wrangler login exist (one-time, §13), every later deploy is one command (deploy/cloudflare/deploy.sh, or the Make targets):

make deploy          # frontend only: build + Pages deploy (fast — most deploys)
make deploy-media    # also re-sync all media to R2 (~2 min, parallel) — when media changed
./deploy/cloudflare/deploy.sh --media-only   # only the R2 media sync, skip Pages

The script rebuilds dist/, (optionally) parallel-uploads dist-media/ to R2, and deploys Pages — printing the live URL. It checks wrangler auth first and fails loudly. Override defaults via env: BUCKET, PROJECT, MEDIA_BASE, APP_PATH, BRANCH, JOBS. The sections below document the one-time setup the script assumes.

Prerequisites (operator gestures — need Cloudflare auth)

wrangler is not installed in the dev box; the deploy runs from a machine with Cloudflare credentials.

npm i -g wrangler
wrangler login

benstull.art is already a Cloudflare zone (no nameserver migration).

1. Build the artifacts

From the repo root (run as a MODULE — tools/http.py shadows stdlib http if run as a script):

.venv/bin/python -m tools.build_static

Produces:

  • dist/ — Pages output: dist/human-machine-strata/ (frontend + baked clips.json/ring.json/media-versions.json + generated config.js) and a root dist/_redirects (apex → app path, and no-slash → slashed).
  • dist-media/ — R2 upload tree, ~2.3 GB / 604 files, manifest-referenced only (clip bases + 154 transition morphs + 5 per-scale audio). No master/mezzanine sources.

2. R2 — bucket, CORS, custom domain, media

Bucket name: human-experience-simulator-media. The default wrangler login OAuth token already has R2 + Pages + zone scopes (no separate API token needed).

wrangler r2 bucket create human-experience-simulator-media

# CORS — load-bearing (the WebGL dream shader taints without it). The wrangler CLI
# wants its own schema, NOT the S3-style array; use the *.wrangler.json file:
wrangler r2 bucket cors set human-experience-simulator-media --file deploy/cloudflare/r2-cors.wrangler.json
#   (deploy/cloudflare/r2-cors.json is the equivalent S3-style policy, for the
#    dashboard / S3 API; r2-cors.wrangler.json is the same rule in wrangler's
#    `{ "rules": [ { "allowed": {...} } ] }` form.)

# Custom domain — wrangler CAN do this (no dashboard needed); needs the zone id.
ZONE=$(curl -s -H "Authorization: Bearer $(grep '^oauth_token' ~/.wrangler/config/default.toml | cut -d'"' -f2)" \
  "https://api.cloudflare.com/client/v4/zones?name=benstull.art" | python3 -c "import sys,json;print(json.load(sys.stdin)['result'][0]['id'])")
wrangler r2 bucket domain add human-experience-simulator-media \
  --domain static.benstull.art --zone-id "$ZONE" --min-tls 1.2

Media upload — there is NO wrangler r2 bucket sync. Upload per-file with wrangler r2 object put ... --remote, setting content-type + immutable cache. The URLs are content-hash-versioned (?v=<hash>), so immutable is safe — a re-baked clip gets a new hash → new URL → cache busts. ~604 files / ~2.3 GB, sequential:

WR=$(command -v wrangler); BUCKET=human-experience-simulator-media
find dist-media -type f | while read -r f; do
  key="${f#dist-media/}"
  case "${f##*.}" in mp4) ct=video/mp4;; mp3) ct=audio/mpeg;; json) ct=application/json;; *) ct=application/octet-stream;; esac
  "$WR" r2 object put "$BUCKET/$key" --file "$f" --remote \
    --content-type "$ct" --cache-control "public, max-age=31536000, immutable"
done

(If you have rclone + an R2 S3 API token, rclone copy dist-media <remote>:bucket is much faster than the loop.)

If you change the media base, rebuild with --media-base https://<host>/ so config.js matches.

3. Pages — deploy + domain + redirect

wrangler pages project create human-experience-simulator --production-branch main
wrangler pages deploy dist --project-name human-experience-simulator --branch main

Then in the dashboard (Workers & Pages → project → Custom domains) — there is no wrangler CLI for Pages custom domains:

  • Bind benstull.art to the project. (Allow a few minutes — a fresh bind serves 522 until the cert/routing provisions.)
  • The app serves at benstull.art/human-machine-strata/; the shipped dist/_redirects sends / and the no-slash form to it.

4. Verify before/after going live

# CORS + Range on the media origin (after step 2)
curl -sI -H "Origin: https://benstull.art" -H "Range: bytes=0-1" \
  https://static.benstull.art/<some>/base.mp4 | grep -iE "access-control-allow-origin|content-range|accept-ranges"

Open https://benstull.art/ → should redirect to https://benstull.art/human-machine-strata/. Turn the Feel knob to max and confirm the dream effect renders (no SecurityError in the console — that would mean CORS is misconfigured and the GL texture is tainted). The local static-build E2E (simulator/e2e/tests/static-build.spec.ts) exercises this same cross-origin path before you ship.