feat(client): the whole client half (phase 3, slice 3) #4

Merged
whitlocktech merged 4 commits from feature/module-extract-client into main 2026-08-12 00:35:35 +00:00
Member

Phase 3, slice 3 of docs/website/MODULE_SYSTEM.md §2.7.1 — the client half arrives. Pairs with website#139 (which deletes it from core) and docs#138. Merge this one first, as every slice but 2.

What moved

35 files / 5,332 lines: twelve public pages, seven admin views, two player views, eight components, two data/ leaves and three lib/ ones, plus the two client tests that came with them. §2.7.1 estimated 51 files / ~3,700 lines; the table is corrected in the docs PR rather than the count massaged to match.

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.

client/src/core.js is the port mechanism and, unlike the server's, a plain read: window.__rg is published before any module chunk evaluates, so there is no gap to defer around and a ported component keeps its ordinary import shape rather than being wrapped in an accessor that would cost it its identity. client/src/api.js rebuilds the UO namespaces over the request primitive — same URLs, because §1.2 freezes the API surface and the shipped Android app calls seven of them.

Only ago() was vendored, not all six of core's date helpers: slice 1's vendoring line (pure leaf helpers may be copied, security controls may not), plus copy what you use.

SPA paths changed; API paths did not

/site/shard/uo/shard, and the admin paths lost their now-redundant prefixes: /admin/shard-ops/admin/uo/ops, /admin/shard-visibility/admin/uo/visibility, /admin/shard/admin/uo/link. A clean break is the only moment that tidy-up is free. Every URL in api.js is byte-identical to the one core called.

The server half of this PR: a defect slice 1 shipped

POST /api/v1/player/shard/account and its staff twin have answered 500 for every caller since slice 1. The ported controller called settings.isGameAccountSignupEnabled(), which is a member of core's settings model and not of ctx.settings — three functions, deliberately. The call was undefined(...), the TypeError landed in the catch, and no test reached the branch.

The gate now lives on the side that uses it (utils/gameSignup.js), which is also where the policy belongs: the setting's help text names Bridge.cfg and says the shard's SignupMode must agree, and core cannot own a sentence about a UO shard. The admin field moves to this module's Shard page and the derived flag onto /public/shard/features. The setting key is unchanged — renaming game_account_signup would silently reset every configured instance to disabled.

Both regression tests were shown to fail against the bug before it was fixed.

Two checks that had never met a real chunk

checkExternals.js rejected this 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. Same wall the server's checkImports.js hit, answered the same way: a character walk. A mask rather than a rewrite, because a real import has its keyword outside a string and its specifier inside one.

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. It has never been able to see it.

