feat(modules): interleave module nav items and derive moderator confinement (phase 2, PR 8) #135
Reference in New Issue
Block a user
No description provided.
Delete Branch "feature/module-nav-interleave"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Phase 2, PR 8 of
docs/website/MODULE_SYSTEM.md§2.7 — the nav half PR 7 deferred, plus the two seams §1.4 and §1.5 asked for. Docs PR: RunicGateway/docs#133.Client-only. 933 server tests (unchanged), 160 client tests (+37).
routes.manifest.jsonunchanged at 230 routes; the OpenAPI spec regenerates byte-identical.The interleave happens before the override merge
withModuleNav(client/src/modules/nav.js) merges an installed module's rows into core's three navs, and it runs first. That ordering is the design, not an implementation detail:applyNavOverridesandbuildPublicNavare keyed bytoand drop any key their base array does not declare, so rows appended after the merge would be unorderable, unrelabellable and unhideable in Admin → Navigation. Today's UO rows are all three of those things, so appending would make the extraction a visible regression for every operator who has ever edited their nav.Merging first means a module row is an ordinary row to everything downstream — nothing in
navOverrides.js,NavEditor.jsxor the layouts knows a module exists.Six details it settled, all in
MODULE_API.md§3.3: an unordered row appends rather than defaulting to 0; an ungrouped admin row gets its own trailing group rather than joining core's Dashboard/Account furniture; an unknown group name creates a group rather than dropping the row; a module-created group is itself a legal override destination; a collidingtois dropped with a warning (it is the key the override layer stores under); andfeatureapplies in all three areas, not just the public one.MOD_PATHSis gone, and it took a defect with itModerator visibility and the redirect that confines a moderator both derive from each row's own
roles, in the new plain-JSclient/src/lib/adminNav.js— plain so the DOM-less test runner can reach what is the closest thing in the client to an authorization decision.Two rows move, both toward what the server already permitted: Dashboard, whose
roleshad always named moderator whileMOD_PATHSomitted it, and My Characters, which is ungated self-service.The defect predates the module system: the redirect was a third hardcoded list — three path prefixes against
MOD_PATHS' five paths — and they disagreed about/admin/houses. A moderator who clicked Houses in their own sidebar was bounced straight back to Moderation. The derived allow-list is computed from the base nav, never the override-merged one: an override is presentation and must not move an authorization boundary in either direction.The feature seam
modules/features.jsx+modules/featureGate.js. A row'sfeatureis resolved by the provider its own module registered, so the namespace comes from the registration and no flag name carries a parsed prefix — a module author writesfeature: 'status'exactly as it reads today.Core registers
useShardFlagsunder the owner idcore(the client twin of the server'sregistries.registerCore()), so the ten shard-gated header rows already run through the seam rather than beside it, and Phase 3 deletes a registration instead of rewritingSiteHeaderunder extraction pressure.Every unknown fails open — no provider, a
nullanswer while the fetch is in flight, or a provider that returned something without ahasall show the link. The server is the gate; hiding a page from someone entitled to it is the worse mistake.Verification
The §7.7 browser smoke was re-run, since this is the seam that rule exists for. A throwaway module registering nav in all three areas plus a provider granting one flag and withholding another, in Chrome with the console open:
/admin/housesand the module's own admin page, and is still redirected off/admin/users;Zero CSP reports, zero console errors.
AI disclosure
Co-Authored-By: Claude <noreply@anthropic.com>.