# 21 — Generators (`generators.${BASE_DOMAIN}`) > Minecraft data pack generators — a pinned, offline-only fork of > [misode.github.io](https://github.com/misode/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/-/…`). 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/.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 `/`. - **A status code is not enough — assert the BODY.** Observed on 2026-07-21, with the nginx vhost installed but the container not yet deployed: caddy answers an unmatched `Host` with an **empty `200`**, so *both* `/` and the mirror path above returned `HTTP 200, 0 bytes` while literally nothing was running. Any check that looks only at the status code reports green on a completely absent service. Match on content (or a minimum body size): ``` curl -s https://generators.${BASE_DOMAIN}/mcmeta/1.21.1-summary/registries/data.min.json \ | head -c 40 # must be JSON, ~536 KB total — not empty ``` - **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.