Engagement Phase 1's documentation (ENGAGEMENT.md §6.0b, row 1).
BACKEND_DESIGN.md §7 is REWRITTEN rather than amended, as the plan
requires: it documented Gmail OAuth2 as the mechanism. It now covers the
transport registry and why credentialFields is a single declaration
three consumers read, the credential blob and its fail-safe decrypt, the
no-phone-home rule and its CI guardrail, the three SMTP postures, why
Send test is the only verification left, the five failure contracts, and
the silent upgrade. The §2 tree, the PR-4 route-count note, the contact
row and the dashboard row are corrected alongside it — the dashboard now
returns warnings[].
UPGRADE_NOTES.md is new, and is the home for every later phase's
operator note as well (1b, 6, 10 and 13 each owe one). Newest first, and
scoped deliberately: an upgrade that needs nothing does not get an entry.
The entries worth writing are the ones that fail QUIETLY, which is
exactly what the Gmail removal does — mail stops, nothing errors, and
the only signal is a dashboard warning.
ENGAGEMENT.md records Phase 1 as built, with the four things that are
not what the plan says: only the transport half of §3.1 was built (a
channel nothing calls is a shape frozen too early), `enabled` now gates
every sender where it used to gate none of the direct ones, the G22
warning reads the deprecated column on purpose, and `npm run swagger`
had to be fixed first — the "per-file route limit" phases 8 and 9
recorded does not exist, and the real rule matters to every later phase
here, all of which add routes.
Co-Authored-By: Claude <noreply@anthropic.com>
Scoping investigation for an in-house, game-agnostic email and engagement
system: modules declare domain events and their data contract, core owns the
rules, preferences, templates and delivery.
Records the current-state map (notifications, email, the module contract and
the job/scheduling infrastructure), a gap list, the proposed schema additions,
the module registration mechanism, an eleven-phase plan with an acceptance
check per phase, and a catalogue of what the system could be used for.
Five findings contradict the brief this started from and shape the plan:
- There is no in-app channel. Core has one sink, the content-free push tickle;
the in-app inbox has to be built, not adapted.
- Email is already two-thirds of an engagement system, scoped to Teams. The
Teams pipeline is generalised and migrated onto the new one, not duplicated.
- The IDOC example's payload is not on the wire, and estimated_collapse is not
exactly knowable in advance: ServUO draws each decay stage's duration at
random when the stage is entered, so it is exact only at IDOC.
- Event-name collision handling already exists (registries.js namespaced() +
apply()), so the brief's forward-compat note is already satisfied.
- MODULE_API_VERSION 1.6.0 is on main now, so the engagement additions take a
real 1.7.0 rather than joining 1.6.0 in place.
Five scope decisions settled by the org lead are recorded at the top: the
in-app channel is in scope, the Teams pipeline is migrated, the house.decay
protocol enrichment is in scope, Gmail OAuth2 is removed rather than retained
as a transport, and the system ships with seeded templates plus an editor.
No code. Nothing is implemented until the org lead approves the phase.
Co-Authored-By: Claude <noreply@anthropic.com>
Four documents still describe the pre-module-system website. None of them
misconfigures anything, which is why they survived; all four mislead a
reader trying to understand how the system is actually put together.
ARCHITECTURE.md placed shardIngest.js and uoLinkClient.js inside the
website backend. Both live in module-uo/server/utils/ - verified, they
are not in website/server/src at all. The document claimed to be "the
canonical copy of the diagram; the same diagram is embedded in the
website's README", and the two had silently diverged: the live README's
diagram has the module subgraph, the loader, and the game behind the
module, and this one did not. The diagram is now the live one verbatim,
the surrounding prose attributes the shard integration to the module, and
the intro no longer frames core as game-aware. The SSE bullet gains the
distinction the live README makes: the module declares which kinds are
public, core enforces the split.
website-README.md had drifted from the live README by 28 lines, all of
them the "Three ways in, and none of them is a build" section - the admin
panel, the MODULES environment variable, and by hand - which is now the
primary module-install story. Re-synced verbatim, since a faithful
snapshot is the file's whole purpose. The diff was purely additive; the
snapshot contained nothing the live README had dropped.
README.md's index was missing thirteen documents, not the four the audit
had found: TEAMS.md, ARCHITECTURE.md, TRUSTED_DEVICES_MFA.md and
MODERATION_APPEALS.md, and also link/v4.md - the current protocol -
android/THEMING_AND_NAV.md, ci/SONARQUBE.md, installer/PROJECT_TREE.md,
modules/kit-acceptance.md, modules/uo/API.md, modules/uo/SCHEMA.md,
website/test-plan.md and the two API_V2 documents. The layout block
already advertised a ci/ directory that had no section. Every markdown
file outside the issue templates is now indexed, and every link resolves.
API_V2_SKELETON.md is listed as superseded, which is what its own header
says.
BACKEND_DESIGN.md was titled "UOMysticmoon Website - Backend Design"
though it is core's contract and core is game-agnostic. Retitled, with a
note that nothing in it is instance-specific. Its hardcoded public
contact address is now described as what it is - seeded from
BRAND_CONTACT_EMAIL into the contact_email setting, with UOMysticmoon as
the example instance.
Co-Authored-By: Claude <noreply@anthropic.com>
Records the slice that closes the extraction, and adds the two documents phase 3
owed: docs/modules/uo/ and the module-rust dry run.
**The slice found an obligation neither repo had built.** MODULE_API.md §2.8 and
§6.1a settle the OpenAPI fragment in detail — a module ships one, core merges the
fragments of started modules into /api/docs.json. Neither half existed, so the 72
URLs module-uo serves were in no spec at all. §2.8 and §6.1a now record what was
built, including the four things settled while building it: the filename is fixed
rather than declared, a module namespaces what it DEFINES and references core's
shared schemas by core's name, the generator derives its prefixes from the
module's own register() call, and swagger-autogen's diagnostics have to be
captured because it reports a broken annotation and then prints Success.
**§5.3 gains the design decision the frozen manifest actually made:** it is a
SUBTRACTION, not a prefix filter. Generating the manifest without the module and
then with it answers "what does the module serve" AND "did core lose anything",
and the second is the one §1.2 promises to the shipped Android app. A module that
shadowed a core route cannot appear as an addition anywhere.
**BACKEND_DESIGN.md §4.0.1** is new: /api/docs.json is assembled per request, the
two generated artifacts are core's alone, and the route count was still 228.
**docs/modules/** is new, per §2.10 (module documentation aggregates here, not in
module repos): docs/modules/uo/README.md orients a reader on what module-uo
serves, owns and needs from an operator, and links out to the feature docs that
already existed rather than restating them.
**docs/modules/rust-dryrun.md** is phase 3's fourth acceptance criterion. A
written, deliberately unimplemented module for Rust — chosen because it wipes
monthly, runs several servers rather than one shard, identifies by Steam, and
ships RCON so there is no sidecar to write. The contract generalises: same
manifest, same seven registration calls, same schema rules, and six of the UI
kit's seven members wanted by a game with nothing in common with the one the kit
was curated from.
It found one real gap — **a module cannot register an identity provider**, and
"Sign in with Steam" is what a Rust community expects. Recorded as the first
candidate for a future MODULE_API_VERSION bump rather than bolted on: an identity
provider participates in session creation, and §2.7's link-only SSO policy has to
survive it.
Also: website-README.md refreshed from the repo (it was several changes stale),
and three settled decisions added (18-20).
Pairs with Module-uo#6 and website#141.
Co-Authored-By: Claude <noreply@anthropic.com>
The normative contract between core and an installed module: the `ctx` handed
to a module's entry point, the `register*` calls, the client-side registry and
shared-dependency global, the schema-fragment rules, and the loader's
validation and failure obligations. Every member is derived from what the UO
code actually imports today, re-read against the working tree.
Part 6 records four places the survey contradicted MODULE_SYSTEM.md:
• OpenAPI generation is STATIC analysis (swagger-autogen parses app.js as
text), unlike routeManifest.js which walks the live Express stack. A
filesystem-scanning loader is invisible to it, so module routes would be
silently absent from swagger-output.json. Three options, one recommended;
needs a decision before Phase 2.
• The client contract is much larger than §2.1 implies — the atlas pages
import five core modules beyond React, so the plan needs a curated UI kit
and a request primitive on window.__rg.
• shardVisibility is module-owned and the atlas depends on it, so the spike
carries it; two copies coexist during the spike by design.
• Two counts corrected: 27 UO tables (not 25), 6 atlas routes (not 5).
MODULE_SYSTEM.md gains a pointer to the contract and the corrected table count.
Co-Authored-By: Claude <noreply@anthropic.com>
Corrects the design doc against the current codebase and locks the open
decisions, in the same shape as HERO_EDITOR.md (locked decisions ->
corrections to reality -> phased build).
Blocking gaps found in the design doc:
- no delete path exists for a settings row, which every "reset to
defaults" in the feature depends on
- editors/moderators/players have no endpoint to read their own nav
overrides (GET /admin/settings is admin-only)
- renderIndexHtml runs once at boot, not per request
- settings values are JSON strings, not objects
- getPublic().brand is a cross-repo contract the Android app and
Discord embeds theme from; new keys would silently bypass it
Locked: effective values resolved server-side into getPublic().brand;
radius + shadow tokens only (spacing/border cut); radius tokens seeded at
today's real values so the promotion is a no-op; three dark presets in v1
with Parchment deferred; 12-option font shortlist across 8 web families
in one request; PNG-only favicons.
Co-Authored-By: Claude <noreply@anthropic.com>
v0.1.0 shipped on 2026-08-07, so every doc that said "not released yet"
was wrong the moment the cutover merged.
- INSTALL.md: replace the pre-release status banner. The installer is the
path the guide leads with; Appendix A is reframed as supported-not-
deprecated, for hosts that cannot run the binary, operators who want to
place files themselves, and development from a working tree.
- PLAN.md: status is Shipped, both cutover gates recorded as met (incl.
the Windows 1053 handshake bug the real SCM run found), Phase 5 table
and 5.4 closed out.
- README.md: point anyone setting up a shard at INSTALL.md first.
- link/link-README.md: mark the pre-split snapshot as historical, so its
deploy.ps1 instructions stop reading as the setup path.
Co-Authored-By: Claude <noreply@anthropic.com>
Closes the last Phase 0 item. INSTALL.md is written before the installer
binary on purpose: everything it installs is already released (0.1-0.3), so
the guide is not speculation about a tool that might exist - it is the
specification of what the run asks, where it writes, what it prints, and what
the operator does next.
It is useful today. Appendix A is the same deployment done by hand - bundle
fetch, tarball verify and overlay copy, the optional patch tier,
--print-config provisioning, systemd unit / sc create - composed from the
released artifacts' actual contents and the sidecar's config and CLI source.
That appendix doubles as Phase 1's acceptance test.
Writing it settled four things the plan had left implicit, now recorded in
PLAN.md:
- The installer does not install itself; day-two commands run from the
downloaded binary.
- The flag surface: --servuo, --patches/--no-patches, --host, --site-url and
--yes join the --verify/--bundle/--purge the plan already named, so every
prompt has a non-interactive equivalent.
- A modified Config/Bridge.cfg is reported, not overwritten - one deliberate
deviation from deploy.ps1, whose overwrite-on-hash-differs rule is right for
a developer and would silently revert an operator's whole shard config on
update. install.json's recorded hashes are what make the distinction
possible.
- Remote-website deployments: widen [web] bind, firewall it to the site's
address, front it with TLS or a VPN off a trusted network. [shard] bind
stays on loopback because that socket carries commands into the game.
Also adds the missing installer/ section to the docs index, and refreshes two
stale examples in PLAN.md (overlay file count, sidecar version).
Co-Authored-By: Claude <noreply@anthropic.com>
CLILOCS.md and SPAWN_ATLAS.md each explain WHY the operator has to supply
something out of their own client, but neither says how. UOFIDDLER.md is the
missing procedure: where to get UOFiddler, which two files in the zip matter,
which runtime it needs, where Cliloc.enu actually lives, the conversion, how to
point the site at the result, and how to confirm it took.
Verified end to end on a stock Windows box: UOFiddler 4.22.2 (Ultima.dll is
net10.0), .NET SDK 9.0.312 building the net8.0 converter, RollForward carrying
it onto runtime 10.0.8, and the site's own parser reading the output back.
Corrects one claim while doing it. CLILOCS.md said a UOFiddler GUI export
"works equally well"; it does not. Its Cliloc tab writes `Number;Text;Flag` --
three columns, flag LAST -- and parseClilocText splits on the first separator
only, so the flag is absorbed into the name and every item renders as
`quarter staff;0`. The parser already handles `number,flag,text` with the flag
in the middle, but a trailing `;0` is indistinguishable from a name that
genuinely ends that way, so this stays a documented `sed` on the operator's
side rather than a heuristic that would corrupt real names.
Co-Authored-By: Claude <noreply@anthropic.com>
Documents order 5b across the four repos, and records what building it changed
about §8 as designed.
- NEW website/MARKETPLACE.md — the operator guide: what the pages must say out
loud and why, the privacy contract (the player's in-game Vendor Search toggle
wins, and no admin setting overrides it), the Bridge.cfg knobs and how they
trade against each other, and the measured sweep costs.
- INTEGRATION.md — catalog entry for vendor.listing / vendor.listing.remove with
its six consumer gotchas, and the GET /market REST section (the sidecar's only
paged read, and why it orders by serial rather than shop name).
- BACKEND_DESIGN.md — shard_vendors / shard_vendor_items, the routes, and the
marketplace search as the only rate-limited public read.
- SHARD_VISIBILITY.md — why the market's fields default to Everyone (the in-game
gump already shows exactly that set), why location is one setting covering
four things, and why hiding the owner name without the owner id achieves
nothing.
- PLAN.md — the amortized round-robin as the one sweep pattern the bridge did not
previously have, and an update to §7's cliloc note: pushing name resolution to
the plugin was never an option, because ServUO cannot read a modern client's
compressed cliloc files either.
- v3.md §8.8 — the four things the build settled differently, chief among them
that §8.1's FLAT location payload would have made Part A's pre-wired
market.location rule inert, exactly like the characterName miss one part
earlier.
Co-Authored-By: Claude <noreply@anthropic.com>
Protocol 3.0 Part A. Admin-configurable, per-feature and per-field
audience control over every shard-derived surface, replacing the static
PUBLIC_KINDS allowlist that used to be the whole boundary.
- SHARD_VISIBILITY.md (new): the admin-facing guide - the ladder, what
each of the ten features exposes, the defaults, the two rules that are
code rather than configuration, and worked examples.
- BACKEND_DESIGN.md 6.5 (new): the same thing as a security contract -
the ladder and how viewerLevel resolves it, the locked acct/webId rule,
the fail-closed kind map, the asymmetric ladder fallbacks, and the
three enforcement points. Plus the shard_feature_visibility schema, the
/public/shard/features route, and the adminOnly tier on
/admin/shard/visibility.
Defaults reproduce pre-3.0 behavior everywhere, with one deliberate
exception which is the leak Part A was written to close: guilds and
governors previously returned the raw stored payload, whose leader and
governor actors carry acct and webId, to anonymous callers.
Co-Authored-By: Claude <noreply@anthropic.com>
Surveys the live ServUO tree against everything the bridge already
surfaces and records the full gap list (17 items), then specs the four
features scoped for 3.0.
3.0 has three scope areas:
- A: the visibility framework. Admin-configurable, per-feature and
per-field audience control over all ten shard-derived surfaces (the
four new ones plus the six that already ship), on an
anonymous -> logged_in -> player -> staff -> admin ladder. Every
default reproduces today's behavior, so the retrofit is a no-op until
an admin changes something. Two rules an admin cannot override: acct
and webId are admin-only always, and an unmapped event kind is never
broadcast below admin. This also fixes a verified leak - guild leader
acct/webId are readable today on the anonymous /public/shard/guilds.
- B: three new wire streams - world.ruleset, points.board, and
vendor.listing/vendor.listing.remove.
- C: the spawn atlas, built from static ServUO data files with no wire
involvement.
Visibility lives entirely on the website; the sidecar stays a dumb
forwarder that defines no access parameters and advertises no
capabilities.
PROTOCOL_VERSION goes 2 -> 3 once, at the end: every part PRs into an
edge branch per repo, and the coordinated edge -> main merge is the
cutover. A schema migration moves uo_link_config.protocol so operators
don't have to.
Co-Authored-By: Claude <noreply@anthropic.com>
Add auto-generated project-tree snapshots for the website, link, and
Android-app repos under docs/<repo>/PROJECT_TREE.md, and link them from
the README (adding a previously-missing android/ section). These files
are maintained going forward by the sync-project-tree CI workflow in each
source repo, which opens a PR here whenever the tracked layout changes.
Co-Authored-By: Claude <noreply@anthropic.com>