docs(link): the shard-name fallback, the atlas places shape, and two traps #76

Merged
whitlocktech merged 1 commits from docs/protocol-3-smoke-findings into edge 2026-08-01 06:03:02 +00:00
2 changed files with 36 additions and 5 deletions

View File

@@ -970,14 +970,22 @@ push, and Play (M6M8) follow the designed app.
(`atlas`). Note the path: `/public/atlas`, **not** `/public/shard` — the atlas is static shard
*content*, not live shard *state*, and unlike `/shard/*` it **is** `siteMode`-gated like
`/posts` and `/wiki`, so a site in maintenance mode withholds it independently of the sidecar.
Two units/naming traps from `v3.md` §6.3: respawn delays are **seconds** throughout, and
`points` is a *count* on the search route while `spawners` is the *list* on the detail route.
Three traps. Two are units/naming, from `v3.md` §6.3: respawn delays are **seconds**
throughout, and `points` is a *count* on the search route while `spawners` is the *list* on
the detail route. The third is a **shape**: `places` is a list of
`{facet, label, spawners, maxAlive}` **objects**, not of place-name strings — it is the
aggregate the screen exists to show ("Shrines, Isamu-Jima, Yew"), it arrives only on the
detail route, and typing it `List<String>` makes that whole route fail to decode while the
request itself returns `200`.
- **Verification** — the five-rung walk (`anonymous`, `logged_in`, `player`, `staff`, `admin`)
against a local website on the cutover branch, per
[`../link/v3.md`](../link/v3.md) §11 and the shard-visibility smoke harness; plus one pass with
**every feature disabled** in Admin → Shard Visibility, confirming the app *hides* each surface
instead of erroring on it. Unit tests cover the menu filter (role × feature set), the
`404`/`403`/`503` mapping, and DTO decode for each new shape.
`404`/`403`/`503` mapping, and DTO decode for each new shape. **Decode tests must feed real
captured JSON**, not DTOs built in Kotlin: the fakes under `data/api/fake/` construct objects
directly, so they can never catch a wire/type mismatch — which is how the `places` shape above
shipped past a green suite.
- **Excluded**, in the same class as M10's exclusions: the admin *configuration* panels — Shard
Visibility, Spawn Atlas and Cliloc import — alongside the hero/CMS block editor, Discord-bot
config, uo-link config and OAuth-provider setup.

View File

@@ -350,8 +350,11 @@ Sidecar — `store.rs`: singleton `ruleset(id CHECK(id=1), rev, json, updated_t)
`main.rs`: new arm in the board-projection match; `web.rs`: `GET /ruleset` served from the store, so
it answers during a shard outage (`PROTOCOL_2.md` §12.2).
Website — `uoLinkClient.getRuleset()`; `uoLinkSocket.backfill()` (object-shaped, so follow the
`getPresence()` block's explicit form, not the array-only `snapshot()` helper); `shardIngest.js`
Website — `uoLinkClient.getRuleset()`; `uoLinkSocket.backfill()` (object-shaped, so it cannot use the
array-only `snapshot()` helper — but it **must still go through `shardIngest.ingest()`**, as
`ingestEach` does, rather than calling `shardState.setRuleset` directly: the two arrival orders have
to produce the same stored frame, and a direct call quietly made backfill a second writer that
skipped the normalization below); `shardIngest.js`
`shardState.setRuleset`, **not** in `LOGGED_KINDS` (it re-arrives every reconnect and `server.hello`
already marks those); `KIND_FEATURE['world.ruleset'] = 'ruleset'`; `shard_ruleset` singleton table
(`rev`, `expansion`, `payload JSON`, `t`); `GET /public/shard/ruleset` behind
@@ -360,6 +363,17 @@ already marks those); `KIND_FEATURE['world.ruleset'] = 'ruleset'`; `shard_rulese
Client — NEW `routes/public/Rules.jsx` at `/site/rules`, alongside
`/site/champs|guilds|governors|houses`; live via `useShardFeed({ filter: new Set(['world.ruleset']) })`.
**The `shard` field falls back to the instance's own name.** ServUO ships `Server.cfg` with
`Name=My Shard`, so an operator who never edited it publishes that verbatim — which is the shard
saying *unnamed*, not naming anything, and the rules page then reads "My Shard" under a header
carrying the real one. `shardIngest` substitutes `settings.getInstanceName()` (the admin-editable
site title, else `BRAND_NAME` — the same resolution `getPublic().brand.name` uses, so one install
never shows two names) when `shard` is absent, blank, or exactly the stock default, matched
case-insensitively and trim-tolerantly 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, because the
ruleset is also broadcast live — the same object goes to the SSE fan-out, so a read-time
substitution would be undone by the next reconnect's frame.
### 5.4 Risk
Perf is nil (~3 KB per connect). The only real risk is publishing a secret, mitigated by the explicit
@@ -638,6 +652,15 @@ Client — NEW `routes/public/Leaderboards.jsx` at `/site/leaderboards`; a "Loya
added to `components/CharacterSheet.jsx`, one edit serving both `PlayerCharacter.jsx` and
`AdminCharacter.jsx`.
**An unscored board still renders a row.** Most systems on a young shard have `top: []`, and a page
of blank cards reads as broken rather than as new — so a board with no entries shows a single
placeholder bearing the **instance's own name** with an em dash where a score goes, above the
existing "nobody has earned points here yet" line. It is deliberately **not** shaped like an entry —
no rank, 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. Purely presentational: the API keeps
sending an empty `top`, so no consumer ever receives an invented row. Web and app render it the same
way (`Leaderboards.jsx`, `LeaderboardsScreen.kt`).
### 7.5 What the run against a real shard changed
The plan above was written from reading `PointsSystem.cs`. Booting the actual shard (ServUO 57.4, a