feat(shard): ingest points.board and publish the leaderboards #114
Reference in New Issue
Block a user
No description provided.
Delete Branch "feat/points-board"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What & why
Protocol 3.0 §7 (
docs/link/v3.md), order 4 of 6. The shard publishes ~25 points/loyalty leaderboards — Queen's Loyalty, Void Pool, the nine city loyalties, Clean Up Britannia — and the site renders them, plus each character's own standings on their sheet.Part of a four-repo change: servuo-plugins #4 → link #18 → website (this) → docs #69.
Server
shard_points_boards— one row per system, keyed by the shard'sPointsTypename. The top-N list stays insidepayload: a fixed-size list read whole, exactly likeshard_governors.candidates. Normalizing into an entries table buys nothing until something needs a per-character reverse lookup, and a character's own standings already ride insidechar.profileinstead.shardIngestroutespoints.boardtoupsertPointsBoardand deliberately does NOT log it — this is board state likeguild.update, and the shard emits a frame every time anyone's score moves a top ten. Logging would growshard_eventswithout bound for something whose only interesting value is its latest version.uoLinkSocketbackfills/pointsthroughsnapshot()withingestEachrather than areplace*: there is nopoints.removeand the system set is fixed, so upserting is the reconciliation — and a system the operator later excludes keeps its last-known board rather than vanishing, which is the right answer for a month-scale standing.GET /public/shard/pointsand/points/:systembehindrequireFeature('leaderboards'), both projected per §3.6.1.:systemis constrained to an identifier before any query runs; 404 for a system never published, distinct from a published board nobody has scored in (200, emptytop).⚠️ The leaderboards field rule now keys on
name, notcharacterName— please look at this onePart A pre-wired
FEATURES.leaderboards.fields = { characterName: 'anonymous' }, and v3.md §7.4 specifies that name. ButprojectValuematches on the literal JSON key, and the wire key for a ranked player isname. As written the rule was inert: an admin tightening character names would have got no enforcement and no error.That is precisely the failure §3.6.1 records for the flattened
ownerAcctspelling — a rule named for the field's meaning rather than its key. Fixed to key onname, with a test that fails if it is renamed back, and the admin panel'sFIELD_LABELcarries the meaning to the operator instead ("Character names on leaderboards"). Within a leaderboards payloadnamecan only be a character name — the board's own display name arrives asnameString/nameNumber.Client
routes/public/Leaderboards.jsxat/site/leaderboards. Apoints.boardframe describes one system, so live frames merge over the fetched set by system key rather than replacing it wholesale the way the ruleset does. The filter matches board name, system key, or any ranked player — the last is what makes it useful ("where do I appear, and who's around me?").CharacterSheet.jsx, one edit serving bothPlayerCharacterandAdminCharacter.maxPoints: 0as UNCAPPED and both fall back to humanising the system key whennameStringis null. Neither is defensive padding — on a real shard, uncapped systems and cliloc-only names are the majority case (see servuo-plugins #4).How it was tested
Verified end to end against the local MariaDB, the Rust sidecar from #18, and the real ServUO shard — not just unit tests.
Full chain — fake shard → sidecar (TCP) → sidecar store + WS →
uoLinkSocket→shardIngest→ MariaDB → public REST → browser:snapshotted points boards from /points {"count":2});toplist replaced) — confirming latest-wins merge by system;The visibility gate, exercised live at every rung (config toggled in the DB, endpoint re-read anonymously each time):
200, ranked names presentfieldRules: { name: 'staff' }audience: 'staff'403, and dropped from/featuresso nav hidesenabled: false404(does not leak that it exists)200with namesPages — rendered at
/site/leaderboards: podium colours, ranks 1–10, per-board participant counts, uncapped vs capped boards, the empty-board state ("Nobody has earned points here yet"), the cliloc fallback renderingCleanUpBritannia→ "Clean Up Britannia", and player-name filtering. No console errors beyond the pre-existing React Router v7 future-flag warnings.Suites —
605 server tests pass(7 new inshardIngest.points.test.js, 6 new controller tests, 2 new visibility tests including the guard on thenamerule above). Client builds clean.routes.manifest.json,routes.guards.jsonand the OpenAPI spec regenerated and committed.Not covered by an automated test: the
CharacterSheetpoints section is presentational React with no DOM test harness in this repo (the client suite covers pure-logic modules only). It builds clean and follows the skills section directly above it, but it was not rendered against a live linked-player profile — that needs a logged-in player with a linked game account and a shard answering a profile RPC.Checklist
AI-assisted contributions (required)
Claude Code (Opus 5). I have reviewed and understand every change, and take responsibility for it. AI-authored commits are marked with aCo-Authored-Bytrailer.License
Protocol 3.0 §7 (docs/link/v3.md). The shard publishes ~25 points/loyalty leaderboards — Queen's Loyalty, Void Pool, the nine city loyalties, Clean Up Britannia — and the site renders them, plus each character's own standings on their sheet. Server - shard_points_boards: one row per system, keyed by the shard's PointsType name. The top-N list stays inside `payload` — a fixed-size list read whole, exactly like shard_governors.candidates. Normalizing into an entries table buys nothing until something needs a per-character reverse lookup, and a character's own standings already ride inside char.profile. - shardIngest routes points.board to upsertPointsBoard and deliberately does NOT log it: this is board state like guild.update, and the shard emits a frame every time anyone's score moves a top ten. - uoLinkSocket backfills /points through snapshot() with ingestEach rather than a replace*: there is no points.remove and the system set is fixed, so upserting IS the reconciliation, and a system the operator later excludes keeps its last-known board rather than vanishing. - GET /public/shard/points and /points/:system behind requireFeature('leaderboards'), both projected per §3.6.1. :system is constrained to an identifier before any query runs; 404 for a system never published, distinct from a published board nobody has scored in (200, empty top). The leaderboards field rule now keys on `name`, not `characterName` Part A pre-wired FEATURES.leaderboards.fields = { characterName: ... }, but projectValue matches on the LITERAL JSON key and the wire key is `name`. As written the rule was inert: an admin tightening character names would have got no enforcement and no error — precisely the failure §3.6.1 records for the flattened `ownerAcct` spelling. Fixed, with a test that fails if it is renamed back, and the admin panel's FIELD_LABEL carries the meaning instead. Client - routes/public/Leaderboards.jsx at /site/leaderboards. A points.board frame describes ONE system, so live frames merge over the fetched set by system key rather than replacing it wholesale the way the ruleset does. Filter matches board name, system key, or any ranked player — the last is what makes it useful ("where do I appear?"). - A "Loyalty & Points" section in CharacterSheet.jsx, one edit serving both PlayerCharacter and AdminCharacter. - Both treat maxPoints: 0 as UNCAPPED and both fall back to humanising the system key when nameString is null. Neither is defensive padding: on a real shard uncapped and cliloc-only names are the majority case. Verified end to end against the local MariaDB, the Rust sidecar, and the real ServUO shard: backfill from /points, live SSE delivery (a board absent from the initial fetch appearing without a reload, and an existing one updating in place), REST reflecting the overwrite, and the gate at every rung — 200 by default with names, names stripped but points kept at fieldRules name=staff, 403 plus dropped from /features at audience=staff, 404 when disabled. Page rendered clean, no console errors beyond the pre-existing React Router v7 warnings. 605 server tests pass; routes.manifest.json, routes.guards.json and the OpenAPI spec regenerated. Co-Authored-By: Claude <noreply@anthropic.com>