Phase 4 slice 4. BACKEND_DESIGN.md had never been de-UO'd: Phase 3 rewrote core's code and README, but §5.2's identifier check reads code, not prose, so nothing ever looked at the design document. It still described 27 shard_* tables, 20 UO route rows and the shard visibility ladder as core's, a phase after core stopped being able to serve any of them. Moved, text unchanged: BACKEND_DESIGN §3 six shard_* schema sections (226 lines) → modules/uo/SCHEMA.md BACKEND_DESIGN §4 13 public + 7 admin UO route rows → modules/uo/API.md BACKEND_DESIGN §6.5 the audience ladder (70 lines) → modules/uo/API.md §4 Core keeps the seam and gains the eight /admin/modules routes it had never documented. §6.5 becomes "Module-owned audience boundaries": core's security boundary ends at authentication, roles and the session, and a module that serves game data brings its own. Also fixed on the way: users.router.js was still listed as 15 routes (it is 9 — six went to the extension slot), and the push section still promised config/shardStreams.js "moves out with it" four slices after it left. The acceptance table now carries results. Criterion 2 was proved for real against module-uo v0.3.0 on an empty database, which turned up the uninstall ordering defect fixed in website#146. AI disclosure: this contribution was AI-assisted (Claude Code). Co-Authored-By: Claude <noreply@anthropic.com>
127 lines
9.1 KiB
Markdown
127 lines
9.1 KiB
Markdown
# module-uo — the Ultima Online module
|
|
|
|
Everything specific to Ultima Online that the website serves. Code:
|
|
[RunicGateway/Module-uo](https://gitea.whitlocktech.com/RunicGateway/Module-uo). Module id `uo`,
|
|
installed at `modules/uo/` on the website's modules volume.
|
|
|
|
This page is **orientation**: what the module is, what it serves, what it owns, and what an operator
|
|
has to know. It is not a second copy of the contract — [`../../website/MODULE_API.md`](../../website/MODULE_API.md)
|
|
is normative for anything about how a module and core fit together, and the module's own repo is
|
|
authoritative for its file layout. Where this page and either of those disagree, they win.
|
|
|
|
Module documentation aggregates here rather than in module repos
|
|
([`MODULE_SYSTEM.md`](../../website/MODULE_SYSTEM.md) §2.10), so the feature docs that describe what
|
|
these routes *mean* are the ones that already existed and did not move:
|
|
|
|
| Doc | What it covers |
|
|
|---|---|
|
|
| [`API.md`](API.md) | **This module's 72 URLs** and the audience ladder that gates them — moved out of core's `BACKEND_DESIGN.md` §4/§6.5 |
|
|
| [`SCHEMA.md`](SCHEMA.md) | **The 27 tables it owns** and why they are shaped that way — moved out of core's `BACKEND_DESIGN.md` §3 |
|
|
| [`SHARD_VISIBILITY.md`](../../website/SHARD_VISIBILITY.md) | Who sees which shard data — the admin-configurable audience framework |
|
|
| [`SPAWN_ATLAS.md`](../../website/SPAWN_ATLAS.md) | The bestiary / spawn atlas, parsed from the shard's own ServUO tree |
|
|
| [`MARKETPLACE.md`](../../website/MARKETPLACE.md) | The player-vendor index |
|
|
| [`CLILOCS.md`](../../website/CLILOCS.md) | UO's id → name table |
|
|
| [`UOFIDDLER.md`](../../website/UOFIDDLER.md) | Operator runbook for extracting the cliloc table and creature art |
|
|
| [`../../link/PLAN.md`](../../link/PLAN.md), [`../../link/INTEGRATION.md`](../../link/INTEGRATION.md) | The wire protocol this module speaks to the sidecar |
|
|
|
|
---
|
|
|
|
## What it serves
|
|
|
|
**72 URLs**, frozen in the module's own
|
|
[`routes.manifest.json`](https://gitea.whitlocktech.com/RunicGateway/Module-uo/src/branch/main/routes.manifest.json)
|
|
and documented in its
|
|
[`swagger-fragment.json`](https://gitea.whitlocktech.com/RunicGateway/Module-uo/src/branch/main/swagger-fragment.json),
|
|
which core merges into `/api/docs.json` while the module is running. Route counts, gates and the
|
|
notes that used to sit in core's contract are in [`API.md`](API.md).
|
|
|
|
| Mount | Tier | What |
|
|
|---|---|---|
|
|
| `/api/v1/public/shard` | public | Status, activity feed, economy, presence, houses/IDOC, champs, guilds, governors, points boards, the player-vendor market, the ruleset, and the SSE event stream |
|
|
| `/api/v1/public/atlas` | public | The bestiary: creatures, regions, landmarks, champion altars, and what is loaded |
|
|
| `/api/v1/admin/shard` | admin | Shard ops (kick/ban/broadcast/pages), the visibility config, atlas and cliloc imports, market admin, account links |
|
|
| `/api/v1/admin/uo-link` | admin | The sidecar connection config, live status, and the town crier |
|
|
| `/api/v1/player/shard` | player | A player's own linked accounts: rosters, character sheets, vendors, sales |
|
|
| `/api/v1/admin/users/:id/shard/*` | admin | Six routes filling core's `admin.users.detail` **extension slot** — a module's routes hanging off a *core* resource, since core owns the user |
|
|
|
|
Every URL is byte-identical to the one core served before the extraction. That is the whole point of
|
|
moving the code and not the paths: the shipped Android app calls
|
|
`POST /api/v1/admin/shard/kick`, the Discord bot reads `/api/v1/public/shard/*`, and neither knows a
|
|
module answers now.
|
|
|
|
**In the SPA**: twelve public pages under `/uo/*`, the admin views under `/admin/uo/*`, the player
|
|
views under `/player/uo/*`, three extension-slot fills, and the nav rows for all of them, interleaved
|
|
into core's nav so an operator can reorder, relabel or hide them like any other row.
|
|
|
|
## What it owns
|
|
|
|
- **27 database tables** — 26 `shard_*` plus `uo_link_config`, documented one by one in
|
|
[`SCHEMA.md`](SCHEMA.md). Created by an idempotent
|
|
`schema.sql` fragment core replays on every boot, after its own schema. The `shard_`/`uo_link_`
|
|
prefixes are **grandfathered** ([`MODULE_API.md`](../../website/MODULE_API.md) §6.5): the rule for
|
|
a new module is `<id>_`, and these predate it.
|
|
- **Seven push notification streams** and the announce leg `towncrier`, likewise grandfathered —
|
|
they are stored in `notification_subs` and `announce_job_legs.leg` and read by the shipped Android
|
|
app, so renaming them would be a data migration plus a client break.
|
|
- **Two settings rows**: `game_account_signup` and the shard's protocol pin.
|
|
- **The public-safety filter.** Which shard event kinds may reach the *public* SSE stream is decided
|
|
here, not in core — the kinds, the streams and the filter are one file that moves together.
|
|
|
|
## For an operator
|
|
|
|
**Installing — three ways in, and none of them is a build.** A release publishes
|
|
`module-uo-<version>.tar.gz`, an install manifest `module-uo-<version>.json` carrying its `sha256`,
|
|
and a `SHA256SUMS`. **You never build anything**: the client chunk is prebuilt and the one runtime
|
|
dependency ships inside the tarball.
|
|
|
|
| | How | Where it fits |
|
|
|---|---|---|
|
|
| **Admin panel** | **Admin → Modules**, paste the URL of the release's `module-uo-<version>.json`, then press Restart when it asks | The click path — no shell on the box. Core downloads the artifact the manifest names, verifies the declared `sha256`, inspects the whole archive before writing anything, and unpacks it as `modules/uo/` |
|
|
| **`MODULES`** | Declare it in the environment and the container resolves it at every start:<br>`MODULES=uo@0.3.0=https://…/module-uo-0.3.0.json` | The compose-managed host. The running set is a line in a file you version-control. Already at that version ⇒ no network at all, so a restart with the internet down comes up unchanged |
|
|
| **By hand** | `tar -xf module-uo-0.3.0.tar.gz -C ./modules && mv modules/module-uo-0.3.0 modules/uo`, then restart | Development, and any host where the other two do not fit. The bundle's top-level directory is named after the release, not after the module id — rename it to `uo` |
|
|
|
|
The install source must be an `https` host on the allowlist (seeded from `MODULE_SOURCE_HOSTS`,
|
|
editable in the panel from then on). The `sha256` in the manifest is the trust anchor; the allowlist
|
|
is what stops a pasted URL from being an SSRF primitive as well.
|
|
|
|
**Uninstalling, and the one destructive choice.** Uninstall removes the module's directory and
|
|
leaves its row `disabled` — **your data is kept**, and reinstalling picks it up exactly where it was.
|
|
Deleting the data is a separate, opt-in tick box *inside* the uninstall dialog, and it has to be
|
|
there rather than after: `purge.sql` is a file inside the directory being deleted. There is also a
|
|
standalone **Purge** action on a module that is still installed but disabled. Purging drops all 27
|
|
tables; it does **not** touch the two `settings` rows (renaming or deleting
|
|
`uo_link_protocol_3_migrated` would re-arm a protocol migration against tables that no longer
|
|
exist), and what the module can re-derive from your ServUO tree — the atlas and the cliloc table — is
|
|
rebuilt at the next boot. Everything the shard and your players produced is gone.
|
|
|
|
**Disabling is a kill switch, not a visibility flag.** Disable runs the module's `onShutdown`
|
|
immediately: the uo-link WebSocket closes, the SSE streams end, and its routes, nav rows and client
|
|
chunk answer 404. Re-enabling flips the row and asks for a restart, because there is no `onBoot`
|
|
re-dispatch — the hooks have never been promised to be re-entrant.
|
|
|
|
**Connecting it to a shard.** The module needs the
|
|
[uo-link sidecar](https://gitea.whitlocktech.com/RunicGateway/link) running next to the ServUO
|
|
shard. Deploy that with the [installer](https://gitea.whitlocktech.com/RunicGateway/installer) —
|
|
[`../../installer/INSTALL.md`](../../installer/INSTALL.md) is the operator guide — and paste the four
|
|
values it prints into **Admin → Shard**. The token is encrypted at rest and write-only in the API.
|
|
|
|
**Nothing requires the shard to exist.** With no sidecar configured the site renders normally and
|
|
shows the shard offline. That is the same bargain the module system makes one level up: a module
|
|
that fails to load never takes the site down.
|
|
|
|
**Environment variables** — four, all optional, all read by the module and documented in its README:
|
|
`UOLINK_BASE_URL`, `UOLINK_WS_URL`, `UOLINK_PROTOCOL`, `TOWNCRIER_DURATION_SEC`. They live in the
|
|
Compose `.env`, because that is what reaches the container.
|
|
|
|
## Compatibility
|
|
|
|
`module.json` declares a `coreApi` semver range, checked at boot against core's
|
|
`MODULE_API_VERSION`. A mismatch fails **loudly** — the module is marked `startup_failed` and the
|
|
site comes up without it. This is a separate number from `PROTOCOL_VERSION`, which versions the
|
|
shard wire and says nothing about a website module.
|
|
|
|
The module's CI clones core at a **pinned** ref ([`MODULE_API.md`](../../website/MODULE_API.md) §5.3)
|
|
to generate its frozen manifest. Bumping that pin is a deliberate commit that says which core the
|
|
module was last proved against — not a tracking reference that turns core's unrelated changes into
|
|
red Xes here.
|