feat(shard): admin write plane, help-page queue, and public champion board
All checks were successful
PR Checks / server-tests (pull_request) Successful in 10m20s
PR Checks / client-build (pull_request) Successful in 9m49s
PR Checks / bot-install (pull_request) Successful in 9m33s

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
This commit is contained in:
2026-07-14 13:16:10 -05:00
parent 2dc360ca48
commit c31553aeb6
25 changed files with 2188 additions and 8 deletions

View File

@@ -16,6 +16,7 @@ const WebSocket = require('ws')
const uoLinkConfig = require('../model/uoLinkConfig/uoLinkConfig.model')
const uoLinkClient = require('./uoLinkClient')
const shardIngest = require('./shardIngest')
const shardState = require('../model/shardState/shardState.model')
const log = require('./logger')('uo-link-socket')
const BACKOFF_MIN_MS = 1000
@@ -59,6 +60,21 @@ async function backfill() {
const series = [...eco.data.series].reverse()
for (const ev of series) await shardIngest.ingest(ev, { fromBackfill: true })
}
// Champ board + help-page queue have no replay stream — snapshot the
// authoritative current state directly (the sidecar guide's advice for both),
// reconciling our tables to it so a stale row from before a disconnect can't
// linger. Live champ.*/page.* deltas keep them fresh thereafter.
const champs = await uoLinkClient.getChamps()
if (champs.ok && champs.data && Array.isArray(champs.data.spawns)) {
await shardState.replaceChamps(champs.data.spawns)
log.info('snapshotted champ board from /champs', { count: champs.data.spawns.length })
}
const pages = await uoLinkClient.getPages()
if (pages.ok && pages.data && Array.isArray(pages.data.pages)) {
await shardState.replacePages(pages.data.pages)
log.info('snapshotted help-page queue from /pages', { count: pages.data.pages.length })
}
} catch (err) {
log.warn('backfill failed (continuing on live feed)', { message: err.message })
}