destroyulator/web
Monster Robot Party 17797b9ba0 LANE5: deploy web toy to digalot.fyi/destroy + depot already hardened
Ship the browser smash toy publicly and document it.

Deploy: web/ is live at https://digalot.fyi/destroy/ — served on the dealgod VPS like
thriftgod (an nginx:alpine static container on dealgod_default + a /destroy/ location in
the shared reverse proxy). README.md now carries the exact deploy/update steps + a full
rollback runbook. No app code changed.

3GOD hardening (LANE5 deliverable B) was found ALREADY DONE on the live instance and
verified — not re-changed:
  - GOD3_OPEN is off: unauthenticated write via Cloudflare → 403; GET /api/list → authed:false.
  - SSRF plugged: POST /api/fetch with file:///etc/hostname and http://169.254.169.254/ →
    rejected ("only public http(s) URLs allowed"), nothing written.
  - Reads stay public (CF GET 200 + CORS *); trusted tailnet peers still push passwordless
    via the TS_ALLOW IP allowlist (this session's meshgod pushes used that path).

Verified public: /destroy/ 200 over CF, JS as application/javascript, props (intact +
fractured) load from /3god/a/* with CORS *, printer boss shatters into 16 GLB chunks,
no console errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 23:24:49 +10:00
..
assets LANE4 (web): catalog Lane 2 batch, printer boss, 404 fix, drag-to-look 2026-07-14 22:28:25 +10:00
index.html Lane 3: browser smash toy in web/ — FP record store, Rapier destruction, 3GOD props 2026-07-14 20:14:32 +10:00
juice.js Lane 3: browser smash toy in web/ — FP record store, Rapier destruction, 3GOD props 2026-07-14 20:14:32 +10:00
main.js LANE4 (web): catalog Lane 2 batch, printer boss, 404 fix, drag-to-look 2026-07-14 22:28:25 +10:00
physics.js Lane 3: browser smash toy in web/ — FP record store, Rapier destruction, 3GOD props 2026-07-14 20:14:32 +10:00
props.js LANE4 (web): catalog Lane 2 batch, printer boss, 404 fix, drag-to-look 2026-07-14 22:28:25 +10:00
README.md LANE5: deploy web toy to digalot.fyi/destroy + depot already hardened 2026-07-14 23:24:49 +10:00

DESTROYULATOR — web smash toy (Lane 3)

A shareable, one-click browser toy: walk a record store first-person, pull a record, and wreck the place. Not a port of the Godot game — a different product in the same universe, built on thriftgod's proven FP-interior patterns and wired to the same 3GOD prop depot.

Zero build step. Three.js + Rapier load over an importmap, exactly like thriftgod.

Run it

cd web && python3 -m http.server 8099
# then open http://localhost:8099

Chrome and Safari both work (Safari is the weakest WASM target — it's tested). Append ?debug to expose a window.DESTROY handle for driving the sim without Pointer Lock (embedded frames / smoke tests).

Controls

  • WASD / arrows — move · mouse — look (click to grab pointer lock)
  • click — smash whatever you're looking at
  • E (or click) on a record — pull it out of the bin; click again to throw it
  • Esc — pause · 📸 share your mess — screenshot + smash-count caption

Architecture

File Role
index.html Importmap (three r0.175 + @dimforge/rapier3d-compat), HUD, boot
main.js FP controller, room, prop placement, interaction, record ritual, share, loop
physics.js Rapier world, one body per prop, support-graph cascade, pre-fractured swap, debris pooling
props.js GLTFLoader/DRACOLoader; loads props live from 3GOD /a/<file> with a assets/ mirror fallback
juice.js Screenshake, hitstop, particle bursts, per-material Web-Audio smash voices, combo counter
assets/ Offline mirror: record/crate/rack (from the Godot lane) + cashbot/council-bin (from 3GOD)

Shared contracts (see ../LANES/PLAN.md)

  • Props load from the 3GOD depot at runtime (GET /api/list, GLB at /a/<slug>), falling back to the local assets/ mirror so the demo never hard-fails offline. The depot serves Access-Control-Allow-Origin: *, so cross-origin loading works.
  • Fracture format (contract #2): a smashable with a sibling <name>.fractured.glb (root children named chunk_*) is swapped in on smash — each chunk becomes a short-lived dynamic body. Until Lane 2 ships those, brittle props use generative shards. props.loadFractured() already probes for them.
  • Read-only against the depot. No write calls (the depot's open-mode/SSRF issue is a write-side concern — see PLAN.md).

Notes

  • Rapier is -compat (WASM inlined as base64) so await RAPIER.init() needs no extra fetch and no COOP/COEP headers.
  • Active physics bodies are capped and settled debris is despawned, so a long smashing session stays bounded (JS GC is the ceiling).

Deploy (public) — https://digalot.fyi/destroy/

Live on the digalot.fyi VPS (dealgod), served exactly like thriftgod: a small nginx:alpine static container on the dealgod_default docker network, with a location /destroy/ proxy in the shared reverse proxy. Zero build step — it's the raw web/ folder. LANE5, 2026-07-14.

Update the live toy (after committing changes to web/), from the monorepo on a tailnet machine with root@dealgod access:

rsync -az --delete --exclude '.DS_Store' web/ root@100.94.195.115:/opt/destroyulator/web/
# static files are volume-mounted read-only, so no container restart is needed;
# hard-refresh past the CF/browser cache. To force-cycle the container:
ssh root@100.94.195.115 'docker restart destroyulator'

How it was wired (one-time):

  1. rsync web/ → dealgod:/opt/destroyulator/web/
  2. docker run -d --name destroyulator --restart unless-stopped --network dealgod_default \ -v /opt/destroyulator/web:/usr/share/nginx/html:ro nginx:alpine
  3. Added to the digalot.fyi server block in /opt/dealgod/nginx/dealgod-servers.conf:
    location = /destroy { return 301 /destroy/; }
    location /destroy/ { proxy_pass http://destroyulator/; proxy_set_header Host $host;
                         proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 60s; }
    
    then docker exec dealgod-nginx nginx -t && docker exec dealgod-nginx nginx -s reload.

Rollback (full teardown, no effect on other sites):

ssh root@100.94.195.115 '
  cp /opt/dealgod/nginx/dealgod-servers.conf.bak-lane5 /opt/dealgod/nginx/dealgod-servers.conf
  docker exec dealgod-nginx nginx -s reload
  docker rm -f destroyulator
  rm -rf /opt/destroyulator'

The depot (/3god) is already hardened — writes require the tailnet-peer allowlist or the 3g auth cookie, /api/fetch rejects file:// + private/metadata hosts, reads stay public with CORS *. The toy only reads, and the assets/ mirror covers a depot outage.