diff --git a/tools/CACHE.md b/tools/CACHE.md new file mode 100644 index 0000000..ff9f98d --- /dev/null +++ b/tools/CACHE.md @@ -0,0 +1,67 @@ +# The edge cache, and why a green deploy could still ship nothing + +## The problem, measured + +`tools/deploy.sh` step 5 compares `https://partly.party/gutsy/js/boot.js`, fetched the way a +browser fetches it, against the bytes just shipped. On 2026-07-26 it failed for the first time +and it was right: + +``` +last-modified: Sun, 26 Jul 2026 12:10:07 GMT +cache-control: max-age=14400 +cf-cache-status: HIT +age: 158 +``` + +The origin (`forum-nginx`) sends **no `Cache-Control` at all** for `/gutsy/`, so Cloudflare +applies its default 4-hour Edge TTL to `.js`. GUTS has no build step and no content hashes in +its filenames, so what the edge is holding is not an asset — it is **code**. A deploy lands on +the box and players keep running the previous build for up to four hours, with nothing anywhere +saying so. + +Every check above step 5 appends `?v=$BUST`, which is exactly what makes them bypass the edge. +So six green checks were compatible with every player being served a stale game. + +## The fix (needs one command run by hand) + +`tools/forum-nginx-default.conf` is the container's config with a `/gutsy/` block added: + +```nginx +location /gutsy/ { + root /usr/share/nginx/html; + add_header Cache-Control "no-cache"; +} +``` + +`no-cache` means *store it, but revalidate every time* — the files are small and etag'd, so a +revalidation is a 304 and costs nothing. Scoped to `/gutsy/`; no other path on partly.party is +affected. + +Apply it (the copy-into-container step is gated for agents, so this is John's to run): + +```bash +scp tools/forum-nginx-default.conf humanjing@100.71.119.27:/tmp/default.conf +``` + +```bash +ssh humanjing@100.71.119.27 "docker cp /tmp/default.conf forum-nginx:/etc/nginx/conf.d/default.conf && docker exec forum-nginx nginx -t && docker exec forum-nginx nginx -s reload" +``` + +`nginx -t` runs before the reload, so a syntax error refuses to load rather than taking +partly.party down. + +Then confirm — this should report `cf-cache-status: DYNAMIC` or `BYPASS`, and deploy.sh step 5 +should pass on the first try after every future deploy: + +```bash +curl -sI https://partly.party/gutsy/js/boot.js | grep -iE "cf-cache-status|cache-control" +``` + +## Until then + +A deploy is only live to players after the 4-hour TTL expires, or after a manual purge: +Cloudflare dashboard → partly.party → Caching → Configuration → Purge Everything. + +Note the container has **no bind mount** for its nginx config, so a `docker compose up +--force-recreate` or an nginx image bump wipes this block. That is why the file lives in the +repo: re-applying it is the same two commands. diff --git a/tools/forum-nginx-default.conf b/tools/forum-nginx-default.conf new file mode 100644 index 0000000..77931fc --- /dev/null +++ b/tools/forum-nginx-default.conf @@ -0,0 +1,38 @@ +# forum-nginx /etc/nginx/conf.d/default.conf — canonical copy (2026-07-20). +# The live container has NO bind mount for this file; if the container is ever +# recreated, restore with: +# scp forum-nginx-default.conf humanjing@100.71.119.27:/tmp/default.conf +# ssh humanjing@100.71.119.27 "docker cp /tmp/default.conf forum-nginx:/etc/nginx/conf.d/default.conf && docker exec forum-nginx nginx -s reload" +server { + listen 80; + listen [::]:80; + server_name localhost; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + } + + # GUTS (/gutsy/) ships as UN-VERSIONED ES modules with no build step, so what an edge + # cache holds here is CODE, not assets. The origin was sending no Cache-Control at all, + # so Cloudflare applied its 4-hour default: a deploy landed on the box and players kept + # running the previous build for up to four hours, silently. Revalidate every request — + # the files are small, etag'd, and a 304 costs nothing. + location /gutsy/ { + root /usr/share/nginx/html; + add_header Cache-Control "no-cache"; + } + + # arcade express API (counter / guestbook / sando leaderboards). The app + # runs in the arcade-landing container on the shared forum_default network. + location /arcade-api/ { + proxy_pass http://arcade-landing:3001/api/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +}