diff --git a/android/PLAN.md b/android/PLAN.md index 47782a1..af75c06 100644 --- a/android/PLAN.md +++ b/android/PLAN.md @@ -970,14 +970,22 @@ push, and Play (M6–M8) 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` 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. diff --git a/link/v3.md b/link/v3.md index 284f886..cd79d2a 100644 --- a/link/v3.md +++ b/link/v3.md @@ -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