Verification

  • 359 server + 34 client tests, all passing (gameSignup.test.js +8, registration.test.js +8, the two ported +12)
  • check:imports clean; check:externals clean on the built chunk
  • registration.test.js is new: a fake window.__rg with a recording registry and the real React, importing the built chunk to read back what it registered — 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 — both artifact-reading tests skip without a build, and were otherwise green while asking nothing.
  • §7.7 browser smoke against a real core, zero CSP reports: public pages under /uo/* with core's chrome, nine header rows, six sidebar rows with their glyph inside core's Moderation and System groups, /admin/uo/link with the migrated signup field showing the live value, all three slots filled, /player resolving to /player/uo/characters, and the invite flow end to end.

The smoke found one defect, fixed in both repos: registering the player nav row without an icon blanked the entire portal with React error #130, because core rendered <n.icon /> unguarded there. This side ships the icon the row had before it moved and asserts one on every admin and player row.

AI disclosure

Written with Claude Code.

Co-Authored-By: Claude noreply@anthropic.com

Phase 3, slice 3 of `docs/website/MODULE_SYSTEM.md` §2.7.1 — the client half arrives. Pairs with website#139 (which deletes it from core) and docs#138. Merge this one first, as every slice but 2. ## What moved **35 files / 5,332 lines**: twelve public pages, seven admin views, two player views, eight components, two `data/` leaves and three `lib/` ones, plus the two client tests that came with them. §2.7.1 estimated 51 files / ~3,700 lines; the table is corrected in the docs PR rather than the count massaged to match. **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. `client/src/core.js` is the port mechanism and, unlike the server's, a plain read: `window.__rg` is published before any module chunk evaluates, so there is no gap to defer around and a ported component keeps its ordinary import shape rather than being wrapped in an accessor that would cost it its identity. `client/src/api.js` rebuilds the UO namespaces over the request primitive — **same URLs**, because §1.2 freezes the API surface and the shipped Android app calls seven of them. Only `ago()` was vendored, not all six of core's date helpers: slice 1's vendoring line (pure leaf helpers may be copied, security controls may not), plus copy what you use. ## SPA paths changed; API paths did not `/site/shard` → `/uo/shard`, and the admin paths lost their now-redundant prefixes: `/admin/shard-ops` → `/admin/uo/ops`, `/admin/shard-visibility` → `/admin/uo/visibility`, `/admin/shard` → `/admin/uo/link`. A clean break is the only moment that tidy-up is free. Every URL in `api.js` is byte-identical to the one core called. ## The server half of this PR: a defect slice 1 shipped `POST /api/v1/player/shard/account` **and its staff twin have answered 500 for every caller since slice 1.** The ported controller called `settings.isGameAccountSignupEnabled()`, which is a member of core's settings model and not of `ctx.settings` — three functions, deliberately. The call was `undefined(...)`, the TypeError landed in the catch, and no test reached the branch. The gate now lives on the side that uses it (`utils/gameSignup.js`), which is also where the policy belongs: the setting's help text names *Bridge.cfg* and says the shard's SignupMode must agree, and core cannot own a sentence about a UO shard. The admin field moves to this module's Shard page and the derived flag onto `/public/shard/features`. **The setting key is unchanged** — renaming `game_account_signup` would silently reset every configured instance to `disabled`. Both regression tests were shown to fail against the bug before it was fixed. ## Two checks that had never met a real chunk `checkExternals.js` **rejected this 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. Same wall the server's `checkImports.js` hit, answered the same way: a character walk. A *mask* rather than a rewrite, because a real import has its keyword outside a string and its specifier inside one. 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. It has never been able to see it. ## Verification - **359 server + 34 client tests**, all passing (`gameSignup.test.js` +8, `registration.test.js` +8, the two ported +12) - `check:imports` clean; `check:externals` clean on the built chunk - **`registration.test.js` is new**: a fake `window.__rg` with a recording registry and the real React, importing the *built* chunk to read back what it registered — 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** — both artifact-reading tests skip without a build, and were otherwise green while asking nothing. - **§7.7 browser smoke against a real core**, zero CSP reports: public pages under `/uo/*` with core's chrome, nine header rows, six sidebar rows with their glyph inside core's Moderation and System groups, `/admin/uo/link` with the migrated signup field showing the live value, all three slots filled, `/player` resolving to `/player/uo/characters`, and the invite flow end to end. The smoke found one defect, fixed in both repos: registering the player nav row **without** an icon blanked the entire portal with React error #130, because core rendered `<n.icon />` unguarded there. This side ships the icon the row had before it moved and asserts one on every admin and player row. ## AI disclosure Written with Claude Code. Co-Authored-By: Claude <noreply@anthropic.com>
wtclaude added 4 commits 2026-08-12 00:01:36 +00:00
The 35 files behind twelve public pages, seven admin views, two player views
and three core-page extensions, ported onto `window.__rg`. Every one of them
imports exactly the seven kit members plus `lib/format.js`, which is the
finding §2.7.1 predicted and this confirms.

`client/src/core.js` is the port mechanism, and unlike the server's it is a
plain read: `window.__rg` is published before any module chunk evaluates, so
there is no gap to defer around and a ported component keeps its ordinary
import shape. `client/src/api.js` rebuilds the UO namespaces over the request
primitive — same URLs, because §1.2 freezes the API surface.

SPA paths changed and API paths did not. `/site/shard` is `/uo/shard`, and the
admin paths lost their now-redundant `shard-` prefixes (`/admin/uo/ops`), a
clean break being the only moment that is free.

`shim/rg.js` becomes the single reader of the global, so the "core did not
publish its dependencies" message is reachable from whichever module the
bundler happens to touch first rather than from whichever one is imported
first — a guarantee that used to last until someone sorted the imports.

Co-Authored-By: Claude <noreply@anthropic.com>
`POST /player/shard/account` and its staff twin have answered 500 for every
caller since slice 1: the ported controller called
`settings.isGameAccountSignupEnabled()`, which is a member of core's settings
model and not of `ctx.settings` — three functions, deliberately. The call was
`undefined(...)`, the TypeError landed in the catch, and no test reached the
branch.

The gate now lives on the side that uses it (`utils/gameSignup.js`), which is
also where the policy belongs: the setting's own help text names Bridge.cfg and
says the shard's SignupMode must agree, and core cannot own a sentence about a
UO shard. The admin field moves to this module's Shard page and the derived
flag onto `/public/shard/features`, beside the visibility flags the same
callers already read.

The setting KEY is unchanged. Renaming `game_account_signup` would silently
reset every configured instance to `disabled` on upgrade, with players
reporting broken signup as the only clue — the same grandfathering as
`spawn_atlas_servuo_path` and the seven stream ids.

Both regression tests were shown to fail against the bug before it was fixed.

Co-Authored-By: Claude <noreply@anthropic.com>
Three things, all about checks that had never met a real chunk.

`checkExternals.js` rejected slice 3's 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 can tell that from a
statement. Same wall the server's `checkImports.js` hit, answered the same way —
a character walk. A mask rather than a rewrite, because a real import has its
keyword outside a string and its specifier inside one.

Writing the test for that false positive found the false NEGATIVE underneath
it: the pattern required whitespace after `import`, so it could not see
`import{useState}from"react"` — the one shape a minified build actually emits,
and the most likely way for a missed alias to reach production. It has never
been able to see it.

`registration.test.js` is new: 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 actually registered —
rather than restating both lists.

CI now builds before it tests, because both of those read `dist/entry.js` and
skip without it. Run the other way round they are green and asking nothing.

Co-Authored-By: Claude <noreply@anthropic.com>
fix(client): give the portal row an icon, and assert every gated nav has one
All checks were successful
PR Checks / server-tests (pull_request) Successful in 19s
PR Checks / client-build (pull_request) Successful in 8m49s
9d559091c5
The §7.7 smoke, running the pair together: registering the player row without an
`icon` blanked the whole portal with React error #130, because core's
PlayerPortalLayout rendered `<n.icon />` unguarded. Core is guarded now
(website), and this is the other half — the row had an icon before it moved and
should have kept one.

A second glyph rather than reusing IconShard: these two rows sit in different
navs and each matched its neighbours before the extraction. The admin sidebar's
UO rows were gems; the portal's Characters row was a person beside Appeals'
shield and Account's gear. Matching the nav a row lands in is the whole reason
`icon` is in the contract.

`registration.test.js` now asserts it for admin AND player rows, which is the
cheap place to catch the next one. The public header is text buttons and is
deliberately excluded.

Co-Authored-By: Claude <noreply@anthropic.com>
whitlocktech merged commit ba092efd8c into main 2026-08-12 00:35:35 +00:00
whitlocktech deleted branch feature/module-extract-client 2026-08-12 00:35:36 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RunicGateway/Module-uo#4
No description provided.