Protocol 2.0/2.1 uo-link integration — boards, cross-links, news gump, account provisioning #65

Merged
whitlocktech merged 9 commits from feature/protocol2-integration into main 2026-07-18 02:34:27 +00:00
Member

Integrates the new uo-link Protocol 2.0 / 2.1 surfaces into the site (spec: uo-link/docs/INTEGRATION.md). Everything reuses the established champs/pages pattern: ingest to our own MariaDB → snapshot on WS (re)connect → public/admin SSE → token-free public endpoints, with the sidecar token never leaving the server.

What's included (9 commits)

Boards (ingest + UI)

  • Guilds (/site/guilds), Governors (/site/governors, City-Loyalty) with term history captured from day one (idempotent transition capture; votes left null since the feed carries only candidate counts), Presence ("Players Online" widget with region buckets), and the Houses registry.
  • Live via new PUBLIC_KINDS; boards snapshotted on every reconnect (guarded so an empty/failed board never wipes another).

Cross-links

  • Character sheet renders titles + guild/governor chips; /admin/users/:id gains a Standing section (governorships held + guilds led). Guild is leadership-only (verifiable from current state — no stale-membership guessing).

News gump (2.1)

  • Published news posts auto-sync into the in-game Town Cryer News gump (utils/newsGump.js): push on publish, silent refresh on edit, pull on unpublish/delete, and reconcile on every reconnect.

Account provisioning

  • createAccount / unlinkAccount sidecar calls; hybrid game-account signup (POST /player/shard/account + staff POST /admin/shard/account), admin email invites (user_invites, sha256-hashed single-use tokens; email or copyable link), tokened accept flow, admin unlink, and account.* ingest reconciliation.
  • New admin controls: Settings → Game-account creation (disabled / website / hybrid / game) and an Invites admin view.

House visibility (role-tiered)

  • Public /site/houses → IDOC-only, location only. Staff /admin/houses → full registry (owner/price/decay). Player portal → own houses ("home status"). Registry deltas (house.update/remove, which carry owner/price) removed from the public SSE channel.

Schema

New tables: shard_guilds, shard_governors, shard_governor_terms, shard_presence, user_invites; shard_houses extended with registry columns. New settings: game_account_signup. (Includes a fix for a ;-in-inline-comment that broke the boot schema splitter.)

Verification

  • Server test suite green (added ingest-routing, governor term-capture, news-gump sync, and invite-model tests); client build clean; Swagger regenerated each backend phase.
  • Live-smoke passed against the real stack (MariaDB + Rust sidecar @ protocol 2 + ServUO "My Shard"): boards populate (9 governor cities, 28-house registry, IDOC public view), news gump re-asserts, invites (email + link) and the game-account creation UI verified in-browser.

Notes for deploy

  • Requires the sidecar/shard on Protocol 2 (client pins X-UOLink-Version); set uo_link_config.protocol = 2.
  • Game-account creation needs the shard's SignupMode = website/hybrid (Bridge.cfg) and RequireIpForCreate satisfied by a real forwarded browser IP (works behind Pangolin; loopback is refused locally by design).
  • Email invites need Gmail OAuth configured; otherwise the admin uses the copyable link.

🤖 Generated with Claude Code

