docs(website): where the module state machine lives, and how boot treats it #125
@@ -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
|
`~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.
|
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
|
## 4. API contract
|
||||||
|
|||||||
@@ -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
|
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.
|
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
|
### 2.5 Install, uninstall, purge
|
||||||
|
|
||||||
Modules live on a **mounted volume**, not in the image — the same treatment `uploads` already gets in
|
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
|
Exit criterion: `routes.manifest.json` diff is zero lines and every existing test passes. If Phase 2
|
||||||
changes one URL, it is wrong.
|
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
|
**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;
|
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
|
`scripts/importSpawnAtlas.js` and `db/spawnAtlas.art.json`; `usersShard.controller.js` **minus
|
||||||
|
|||||||
Reference in New Issue
Block a user