fix(shard): answer with the instance name when the shard is unnamed #119

Merged
whitlocktech merged 1 commits from fix/ruleset-shard-name into edge 2026-08-01 06:04:23 +00:00
Member

What & why

From the live Protocol 3.0 smoke test (ServUO + sidecar + website + AVD).

The rules page read "My Shard" under a header reading "UOMysticmoon". ServUO ships Server.cfg with Name=My Shard, so an operator who never edited it publishes that verbatim. That value is the shard saying unnamed, not naming anything — so the site now answers with its own name.

Three things in here

1. The fallback. New settings.getInstanceName() resolves site_title || BRAND_NAME — the same resolution getPublic().brand.name already uses, so an install that set only the site title can never show two different names on two pages. Bare brand.name would have been wrong for exactly that case (this test install has BRAND_NAME unset and site_title set, and would have printed "Runic Gateway").

Matched case- and padding-insensitively, but only as a whole value: a shard genuinely called "My Shard Reborn" has named itself and keeps it.

Applied at ingest, not on readworld.ruleset is also broadcast live, and the same object is handed to the SSE fan-out, so a read-time substitution would be undone by the next reconnect's frame. Both halves are asserted.

2. A second ruleset writer, found on the way. uoLinkSocket.backfill() called shardState.setRuleset directly instead of going through shardIngest.ingest() as ingestEach does for the array-shaped boards — so the boot/reconnect snapshot silently skipped the normalization the live frame got. Symptom was the fix appearing to not work on a fresh boot. The two arrival orders have to produce the same stored frame; routed through the dispatcher so there is one writer.

Worth generalising: any state a kind can arrive by two orders (live frame vs backfill snapshot) must funnel through the dispatcher, or the two paths drift.

3. Unscored leaderboards render a row. Most systems on a young shard have top: [], and a page of blank cards reads as broken rather than as new. An empty board now shows a single placeholder bearing the instance name with an em dash where a score goes — deliberately not shaped like an entry (no medal, no bar, muted), because a placeholder that looked like a real standing would be a fabricated one. The first real entry replaces it outright.

