Files
ulicraft-server-v1/plan/21-generators.md
T
oier b6b02398da feat(generators): offline data pack generators at generators.
Add generators.${BASE_DOMAIN}: a pinned fork of misode.github.io served
with its own mcmeta + vanilla-mcdoc mirror, so it works with zero internet
at the LAN party. Fork lives in ulicraft-generators (branch ulicraft),
pinned at upstream 2bfe5522.

The app is 100% client-side: every data fetch comes from the player's
browser, not from cochi. So offline capability is entirely about which
origins the bundle refers to, plus a mirror to serve them from. The vhost
sets Content-Security-Policy with connect-src 'self' as ENFORCEMENT: an
external origin that sneaks back in (or returns via an upstream merge)
then breaks loudly in prod, on an ordinary day, instead of silently at the
venue with no internet to debug it.

sync-generators-data.sh mirrors ~109M: the 1.21.1 summary/data/atlas tags,
assets-tiny (assets minus hundreds of MB of .ogg — hence no sounds
generator), vanilla-mcdoc symbols.json, and summary/versions/data.min.json,
which comes from a BRANCH head rather than a version tag and runs during
Spyglass init. It verifies a sentinel file per source instead of trusting a
clean clone exit, since a renamed upstream path would otherwise sync
"successfully" and only fail in the browser at the party.

The container serves the bundle and the mirror on one origin, so there is
no CORS, no second cert and no second DNS name.

DEPLOY ORDER MATTERS. GENERATORS_REPO must be set in cochi's .env BEFORE
pulling this: compose uses ${GENERATORS_REPO:?...}, and an unset required
variable makes docker compose fail to parse the file — taking down every
service including Minecraft, not just this one. Full runbook and the rest
of the prerequisites: plan/21-generators.md.

Verified offline in real Chromium with every non-origin request aborted:
zero off-origin attempts, zero same-origin failures, zero console errors,
generators rendering schemas and live JSON.
2026-07-21 18:59:01 +02:00

8.0 KiB
Raw Blame History

21 — Generators (generators.${BASE_DOMAIN})

Minecraft data pack generators — a pinned, offline-only fork of misode.github.io, served with its own data mirror so it works with zero internet at the LAN party.

Why self-host at all

Upstream is public, free and CDN-hosted, so a plain mirror would buy nothing. Three things justify this:

  1. Offline. At the party the machine is present but the network is not.
  2. Version pin. The generators can never drift ahead of a 1.21.1 server.
  3. Modded schemas. public/mcdoc/ is the extension point for mods (see C-lite / C-full below).

The one fact that drives every decision

The app is 100% client-side. Every data fetch comes from the player's browser, not from cochi. So:

  • "cochi has no WAN at the party" protects nothing — a leftover upstream URL just fails in the browser, at the venue, with no internet to debug it.
  • Blocking the container's egress proves nothing either.
  • The only things that make it offline-capable are (a) which origins the bundle refers to, and (b) the mirror we serve them from.

That is why the CSP is enforcement, not decoration — see Verification.

Architecture

host nginx (TLS, LE cert for generators.) ─► caddy ─► generators:80
                                              │        (caddy in-container)
   caddy/conf.d/70-generators.caddy  ── sets Content-Security-Policy
                                              │
   generators container: /srv/app   ← built Preact/Vite bundle (from the fork)
                         /srv/data  ← ~109M mirror, bind-mounted :ro

One origin serves both the app and its data. No CORS, no second cert, no second DNS name.

Piece Where
Fork git.fosil.eu/oier/ulicraft-generators, branch ulicraft, GENERATORS_REPO in .env
Dockerfile + Caddyfile in the fork (it is unpublished upstream, so unlike nmsr it cannot clone a pinned ref)
Mirror sync tooling/sync-generators-data.sh → GENERATORS_DATA_DIR (default ./generators-data, gitignored)
Stack vhost caddy/conf.d/70-generators.caddy
TLS vhost nginx/ulicraft-caddy.conf.tmpl

The mirror (~109M)

mcmeta publishes per-version snapshots as tags (~4000 of them), not branches.

Source Ref Size
misode/mcmeta tag 1.21.1-summary 31M
misode/mcmeta tag 1.21.1-data 34M
misode/mcmeta tag 1.21.1-assets-tiny → served as 1.21.1-assets 36M
misode/mcmeta tag 1.21.1-atlas 3.5M
misode/mcmeta branch summary, versions/data.min.json only 172K
SpyglassMC/vanilla-mcdoc branch generated, symbols.json 2.4M

