refactor(client): delete the UO client half (phase 3, slice 3) #139

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

Phase 3, slice 3 of docs/website/MODULE_SYSTEM.md §2.7.1 — core's half. Pairs with Module-uo#4, which must merge first, and docs#138.

35 files / 5,332 lines out, plus the three additions core owed before they could leave. MODULE_API_VERSION 1.2.0 → 1.3.0.

The three additions (commit 1)

  • player.invite.accepted, a third extension slot. Core's invite page owned a UO game-account step — it read a gameAccountSignup flag out of core's own settings and posted to a shard route — and an invite is a core concept staff receive too, so the page stays and its optional next step becomes a slot. Whether there is a step at all is the module's call; core keeps the shell, the skip control and the destination.
  • icon on a nav item. Without it the six extracted rows would have been the only text-only entries in a sidebar where every other row has a glyph. Core supplies no fallback — an invented one is core making a presentation choice for content it knows nothing about. icon was already among the fields an override may not touch, so the concept predates a module being able to send one.
  • api.BASE, which §3.5 specified from the first draft and shared.js never published. request is fetch-only, so an EventSource builds its own URL, and the shard's live feed is two of them; the alternative is a module hardcoding /api/v1.

AcceptInvite is the only core caller of extensionFor outside Slot.jsx, and legitimately: the answer decides a navigation, not a decoration. Decoration goes inside <Slot wrap> — that is the footer-separator bug from slice 2, and hasExtension stays deleted.

The deletion (commit 2)

Nine rows leave the public header and six leave the admin sidebar; both lists are now free of feature gates and of IconShard. moduleTitle already handled a module page's heading, so the TITLES entries and the sectionTitle branch simply go.

/player had PlayerCharacters as its index — a UO page. Rather than name a replacement or invent a landing screen, it 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. From the BASE nav, never the override-merged one: an override is presentation, and where everybody lands is behaviour.

It is deliberately generic and deliberately not in the portal layout. The admin index is the same question with a hardcoded answer, and the direction of travel is one logged-in area that shows the right things for the viewer's permissions — this is the function that serves both, ready when that happens and nothing to unwind if it does.

game_account_signup goes with the rest of core's UO prose: the mode list, the derived public flag, the validation, and a Site Settings field whose help text named Bridge.cfg. The row itself is untouched and module-uo reads it through ctx.settings — the data stays, the semantics move.

⚠️ Known break, accepted by the org lead

The shipped Android app reads gameAccountSignup off /public/settings (PublicDto.kt:80CharactersViewModel.kt:72). The field defaults to false, so nothing crashes — the app silently stops offering game-account creation until it reads module-uo's /public/shard/features instead. Out of scope here; recorded as an OWED note in docs/android/PLAN.md. It must land before this workstream's cutover reaches main.

Two fixes the browser smoke produced (commit 3)

  • PlayerPortalLayout rendered <n.icon /> unguarded where AdminLayout guarded its equivalent. Correct for as long as every row there was core's own and had an icon; React error #130 and a blank player portal the moment a module registered one without.
  • MODULES_DIR is resolved absolute. resolveClient checks containment by comparing an absolute path.resolve(dir, entry) against the module directory, so 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", pointing at the module for core's problem.

Verification

  • 620 server + 161 client tests (+4 settings, +5 firstDestinationFor, +2 nav icon)
  • routes.manifest.json 158 public + 2 internal, unchanged; routes.guards.json unchanged
  • swagger-output.json loses exactly one property, and only because it was hand-written in swagger.js — regeneration alone would have left the spec documenting a field core no longer returns
  • Client build clean
  • §7.7 browser smoke with module-uo#4 installed, zero CSP reports: public pages under /uo/*, the interleaved nav in all three areas, all three slots filled, /player/player/uo/characters, the invite flow end to end, and the SSE feed connected (which is api.BASE working)

