Commit Graph

452 Commits

Author SHA1 Message Date
d9abe3d941 docs(website): the in-app channel as built (engagement Phase 7)
ENGAGEMENT.md gains the Phase 7 as-built: the four decisions the org lead
settled before any code (in-app defaults to `instant`; the phase takes push's
`deliver` and the web preferences screen; the inbox takes `/notifications` and
the settings move under it; `ctx.inbox.push` respects a preference where one
exists), the block-role mapping that turns a template into a row, and five
things the tree contradicted or the build found — including the one only the
live rig could see, that staff had no reachable inbox at all.

Two earlier passages amended where the phase made them false: Phase 2's
"`ctx.inbox.push` throws until Phase 7" and Phase 3's "`inapp` is declared
`off`". Both kept as history with the correction beside them.

BACKEND_DESIGN.md gains the four inbox routes in the `/auth/me` table and a
`user_notifications` entry in the table inventory — the user-scoped dedupe
index, why `body` is text rather than the email HTML, and the retention policy.

Code: RunicGateway/website#TBD

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-31 02:11:46 -05:00
73c3a467e7 Merge pull request 'docs(website): the email channel and the Teams migration as built (engagement Phase 6)' (#187) from docs/engagement-email-channel into edge
Reviewed-on: #187
2026-08-31 06:07:57 +00:00
c93151b352 docs(website): the email channel and the Teams migration as built (engagement Phase 6)
ENGAGEMENT.md gains the Phase 6 as-built: the seven decisions settled up front,
the audience problem that shaped the phase, why scope_key is not subject_key, the
one place decision 4 as phrased could not ship, the projection's rule and what it
refuses to guess, the digest correcting Phase 4a rather than only implementing
§4.2b, and the three defects the build found. §4.2b and §6.0b updated with it.

TEAMS.md §6.4 rewritten: the pipeline it described no longer exists as its own
thing. It now says which of the three sinks moved and which did not, where each
of the four properties it always claimed lives now, that Team email is OFF until
an operator turns a rule on, and what the generalized unsubscribe token does.
§6.3's `last_digest_at` is marked as no longer read.

BACKEND_DESIGN.md: engagement_digest_state, engagement_outbox.scope_key, and the
unsubscribe routes — the canonical /public/engagement pair plus the /public/teams
path kept permanently because mail is not editable once sent.

Code: RunicGateway/website#TBD

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-29 20:12:15 -05:00
2161119c8c Merge pull request 'docs(website): the template editor as built (engagement Phase 5b)' (#186) from docs/engagement-template-editor into edge
Reviewed-on: #186
2026-08-29 23:37:22 +00:00
556124562b docs(website): the template editor as built (engagement Phase 5b)
ENGAGEMENT.md gains an "As built - 5b" section and BACKEND_DESIGN.md the eight
routes under /admin/engagement.

The scope decision is recorded first because the plan contradicted itself: the
Phase 5 body names only the editor, while Q4's answer and 6.2 both promise
"Triggers, Templates and the send log" in Phase 5. All three shipped - leaving
either out would have left the nav group half-built and G15 open with the rows
already on disk.

Five more decisions, each because the tree said something the plan did not: the
preview is rendered server-side and framed; `status` is now enforced by
renderByKey; a test send is logged under a synthetic trigger rather than making
the column nullable; a template a rule uses refuses deletion with a 409; and
duplicate is the only creation path.

Also records the correction that changed the most code - 4.6.2 says duplicate is
how a protected template is customized, the schema says "Editable, NOT deletable",
and the org lead's ruling is the schema's - and the three things only building it
found, including the Phase 4a template-key pattern that could not match any key
this system uses.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-29 18:14:12 -05:00
c03dfc14ba Merge pull request 'docs(website): the seeded template set as built (engagement Phase 5a)' (#185) from docs/engagement-templates into edge 2026-08-29 18:13:47 +00:00
aa5c1d63b0 docs(website): the seeded template set as built (engagement Phase 5a)
Companion to website#TBD. §6.0b assigns this phase two documents; both land here.

ENGAGEMENT.md gains an "As built — 5a" section: the three decisions the survey
forced, the argument for two registries rather than one, why the ternaries stayed
at the call site, and the two behaviour changes an operator will notice.

BACKEND_DESIGN.md gains the `engagement_templates` table, the two-block-registry
split, the token grammar, and the §7 note that mail is now multipart.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-29 13:07:50 -05:00
315a7c7fca Merge pull request 'docs(website): the engagement admin surface as built (Phase 4b)' (#184) from docs/engagement-rules-admin into edge
Reviewed-on: #184
2026-08-29 17:29:06 +00:00
73e1669a5c docs(website): the browser pass on the engagement screens (Phase 4b)
Six defects that only driving the two screens in a browser could find, plus two
environment facts worth not re-deriving:

  - rebuilding client/dist while the server is running blanks the whole SPA. The
    HTML shell resolves core's hashed bundle filename at boot, so after a
    rebuild it points at a file that no longer exists; core's bundle 404s,
    window.__rg is never published, and modules/uo/entry.js throws MODULE_API.md
    §3.1's "core did not publish its shared dependencies" into a blank page. The
    error names core, and core is not at fault. Restart after every build.
  - a window.confirm blocks CDP entirely, so the two destructive actions cannot
    be driven from a script and a session that opens one is stuck until a human
    dismisses it. Exercise those paths over the API.

- [x] AI-assisted: written with Claude Code (Opus)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-29 12:26:41 -05:00
97b652fed3 docs(website): the engagement admin surface as built (Phase 4b)
Companion to website#171.

ENGAGEMENT.md gains an "As built - 4b" section: the four decisions the org lead
settled, the correctness argument behind the standalone enable route, the three
honesty fields on the reach preview, and the two defects the live walk found in
Phase 4a's own code (a rule pointing at a dormant segment reading as healthy,
and the delete refusal's "1 rule still use"). It also records the throwaway
module rig that made the whole segment half walkable at all - core declares no
audiences, so on a stock local stack none of the §5.1a arithmetic can be
exercised without one.

§5.1a gains the composition UI it was owed: its own nav entry rather than a tab,
"exclude" offered only under "all of", the stored ceiling displayed and never
chosen, and a tree nested deeper than the composer renders shown read-only
rather than flattened.

BACKEND_DESIGN.md's route table gains the twelve routes, including why the
enable switch is a PATCH of one column and why deleting a segment in use is a
409 rather than a cascade.

- [x] AI-assisted: written with Claude Code (Opus)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-29 12:10:18 -05:00
5924276fe7 Merge pull request 'docs(website): the engagement engine as built (Phase 4a)' (#183) from docs/engagement-engine into edge
Reviewed-on: #183
2026-08-29 13:24:26 +00:00
deaf491dc5 docs(website): the engagement engine as built (Phase 4a)
Companion to website#170. Section 6.0b's assignment for Phase 4 - ENGAGEMENT.md's
as-built and BACKEND_DESIGN.md's table inventory - plus the two corrections
building it forced on this document's own design sections.

ENGAGEMENT.md
  - Phase 4 is split 4a / 4b, with what each owes.
  - Section 7.1 Q2 and Q4 answered, so seven of eight are settled and only Q8
    (android CI) is open.
  - The as-built: the gate order and why two of its placements are load-bearing,
    the segment rule the design never stated (not is legal only inside an and,
    and contributes no ceiling), dormancy three ways and why audience_segment_id
    has no foreign key, the conditions grammar's two fail-closed properties, and
    why emit does not await the engine.
  - Section 4.1's cooldown statement and section 4.2a's dedupe index are
    corrected in place, so the design sections stop teaching the two defects.

BACKEND_DESIGN.md
  - Five new tables in the schema inventory, each with the reasoning a reader
    would otherwise have to reconstruct: why subject_key is in the primary key,
    why the dedupe index is scoped, why the send log survives an account
    deletion and is not a second address book, and why a rule points at a
    segment without a foreign key doing it.

No route table changes - Phase 4a adds no routes.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-29 08:07:42 -05:00
713e6fa6c8 Merge pull request 'docs(website): per-channel notification preferences (engagement Phase 3)' (#182) from docs/engagement-channel-prefs into edge
Reviewed-on: #182
2026-08-29 12:11:04 +00:00
7b7a26f0ac docs(website): per-channel notification preferences (engagement Phase 3)
The documentation §6.0b assigns Phase 3 — `BACKEND_DESIGN.md`'s route table and
`android/PLAN.md` §11 — plus the phase's as-built section and one correction the
build forced.

`ENGAGEMENT.md`:
- Phase 3's as-built: the three decisions settled before any code, why the
  channel registry could not wait for Phase 6, what the sparse PUT buys, the
  projection stated as an invariant, and the staff-ceiling filter.
- **The phase's own acceptance line was wrong and is struck through.** "A fresh
  user's … push defaults `instant`" reads naturally beside §3.1's "email opt-IN,
  push opt-OUT", but §3.1 borrowed that from `team_notification_prefs`, where no
  row genuinely does mean notified. Push STREAM subscriptions have never worked
  that way — `notification_subscriptions` holds a row only on opt-in — so
  `instant` would have projected the entire catalog into the legacy GET for
  every existing user. §3.1's comment is corrected in the same pass.

`BACKEND_DESIGN.md`: the `/me/notifications/channels` row, the
`notification_channel_prefs` table entry (including that absence means the
channel's default rather than `off`, and that all three agreeing on `off` today
is a fact about the declarations and not about the table), and a note on
`notification_subscriptions` that it is now the push projection.

`android/PLAN.md` §11: nothing above it changed — the shipped APK keeps working
and the `{"streams":[]}` gotcha still applies to that endpoint. The new section
documents the superset endpoint for whenever the app adopts it: render toggles
from each item's `channels` rather than a hardcoded three, `modes` is the
effective mode and the client must not re-implement the defaulting, the PUT is
sparse so the empty-array gotcha does NOT apply here, and one id may be missing
that the app expects (a `staff`-ceilinged trigger is not offered to a non-staff
caller).

Companion to website#169.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-29 07:09:16 -05:00
6c19a0608a Merge pull request 'docs(website): event triggers, audiences and the ceiling lattice (engagement Phase 2)' (#181) from docs/engagement-trigger-registry into edge
Reviewed-on: #181
2026-08-29 11:47:23 +00:00
58c0797fc2 docs(website): event triggers, audiences and the ceiling lattice (engagement Phase 2)
MODULE_API.md gets a real 1.7.0 — and §1.1's "1.6.0 has only ever been on
`edge`" paragraphs are now marked historical rather than current, which is the
correction §0.5 asked for: 1.6.0 reached `main` with the Teams cutover, so the
in-place-amendment rule those paragraphs invoke no longer applies and every
addition from here takes a version of its own.

§2.3 gains `ctx.events.emit` and `ctx.inbox.push` (present and throwing until
Phase 7, with the reason stated). §2.4 gains `registerEventTriggers` and
`registerAudiences`. A new §6.8 carries the forward-compat note ENGAGEMENT.md
§7.3 asked this document to hold: a rule, a template and an audience outlive
the module that declared them, so `trigger_id` is a plain VARCHAR with no
cascade and an unregistered id shows DORMANT rather than erroring or being
auto-deleted. The failure that prevents is specific — an id that stops
resolving must never silently become a send to a different set of people.

ENGAGEMENT.md records Phase 2 as built, and three things it did not previously
say:

§5.1a now DEFINES the ceiling lattice. The document named "narrowest" and
"tightest" throughout and never said what narrower meant, and the obvious
reading is a security defect: under a flat total order a `staff`-ceilinged
trigger permits an `owner` audience, i.e. a rule that mails cheat detection to
the player it detected. It is containment, not size, and incomparable ceilings
have no bound at all.

§7.2 is answered — ONE namespace, against the recommendation in its own text —
with the two knock-on effects that only appeared once it was built (a relaxed
id grammar, a shared legacy allowlist) and the risk that did not materialise
(the push catalog is untouched, so the shipped app sees no change).

§7.1 Q6 is answered: declare `kind: 'scheduled'` now, build the evaluator after
Phase 9.

§4.3 is kept true to what shipped: `audience` and `ceiling` are two fields
rather than one, `'computed'` is gone from the audience vocabulary, and
`example` is enforced rather than encouraged.

§6.0b's Phase 2 row is corrected. Its two "other repos" cells are cutover-window
work, not this window: the integration kit pins a website `main` sha and
runicgateway.com FETCHES its facts from `main`, so doing either now would turn a
green repo red for the whole edge period — for the kit, against the explicit
rule in its own pin ("written against what shipped, never what is in flight").

BACKEND_DESIGN.md gains the two admin routes, the router-tree entry and the
adminOnly line; api-route-inventory.json regenerated.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-29 06:40:48 -05:00
88de7e3331 Merge pull request 'docs(website): unique, changeable, verifiable email addresses (engagement Phase 1b)' (#180) from docs/unique-verifiable-email into edge
Reviewed-on: #180
2026-08-29 07:08:29 +00:00
84481ee6b1 docs(website): unique, changeable, verifiable email addresses (engagement Phase 1b)
Companion to website#<pr>. Records Phase 1b as built, and corrects two things
the plan got wrong before anyone builds on them.

ENGAGEMENT.md
  - Phase 1b step 2 said to pin the index to a case-insensitive collation. Every
    _ci collation MariaDB offers here is also accent-insensitive, so that index
    would refuse jose@x.com once josé@x.com existed and the de-duplication would
    have cleared a legitimate account's address. The as-built block records the
    generated-column design that shipped instead, and the second-order version of
    the same bug that a seeded fixture caught in the de-dupe query itself.
  - §0.6 named two callers of isDuplicateUsername(). There are five, and the
    three it omits fail worse than the two it names.

BACKEND_DESIGN.md — the users table (already stale: it predated the player
account work), plus email_verifications and email_dedupe_report, and the five
new routes.

UPGRADE_NOTES.md — an operator entry, because the de-duplication is the kind of
quiet change this file exists for: nothing breaks, and the affected users find
out the next time they try to reset a password.

api-route-inventory.json — regenerated from the manifest; still ungated.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-29 01:54:05 -05:00
e833874e8b Merge pull request 'docs(website): one self-service surface — /auth/me/account (engagement Phase 1a)' (#179) from docs/collapse-account-surfaces into edge
Reviewed-on: #179
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-08-29 05:53:01 +00:00
35abf61277 docs(website): one self-service surface — /auth/me/account
Companion to RunicGateway/website's collapse of /admin/account (6 routes) and
/player/account (8 routes) onto /auth/me/account, which was already a strict
superset of both.

  - BACKEND_DESIGN.md: the two router-tree entries go; the /auth/me prose is
    rewritten from "additive, the older routes stay for web back-compat" to the
    single surface it now is, recording why /auth/me was the one to keep and
    that gating was equivalent. account.controller.js moved to router/v1/auth/.
    The /player-group paragraph loses account.router.js from its mount list.
  - ENGAGEMENT.md: new Phase 1a records the collapse as built, and §0.6 finding 4
    is corrected — it named router/v1/player/account.router.js, which is gone.
    Phase 1b's change-and-verify flow now lands on /auth/me/account and nowhere
    else, which was the reason to do this first: a self-service field would
    otherwise have been written three times, in 1b and again in Phase 3.
  - android/PLAN.md §6.4/§8: the "routes stay for web back-compat" note is now
    false. The app needed no change — MeApi.kt was already 100% /auth/me/*.
  - API_V2_PLAN.md: a forward pointer only. Its router inventories are a record
    of the domain split as it landed and are deliberately left as written.

api-route-inventory.json is regenerated wholesale, not partially updated. It is
a generated mirror of server/routes.manifest.json with no CI gate, and it had
drifted on its own: 168 entries against the manifest's 203, missing every Teams
route and still listing the two Gmail connect routes Phase 1 deleted. It is now
byte-identical to the manifest. A gate for it is flagged in ENGAGEMENT.md
Phase 1a as later work — a mirror nothing checks will drift again.

PROJECT_TREE.md is deliberately untouched: it is auto-generated by the
sync-project-tree workflow in the website repo, which regenerates it from
tracked files on main.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-29 00:49:40 -05:00
cde362e588 Merge pull request 'docs(website): §7 rewritten for SMTP, plus an upgrade-notes home' (#178) from docs/engagement-phase-1 into edge
Reviewed-on: #178
2026-08-29 02:10:01 +00:00
6f6b54a509 docs(website): §7 rewritten for SMTP, plus an upgrade-notes home
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>
2026-08-28 21:01:19 -05:00
7a2ad727cb Merge pull request 'docs(website): record the org lead's answers to Q1/Q3/Q5/Q7, and add Phase 1b' (#177) from docs/engagement-decisions into edge
Reviewed-on: #177
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-08-29 00:48:02 +00:00
a9ba4707c8 docs(website): revert the §6.0a "correction" — android-app really had no edge
Phase -1 is now executed, and running it disproved the correction made in the
previous commit. android-app's `edge` did not exist: the push reported
`[new branch]`, and `git ls-remote --heads origin edge` confirms it. §6.0a was
right the first time — three repos needed a branch cut, not two.

The cause is worth keeping, because it read as a real measurement. `git fetch
origin` does not prune, so android-app's `refs/remotes/origin/edge` survived the
server-side deletion after the M12 cutover. rev-parse resolved it and
`rev-list --left-right --count` returned "1 behind" — a plausible number for a
stale branch, which is exactly what it looked like. A remote-tracking ref is a
cache, not an answer; ls-remote is.

Records Phase -1 as complete: five fast-forwards (module-uo 9, installer 7,
servuo-plugins 7, website 5, link 3) plus three branches cut from main
(android-app, runicgateway.com, Integration-kit).

Assisted-By: Claude Code (Opus 5)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-28 19:44:46 -05:00
3d93b3865c docs(website): record the org lead's answers to Q1/Q3/Q5/Q7, and add Phase 1b
Four of the eight open questions in ENGAGEMENT.md §7.1 were answered on
2026-08-28. Q1's answer turned out to carry a whole phase with it.

- Q5: document all three SMTP postures, lead with a relay, name
  Gmail-app-password as the migration path off OAuth2.
- Q3: rules stay operator-editable data, but `enabled` defaults to 0 and
  every rule carries a hard per-hour send ceiling. Adds max_sends_per_hour
  to engagement_rules — the ceiling is what makes "data" safe to choose
  over "code".
- Q7: no campaigns surface at all. Lists DO exist, but only module-declared
  and powered by module data, on a surface core exposes to every module.
  Operators may compose them; composition must NARROW, never widen. Adds
  §5.1a, api.registerAudiences and engagement_audience_segments.
- Q1: opt-in only, users.email becomes UNIQUE, and the verification gate is
  an admin setting (on for fresh installs, off for upgrades).

New §0.6 records why the UNIQUE index is not a one-line ALTER, verified in
the tree rather than assumed:

- ensureSchema() runs the ALTER block on every boot, so ADD UNIQUE INDEX
  against a table holding duplicates stops the site from starting.
- isDuplicateUsername() tests only ER_DUP_ENTRY/1062 and never which index
  collided, so register would answer "that username is already taken" for a
  duplicate email, and provisionSsoPlayer would retry usernames for an email
  conflict until it exhausts PROVISION_MAX_TRIES and fails opaquely.
- SSO auto-provisioning manufactures those duplicates and marks addresses
  verified merely for existing — which is also why dedupe is oldest-wins
  rather than verified-wins. CLAUDE.md's "identities are never
  auto-provisioned" is stale.
- No route lets a user change their own address, so a verification gate has
  no flow to gate; Phase 1b builds one.

New Phase 1b sequences the fix before the index, dedupes oldest-wins with an
admin report, and keeps the collision error generic, rate-limited and out of
the bot scorer. Phase 9 loses the verification flow to it and therefore no
longer blocks Phase 11.

Also corrects §6.0a: android-app DOES have an edge (1 behind main), so
Phase -1 is six fast-forwards and two branch creations, not five and three.

Assisted-By: Claude Code (Opus 5)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-28 19:40:49 -05:00
2a122445c9 Merge pull request 'docs(website): add ENGAGEMENT.md, the engagement system design of record' (#176) from docs/engagement-plan into edge
Reviewed-on: #176
2026-08-29 00:07:07 +00:00
80284c0a2e docs(website): add the branching model, per-phase docs, and the site phases
Three additions the org lead called for, all in ENGAGEMENT.md.

The branching model (new §6.0a). Every phase PR in every repo targets `edge`;
`main` is touched exactly once, by the cutover. Two blocking findings checked
on 2026-08-28: every existing `edge` is stale (0 ahead of `main`, behind by 16
docs / 9 module-uo / 7 installer / 7 servuo-plugins / 5 website / 3 link) and
must be fast-forwarded before the first phase PR, and three repos have no
`edge` at all — android-app, runicgateway.com and Integration-kit. Also records
that android-app's pr-checks.yml triggers only on PRs into `main`, so Phase 8
lands with no CI and the cutover is its first real build, as happened to all
nine M12 phase PRs.

Documentation as a phase deliverable (new §6.0b). A phase-by-phase table
assigning the specific docs each phase owes, in `docs/` and in every other
repo, so nothing is deferred to a cleanup pass. §7.4 becomes the inventory
that table draws from rather than a list of things to do at the end.

Two new phases. Phase 12 is runicgateway.com, which is not optional polish:
scripts/checkFacts.mjs fetches each fact's authority from the source repo's
`main`, so `protocol` 4 to 5 and `moduleApi` 1.6.0 to 1.7.0 fail its build on
their own. The site also currently documents the opposite of what Phase 1
ships — notifications-and-email.mdx carries a "There is no SMTP option" aside —
its capabilities list claims a web notification channel that will not exist
until Phase 7, and PLAY_DATA_SAFETY.md and /privacy generate from one inventory
that an engagement mailer materially changes. Because checkFacts reads `main`,
the site stays green through the whole `edge` period and breaks at the cutover,
so Phase 12 must be written before Phase 13 and merged in the same window.

Phase 13 is the cutover itself, ordered rather than per-repo-independent: docs,
then servuo-plugins and link together (a protocol bump has three declaration
sites and a `main` holding a v5 sidecar with a v4 overlay cannot pair), then
website, module-uo, Integration-kit, android-app, and runicgateway.com last
because every fact it fetches has to be true on `main` first.

Adds an eighth open question (fix the Android CI trigger, or accept the cutover
as its first build) and a Phase -1 to the sequencing diagram for the edge
fast-forward.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-28 18:53:51 -05:00
04863993aa docs(website): add ENGAGEMENT.md, the engagement system design of record
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>
2026-08-28 18:47:50 -05:00
e719c3c32e Merge pull request 'fix(link): a protocol bump owes four declaration sites, not three' (#175) from fix/protocol-4-module-pin into main
Reviewed-on: #175
2026-08-25 00:34:04 +00:00
36f8e1071d fix(link): a protocol bump owes four declaration sites, not three
v4's cross-repo obligation table named module-uo's ingest work and stopped
there, so both of that module's PIN sites — DEFAULT_PROTOCOL and the
uo_link_config.protocol column default — stayed at 3 when protocol 4 shipped.

The consequence is not partial degradation. A sidecar answers a stale client
`409 protocol version mismatch` rather than mis-parsing it, so a fresh install
read nothing at all from its shard — empty marketplace, empty guild board, no
status — until an admin edited the number by hand in Admin → Shard. Existing
installs were unaffected, which is why it went unnoticed: their stored row had
already been moved by the protocol-3 one-shot, and the stored row wins.

Found while standing up a demo deployment for the marketing site's screenshots;
fixed in Module-uo.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 19:23:24 -05:00
743fdbeb23 Merge pull request 'docs: the shard screen's real path, and the two keys a deploy cannot boot without' (#174) from fix/admin-shard-path-and-secret-enc-key into main
Reviewed-on: #174
2026-08-24 16:30:13 +00:00
b525386047 docs: the shard screen's real path, and the two keys a deploy cannot boot without
Two operator-facing errors, both found by following the documentation exactly
against a real deployment while writing the runicgateway.com installation
journey.

**The shard screen moved and the guide did not.** INSTALL.md §5 and PLAN.md both
print the installer's handoff block, which said to paste the four values at
`<site>/admin/shard`. Those screens belong to the `uo` module now, and a module
owns one path segment wherever it appears (MODULE_SYSTEM.md §2.8), so the page
is `/admin/uo/link`. The old path does not 404 — the SPA sends the operator to
the dashboard, so the link looks like it worked and the values have nowhere to
go. The instruction under the block now names the path, and a note warns anyone
whose earlier run printed the old one. API routes are unaffected by the module
rule and keep `/api/v1/admin/shard/*`; the note says so, because that is the
distinction the next editor will need. Matches installer#22.

**The README snapshot's quickstart could not boot.** website's root
`.env.example` never listed SECRET_ENC_KEY, and the "set at least" list omitted
both it and BOT_INTERNAL_KEY — each of which the server refuses to start without
in production, BOT_INTERNAL_KEY even on a deployment running no bot. Synced from
website#163, which fixes the file itself and was verified by booting the
published image from a clean `cp .env.example .env`.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 11:24:41 -05:00
afb2fea50a Merge pull request 'docs(link,installer): protocol 4 is live — correct every stale 3' (#172) from docs/protocol-4-accuracy into main
Reviewed-on: #172
2026-08-19 23:25:49 +00:00
274b2c9564 Merge pull request 'docs: hygiene sweep — the four places the docs describe a site that no longer exists' (#173) from docs/hygiene-sweep into main
Reviewed-on: #173
2026-08-19 23:25:13 +00:00
76da8ee88d docs: hygiene sweep - the four places the docs describe a site that no longer exists
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>
2026-08-19 18:04:41 -05:00
7579d6baa2 docs(link,installer): protocol 4 is live - correct every stale 3
Protocol 4 shipped on 2026-08-19 as sidecar v2.0.0 and overlay v1.0.0,
paired in bundle 2026.08.19. Three documents still said 3.

INTEGRATION.md contradicted itself: its event catalog fully documents the
protocol-4 kinds, including a subsection headed "On Protocol 4", while
its normative section 2 still declared X-UOLink-Version: 3, "protocol": 3
in /health and ws.hello, and a worked JS client sending "3". That is the
contract a third-party integrator implements against, so it mattered
most. Section 2 now states the current version and what shipped it, and
gains a v4 paragraph alongside v2 and v3. The "(Protocol 3.0)" markers on
individual features are left alone - those record which version
introduced a kind and are correct.

Two stale examples the audit had not spotted: the --print-config sample
reported version 0.1.0, and the minimal browser client told readers to
check ev.protocol === 1, a leftover from protocol 1.

INSTALL.md is the one that actively misconfigured a real install. Line
413 is the "Protocol version" value an operator pastes into Admin ->
Shard, and the document's own troubleshooting table says a stale number
comes back as a 409 that "looks exactly like your shard going offline".
Corrected along with the stale bundle, overlay and sidecar versions
throughout, including Appendix A's copy-pasteable curl commands, which
pointed at overlay v0.1.1 and sidecar v1.1.0.

v4.md still said the cutover had not happened. It has. Its outstanding-
work note said the five-rung shard visibility walk was "outstanding for
the cutover", which is now self-contradictory - so it says plainly that
the cutover shipped without it and no result is recorded anywhere.
guild.roster carries actor objects for every member of every guild, the
widest surface any protocol version has added to that check, so it is
worth walking against the released pair.

No code or contract changes. Every value verified against the Gitea API:
link main sidecar/src/main.rs PROTOCOL_VERSION, servuo-plugins
overlay.toml, and current.json on the installer bundles branch.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-19 17:58:26 -05:00
b6d059b1e5 Merge pull request 'docs(tree): sync website/PROJECT_TREE.md' (#168) from chore/sync-website-tree into main
Reviewed-on: #168
2026-08-19 18:07:35 +00:00
8abce24700 Merge pull request 'docs(modules): the module release version is derived, not declared' (#171) from docs/module-uo-release-cadence into main
Reviewed-on: #171
2026-08-19 18:07:18 +00:00
a605c03eae docs(modules): the module release version is derived, not declared
Reverses settled decision 19. The argument for a declared version is still true
and the trade it made was still wrong, which is worth separating: its cost is
paid on every release, and the drift it prevents is something review catches
anyway. Between the phase 3 release on 2026-08-12 and today, v0.3.0 was the only
release Module-uo ever cut, while nine Teams phases and the cutover landed on its
`main` - none of them touching `module.json`'s version line, so none of them
producing a bundle.

Records what replaced it: `link`'s engine, the tag as the number that ships,
CI writing it into the bundle's `module.json`, the declaration demoted to a
floor, and the `workflow_dispatch` backdoor for a `module.json` change with no
releasable code behind it.

The original subsection is left standing with the amendment beneath it rather
than rewritten, as decision 33 asks of a record that has been overtaken.

Pairs with Module-uo's `.gitea/workflows/release.yml` and the same change to the
Integration kit's template.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-19 13:01:50 -05:00
e38792b21b Merge pull request 'docs(modules): the Rust dry run reaches the game through an Oxide plugin, not RCON' (#170) from docs/rust-dryrun-mod-transport into main
Reviewed-on: #170
2026-08-19 11:16:30 +00:00
11d20a9a17 docs(modules): name Oxide, and pair one sidecar to one server
Two corrections from the org lead on the transport rewrite.

It is an OXIDE plugin, not "a mod loaded by the server's mod framework". Oxide is
what modded Rust servers run, and naming it is the difference between a design a
reader can start from and one they have to go and pick a framework for.

And the architecture is ONE SERVER, ONE SIDECAR - not one sidecar fronting a
community's several servers, which is the arrangement a UO-shaped reading reaches
for and which this document had. Rust servers in practice sit on separate VMs, so
a shared sidecar would have to be reached across a network by plugins that are
supposed to talk to it over loopback: it trades the invariant that makes the
design safe for a saving in process count.

The cost lands on the module, which is the right place for it - it holds one
client per configured server rather than one client to an aggregator - and it
makes the Team provider's `complete` answerable rather than vague, since "every
team there is" now means every team on THIS server. Five of six sidecars
reachable is `complete` left off, and core adds and updates without archiving.

Recorded as a reevaluable assumption rather than a principle, because that is
what it is. Nothing in the contract objects either way: core is not in this
conversation at all, which finding 2 now says.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-19 04:16:06 -05:00
4e98ed6bd5 docs(modules): the Rust dry run reaches the game through a MOD, not RCON
Overruled by the org lead: RCON is not used. Rust gets the same three-part shape
UO has - a plugin inside the game that dials out, a sidecar that persists before
it forwards, a module that talks only to the sidecar - and the plugin is a mod
loaded by the server's mod framework, exposing data through hooks.

The document had RCON as its premise, so the correction reaches further than the
transport paragraph:

- The reason Rust is a good second game changes. It was "its server speaks a
  protocol nobody has to write". It is now "its server is a BINARY" - the
  opposite of ServUO, which is source a shard owner compiles - so the way in is
  a published mod API and the shard-dials-out invariant has to survive that
  change of footing. It does, unchanged, which is a stronger result than the
  one the document originally claimed.
- The announce leg sends a command down the socket the mod already holds,
  rather than calling rcon.say.
- The provider refuses when no mod is connected, not when RCON is unreachable.
- Two hooks answer questions UO had to work for: a wipe arrives as an event, and
  membership is real-time - so this module's Team provider is event-driven with
  a baseline on connect rather than sweep-driven. The provider contract does not
  change by a line, which is the part worth keeping: core never needed to know
  how the data arrives.

The 2026-08-12 correction block stays and a second one is added beside it rather
than editing the history out - this document's own convention, and the thing that
makes it worth reading twice. It also records what the correction COSTS: this
project no longer has a worked example of "a game that already speaks a
remote-control protocol, so its sidecar is thin".

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-19 04:09:41 -05:00
12594f2b13 Merge pull request 'docs(teams): the Teams bet, as built (Teams cutover 6/6)' (#169) from edge into main
Reviewed-on: #169
2026-08-19 09:02:13 +00:00
runic-docs-bot
67c9a4773c docs(tree): sync website/PROJECT_TREE.md from RunicGateway/website@963d734 [skip ci] 2026-08-19 08:57:34 +00:00
508e738527 Merge pull request 'docs(modules): the Rust dry run, revisited for Teams' (#167) from docs/rust-dryrun-teams into edge
Reviewed-on: #167
2026-08-19 08:53:54 +00:00
7e08a55f76 docs(modules): the Rust dry run, revisited for Teams
The kit's README sends a reader here FIRST - it is the shortest honest picture of
the whole job - and it predated Teams, so it taught a second game to build its
teams as private module data and never mentioned the provider. The two places the
contract changed since it was written are now in it, and nothing else moved:
all four findings stand, including the identity gap, which is still the one a
real second module hits first.

What Rust adds that UO does not, and why it was worth revisiting rather than
noting:

- externalId must survive a rename and a Rust team HAS no name - it is a numeric
  team id in the save. The right answer, and the one a designer is least likely
  to reach for.
- `complete` is per SERVER, not per community. Six servers are six team spaces,
  so a provider that can reach five must leave `complete` off or core archives
  every team on the sixth.
- A wipe empties every team, so { ok: true, complete: true, teams: [] } is TRUE
  once a month and core archiving all of them is correct - which is exactly why
  an unreachable RCON must answer { ok: false } instead. The two states are one
  API call apart and only the module can tell them apart.
- The team route carries a server id as well as a team id, so the external id is
  <serverId>:<teamId>. Core stores that and never parses it; an external id is
  opaque to core by design, and this is the case that shows why.

Also: rust_teams stays the module's table and core's teams stays core's, which is
the boundary worth stating in the one document where both appear; and the kit is
nine members now, not seven.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-19 03:51:07 -05:00
987c0b02d2 Merge pull request 'docs(teams): phase 11 as built, and what the rig walk found' (#166) from docs/teams-phase11-record into edge
Reviewed-on: #166
2026-08-19 06:37:24 +00:00
2adf397da4 docs(teams): phase 11 as built, and what the rig walk found
The phase ran before the cutover as Part 12 planned, and found what it was meant
to: the inverted slot direction worked for module-uo and nobody else. Recorded
beside the phase entry, with the org lead's two decisions of the day - core
offers a contribution rather than naming a slot, and the template grows a real
provider rather than a snippet.

Also records the live-rig walk and the one defect it found that no test could:
PageHeader takes `lead`, not `subtitle`, and React drops an unknown prop in
silence, so every page built from the kit's template had been rendering its
heading with nothing under it.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-19 01:31:52 -05:00
f1fb2d40b4 Merge pull request 'docs(modules): core offers a contribution, never a slot name' (#165) from docs/teams-slot-contributions into edge
Reviewed-on: #165
2026-08-19 06:18:55 +00:00
d8af33c805 Merge pull request 'docs(teams): cancel phase 10, and say what that leaves behind' (#164) from docs/teams-phase10-deferred into edge
Reviewed-on: #164
2026-08-19 06:18:10 +00:00