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 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>
Wire up the three uo-link sidecar surfaces that weren't integrated yet.
Champion spawns
- Ingest champ.update/champ.remove into a new shard_champs table (served from
our own store, like online/houses); public /site/champs board with a nav link,
live via the existing SSE feed (champ.* added to the public allowlist).
Staff write plane (admin + moderator)
- kick / ban / unban / broadcast via /admin/shard/*; actor is stamped server-side
from the session, never the browser. Sidecar status codes mapped (403 disabled/
protected, 404 unknown, 503/504 transient). admin.audit events are logged and
surfaced at /admin/shard/audit.
- New admin "In-Game Ops" view (/admin/shard-ops), plus per-account Kick/Ban/Unban
on the user-detail and character views (ShardAccountActions, self-gated to staff).
Help-page (support) queue
- Ingest page.new/updated/closed into a new shard_pages table; respond/close via
/admin/shard/pages/*. Champ board and page queue are snapshotted from the
sidecar's /champs and /pages on every WS (re)connect (guarded so a failed call
never wipes local state).
Verified live end-to-end against MariaDB + the Rust sidecar + ServUO; unit tests
cover ingest routing (shardIngest.champsPages.test.js). Swagger regenerated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0114TpmrNW4wNXsHq5CR72jQ
Add a "View" action beside Edit in the users table that opens a dedicated,
read-only page showing everything the uo-link shard knows about a user,
scoped to their linked game accounts: character rosters, currently-online
characters, houses (IDOC-first), and recent vendor sales.
Backend (admin-only, under the existing /users adminOnly gate):
- GET /admin/users/:id — single sanitized user (page is deep-linkable)
- GET /admin/users/:id/shard/{accounts,sales,houses,online}
- shardState: listHousesByAccounts / listOnlineByAccounts (+ model shapers)
- Extract salesForAccounts into utils/shardSales; reuse in player getSales
- Live rosters reuse the existing admin-bypass /admin/shard/* endpoints,
so no new routes for roster/vendors/char
Frontend:
- UserDetail page reusing CharacterStats / GameAccounts / VendorSales
- GameAccounts gains a readOnly prop (drops link form + self-voice copy)
- api.admin.getUser + api.admin.userShard(id) scope; route + layout title
Tests: adminUserShard.test.js (404, account scoping, empty accounts,
salesForAccounts cap/filter). Full server suite 164 pass; client builds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0114TpmrNW4wNXsHq5CR72jQ
Public "Online now" now lists only players whose game account is linked
to a STAFF website user (admin/editor/moderator) — linked players are no
longer exposed publicly with their name and location. listOnlineLinked
joins through to users and filters on role; the section is relabeled
"Staff online".
Character/roster/vendor reads gain an admin bypass: admins may view any
character's data, while players (and editor/moderator staff) stay limited
to accounts they have personally linked. The bypass lives in the shared
player controller and only ever widens access for genuine admins.
Also finalizes the uo-link character/vendor front end (player + admin
character sheets, VendorSales component, ShardChar removed) and
regenerates swagger-output.json.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018kj5s1QCKobuFPYmqxjy1q
The site now ingests the sidecar's live WebSocket feed and persists it to its
own MariaDB, and re-broadcasts curated events to browsers over SSE.
- schema: shard_events (append-only notable-kind log, sha1 dedupe_key +
INSERT IGNORE for idempotent reconnect backfill), shard_online (current
players, upsert/refresh/remove), shard_economy (gold-supply series),
shard_houses (per-house decay stage + derived is_idoc).
- model/shardEvents + model/shardState: the .db.js/.model.js split; writes
take camelCase event data, reads are shaped; online upsert uses COALESCE so
a partial char.vitals refresh never blanks login fields.
- utils/shardIngest: single dispatcher routing each kind to state writes
and/or the event log, then the broadcaster. High-frequency kinds
(char.vitals, economy.supply) update state only. A changed server.hello
bootId clears the stale online roster. Deps are injected for unit testing.
- utils/uoLinkSocket: the server's first outbound WS client (ws dep). Verifies
the ws.hello protocol, backfills via /history + /economy on every
(re)connect (dedupe handles overlap), reconnects with capped backoff, and
mirrors connection state into uo_link_config. Self-guards: only connects when
the integration is enabled with a token.
- utils/shardBroadcast: SSE fan-out with public (safe kinds only) and admin
(all) channels, keepalive pings, per-client cleanup.
- server.js: start the ingest socket on boot (no-op until configured) and stop
it + close SSE streams on graceful shutdown.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qPmpmVH1xGCiZoz9m9vW3