Files
docs/modules/uo
wtclaude 42867d7ef8 docs(modules): close Phase 4 — de-UO the backend reference, record the acceptance run
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>
2026-08-12 09:16:29 -05:00
..

module-uo — the Ultima Online module

Everything specific to Ultima Online that the website serves. Code: 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 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 §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 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 The 27 tables it owns and why they are shaped that way — moved out of core's BACKEND_DESIGN.md §3
SHARD_VISIBILITY.md Who sees which shard data — the admin-configurable audience framework
SPAWN_ATLAS.md The bestiary / spawn atlas, parsed from the shard's own ServUO tree
MARKETPLACE.md The player-vendor index
CLILOCS.md UO's id → name table
UOFIDDLER.md Operator runbook for extracting the cliloc table and creature art
../../link/PLAN.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 and documented in its 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.

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. 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 §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:
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 disabledyour 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 running next to the ServUO shard. Deploy that with the installer../../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 §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.