Integrates the new **uo-link Protocol 2.0 / 2.1** surfaces into the site (spec: `uo-link/docs/INTEGRATION.md`). Everything reuses the established champs/pages pattern: ingest to our own MariaDB → snapshot on WS (re)connect → public/admin SSE → token-free public endpoints, with the sidecar token never leaving the server. ## What's included (9 commits) **Boards (ingest + UI)** - **Guilds** (`/site/guilds`), **Governors** (`/site/governors`, City-Loyalty) with **term history captured from day one** (idempotent transition capture; `votes` left null since the feed carries only candidate counts), **Presence** ("Players Online" widget with region buckets), and the **Houses registry**. - Live via new `PUBLIC_KINDS`; boards snapshotted on every reconnect (guarded so an empty/failed board never wipes another). **Cross-links** - Character sheet renders `titles` + guild/governor chips; `/admin/users/:id` gains a **Standing** section (governorships held + guilds led). Guild is leadership-only (verifiable from current state — no stale-membership guessing). **News gump (2.1)** - Published news posts auto-sync into the in-game Town Cryer News gump (`utils/newsGump.js`): push on publish, silent refresh on edit, pull on unpublish/delete, and reconcile on every reconnect. **Account provisioning** - `createAccount` / `unlinkAccount` sidecar calls; **hybrid game-account signup** (`POST /player/shard/account` + staff `POST /admin/shard/account`), **admin email invites** (`user_invites`, sha256-hashed single-use tokens; email **or** copyable link), tokened **accept** flow, admin **unlink**, and `account.*` ingest reconciliation. - New admin controls: **Settings → Game-account creation** (disabled / website / hybrid / game) and an **Invites** admin view. **House visibility (role-tiered)** - Public `/site/houses` → IDOC-only, location only. Staff `/admin/houses` → full registry (owner/price/decay). Player portal → own houses ("home status"). Registry deltas (`house.update/remove`, which carry owner/price) removed from the public SSE channel. ## Schema New tables: `shard_guilds`, `shard_governors`, `shard_governor_terms`, `shard_presence`, `user_invites`; `shard_houses` extended with registry columns. New settings: `game_account_signup`. (Includes a fix for a `;`-in-inline-comment that broke the boot schema splitter.) ## Verification - Server test suite green (added ingest-routing, governor term-capture, news-gump sync, and invite-model tests); client build clean; Swagger regenerated each backend phase. - **Live-smoke passed** against the real stack (MariaDB + Rust sidecar @ protocol 2 + ServUO "My Shard"): boards populate (9 governor cities, 28-house registry, IDOC public view), news gump re-asserts, invites (email + link) and the game-account creation UI verified in-browser. ## Notes for deploy - Requires the sidecar/shard on **Protocol 2** (client pins `X-UOLink-Version`); set `uo_link_config.protocol = 2`. - Game-account creation needs the shard's `SignupMode` = website/hybrid (Bridge.cfg) and `RequireIpForCreate` satisfied by a real forwarded browser IP (works behind Pangolin; loopback is refused locally by design). - Email invites need Gmail OAuth configured; otherwise the admin uses the copyable link. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
wtclaude added 9 commits 2026-07-18 02:14:40 +00:00
Phase 1 of the Protocol 2.0/2.1 integration: the read/ingest backend for the four
new uo-link boards, following the established champs/pages pattern (ingest → our
MariaDB + snapshot-on-reconnect + public SSE + token-free public endpoint).

- Schema: shard_guilds, shard_governors, shard_governor_terms, shard_presence;
  extend shard_houses with the house.update registry columns (owner_name,
  co_owners, friends, price, decay, in_registry) so the decay-transition and
  registry feeds share one house row without clobbering each other.
- Ingest: route guild.update/remove, city.update, presence.online,
  house.update/remove; log guild.join (real-time joins feed); region.enter is
  broadcast-only. All new public kinds added to the SSE allowlist.
- Governor term history captured from day one: on every observed governor CHANGE
  the open term is closed and a new one opened, idempotent so backfill/duplicate
  city.update never spawn spurious terms. votes stays NULL (the feed carries only
  candidate count, not tallies) — we never fabricate vote numbers.
- Client + backfill: getGuilds/getGovernors/getHouses/getPresence; snapshot each
  board on every WS (re)connect, independently guarded so an empty/failed board
  (e.g. no City Loyalty) never wipes another.
- Public endpoints: /shard/{guilds,governors,governors/:city/history,presence,houses}.
- Tests: ingest routing for all new kinds + governor term-capture idempotency
  (15 new; full suite 179/179). Swagger regenerated.

Refs .plans/protocol2-integration.md (Phase 1).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 2: the public UI for the four new boards, following the ChampSpawns live
pattern (snapshot via useAsync + merge SSE deltas with useShardFeed).

- Players Online widget (components/PlayersOnline.jsx): total + region breakdown
  rolled up into display buckets (data/regionBuckets.js — the one place to retune
  the grouping); live via presence.online. Placed on the Shard page, replacing the
  static players-online stat tile.
