docs(website): record slice 3, and MODULE_API 1.3.0 #138
@@ -85,7 +85,7 @@ change/set password (the SSO-account "no current password" path from `has_passwo
|
||||
list/unlink SSO identities — each mutation folding its `ApiResult` into a section-scoped, localized
|
||||
banner (§7). **Game-account linking** (§6.3) — the in-game `[link` one-time code (`POST
|
||||
/player/shard/link`) plus the hybrid signup (`POST /player/shard/account`, shown only when the public
|
||||
`gameAccountSignup` flag is set), and the linked-accounts list. **Own game data**, text-only (§6.3):
|
||||
`gameAccountSignup` flag is set — see the OWED note below), and the linked-accounts list. **Own game data**, text-only (§6.3):
|
||||
per-account character roster → a character sheet (attributes, vitals, resistances, best-first skills,
|
||||
equipment with AOS mods, and guild/governor standing chips — bare cliloc-number titles/item names are
|
||||
skipped, as the app ships no cliloc table, matching the website's `CharacterSheet.jsx`); player
|
||||
@@ -99,6 +99,18 @@ spec-aligned (as recorded for M1); 17 new JVM unit tests cover the account + pla
|
||||
**No backend/API change** — the `/auth/me/*` and `/player/shard/*` surfaces the app consumes were the
|
||||
§8 prerequisites, already landed.
|
||||
|
||||
> **OWED — `gameAccountSignup` moved and the app has not followed (2026-08-11).** Phase 3 slice 3 of
|
||||
> the website's module extraction (`docs/website/MODULE_SYSTEM.md` §2.7.1) moved game-account signup
|
||||
> policy out of core: the flag is no longer on `GET /public/settings` and is now on module-uo's
|
||||
> `GET /public/shard/features`, beside the visibility flags. `PublicDto.kt:80` defaults it to
|
||||
> `false`, so **nothing crashes** — `CharactersViewModel.kt:72` reads `false` and the app simply
|
||||
> stops offering game-account creation. The fix is one DTO field, one ViewModel line and two tests,
|
||||
> reading the flag from the features endpoint the app already calls.
|
||||
>
|
||||
> Deferred rather than coordinated into the slice (org lead, 2026-08-11): the website work lands on
|
||||
> `edge` and reaches `main` as one cutover, so there is room. **It must land before that cutover** —
|
||||
> after it, a shipped app quietly loses a feature.
|
||||
|
||||
✅ **M5 — design pass** (2026-07-20, `RunicGateway/Android-app#10`): the shard-website theme applied
|
||||
across every screen, restyling the working M1–M4 UI with **no architecture, data-flow, endpoint, or DTO
|
||||
change** (§2.1). The design was produced in Claude Design (`Runic Gateway Screens.dc.html`) and
|
||||
|
||||
@@ -908,7 +908,7 @@ from the per-route **siteMode** middleware (§5), never from an auth gate.
|
||||
|
||||
| Method | Path | Notes |
|
||||
|---|---|---|
|
||||
| GET | `/settings` | whitelisted public keys, derived `registration`/`gameAccountSignup` flags, the per-shard **`brand`** block (name, `accent` color, logo/hero/favicon) a client themes itself from — one image runs as any shard, asset fields may be site-relative paths (resolve against the base URL); these are **effective** values, so an admin theme (`theme_visual`) beats `BRAND_ACCENT_COLOR` and an uploaded `brand_assets` asset beats its `BRAND_*` path — an optional **`theme`** block, the resolved CSS custom properties for that admin theme (absent when the instance was never themed, which is what makes it render from the shipped stylesheet unchanged) — and a **`push`** block `{ ntfyUrl }` (M7): the client-facing ntfy relay URL the app's embedded distributor registers its device topic against, from `NTFY_PUBLIC_URL` / first `NTFY_ALLOWED_ORIGINS` (never the internal `NTFY_BASE_URL`); `null` when push isn't configured for the shard. |
|
||||
| GET | `/settings` | whitelisted public keys, the derived `registration` flags (`gameAccountSignup` was one of these until the module extraction moved game-account policy to module-uo — it is on that module's `GET /public/shard/features` now, and the `game_account_signup` settings row is unchanged), the per-shard **`brand`** block (name, `accent` color, logo/hero/favicon) a client themes itself from — one image runs as any shard, asset fields may be site-relative paths (resolve against the base URL); these are **effective** values, so an admin theme (`theme_visual`) beats `BRAND_ACCENT_COLOR` and an uploaded `brand_assets` asset beats its `BRAND_*` path — an optional **`theme`** block, the resolved CSS custom properties for that admin theme (absent when the instance was never themed, which is what makes it render from the shipped stylesheet unchanged) — and a **`push`** block `{ ntfyUrl }` (M7): the client-facing ntfy relay URL the app's embedded distributor registers its device topic against, from `NTFY_PUBLIC_URL` / first `NTFY_ALLOWED_ORIGINS` (never the internal `NTFY_BASE_URL`); `null` when push isn't configured for the shard. |
|
||||
| GET | `/status` | status message + current mode, **plus a `version` block** (`{ service:'runic-gateway', api, server }`) so a client first-run probe recognizes the backend and can run a version-mismatch guard |
|
||||
| GET | `/version` | lightweight, **DB-free** backend identity/version (`{ service, api, server }`) — the canonical target for the version guard and a cheap liveness check |
|
||||
| GET | `/modules` | `{ modules: [{ id, name, version, capabilities }] }` — the modules this backend is currently **serving**, in scan order (module system, `MODULE_API.md` §2.9). A module that is disabled or failed to load is **absent**, not listed with a state: its routes and nav are absent too, so the client renders a site without that capability rather than advertising one that 503s. The recorded failure stage and reason are admin-panel detail and are never published here. `capabilities` are opaque strings the module declares — feature-detect against them and treat an unknown one as absent. Like `/status` and `/version` it is **DB-free and not site-mode gated**, so a client can still feature-detect during maintenance. It is *not* how a module's client chunk loads — `htmlShell` injects a `<script type="module">` per started module. |
|
||||
|
||||
@@ -26,13 +26,20 @@ here extends the contract first, in this file, before the module is written agai
|
||||
Core exports a single integer-major semver string from `server/src/modules/version.js`:
|
||||
|
||||
```js
|
||||
const MODULE_API_VERSION = '1.2.0'
|
||||
const MODULE_API_VERSION = '1.3.0'
|
||||
```
|
||||
|
||||
The client half carries the same number (`client/src/modules/version.js`) and a test asserts the two
|
||||
agree. Duplicated rather than fetched because the value has to be on `window.__rg` before the first
|
||||
module chunk evaluates, which is earlier than any network round trip could answer.
|
||||
|
||||
**1.3.0 — Phase 3 slice 3, the client half's move.** Three client additions, each because the
|
||||
extraction needed it: a nav item may carry an **`icon`** component (§3.3), core declares a third slot
|
||||
**`player.invite.accepted`** (§3.7), and `window.__rg.api` gained **`BASE`** — which §3.5 specified
|
||||
from the first draft and `shared.js` had never actually published, because nothing needed it until a
|
||||
module had to build an EventSource URL. Additions only; the server half is untouched and bumps
|
||||
because the two halves state ONE version.
|
||||
|
||||
**1.2.0 — Phase 3, the client half.** `registry` gained `registerExtension` and core gained client
|
||||
extension slots (§3.7). An addition only, and the first change to `window.__rg` since 1.0.0 — the
|
||||
server half is untouched, and both files bump because the two halves state ONE version.
|
||||
@@ -156,7 +163,12 @@ Three narrowings from `MODULE_SYSTEM.md` §2.1, all deliberate:
|
||||
`setAuthCookie` and the TOTP challenge primitives. Minting sessions is core's job; a module that
|
||||
needs an identity needs to *read* one.
|
||||
- **`ctx.settings` is three functions, not the model.** The model exports 24 names, most of them
|
||||
registration/game-signup/app-links policy that is core's business.
|
||||
registration and app-links policy that is core's business. It said *game-signup* policy too until
|
||||
Phase 3 slice 3, which is when that turned out to be wrong in both directions: the setting's own
|
||||
help text names Bridge.cfg, so it was never core's — and slice 1 had shipped a ported controller
|
||||
calling `settings.isGameAccountSignupEnabled()`, which this narrowing does not expose, so
|
||||
`POST /player/shard/account` answered 500 for every caller until slice 3 found it. A narrowing is
|
||||
only as safe as the tests that cross it.
|
||||
- **`ctx.posts` is four functions.** `create`/`update`/`remove` are the CMS, not a module's.
|
||||
|
||||
And one addition the spike forced: **`ctx.express` and `ctx.validator`**. A module lives at
|
||||
@@ -579,7 +591,27 @@ registry.registerNav('uo', {
|
||||
|
||||
`group` names an existing core group; an unknown group name appends a new group at the end rather
|
||||
than dropping the item. `order` sorts within the group, core items keeping their current positions.
|
||||
`feature` names a flag resolved by the provider below.
|
||||
`feature` names a flag resolved by the provider below. **`icon`** (1.3.0) is a component core renders
|
||||
exactly as it renders its own rows' icons.
|
||||
|
||||
**Core supplies no fallback icon, and both layouts tolerate a row without one.** A module that omits
|
||||
`icon` gets no icon, the same as a core row that omits it — inventing one would be core making a
|
||||
presentation choice for content it knows nothing about. The field exists because without it the six
|
||||
UO rows would have extracted as the only text-only entries in a sidebar where every other row has a
|
||||
glyph, which reads as breakage rather than as a design; `icon` was already among the fields an
|
||||
override may **not** touch, so the concept predates a module being able to send one.
|
||||
|
||||
A module should match the nav its row lands in rather than ship one glyph for everywhere: the admin
|
||||
sidebar draws at 18px with a 1.6 stroke and the player portal at 16px with a 2. That is presentation,
|
||||
deliberately not in the kit — putting core's icon frame in the contract would make changing it a
|
||||
major bump.
|
||||
|
||||
**The tolerance is newer than the field.** `PlayerPortalLayout` rendered `<n.icon />` unguarded,
|
||||
which was correct for as long as every row in it was core's own and had one; the first module row
|
||||
without an icon blanked the entire portal with React error #130. Both layouts guard now, and
|
||||
module-uo's `registration.test.js` asserts an icon on every admin and player row it registers —
|
||||
belt and braces, because the failure is invisible to a DOM-less test runner and the smoke caught it
|
||||
only by chance of registering that row last.
|
||||
|
||||
Six details settled when this was built (Phase 2 PR 8, `client/src/modules/nav.js`):
|
||||
|
||||
@@ -697,6 +729,12 @@ Core exposes the primitive, not the object:
|
||||
window.__rg.api = { request, ApiError, BASE } // request(path, { method, body, headers, raw })
|
||||
```
|
||||
|
||||
`BASE` is `/api/v1`, and it was specified here from the first draft while `shared.js` published only
|
||||
the first two — nothing needed it until slice 3, when a module first had to build an **EventSource**
|
||||
URL. `request` is fetch-only, so an SSE subscriber constructs its own; the alternative is a module
|
||||
hardcoding `/api/v1`, which asserts something about where core mounts its API that core has never
|
||||
promised to keep. Published as of 1.3.0.
|
||||
|
||||
`request` is `client.js`'s existing `req` — same-origin `/api/v1`, `credentials: 'include'`, JSON
|
||||
in/out, throwing `ApiError(status, message, body)`. A module builds its own namespace over it and
|
||||
owns the paths it calls, which is correct: it owns the routes at the other end.
|
||||
@@ -769,7 +807,8 @@ by checking each imported binding is **identity-equal** to the one core publishe
|
||||
|
||||
### 3.7 Extension slots — module content inside a core page
|
||||
|
||||
Added in **1.2.0** (settled 2026-08-11). The client twin of §2.4's `registerExtension`, and the same
|
||||
Added in **1.2.0** (settled 2026-08-11), third slot added in **1.3.0**. The client twin of §2.4's
|
||||
`registerExtension`, and the same
|
||||
rule in both halves: **core declares a slot, only core may declare one, and at most one module may
|
||||
fill it.**
|
||||
|
||||
@@ -795,12 +834,13 @@ unfilled case and wrong about the failed one: the slot *is* filled, so the separ
|
||||
the component then throws into the boundary and leaves the separator behind on its own. Found in a
|
||||
browser with this exact footer separator, which is the only place it could have been found.
|
||||
|
||||
The slots in 1.2.0:
|
||||
The slots:
|
||||
|
||||
| Slot | Rendered in | Props core passes |
|
||||
| --- | --- | --- |
|
||||
| `site.footer.status` | `components/SiteFooter.jsx`, in the info row | `linkStyle` — the row's own link styling |
|
||||
| `admin.users.detail` | `routes/admin/views/UserDetail.jsx`, below the security panel | `userId` |
|
||||
| `player.invite.accepted` (1.3.0) | `routes/player/AcceptInvite.jsx`, after an invite is accepted | `onDone` — send the invitee on to the portal |
|
||||
|
||||
**A slot is named for a PLACE, never for a meaning.** `site.footer.status` is "the status-ish spot in
|
||||
the footer", not a declaration that core knows what a game server's status is: core supplies the
|
||||
@@ -819,6 +859,24 @@ binding that leaves core in the client half of Phase 3, so a slot that handed it
|
||||
something core is about to delete. The extension builds its own client for the routes it registered
|
||||
at the other end, which is §3.5's rule applied to a slot.
|
||||
|
||||
**`player.invite.accepted` is the one slot whose emptiness core reads.** Core rendered a UO
|
||||
game-account step on that page until slice 3 — it read a `gameAccountSignup` flag out of its own
|
||||
settings and posted to a shard route — and an invite is a core concept that staff receive too, so the
|
||||
page stays and its optional step becomes a slot. With the slot unfilled there is no screen to show at
|
||||
all, so core navigates straight on; with it filled, core renders its shell, the slot, and a "skip"
|
||||
control *outside* the boundary, because an extension that throws must not take the way out with it.
|
||||
|
||||
That makes `extensionFor` legitimate for a core page to call — once, and only for this reason. The
|
||||
rule is not "never ask whether a slot is filled", it is **ask only when the answer changes control
|
||||
flow, never when it changes decoration**; decoration goes inside `<Slot wrap>`. `AcceptInvite` is the
|
||||
only caller in core, and the difference between the two cases is exactly the footer-separator bug
|
||||
above.
|
||||
|
||||
Core's subtitle for that screen says nothing about what the step is. Naming it would be core
|
||||
describing content it does not own, and there is no wording that stays true for the next game.
|
||||
**Whether the step should appear at all is the module's decision too** — it is made from a setting
|
||||
core no longer reads, so the filling component either renders or calls `onDone` itself.
|
||||
|
||||
**Errors are contained.** Core renders a filled slot inside an error boundary: a component that
|
||||
throws costs its own section and a console error, never core's page. That asymmetry is deliberate and
|
||||
is where the client differs from the server — a module route that throws costs the module's own page,
|
||||
@@ -1280,3 +1338,23 @@ chunk executes under the **enforced** `script-src 'self'` with no CSP report, wh
|
||||
(`htmlShell.init`), so rebuilding the client without restarting the server serves an `index.html`
|
||||
pointing at a hashed bundle that no longer exists — core never runs, `window.__rg` is undefined, and
|
||||
the failure looks exactly like a contract violation in the module.
|
||||
|
||||
**Run it against the real module too, not only a throwaway.** A hand-written chunk proves the
|
||||
delivery path; it does not prove the module. Slice 3's run — module-uo checked out into
|
||||
`website/modules/uo`, the deletion branch checked out in `website/` — found two things a throwaway
|
||||
never would have, and neither is visible to any test in either repo:
|
||||
|
||||
- **`PlayerPortalLayout` rendered `<n.icon />` unguarded.** `icon` is optional in the nav contract
|
||||
and every core row in that sidebar had one, so the difference from `AdminLayout` cost nothing until
|
||||
a module registered a row without — then it was React error #130 and a **blank player portal**,
|
||||
not a missing glyph. Both layouts guard now.
|
||||
- **A relative `MODULES_DIR` failed every module** with `client.entry escapes the module directory`,
|
||||
because `resolveClient` compared an absolute resolved path against a relative directory. Running
|
||||
from `server/` is what produces one, so this recipe was the thing that triggered it. `MODULES_DIR`
|
||||
is resolved absolute now, and the recipe works either way.
|
||||
|
||||
Two operational notes for the run itself. The module's own `server/package.json` has dependencies
|
||||
(`ws`), so a module copied into place needs `npm ci --omit=dev` in its `server/` before it will
|
||||
`register` — the release tarball carries them, a working copy does not. And **copy the module
|
||||
directory, do not symlink it**: the loader's `filter(e => e.isDirectory())` reports a Windows
|
||||
junction as a symlink and skips it silently.
|
||||
|
||||
@@ -780,7 +780,7 @@ Each slice is one `module-uo` PR (adds), one `website` PR (deletes), and one `do
|
||||
| 0 | **The bundle skeleton** | `module.json`, both `package.json`s, `server/index.js` registering nothing, the Vite library build + the four shared-dep shims, CI armed, the §5.1 zero-internal-imports check. `website` untouched. |
|
||||
| 1 | **The whole server half** | 40 files / ~9,674 lines, 25 of core's 82 test files, 27 of its 68 tables — every UO model, util, router and controller, `config/shardStreams.js`, `scripts/importSpawnAtlas.js` and the art JSON. **One merge, five commits** (below). |
|
||||
| 2 | **Client extension slots** | Core only, and the one slice that adds rather than moves: the client twin of `declareSlot`/`registerExtension` (API §3.7), the `site.footer.status` and `admin.users.detail` slots, and core filling both itself. `module-uo` untouched. |
|
||||
| 3 | **The whole client half** | 51 files / ~3,700 lines — all twelve public pages (`Shard`, `ShardActivity`, `Rules`, `Atlas`, `AtlasCreature`, `ChampSpawns`, `Market`, `MarketVendor`, `Governors`, `Guilds`, `Houses`, `Leaderboards`) under `/uo/*`, every admin and player view under `/admin/uo/*` and `/player/uo/*`, `PlayersOnline`, `VendorSales`, `CharacterStats`, `GameAccounts`, the `data/` and `lib/` UO leaves, the public nav rows, the feature provider, and both slot fills. |
|
||||
| 3 | **The whole client half** | **35 files / 5,332 lines** (measured; the 51/~3,700 above was counted differently) — all twelve public pages (`Shard`, `ShardActivity`, `Rules`, `Atlas`, `AtlasCreature`, `ChampSpawns`, `Market`, `MarketVendor`, `Governors`, `Guilds`, `Houses`, `Leaderboards`) under `/uo/*`, every admin and player view under `/admin/uo/*` and `/player/uo/*`, `PlayersOnline`, `VendorSales`, `CharacterStats`, `GameAccounts`, the `data/` and `lib/` UO leaves, the public nav rows, the feature provider, and all three slot fills. |
|
||||
| 4 | **De-UO core's copy** | `About`, `Screenshots`, `Website`, `SiteFooter`'s prose, `heroLayout`'s defaults, `api/client.js`'s `shard`/`atlas` namespaces, and the comments in `navOverrides.js` — plus the §5.2 CI grep that keeps them out |
|
||||
| 5 | **Close the phase** | `module-uo`'s frozen route manifest and release workflow; `docs/modules/uo/` and `docs/modules/rust-dryrun.md` |
|
||||
|
||||
@@ -1019,6 +1019,112 @@ junction as a symlink, so a module linked rather than copied into `modules/` is
|
||||
with nothing logged. Not a defect for a real install — `modules/` is a bind mount of real
|
||||
directories (§2.5) — but it is the first thing to check when a module fails to appear.
|
||||
|
||||
#### Slice 3 — the whole client half (Module-uo#4 + website#139, 2026-08-11)
|
||||
|
||||
35 files / 5,332 lines and two of core's thirteen client tests, leaving core with **620 server + 161
|
||||
client** tests, a manifest unchanged at 158 public + 2 internal, and an OpenAPI diff of exactly one
|
||||
property. `MODULE_API_VERSION` 1.2.0 → **1.3.0**. The estimate in the table above said 51 files /
|
||||
~3,700 lines; the real closure is fewer files and more lines, and the table is corrected rather than
|
||||
the count re-derived to match it.
|
||||
|
||||
**The dependency claim held exactly.** Every one of the 35 imports the seven §3.4 kit members plus
|
||||
`lib/format.js`, and nothing else — so the kit needed no additions and `client/src/core.js` is the
|
||||
whole boundary. Unlike the server's `core.js` it is a plain read, because `window.__rg` is published
|
||||
before any module chunk evaluates and there is no gap to defer around; a ported component keeps its
|
||||
ordinary import shape instead of being wrapped in an accessor that would cost it its identity. Only
|
||||
`ago()` was vendored, not all six of core's date helpers — the vendoring line from slice 1 (pure leaf
|
||||
helpers may be copied, security controls may not) plus "copy what you use".
|
||||
|
||||
##### The three things core owed first
|
||||
|
||||
`player.invite.accepted`, the third slot (API §3.7). `icon` on a nav item (§3.3). And `api.BASE`,
|
||||
which §3.5 had specified from the first draft and `shared.js` had never published — nothing needed it
|
||||
until a module had to build an EventSource URL, `request` being fetch-only.
|
||||
|
||||
##### `/player` had no index, and the answer generalises
|
||||
|
||||
`PlayerCharacters` was the portal's index route *and* its first nav row, both UO. Rather than name a
|
||||
replacement or invent a landing screen — the org lead is merging the two logged-in areas into one
|
||||
permission-driven area, so a new core page would be something to unwind — `/player` resolves to the
|
||||
**first row of the base player nav this viewer can reach** (`firstDestinationFor`, beside
|
||||
`allowedPathsFor`). With the module installed that is still Characters, so a player's first screen
|
||||
after signing in does not change; with nothing installed it is Account.
|
||||
|
||||
From the BASE nav, never the override-merged one, and here that rule is sharper than it is for
|
||||
`allowedPathsFor`: an override is presentation, and where everybody lands is behaviour. It also means
|
||||
an admin cannot move the landing page somewhere a role cannot follow.
|
||||
|
||||
##### `game_account_signup` was never core's, and slice 1 had broken it
|
||||
|
||||
The setting's help text names *Bridge.cfg* and says the game server's own SignupMode must agree —
|
||||
core cannot own a sentence about a UO shard. The mode list, the derived public flag, the validation
|
||||
and the Site Settings field all moved; the **row keeps its key and value**, so a configured instance
|
||||
finds its mode where it left it, the same grandfathering as `spawn_atlas_servuo_path` (API §6.5).
|
||||
|
||||
Moving it surfaced a defect slice 1 had shipped: the ported controller called
|
||||
`settings.isGameAccountSignupEnabled()`, which `ctx.settings` does not expose, so
|
||||
`POST /player/shard/account` and its staff twin answered **500 for every caller** and no test in
|
||||
either repo reached the branch. Both regression tests were shown to fail before the fix went in.
|
||||
|
||||
**A known break, accepted:** the shipped Android app reads `gameAccountSignup` off
|
||||
`/public/settings` (`PublicDto.kt:80`). The field defaults to `false`, so nothing crashes — the app
|
||||
silently stops offering game-account creation until it reads the module's `/public/shard/features`
|
||||
instead. Out of scope for this slice, recorded in the Android plan, and it lands well before this
|
||||
workstream's cutover reaches `main`.
|
||||
|
||||
##### What the browser found this time
|
||||
|
||||
The §7.7 smoke, run against the real pair, earned its place again. `PlayerPortalLayout` rendered
|
||||
`<n.icon />` **unguarded** where `AdminLayout` guarded its equivalent — correct for as long as every
|
||||
row in that sidebar was core's own and had an icon, and **React error #130 with a blank portal** the
|
||||
moment a module registered a row without one. Core is guarded now and module-uo asserts an icon on
|
||||
every admin and player row; the public header is text buttons and is excluded.
|
||||
|
||||
It also found that a **relative `MODULES_DIR`** — which §7.7's own recipe produces when run from
|
||||
`server/` — failed every module with `client.entry escapes the module directory`, because
|
||||
`resolveClient` compares an absolute resolved path against a relative one. `MODULES_DIR` is resolved
|
||||
absolute now.
|
||||
|
||||
Verified in Chrome, zero CSP reports: the public pages under `/uo/*` with core's chrome, the nine
|
||||
header rows, the six sidebar rows with their glyph in core's Moderation and System groups and My
|
||||
Characters in its own trailing group, `/admin/uo/link` with the migrated signup field showing the
|
||||
live value, the user-detail slot filled with real data, the footer slot with its separators intact,
|
||||
`/player` resolving to `/player/uo/characters`, and the invite flow end to end — core's shell and
|
||||
skip control, the module's prose and form. The SSE feed connected, which is `api.BASE` working.
|
||||
|
||||
##### The nav-override cost, in the wild
|
||||
|
||||
Decision 9 (clean break, no nav-override migration) has a visible price and the review instance shows
|
||||
it exactly. Overrides are keyed by `to`, so all fifteen changed paths orphan and are dropped. On that
|
||||
instance the operator had gathered the nine UO rows into a **dropdown section**; after the extraction
|
||||
those keys match nothing, the section is empty, and the nine rows render flat across two lines of the
|
||||
header. A stored order on a core row that survived (`/site/about` at 8) now interleaves with the
|
||||
module rows rather than preceding them.
|
||||
|
||||
So the release note is stronger than "a hidden row may reappear": **an operator's nav customisation
|
||||
of the UO rows — order, labels, hidden state and section grouping — is lost and must be redone.**
|
||||
Accepted rather than migrated, on the grounds that the site is not public yet.
|
||||
|
||||
##### Two checks that had never met a real chunk
|
||||
|
||||
`checkExternals.js` **rejected the build outright**, naming a fragment of minified JSX as an imported
|
||||
specifier: a button reading "Approve and import" puts the token immediately before a quote, and no
|
||||
regexp distinguishes a keyword from the same letters inside a string. The server's `checkImports.js`
|
||||
hit this from the other side and answered it the same way — a character walk. Here it is a *mask*
|
||||
rather than a rewrite, because a real import has its keyword outside a string and its specifier
|
||||
inside one, so blanking strings would take the answer with the noise.
|
||||
|
||||
Writing the test for that false positive found the false **negative** underneath it: the pattern
|
||||
required whitespace after `import`, so it could never see `import{useState}from"react"` — the one
|
||||
shape a minified build actually emits, and the likeliest way for a missed alias to reach production.
|
||||
A check tested against only one of its two answers is half a check.
|
||||
|
||||
`registration.test.js` is new and closes the gap those two left: stand up a fake `window.__rg` with a
|
||||
recording registry and the real React, import the **built** chunk, and read back what it asked for —
|
||||
no DOM, because nothing renders. It holds the agreement that rots quietly (every nav row points at a
|
||||
route this module registered) rather than restating both lists. CI now builds before it tests, since
|
||||
both artifact-reading tests skip without a build and were otherwise green while asking nothing.
|
||||
|
||||
**Phase 4 — Delivery.** The admin-panel Modules screen (install, enable, disable, retry, purge,
|
||||
`startup_failed` with its recorded reason) and the Docker-environment path from §2.5. Deliberately
|
||||
last, so loader, packaging, schema and chunk-loading problems are not all being debugged at once.
|
||||
|
||||
@@ -257,134 +257,10 @@
|
||||
"method": "POST",
|
||||
"path": "/api/v1/admin/settings/brand-asset/:slot"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/v1/admin/shard/account"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/admin/shard/accounts"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/admin/shard/atlas"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/v1/admin/shard/atlas/approve"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/v1/admin/shard/atlas/import"
|
||||
},
|
||||
{
|
||||
"method": "PUT",
|
||||
"path": "/api/v1/admin/shard/atlas/path"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/v1/admin/shard/atlas/reject"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/admin/shard/audit"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/v1/admin/shard/ban"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/v1/admin/shard/broadcast"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/admin/shard/char/:serial"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/admin/shard/clilocs"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/v1/admin/shard/clilocs/import"
|
||||
},
|
||||
{
|
||||
"method": "PUT",
|
||||
"path": "/api/v1/admin/shard/clilocs/path"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/admin/shard/houses"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/v1/admin/shard/kick"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/v1/admin/shard/link"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/admin/shard/pages"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/v1/admin/shard/pages/:id/close"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/v1/admin/shard/pages/:id/respond"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/admin/shard/roster/:account"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/admin/shard/sales"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/v1/admin/shard/unban"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/admin/shard/vendors/:account"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/admin/shard/visibility"
|
||||
},
|
||||
{
|
||||
"method": "PUT",
|
||||
"path": "/api/v1/admin/shard/visibility"
|
||||
},
|
||||
{
|
||||
"method": "PUT",
|
||||
"path": "/api/v1/admin/site-mode"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/admin/uo-link/config"
|
||||
},
|
||||
{
|
||||
"method": "PUT",
|
||||
"path": "/api/v1/admin/uo-link/config"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/admin/uo-link/stream"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/v1/admin/uo-link/towncrier"
|
||||
},
|
||||
{
|
||||
"method": "DELETE",
|
||||
"path": "/api/v1/admin/uo-link/towncrier/:id"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/v1/admin/uploads"
|
||||
@@ -413,30 +289,6 @@
|
||||
"method": "POST",
|
||||
"path": "/api/v1/admin/users/:id/mfa/reset"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/admin/users/:id/shard/accounts"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/admin/users/:id/shard/houses"
|
||||
},
|
||||
{
|
||||
"method": "DELETE",
|
||||
"path": "/api/v1/admin/users/:id/shard/link/:account"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/admin/users/:id/shard/online"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/admin/users/:id/shard/sales"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/admin/users/:id/shard/standing"
|
||||
},
|
||||
{
|
||||
"method": "DELETE",
|
||||
"path": "/api/v1/admin/users/:id/trusted-devices"
|
||||
@@ -721,62 +573,6 @@
|
||||
"method": "GET",
|
||||
"path": "/api/v1/player/appeals/eligible"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/v1/player/shard/account"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/player/shard/accounts"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/player/shard/char/:serial"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/player/shard/houses"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/v1/player/shard/link"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/player/shard/roster/:account"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/player/shard/sales"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/player/shard/vendors/:account"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/public/atlas/champions"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/public/atlas/creatures"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/public/atlas/creatures/:slug"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/public/atlas/landmarks"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/public/atlas/meta"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/public/atlas/regions"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/v1/public/contact"
|
||||
@@ -805,82 +601,6 @@
|
||||
"method": "GET",
|
||||
"path": "/api/v1/public/settings"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/public/shard/champs"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/public/shard/economy"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/public/shard/features"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/public/shard/feed"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/public/shard/governors"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/public/shard/governors/:city/history"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/public/shard/guilds"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/public/shard/houses"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/public/shard/idoc"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/public/shard/market"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/public/shard/market/meta"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/public/shard/market/vendors/:serial"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/public/shard/online"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/public/shard/points"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/public/shard/points/:system"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/public/shard/presence"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/public/shard/ruleset"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/public/shard/status"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/public/shard/stream"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/public/status"
|
||||
|
||||
Reference in New Issue
Block a user