diff --git a/website/MODULE_API.md b/website/MODULE_API.md index 72aa5a9..68c1d24 100644 --- a/website/MODULE_API.md +++ b/website/MODULE_API.md @@ -703,7 +703,13 @@ half needs rethinking before Phase 2 builds on it. `app.js` scans `modules/*/module.json` with `fs.readdirSync` at require time and mounts what it finds (`MODULE_SYSTEM.md` §1.12). The database is not consulted. `MODULES_DIR` defaults to -`/modules` and is overridable by env for tests and for the Docker volume mount. +`/modules` and is overridable by env for tests and for the Docker mount. Under Compose it is +set to `/app/modules`, where `./modules` is bind-mounted read-write (`MODULE_SYSTEM.md` §2.5); the +image itself carries that directory empty and owned by the container user, and `.dockerignore` +excludes any local one so a module can never be baked in. + +**A missing modules directory is not an error.** The scan catches and returns, because "no modules +installed" is the normal state of bare core and the loader must not make the mount mandatory to boot. **The trigger is one explicit call, and there is no lazy self-scan** (§7.6). `app.js` calls diff --git a/website/MODULE_SYSTEM.md b/website/MODULE_SYSTEM.md index 82be038..5440d57 100644 --- a/website/MODULE_SYSTEM.md +++ b/website/MODULE_SYSTEM.md @@ -428,8 +428,12 @@ leaves the row `disabled`, which the reset never touches. ### 2.5 Install, uninstall, purge -Modules live on a **mounted volume**, not in the image — the same treatment `uploads` already gets in -`docker-compose.yml`. That is what makes the WordPress model work against a pull-only image. +Modules live on a **mounted volume**, not in the image. That is what makes the WordPress model work +against a pull-only image. *(Settled in Phase 2 PR 9: it is a **bind mount** of `./modules`, not the +named volume this sentence originally reached for by analogy with `uploads` — hand-placing a module +directory is a supported install below, and a named volume would route it through `docker cp`. The +image's copy is excluded by `.dockerignore`, so a module in a builder's working tree can never ship +inside an image; see `modules/README.md` in the website repo.)* **Install:** admin selects the module → bundle downloaded from the module repo's release and verified against its `sha256` → unpacked into `modules//` on the volume → `installed_modules` row written → @@ -511,14 +515,14 @@ too (API §7.2). 7. Client `src/modules/registry.js`, the `window.__rg` shared-dependency global, the chunk's static mount and the `htmlShell` script injection — empty registry, no visible change. 8. `MOD_PATHS` → `roles`-derived (§1.4); the generic feature-provider seam (§1.5). -9. `docker-compose.yml` gains the `modules` volume. +9. `docker-compose.yml` gains the `modules` mount. Exit criterion: no **existing** URL moves and every existing test passes. If Phase 2 changes one URL, it is wrong. PR 6 is the single deliberate exception in the phase and it *adds*: `routes.manifest.json` gains exactly one line, `GET /api/v1/public/modules`, and nothing else in the file moves. Every other PR in Phase 2 produces a zero-line diff. -**Progress: PRs 1-7 done.** +**Progress: complete — all nine PRs landed.** - **PR 1** — `installed_modules` and the state machine, with the stored shape and the boot rules settled in §2.4 above. @@ -678,6 +682,54 @@ fixed enum because the leg set is whatever has been registered. admin can relabel a module row in Admin → Navigation and have it persist and apply — the whole point of merging before the override layer. Zero CSP reports, zero console errors. +- **PR 9** — the mount itself, which closes the phase: `docker-compose.yml` gains `./modules` at + `/app/modules`, the `Dockerfile` creates that directory node-owned, `.dockerignore` keeps any local + one out of the image and `modules/README.md` documents the directory for whoever opens it. + + It is a **bind mount, not the named volume** §2.5 assumed by analogy with `uploads`. Placing a + module directory by hand is a supported install, and a named volume makes that a `docker cp` into a + running container — the one install path an operator without the admin panel has, routed through + the least discoverable mechanism Docker offers. A bind mount makes it `tar -xf … -C ./modules`, and + makes the installed set something an operator can *see*. §2.5 is amended above; nothing else about + install, uninstall or purge changes. + + Two consequences worth stating, because both are silent failures rather than errors. The directory + is **tracked** — via its README, the same shape `brand/` already uses — because Docker recreates a + missing bind-mount source as `root:root`, and the container runs as uid 1000: delete `modules/` from + a checkout and the next install fails on a permission error that names no cause. And the mount is + **read-write**, since §2.5's install unpacks into it from inside the container; deferring that to + Phase 4 would have bought nothing, as a mount-mode change is a redeploy either way. + + `.dockerignore` matters more than it looks. `COPY . .` would otherwise bake whatever module the + builder had checked out into every image — and because Docker seeds a *fresh* named volume from + the image's contents, that module could have appeared on a production deployment that never + installed it. The exclusion is what makes "modules live on a mount, never in the image" true rather + than merely intended. + + Verified against a **running container**, which is the only thing that can check any of the above — + a compose file that parses proves nothing about ownership, and nothing about what the image + contains. The image carries an empty, node-owned `/app/modules` despite a module sitting in the + build context. A module on the mount loads, mounts, runs `onBoot` and reaches `started`; + `/api/v1/public/modules` lists it; its chunk serves from the entry's directory alone, with the + module's own server source and `module.json` both `404`. The [§7.7](MODULE_API.md#77-the-client-half-has-to-be-verified-in-a-browser--the-timing-bug-no-test-could-see) + browser smoke was re-run against the containerised stack rather than a working tree: in Chrome, the + page renders on first paint inside core's `PublicLayout`, drawing React and the UI kit off + `window.__rg`, with its nav row interleaved into core's public nav — under the enforced + `script-src 'self'`, with zero CSP reports and no console errors. Removing the directory by hand and + restarting reconciles the row to `startup_failed`/`require` exactly as §2.4 says, and leaves core + healthy with no script injected and `{"modules":[]}` published. + + **933 server tests** and **160 client tests** pass, both unchanged — this PR ships no application + code. `routes.manifest.json` stays at 230 routes and the OpenAPI spec regenerates byte-identical. + The one source change is a comment: `scripts/routeManifest.js` enumerated the filesystem-conditional + mounts it excludes and had never been told about `/modules`. Its filter is an allowlist, so the + behaviour was always right and only the explanation was stale. + +**Phase 2 is complete.** Core can discover, validate, mount, migrate, boot, publish, serve and +navigate a module it does not contain, on a deployment that builds nothing — and it does all of that +while no existing URL has moved. The exit criterion held: `routes.manifest.json` went 229 → 230 across +the whole phase, and the one added line is PR 6's deliberate `GET /api/v1/public/modules`. + **Phase 3 — Extract `module-uo`.** Moves out of `website/`: the 8 model directories and their 25 tables; the nine UO `utils/` files plus `newsGump.js`; the 13 router/controller files; `scripts/importSpawnAtlas.js` and `db/spawnAtlas.art.json`; `usersShard.controller.js` **minus