One thing the smoke made concrete, worth reading before merge: decision 9 (clean break, no nav-override migration) costs more than "a hidden row may reappear". The review instance had the nine UO rows gathered into a dropdown section; those override keys now match nothing, the section is empty, and the rows render flat across two lines of the header. An operator's ordering, labels, hidden state and grouping of the UO rows are lost and must be redone.

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 — core's half. Pairs with Module-uo#4, which must merge first, and docs#138. **35 files / 5,332 lines out**, plus the three additions core owed before they could leave. `MODULE_API_VERSION` 1.2.0 → **1.3.0**. ## The three additions (commit 1) - **`player.invite.accepted`**, a third extension slot. Core's invite page owned a UO game-account step — it read a `gameAccountSignup` flag out of core's own settings and posted to a shard route — and an invite is a core concept staff receive too, so the page stays and its optional next step becomes a slot. Whether there is a step at all is the module's call; core keeps the shell, the skip control and the destination. - **`icon`** on a nav item. Without it the six extracted rows would have been the only text-only entries in a sidebar where every other row has a glyph. Core supplies no fallback — an invented one is core making a presentation choice for content it knows nothing about. `icon` was already among the fields an override may not touch, so the concept predates a module being able to send one. - **`api.BASE`**, which §3.5 specified from the first draft and `shared.js` never published. `request` is fetch-only, so an EventSource builds its own URL, and the shard's live feed is two of them; the alternative is a module hardcoding `/api/v1`. `AcceptInvite` is the only core caller of `extensionFor` outside `Slot.jsx`, and legitimately: the answer decides a **navigation**, not a decoration. Decoration goes inside `<Slot wrap>` — that is the footer-separator bug from slice 2, and `hasExtension` stays deleted. ## The deletion (commit 2) Nine rows leave the public header and six leave the admin sidebar; both lists are now free of `feature` gates and of `IconShard`. `moduleTitle` already handled a module page's heading, so the TITLES entries and the `sectionTitle` branch simply go. **`/player` had `PlayerCharacters` as its index — a UO page.** Rather than name a replacement or invent a landing screen, it 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. From the BASE nav, never the override-merged one: an override is presentation, and where everybody lands is behaviour. It is deliberately generic and deliberately not in the portal layout. The admin index is the same question with a hardcoded answer, and the direction of travel is one logged-in area that shows the right things for the viewer's permissions — this is the function that serves both, ready when that happens and nothing to unwind if it does. **`game_account_signup` goes with the rest of core's UO prose**: the mode list, the derived public flag, the validation, and a Site Settings field whose help text named *Bridge.cfg*. The row itself is untouched and module-uo reads it through `ctx.settings` — the data stays, the semantics move. > ### ⚠️ Known break, accepted by the org lead > > The shipped **Android app** reads `gameAccountSignup` off `/public/settings` (`PublicDto.kt:80` → `CharactersViewModel.kt:72`). The field defaults to `false`, so nothing crashes — the app silently stops offering game-account creation until it reads module-uo's `/public/shard/features` instead. Out of scope here; recorded as an OWED note in `docs/android/PLAN.md`. It must land before this workstream's cutover reaches `main`. ## Two fixes the browser smoke produced (commit 3) - **`PlayerPortalLayout` rendered `<n.icon />` unguarded** where `AdminLayout` guarded its equivalent. Correct for as long as every row there was core's own and had an icon; React error #130 and a **blank player portal** the moment a module registered one without. - **`MODULES_DIR` is resolved absolute.** `resolveClient` checks containment by comparing an absolute `path.resolve(dir, entry)` against the module directory, so 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", pointing at the module for core's problem. ## Verification - **620 server + 161 client tests** (+4 settings, +5 `firstDestinationFor`, +2 nav icon) - `routes.manifest.json` **158 public + 2 internal, unchanged**; `routes.guards.json` unchanged - `swagger-output.json` loses **exactly one property**, and only because it was hand-written in `swagger.js` — regeneration alone would have left the spec documenting a field core no longer returns - Client build clean - **§7.7 browser smoke with module-uo#4 installed**, zero CSP reports: public pages under `/uo/*`, the interleaved nav in all three areas, all three slots filled, `/player` → `/player/uo/characters`, the invite flow end to end, and the SSE feed connected (which is `api.BASE` working) One thing the smoke made concrete, worth reading before merge: decision 9 (clean break, no nav-override migration) costs more than "a hidden row may reappear". The review instance had the nine UO rows gathered into a **dropdown section**; those override keys now match nothing, the section is empty, and the rows render flat across two lines of the header. An operator's ordering, labels, hidden state and grouping of the UO rows are lost and must be redone. ## AI disclosure Written with Claude Code. Co-Authored-By: Claude <noreply@anthropic.com>
wtclaude added 3 commits 2026-08-12 00:02:10 +00:00
The three things core owes the client half before it can leave, all additive,
all MODULE_API 1.2.0 → 1.3.0.

`player.invite.accepted` is the third extension slot. Core's invite page owned a
UO game-account step — it read a `gameAccountSignup` flag out of core's 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 next step becomes a slot.
Named for the place, like the other two. Whether there is a step at all is the
filling module's call, made from data core does not have; core keeps the shell,
the skip control and the destination.