Presentation only: the API still sends an empty top, so no consumer ever receives an invented row. The app renders it the same way (RunicGateway/Android-app#32).

How it was tested

  • npm test (server) — 679 tests, 0 failures; node --test (client) — 47, 0 failures.
  • Six new tests in shardIngest.ruleset.test.js cover the stock name, blank/missing, the live-broadcast half, the whole-value match, a shard that named itself, and a settings-read failure. One of them caught a real slip while writing this (a wrong variable name that silently skipped the write).
  • Verified end to end against the running shard + sidecar: GET /public/shard/ruleset returns "shard": "UOMysticmoon" after a cold boot through the snapshot path, and /site/rules + /site/leaderboards render correctly in the browser.
  • npm run swagger regenerates with no content change (no route signature changed), so the committed spec is untouched.

Docs: RunicGateway/docs#76.

Checklist

  • I have read CONTRIBUTING.md.
  • The change builds and existing tests/checks pass locally.
  • I have added or updated tests/docs where it makes sense.
  • My commits are reasonably scoped with clear messages.

AI-assisted contributions (required)

  • No AI tools were used to produce this contribution.
  • AI tools were used. Tool(s): Claude Code (Opus 5). I have reviewed and understand every change, and take responsibility for it. AI-authored commits are marked with a Co-Authored-By / Assisted-By trailer.

License

  • I agree that my contribution is licensed under this project's license (GNU GPL v3.0 or later), and I have the right to contribute it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01U7CBg11prhLimL9iHSX1bP

## What & why From the live Protocol 3.0 smoke test (ServUO + sidecar + website + AVD). **The rules page read "My Shard" under a header reading "UOMysticmoon".** ServUO ships `Server.cfg` with `Name=My Shard`, so an operator who never edited it publishes that verbatim. That value is the shard saying *unnamed*, not naming anything — so the site now answers with its own name. ### Three things in here **1. The fallback.** New `settings.getInstanceName()` resolves `site_title || BRAND_NAME` — the *same* resolution `getPublic().brand.name` already uses, so an install that set only the site title can never show two different names on two pages. Bare `brand.name` would have been wrong for exactly that case (this test install has `BRAND_NAME` unset and `site_title` set, and would have printed "Runic Gateway"). Matched case- and padding-insensitively, but only as a **whole** value: a shard genuinely called *"My Shard Reborn"* has named itself and keeps it. Applied at **ingest, not on read** — `world.ruleset` is also broadcast live, and the same object is handed to the SSE fan-out, so a read-time substitution would be undone by the next reconnect's frame. Both halves are asserted. **2. A second ruleset writer, found on the way.** `uoLinkSocket.backfill()` called `shardState.setRuleset` directly instead of going through `shardIngest.ingest()` as `ingestEach` does for the array-shaped boards — so the boot/reconnect snapshot silently skipped the normalization the live frame got. Symptom was the fix appearing to not work on a fresh boot. The two arrival orders have to produce the same stored frame; routed through the dispatcher so there is one writer. Worth generalising: **any state a kind can arrive by two orders (live frame vs backfill snapshot) must funnel through the dispatcher, or the two paths drift.** **3. Unscored leaderboards render a row.** Most systems on a young shard have `top: []`, and a page of blank cards reads as broken rather than as new. An empty board now shows a single placeholder bearing the instance name with an em dash where a score goes — deliberately **not** shaped like an entry (no medal, no bar, muted), because a placeholder that looked like a real standing would be a fabricated one. The first real entry replaces it outright. Presentation only: **the API still sends an empty `top`**, so no consumer ever receives an invented row. The app renders it the same way (RunicGateway/Android-app#32). ## How it was tested - `npm test` (server) — **679 tests, 0 failures**; `node --test` (client) — **47, 0 failures**. - Six new tests in `shardIngest.ruleset.test.js` cover the stock name, blank/missing, the live-broadcast half, the whole-value match, a shard that named itself, and a settings-read failure. One of them caught a real slip while writing this (a wrong variable name that silently skipped the write). - Verified end to end against the running shard + sidecar: `GET /public/shard/ruleset` returns `"shard": "UOMysticmoon"` after a cold boot through the *snapshot* path, and `/site/rules` + `/site/leaderboards` render correctly in the browser. - `npm run swagger` regenerates with no content change (no route signature changed), so the committed spec is untouched. Docs: RunicGateway/docs#76. ## Checklist - [x] I have read [CONTRIBUTING.md](CONTRIBUTING.md). - [x] The change builds and existing tests/checks pass locally. - [x] I have added or updated tests/docs where it makes sense. - [x] My commits are reasonably scoped with clear messages. ## AI-assisted contributions (required) - [ ] No AI tools were used to produce this contribution. - [x] AI tools were used. Tool(s): `Claude Code (Opus 5)`. I have reviewed and understand every change, and take responsibility for it. AI-authored commits are marked with a `Co-Authored-By` / `Assisted-By` trailer. ## License - [x] I agree that my contribution is licensed under this project's license (**GNU GPL v3.0 or later**), and I have the right to contribute it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01U7CBg11prhLimL9iHSX1bP
wtclaude added 1 commit 2026-08-01 06:00:20 +00:00
ServUO ships Server.cfg with `Name=My Shard`. An operator who never edited it
publishes that verbatim, so the rules page read "My Shard" under a header
carrying the real name. That value is the shard saying *unnamed* rather than
naming anything, so the site now answers with its own.

`settings.getInstanceName()` resolves `site_title || BRAND_NAME` — the same
resolution `getPublic().brand.name` already uses, so an install that set only
the site title can never show two different names on two pages. Bare
`brand.name` would have been wrong for exactly that case.

Substituted at INGEST rather than on read: world.ruleset is also broadcast
live, and the same object is handed to the SSE fan-out, so a read-time fix
would be undone by the next reconnect's frame. Matched case- and
padding-insensitively but only as a whole value, so a shard genuinely called
"My Shard Reborn" keeps its name.

Fixes a second ruleset writer found on the way: uoLinkSocket.backfill() called
shardState.setRuleset directly instead of going through the dispatcher as
ingestEach does, so the boot/reconnect snapshot silently skipped this
normalization. The two arrival orders have to produce the same stored frame.

Also renders a placeholder row on an unscored leaderboard — the instance name
with an em dash where a score goes, deliberately not shaped like an entry (no
medal, no bar) because a placeholder that looked like a real standing would be
a fabricated one. Presentation only; the API still sends an empty `top`.

Verified live against the shard + sidecar: rules page and leaderboards on web
and Android both correct.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U7CBg11prhLimL9iHSX1bP
whitlocktech approved these changes 2026-08-01 06:04:15 +00:00
whitlocktech merged commit c91fd128bf into edge 2026-08-01 06:04:23 +00:00
whitlocktech deleted branch fix/ruleset-shard-name 2026-08-01 06:04:24 +00:00
Sign in to join this conversation.
No description provided.