- Guilds (/site/guilds): searchable board of rosters/alliances/leaders with a
  "recently joined" strip from guild.join.
- Governors (/site/governors): one card per city with a placeholder crest
  (data/cityCrests.js — swap for real art without touching components), election
  phase badge + autoPickAt countdown, and an on-demand "past governors" term
  history (the look-back reads the ledger captured in Phase 1). Clean empty state
  when City Loyalty isn't enabled.
- Houses (/site/houses): searchable registry with decay badges; price labelled
  "placement value", not a for-sale flag.
- API client methods + nav links (Guilds / Governors / Houses).

Client build clean (240 modules).

Refs .plans/protocol2-integration.md (Phase 2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 3: surface the new board data on existing character/user pages.

- Character sheet: render the char.profile titles block (fame/karma + skill +
  selected reward title; numeric clilocs skipped since the site has no cliloc
  table yet), plus "Guildmaster" and "Governor of <city>" chips.
- Char profile enrichment (player/admin /shard/char/:serial, one shared path):
  attach guild + governorOf from our own boards. Guild is LEADERSHIP-ONLY — it's
  verifiable from current board state, whereas guessing membership from stale
  guild.join events risks showing a wrong guild, so we return null instead.
- Admin user detail (/admin/users/:id): new "Standing" section (governorships
  held + guilds led) via GET /users/:id/shard/standing; Houses rows now show the
  registry fields (decay level, placement price, co-owner/friend counts) already
  returned by listHousesForAccounts.

Server 179/179, client build clean, swagger regenerated.

Refs .plans/protocol2-integration.md (Phase 3).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 4: sync the site's published news posts into the Protocol 2.1 News gump.

- uoLinkClient.postNews / deleteNews.
- utils/newsGump.js — a STATE SYNC (not a one-shot announce leg): an article
  stays in the gump while its post is published news and is pulled when it leaves
  that state. buildArticle renders a compact gump-HTML block (centred title +
  plain-text excerpt — the gump supports only a small HTML subset) with a
  "more info" link to /site/news and an optional gump image from the
  `news_gump_image` setting. Every call is best-effort / never-throws.
- Hooked into the posts pipeline alongside the existing announce enqueue:
  syncPost on create/update/publish (fresh publish announces; edits refresh
  silently; leaving published-news pulls the article), removePost on delete.
- reassertAll() runs in uoLinkSocket.backfill on every WS (re)connect —
  reconciles the gump to our source of truth and recovers any article whose
  original live push failed (silent, so a reconnect never re-proclaims old news).

Server 185/185, swagger regenerated. Refs .plans/protocol2-integration.md (Phase 4).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 5: the account-provisioning backend — link-only stays, plus hybrid
self-signup, an admin email-invite tool, and site-side unlink.

- uoLinkClient.createAccount / unlinkAccount (v2). Password is forwarded to the
  shard (hashed there) and never stored/logged; the end-user browser IP is passed
  for the shard's per-IP cap; actor is stamped server-side.
- Hybrid signup: POST /player/shard/account provisions a game account (its own
  username + password) for the signed-in user and mirrors the link locally. Gated
  by the new game_account_signup setting AND the shard's own mode (mapped 403/409/
  429/400/503). Serves both self-serve signup and the invite-accept game step.
- Email invites: user_invites table (sha256 token hash, single-use, expiring);
  invites model + admin CRUD (POST/GET/DELETE /admin/invites, admin-only) +
  mailer.sendInvite (falls back to returning the accept link if email is off);
  public token-gated accept (GET /auth/invite/:token, POST .../accept) creates the
  user at the invite's preset role and logs them in, bypassing the registration
  gate. Accept is race-safe (atomic single-use; rolls back the user if it loses).