`icon` on a nav item, because without it the six extracted UO rows would have
been the only text-only entries in a sidebar where every other row has a glyph.
Core supplies no fallback — an invented one is core making a presentation choice
for content it knows nothing about. `icon` was already among the fields an
override may not touch, so the concept predates a module being able to send one.

`api.BASE` was in §3.5 from the first draft and never actually published.
`request` is fetch-only, so an EventSource builds its own URL, and the shard's
live feed is two of them; the alternative is a module hardcoding `/api/v1`,
which asserts something about core that core has not promised.

`AcceptInvite` is the one legitimate reader of `extensionFor` outside Slot.jsx:
the answer decides a NAVIGATION, not a decoration. Decoration goes inside
`<Slot wrap>`, which is why `hasExtension` stayed deleted.

Co-Authored-By: Claude <noreply@anthropic.com>
35 files and 5,332 lines out — twelve public pages, seven admin views, two
player views, eight components, the two `data/` leaves and the three `lib/`
ones, plus the two tests that came with them. §2.7.1's estimate of 51 files /
~3,700 lines was measured differently and is corrected in the docs PR.

The seams core keeps, each smaller than what it replaced:

Nine rows leave the public header and six leave the admin sidebar, and both
lists are now free of `feature` gates and of `IconShard`. `moduleTitle` already
handled a module page's heading, so the six TITLES entries and the
`/admin/characters` branch of `sectionTitle` simply go.

`/player` had `PlayerCharacters` as its index — a UO page — and rather than name
a replacement or invent a landing screen it now resolves to the first row of the
portal nav this viewer can reach (`firstDestinationFor`, beside
`allowedPathsFor` and reading the BASE nav for the same reason: an override is
presentation and where everybody lands is behaviour). With the module installed
that is still Characters, so a player's first screen after signing in does not
change. Deliberately generic and deliberately not in the portal layout — the
admin index is the same question with a hardcoded answer, and if the two
logged-in areas ever become one this is what serves both.

`game_account_signup` goes with the rest of core's UO prose: the mode list, the
derived public flag, the validation and a Site Settings field whose help text
named Bridge.cfg. The row itself is untouched and module-uo reads it through
ctx.settings — the data stays, the semantics move.

KNOWN BREAK, accepted by the org lead: the shipped Android app reads
`gameAccountSignup` off `/public/settings` (PublicDto.kt:80). The field has a
`= false` default so nothing crashes; the app silently stops offering
game-account creation until it reads the module's `/public/shard/features`
instead. Out of scope here, recorded in the Android plan, and it lands well
before this workstream's cutover reaches `main`.

620 server + 161 client tests. Manifest 158 public + 2 internal, unchanged;
routes.guards unchanged. The OpenAPI spec loses exactly one property, and only
because it was hand-written in swagger.js — regeneration alone would have left
the spec documenting a field core no longer returns.

Co-Authored-By: Claude <noreply@anthropic.com>
fix(modules): guard the portal's nav icon, resolve MODULES_DIR absolutely
All checks were successful
PR Checks / bot-install (pull_request) Successful in 18s
PR Checks / client-build (pull_request) Successful in 26s
PR Checks / server-tests (pull_request) Successful in 29s
5bdb6a7e10
Both found by the §7.7 browser smoke, running the slice-3 pair together, and
neither is visible to any test in either repo.

`PlayerPortalLayout` rendered `<n.icon />` unguarded while `AdminLayout` guarded
its equivalent. `icon` is optional in the nav contract, and every core row in
that sidebar has always had one — so the difference cost nothing until a module
registered a row without, and then it was not a missing glyph, it was React
error #130 and a blank player portal. Guarded now, like its neighbour.

`MODULES_DIR` is resolved absolute. `resolveClient` checks containment by
comparing an absolute `path.resolve(dir, entry)` against the module directory,
so a RELATIVE `MODULES_DIR` — which is what §7.7's own recipe produces when run
from `server/` — failed every module with "client.entry escapes the module
directory". A perfectly-placed entry, and a message pointing at the module.

Co-Authored-By: Claude <noreply@anthropic.com>
whitlocktech merged commit a99ead4ee4 into edge 2026-08-12 00:35:53 +00:00
whitlocktech deleted branch feature/module-extract-client 2026-08-12 00:35:54 +00:00
Sign in to join this conversation.
No description provided.