From 3510c2ecf18f1c6c97e66807c6047d4daf976eba Mon Sep 17 00:00:00 2001 From: wtclaude Date: Mon, 10 Aug 2026 06:35:37 -0500 Subject: [PATCH] docs(website): settle where the module state machine lives and how boot treats it Phase 2 PR 1 of the module system records two things 2.4 left open: the states are stored in one `state` column rather than a policy flag beside a runtime one, and every boot recomputes the outcome states while leaving `disabled` alone. That second rule is the one with consequences worth writing down -- a startup_failed module is retried on every restart, so an operator who fixes the cause needs no admin-panel visit; a running module can never display a stale failure reason; and disabling, the one operator decision rather than outcome, survives restarts. Also states what the row does NOT decide: the loader scans the filesystem before the database is reachable, so the URL surface is a property of the volume, which is what keeps routes.manifest.json generatable against a dead database. BACKEND_DESIGN.md 3 gains the installed_modules columns alongside the other tables. Co-Authored-By: Claude --- website/BACKEND_DESIGN.md | 28 ++++++++++++++++++++++++++++ website/MODULE_SYSTEM.md | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/website/BACKEND_DESIGN.md b/website/BACKEND_DESIGN.md index 7328a98..bd88772 100644 --- a/website/BACKEND_DESIGN.md +++ b/website/BACKEND_DESIGN.md @@ -665,6 +665,34 @@ JSON. `resolveMany()` returns only ids that resolved to something displayable `~1_val~` are stripped, since the bridge sends the id and never the property packet that carries the arguments — and it never throws, because a cliloc lookup is decoration on a character sheet. +### installed_modules — what is installed, and what happened to it (module system) + +One row per installed module, keyed by the `id` from its `module.json` — the same id that names its +directory on the modules volume and its URL segment. + +| Column | Shape | +|---|---| +| `id` | VARCHAR(32) PK — the module id | +| `name`, `version` | the manifest's label and semver, for the admin Modules screen | +| `state` | ENUM `installed` / `enabled` / `disabled` / `started` / `startup_failed` | +| `failure_stage`, `failure_reason` | the stage a failure happened at (`manifest`, `core_api`, `mounts`, `extensions`, `schema`, `require`, `register`, `boot`) and its recorded reason | +| `source`, `sha256` | the release the bundle came from and the digest verified before unpacking; both NULL for a directory placed on the volume by hand | +| `installed_at`, `started_at`, `updated_at` | `started_at` is the last **successful** start | + +**This table never decides which routes exist.** The module loader scans the filesystem at require +time, before the database is reachable, so the URL surface is a property of the volume — which is what +lets `routes.manifest.json` be generated against a dead database. A disabled module stays mounted and +is guarded; the row decides whether it *answers*, not whether it is there. + +**Every boot resets each non-disabled row to `enabled`** and clears its recorded failure, then the load +writes that boot's outcome. So a `startup_failed` module is retried on the next restart (an operator +who fixes the cause needs no admin-panel visit), a running module can never display a stale reason, +and `disabled` — the one operator *decision* rather than outcome — survives untouched. A re-install or +upgrade refreshes the metadata and leaves `state` alone. + +Design of record: [`MODULE_SYSTEM.md`](MODULE_SYSTEM.md) §2.4; the loader's obligations are +[`MODULE_API.md`](MODULE_API.md) Part 4. + --- ## 4. API contract diff --git a/website/MODULE_SYSTEM.md b/website/MODULE_SYSTEM.md index 6a3c406..06f676b 100644 --- a/website/MODULE_SYSTEM.md +++ b/website/MODULE_SYSTEM.md @@ -337,6 +337,41 @@ marks that one module `startup_failed`, records the reason, and the site comes u routes and nav absent. `startup_failed` is recoverable from the admin panel — disable, retry, or roll back to the previous version — with no shell access to the box. +**Where the states live.** One `installed_modules` row per module, keyed by its id, with the machine +held in a single `state` column carrying all five values — the shape this section already describes, +rather than a policy flag beside a runtime one. The table also carries `name`/`version` for the admin +screen, `failure_stage` + `failure_reason` for [`MODULE_API.md`](MODULE_API.md) §4.4's recorded +reason, `source` + `sha256` for the +install provenance of §2.5 below (both null for a directory placed on the volume by hand, which stays +supported), and `installed_at` / `started_at` / `updated_at`. Full column list in +[`BACKEND_DESIGN.md`](BACKEND_DESIGN.md) §3. + +**The row is a record of what happened, never the source of truth for what is mounted.** The loader +scans the filesystem at require time, before the database is reachable (API §4.1), so the URL surface +is a property of the volume and not of a row here. What the row decides is whether a mounted module +*answers* (`disabled` ⇒ its guard 404s, API §4.5) and what the admin panel shows after a failure. +This is also why +`routes.manifest.json` can be generated against a dead database. + +**`disabled` is the only state a boot leaves alone.** Every boot resets each non-disabled row to +`enabled`, clearing any recorded failure, and the load that follows writes this boot's outcome — +`started` or `startup_failed`. Three consequences, all deliberate: + +- **A `startup_failed` module is retried on every restart.** An operator who fixes the underlying + cause — a truncated file, a missing dependency, a database that was not up yet — gets the module + back by restarting, with no admin-panel visit. The cost is that a deterministically broken module + re-records its failure each boot, which is the honest thing for it to do. +- **A stale reason can never be shown against a running module**, because every non-failing + transition clears the failure columns. +- **Disabling is an operator decision, not an outcome**, so it survives restarts untouched — and a + module the operator switched off is neither started nor re-recorded as failed if it happens to be + broken. `installed` is likewise transient: it is the gap between an install writing the row and the + restart that resolves it. + +A re-install or an upgrade refreshes `name`/`version`/provenance and deliberately leaves `state` +alone: upgrading an enabled module must not silently switch it off, and re-installing a disabled one +must not silently switch it on. + ### 2.5 Install, uninstall, purge Modules live on a **mounted volume**, not in the image — the same treatment `uploads` already gets in @@ -418,6 +453,10 @@ too (API §7.2). Exit criterion: `routes.manifest.json` diff is zero lines and every existing test passes. If Phase 2 changes one URL, it is wrong. +**Progress: PR 1 done** — `installed_modules` and the state machine, with the stored shape and the +boot rules settled in §2.4 above. No loader, no routes, no boot wiring yet, so it changes nothing an +operator or a client can see. + **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 -- 2.49.1