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:
@@ -1,6 +1,6 @@
|
||||
# Spawn atlas
|
||||
|
||||
**Status:** Data pipeline landed on `edge` (website [#112](https://gitea.whitlocktech.com/RunicGateway/website/pulls/112)); API and client pages follow in a second PR.
|
||||
**Status:** Complete on `edge` — data pipeline in website [#112](https://gitea.whitlocktech.com/RunicGateway/website/pulls/112), API + pages in website [#113](https://gitea.whitlocktech.com/RunicGateway/website/pulls/113).
|
||||
**Design:** [`docs/link/v3.md` §6](../link/v3.md) — Protocol 3.0 Part C.
|
||||
|
||||
The spawn atlas is a browsable catalogue of what the shard *contains*: which
|
||||
@@ -91,7 +91,7 @@ A rejection is remembered against those exact source hashes, so a declined
|
||||
refresh does not re-prompt on every restart. Change the tree and the hashes
|
||||
differ, which asks again.
|
||||
|
||||
From the admin panel (second PR), or from the CLI:
|
||||
From **Admin → Spawn Atlas**, or from the CLI:
|
||||
|
||||
```bash
|
||||
cd website/server
|
||||
@@ -259,3 +259,96 @@ Parsing notes:
|
||||
- `<Objects2>` is `Type:MX=n:SB=…` segments joined by `:OBJ=`. Split on `:OBJ=`
|
||||
*first* — a naive `split(':')` shreds it. A single Trammel point carries six
|
||||
types.
|
||||
- **Respawn delays are stored in two different units, per record.** XmlSpawner
|
||||
writes `MinDelay`/`MaxDelay` in minutes, and switches to seconds only when a
|
||||
spawner's delay does not divide into whole minutes — flagging that with
|
||||
`DelayInSec` on the same record. A `5` therefore means five *minutes* on one
|
||||
spawner and five *seconds* on the next, and both are plausible respawn times,
|
||||
so a reader assuming either unit is silently wrong about the other. Stock
|
||||
ServUO 57.4 has ~170 second-flagged spawners out of 6,455. The parser
|
||||
normalises everything to **seconds**; the API and UI carry seconds throughout.
|
||||
|
||||
### The parser version
|
||||
|
||||
`spawnAtlasSource.js` exports `PARSER_VERSION`, stored in `shard_atlas_meta`
|
||||
alongside the source hashes and bumped whenever the parser derives **different
|
||||
data from identical files** — a fixed misreading, a new field, a changed unit.
|
||||
|
||||
A refresh re-derives when the tree changed **or** the parser did. Hashing the
|
||||
tree alone would be a trap: an install whose maps never change would keep serving
|
||||
whatever an older build derived, indefinitely, and a deploy that corrects the
|
||||
parse would never reach the data. A version mismatch counts as drift, so the
|
||||
correction lands on the next boot without an operator having to know it happened.
|
||||
|
||||
## The API
|
||||
|
||||
Everything is served from MariaDB. Nothing on this path touches the sidecar, so
|
||||
the pages stay complete while the shard is down — which is why the routes sit at
|
||||
`/api/v1/public/atlas` and **not** under `/public/shard`, where a prefix means
|
||||
"sidecar-dependent". Unlike `/shard/*`, they *are* `siteMode`-gated, like
|
||||
`/posts` and `/wiki`: a bestiary is site content and follows site content's rules.
|
||||
|
||||
Every route carries `requireFeature('atlas')` — **404** when an admin has
|
||||
disabled the feature (its pages must not reveal that it exists) and **403** when
|
||||
the caller sits below its configured audience. The default is `anonymous`, so the
|
||||
gates are inert until an admin changes something. Responses are field-projected
|
||||
like every other shard read; `atlas` declares no sensitive fields today, and the
|
||||
projection call is there so the first one that does is covered by construction
|
||||
rather than by a retrofit ([`v3.md` §3.6.1](../link/v3.md)).
|
||||
|
||||
| Route | Answers |
|
||||
|---|---|
|
||||
| `GET /atlas/creatures?q=&facet=&limit=&offset=` | The bestiary, most numerous first, paginated with an unpaginated `total` |
|
||||
| `GET /atlas/creatures/:slug?facet=&points=` | One creature: `places`, `spawners`, `alsoHere` |
|
||||
| `GET /atlas/regions?facet=&q=` | Named regions and their rectangles |
|
||||
| `GET /atlas/landmarks?facet=&q=` | Points of interest, labelled by `group` |
|
||||
| `GET /atlas/champions?facet=` | The configured altar roster |
|
||||
| `GET /atlas/meta` | Facets, counts and when the atlas was parsed |
|
||||
|
||||
Two shapes worth knowing:
|
||||
|
||||
- **`places` is the aggregate the atlas exists for.** "Lizardman → Shrines,
|
||||
Isamu-Jima, Yew", grouped in SQL rather than by summing 6,455 point rows in
|
||||
Node. `spawners` is the raw list underneath it, bounded, with
|
||||
`spawnersTruncated` saying when it was cut.
|
||||
- **`points` is a COUNT, `spawners` is the LIST.** The two are named apart
|
||||
deliberately: the same key meaning a number on the search route and an array on
|
||||
the detail route is the kind of thing a client only discovers in production.
|
||||
|
||||
`GET /atlas/meta` reports the **game world only**. The ServUO path, the per-file
|
||||
hashes and any pending refresh describe the operator's filesystem, and live on
|
||||
the admin route instead.
|
||||
|
||||
A facet is never validated against a list — nothing in the codebase names one.
|
||||
`?facet=` is length-bounded and matched exactly, so an unknown name returns an
|
||||
empty result rather than an error. The filter is an `EXISTS` over the points and
|
||||
deliberately not a JSON path or `JSON_SEARCH` built from caller input: that
|
||||
function treats `%` and `_` as wildcards, which would make `?facet=%` match
|
||||
everything.
|
||||
|
||||
## The admin panel
|
||||
|
||||
**Admin → Spawn Atlas** (`/admin/shard-atlas`, admin-only — it reads a path on
|
||||
the server's filesystem and replaces every atlas table, which is closer to a
|
||||
deploy action than to moderation).
|
||||
|
||||
| Route | Does |
|
||||
|---|---|
|
||||
| `GET /admin/shard/atlas` | Status: path, readable, drift, counts, facets, pending |
|
||||
| `POST /admin/shard/atlas/import` | Import now; `{ force: true }` ignores the hash gate |
|
||||
| `POST /admin/shard/atlas/approve` | Apply a staged refresh, facet loss and all |
|
||||
| `POST /admin/shard/atlas/reject` | Keep the current atlas; remember the decision |
|
||||
| `PUT /admin/shard/atlas/path` | Point the atlas at a different tree |
|
||||
|
||||
Three behaviours that are deliberate:
|
||||
|
||||
- **An unreadable tree is a 200, not a 500.** `refresh()` reports outcomes rather
|
||||
than throwing, because the boot path must never be stopped by a bad tree, and
|
||||
that contract is preserved at the API. The panel says *"The tree could not be
|
||||
read: …"*; a 500 would say only that something broke.
|
||||
- **Setting the path does not import.** Moving the mount and reloading the world
|
||||
are separate decisions, and an operator fixing a typo should not have a
|
||||
multi-thousand-row replace happen under them. The response carries fresh status
|
||||
so the panel can offer the import as the next step.
|
||||
- **Every action is written to the admin activity log** (`shard.atlas.import` /
|
||||
`.approve` / `.reject` / `.path`).
|
||||
|
||||
Reference in New Issue
Block a user