- Admin unlink: DELETE /admin/users/:id/shard/link/:account (admin-only) + local
  mirror drop; account.unlinked ingest reconciles the mirror when a player runs
  [unlink in game. account.audit / account.unlinked are logged (admin channel
  only — never on the public SSE allowlist).

Tests: invites model (hashing, single-use, expiry, revoke) + account.* ingest
reconcile/visibility. Full suite 193/193; swagger regenerated.

Refs .plans/protocol2-integration.md (Phase 5).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 6: the UI for the Phase 5 provisioning backend.

- CreateGameAccountForm: reusable game-account form (own username + password),
  mapping the sidecar errors (409/429/403/503) to friendly messages. Wired into
  GameAccounts (self-serve) — shown alongside the [link flow when the
  game_account_signup flag is on (exposed via public settings), so a registered
  player can create + link a game account from their portal.
- Admin Invites view (/admin/invites, admin-only): send an invite at a chosen
  access level, list invites with status, revoke pending ones. When email isn't
  configured the create response's accept link is surfaced to copy manually.
- Public accept page (/invite/:token): validates the invite, sets username +
  password (email + role pre-assigned), creates the account at that role and logs
  in; for a player invite it then offers the built-in "create game account" step
  before the portal. Honeypot-guarded like registration.
- Admin unlink wired into UserDetail via GameAccounts (per-account Unlink button,
  confirm + reconcile).
- Backend: expose gameAccountSignup availability in public settings.

Client build clean; server 193/193.

Refs .plans/protocol2-integration.md (Phase 6). Completes the Protocol 2.0/2.1 integration.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ensureSchema() splits schema.sql on ';' and is not comment-aware, so the inline
comments "epoch ms; NULL = current" and "not in the feed; reserved" shattered the
CREATE TABLE into invalid fragments (ER_PARSE_ERROR on a fresh boot). Reworded both
to drop the semicolons. Caught during live-stack bring-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per request, split the single public house registry into three role-scoped views:

- Public /site/houses → only houses in DANGER (IDOC), by LOCATION (region + map/
  coords). No owner, price, co-owners or decay detail. Renamed "Houses in danger";
  kept live via the public house.decay feed. The full-registry deltas
  (house.update / house.remove — which carry owner/price) are REMOVED from the
  public SSE allowlist so they never reach the public channel.
- Staff full registry → new /admin/houses (admin + moderator, RoleGate + MOD_PATHS)
  backed by GET /admin/shard/houses (modAccess), with owner/price/co-owners/decay
  and search, kept live on the admin SSE channel.
- Player portal → "My houses" home-status section (own houses only, with decay/
  IDOC status) via GET /player/shard/houses, scoped to the caller's linked accounts.

Server tests green, client build clean, swagger regenerated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat(provisioning): admin game-signup mode setting, invite link option, staff self-create
All checks were successful
PR Checks / server-tests (pull_request) Successful in 9m37s
PR Checks / client-build (pull_request) Successful in 10m18s
PR Checks / bot-install (pull_request) Successful in 9m22s
3ef1c8e438
Follow-ups from live testing:

- Game-account creation is now an admin Settings control (disabled / website /
  hybrid / game) instead of a hidden on/off flag. The site offers creation for
  website+hybrid; help text notes the shard's SignupMode (Bridge.cfg) has the final
  say. game_account_signup setting widened to a 4-value enum + validated on save.
- Invites: the accept link is ALWAYS returned and shown with a Copy button, and a
  "Email the invitation" toggle lets an admin create a link-only invite (no email)
  or email it. Backend takes sendEmail (default true) and always returns acceptUrl.
- Staff can create a game account from their own /admin/characters page too
  (POST /admin/shard/account → the shared createGameAccount controller), so the
  form is reachable in both the player and admin portals.

Note: the admin Houses view (/admin/houses) already worked; the earlier failure
was a stale Vite HMR state for the new route (needs a hard refresh).

Client build clean; server routes load; swagger regenerated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
whitlocktech approved these changes 2026-07-18 02:29:19 +00:00
whitlocktech scheduled this pull request to auto merge when all checks succeed 2026-07-18 02:29:26 +00:00
whitlocktech merged commit ee085496ab into main 2026-07-18 02:34:27 +00:00
whitlocktech deleted branch feature/protocol2-integration 2026-07-18 02:34:27 +00:00
Sign in to join this conversation.
No description provided.