docs(website): the spawn atlas API, the admin panel, and the delay-unit trap

Docs half of website #113 (Protocol 3.0 Part C, second website PR).

Carries the atlas rewrite that missed #67: that PR merged before the "derive
from the tree on every boot" commit was pushed, so `edge` currently describes
the build/import-artifact design that was rejected in review, not what shipped
in website #112. It lands here.

New in SPAWN_ATLAS.md: the six public routes and five admin ones, and three
behaviours that read as bugs unless they are written down — an unreadable tree
answers 200 with status "unavailable" rather than 500 (refresh reports outcomes
so boot is never blocked by a bad tree, and the contract is preserved at the
API), setting the ServUO path deliberately does not import, and `points` is a
count while `spawners` is the list.

Also the delay-unit trap: XmlSpawner stores MinDelay/MaxDelay in minutes OR
seconds per record, decided by that record's own DelayInSec flag, so a `5` is
five minutes on one spawner and five seconds on the next. Both are plausible
respawn times, which is what makes it silent. 170 of 6,455 stock spawners are
second-flagged. And PARSER_VERSION, which exists because hashing the tree alone
would strand an install whose maps never change on whatever an older parser
derived.

BACKEND_DESIGN.md gains the routes, the router-map entry, and the parser-version
rule. v3.md marks Part C done and records in 6.3 what the API half found.

api-route-inventory.json refreshed from the live manifest — it had drifted to
200 routes before this PR (real count was 204) and is now 215.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U7CBg11prhLimL9iHSX1bP
This commit is contained in:
2026-07-28 19:51:50 -05:00
parent 1b7da860b5
commit be7e1a69ce
4 changed files with 214 additions and 8 deletions

View File

@@ -13,7 +13,7 @@ Each part is marked off here as it lands on `edge`. §9 carries the same state p
|---|---|---|---|
| 1 | **A** — visibility framework + actor-leak fix (§3) | ✅ **Done** | website [#109](https://gitea.whitlocktech.com/RunicGateway/website/pulls/109) + [#110](https://gitea.whitlocktech.com/RunicGateway/website/pulls/110), docs [#64](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/64) + [#65](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/65) |
| 2 | **B/1**`world.ruleset` (§5) | ✅ **Done** | servuo-plugins [#3](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/3), link [#17](https://gitea.whitlocktech.com/RunicGateway/link/pulls/17), website [#111](https://gitea.whitlocktech.com/RunicGateway/website/pulls/111), docs [#66](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/66) |
| 3 | **C** — spawn atlas (§6) | 🟡 **Data pipeline done** | website [#112](https://gitea.whitlocktech.com/RunicGateway/website/pulls/112) (parsers + CLI + tables); API/client PR next |
| 3 | **C** — spawn atlas (§6) | **Done** | website [#112](https://gitea.whitlocktech.com/RunicGateway/website/pulls/112) (parsers + CLI + tables) + [#113](https://gitea.whitlocktech.com/RunicGateway/website/pulls/113) (API + pages + admin panel), docs [#67](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/67) + [#68](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/68) |
| 4 | **B/2**`points.board` (§7) | ⬜ Not started | — |
| 5 | **B/3**`vendor.listing` (§8) | ⬜ Not started | — |
| 6 | **Cutover**`PROTOCOL_VERSION` 2→3 (§4) | ⬜ Not started | — |
@@ -329,8 +329,10 @@ frame during verification.
**No plugin, no sidecar, no `Bridge.cfg` knob, no new kinds.** Not part of the v3 wire change.
> **Status:** data pipeline landed on `edge` — website [#112](https://gitea.whitlocktech.com/RunicGateway/website/pulls/112)
> (parsers, build/import CLI, tables, artifact). API + client pages are the second website PR.
> **Status:** complete on `edge` — website [#112](https://gitea.whitlocktech.com/RunicGateway/website/pulls/112)
> (parsers, import CLI, tables) and [#113](https://gitea.whitlocktech.com/RunicGateway/website/pulls/113)
> (the six public routes, the five admin ones, `/site/atlas` + `/site/atlas/:slug`, and the
> Admin → Spawn Atlas panel).
> Part C ships as **two** website PRs, not one: the parsing half is where the correctness risk
> lives, and burying it under routes and React would have meant reviewing it in a 10k-line diff.
> Full operator documentation: [`docs/website/SPAWN_ATLAS.md`](../website/SPAWN_ATLAS.md).
@@ -471,6 +473,38 @@ region, 1,690 by landmark, 1,086 Wilderness).
**One thing the design got exactly right:** the point-in-rect transform really is the reason to
build this. "Where does a lizardman spawn?" answers *Shrines, Isamu-Jima, Yew* across three facets.
### 6.3 What the API/client half added
The second website PR built the six public routes, the five admin ones, `/site/atlas` +
`/site/atlas/:slug`, and the Admin → Spawn Atlas panel. Three things it changed or established:
**1. Respawn delays were being read in the wrong unit — sometimes.** XmlSpawner writes
`MinDelay`/`MaxDelay` in minutes and switches to seconds only when a delay does not divide into
whole minutes, flagging that per record with `DelayInSec`
(`XmlSpawner2.cs:7462-7480`, read back at `:6345-6358`). So a `5` means five *minutes* on one
spawner and five *seconds* on the next, both plausible, and the pipeline stored the raw number.
170 of 6,455 stock spawners are second-flagged — few enough to look like noise on a page and be
believed. The parser now normalises to **seconds**, and the API and UI carry seconds throughout.
*This is the class of bug §6.2 is a list of: the atlas still builds, it is just quietly wrong.*
**2. The hash gate needed a parser version, and this generalises.** Fixing the parse exposed that
"has the tree changed?" is the wrong question on its own — an install whose maps never change would
have kept serving the old readings forever, because the only thing compared was the tree.
`spawnAtlasSource.PARSER_VERSION` is stored in `shard_atlas_meta` beside the source hashes, and a
mismatch counts as drift. Any future parse correction lands on the next boot without an operator
having to know it happened. **Bump it whenever the parser derives different data from identical
files.**
**3. `points` is a count; `spawners` is the list.** The first cut of the detail route spread the
creature row and then set `points` to the array of spawn points — the same key meaning a number on
the search route and an array on the detail route. Renamed before it shipped, and worth recording
because the two names are one letter apart in meaning and it reads as correct.
**On projection.** The `atlas` feature declares no sensitive fields, so `projectFeature` is a no-op
on every one of these routes today. Every handler calls it anyway, per §3.6.1's rule — the point of
the rule is that the *first* field that needs gating is covered by construction rather than by a
retrofit nobody remembers to do.
---
## 7. Part B/2 — `points.board`
@@ -665,7 +699,7 @@ inherently up to one full cycle old, and the UI must say so.
|---|---|---|---|---|
| 1 | **A** — visibility framework + actor-leak fix | website, docs | none | ✅ Done |
| 2 | **B/1**`world.ruleset` (§5) | all four | new kind | ✅ Done |
| 3 | **C** — spawn atlas (§6) | website, docs | none | 🟡 Pipeline done, API/client next |
| 3 | **C** — spawn atlas (§6) | website, docs | none | ✅ Done |
| 4 | **B/2**`points.board` (§7) | all four | new kind + `char.profile` field | ⬜ |
| 5 | **B/3**`vendor.listing` (§8) | all four | new kinds | ⬜ |
| 6 | **Cutover**`PROTOCOL_VERSION` 2→3, `edge``main` | all four | the bump | ⬜ |