Three non-obvious points:

  • assets-tiny is assets minus the .ogg sounds (hundreds of MB). It is served at the -assets path because that is what the app requests. Sound playback would 404, which is why the fork deletes the sounds generator.
  • summary/versions/data.min.json comes from the branch head, not a tag (DataFetcher.ts:205), and it runs during Spyglass init. Miss it and the app dead-ends offline while looking perfectly healthy online.
  • The directory layout mirrors upstream's URL shape (/mcmeta/<ref>-<type>/…). That is what keeps the fork patch to a couple of constants.

The sync script verifies a sentinel file per source rather than trusting a clean git clone exit — a renamed upstream path would otherwise "sync successfully" and die in the browser at the party.

Fork diff (pinned at upstream 2bfe5522)

Change File
mcmetaUrl → /mcmeta, vanillaMcdocUrl → /vanilla-mcdoc services/DataFetcher.ts
Removed jsdelivr fallback services/DataFetcher.ts
Removed hardcoded immersive_weathering raw.githubusercontent URL services/DataFetcher.ts
Version locked to 1.21 services/Versions.ts (MIRRORED_VERSION)
Switcher filters the view, not the data components/VersionSwitcher.tsx
Routes /sounds, /changelog, /versions, /whats-new deleted App.tsx, pages/Home.tsx
Share disabled (write API, unmirrorable) services/Sharing.ts
gtag → no-op, EthicalAds/Giscus/contributors removed, footer + SOURCE_REPO_URL index.html, Home.tsx, Footer.tsx, Utils.ts

Version lock — the trap

checkVersion() compares versions by index (config.versions.findIndex). Deleting entries from config.versions / VersionIds makes a removed minVersion resolve to -1, so -1 <= version passes and generators that do not exist in 1.21 silently render. Filter the view, never the data.

Upstream policy: pin and freeze

Do not chase master. Upstream ships 26.x while this server sits at 1.21.1, and every merge risks reintroducing an external origin. Merges are a rare, deliberate chore — and the CSP is what catches a regression.

C-lite vs C-full (modded generators)

Currently C-lite. public/mcdoc/create.mcdoc and neoforge.mcdoc ship upstream and cover the pack's two biggest datapack surfaces. .mcdoc files are fetched same-origin from /mcdoc/<dep>.mcdoc, so adding a mod = drop a file in public/mcdoc/ + add entries to src/config.json.

C-full (autocomplete for the pack's own items/blocks) additionally needs:

  • a registry dump from the live server — KubeJS is already in the pack and already mounted, so a startup script can dump every registry;
  • a patch to fetchRegistries, which currently hardcodes the minecraft: namespace (data[id].map(e => 'minecraft:' + e)), so modded ids cannot enter autocomplete from data alone;
  • modded item icons would still be missing (the atlas is vanilla-only).

mcdoc is hand-written per mod (upstream's range: 19–1389 lines), so C-full needs a named shortlist, never "the pack".

Deploy

Prerequisites on cochi, in this order — the first one can take the whole stack down if skipped:

  1. GENERATORS_REPO must be set in cochi's .env. compose uses ${GENERATORS_REPO:?…}, and an unset required variable makes docker compose up fail to parse the file — every service, including Minecraft, not just this one.
  2. Clone the fork on cochi at that path (branch ulicraft).
  3. tooling/sync-generators-data.sh on cochi (~109M, needs WAN).
  4. generators in LE_SUBDOMAINS → tooling/issue-letsencrypt.sh.
  5. Only then tooling/render-nginx.sh --install (nginx fails its config test if the cert file does not exist).
  6. DNS A generators.${BASE_DOMAIN} — and the party DNS too.

Then the normal deploy (deploy skill): git pull + docker compose up -d --build.

Verification

A 200 on / proves the server is up, not that the app works. This is an SPA whose failure mode is a healthy-looking shell over dead data.

  • curl /mcmeta/1.21.1-summary/registries/data.min.json — proves the mirror is actually mounted. An Uptime Kuma monitor should probe this, not /.
  • Offline acceptance test: a real browser with every non-origin request blocked, walking the generators you intend to use. tooling/-adjacent harness lives in the scratchpad; the check is: zero off-origin attempts, zero same-origin failures, zero console errors, schemas actually rendered.
  • The CSP (connect-src 'self') is the standing enforcement: an external origin that sneaks back in breaks loudly in prod, on an ordinary day, instead of silently at the party.

script-src/style-src are deliberately permissive for now — Spyglass/deepslate wasm+eval needs were not characterised under deadline. Tightening them is a post-party task; loosening connect-src is not.

Party-day notes

  • HSTS is max-age=31536000; includeSubDomains on every vhost, so there is no HTTP fallback and no cert-warning click-through. The cert must be valid.
  • LE certs bind names, not IPs, so a party DNS override pointing generators.${BASE_DOMAIN} at the LAN IP validates fine — provided the cert is unexpired and was issued while WAN existed.