0a3f1eb9fa7fd0b337cafd6beca3bd7ab9bdf1ef
8 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| a45a3d120a |
feat(modules): interleave module nav, derive moderator confinement
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. withModuleNav (client/src/modules/nav.js) merges an installed module's rows into core's three navs BEFORE the admin-override merge, and that ordering is the design. applyNavOverrides and buildPublicNav are keyed by `to` and 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 anyone who has ever edited their nav. Merging first means a module row is an ordinary row downstream: nothing in navOverrides.js, NavEditor.jsx or the layouts knows a module exists. MOD_PATHS is gone. Moderator visibility and the redirect that confines a moderator both derive from each row's own `roles`, in the new plain-JS lib/adminNav.js (plain so the DOM-less runner can reach it). Two rows move, both toward what the server already permitted: Dashboard, whose roles had always named moderator, and My Characters, which is ungated self-service. That also fixes a defect predating the module system. The redirect was a THIRD hardcoded list - three path prefixes against MOD_PATHS' five paths - and they disagreed about /admin/houses, so a moderator who clicked Houses in their own sidebar was bounced 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 either way. The feature seam (modules/features.jsx + modules/featureGate.js) resolves a row's `feature` against the provider its OWN module registered, so the namespace comes from the registration and no string carries a parsed prefix. Core registers useShardFlags under the owner id `core` - the client twin of registries.registerCore() - so the ten shard-gated header rows already run through the seam and Phase 3 deletes a registration instead of rewriting SiteHeader. Every unknown fails open: no provider, a null answer while a fetch is in flight, or a junk return all show the link, because the server is the gate and hiding a page from someone entitled to it is the worse mistake. 933 server tests (unchanged - this PR is client-only), 160 client tests (+37). routes.manifest.json unchanged at 230 routes; the OpenAPI spec regenerates byte-identical. Re-ran the MODULE_API.md 7.7 browser smoke, since this is the seam that rule exists for. A throwaway module registering nav in all three areas and a provider granting one flag and withholding another: the row lands inside core's Moderation group rather than an appended block, the withheld row does not render, a moderator reaches both /admin/houses and the module's admin page, and an admin can relabel a module row and have it persist and apply. Zero CSP reports, zero console errors. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| e0927bc255 |
feat(modules): the client registry, window.__rg and the chunk's script injection
Phase 2, PR 7 of docs/website/MODULE_SYSTEM.md 2.7 — the client half's
delivery. A module's prebuilt chunk is served, injected, handed core's React
and its UI kit, and its routes are rendered by App.jsx. The registry is empty
on a bare core, so nothing an operator can see changes.
Client:
- modules/registry.js — registerRoutes/registerNav/registerFeatureProvider,
with the URL namespace written by core, never by the module
- modules/shared.js — window.__rg: React, react-dom/client, react-router-dom,
react/jsx-runtime, the registry, the seven-member UI kit and the request
primitive, frozen
- App.jsx reads routesFor for all three areas; nav consumption is PR 8
- main.jsx publishes the global, then mounts on DOMContentLoaded
Server:
- the loader validates client.entry and publishes clientChunks() and
clientEntryUrls(); an entry in the module root is rejected, because the
directory it sits in is what gets served
- app.js mounts each chunk at /modules/<id>/ behind the module's state guard
with no-cache; anything else under /modules is a 404, not the SPA shell
- htmlShell injects the tag before </body>, so core's bundle runs first
wherever a bundler puts it
Found by loading a real chunk in a browser, and fixed here: core mounted before
any module chunk had evaluated, because document.readyState during a deferred
script is 'interactive', not 'loading'. Every test passed against that build.
The smoke is written down in MODULE_API.md 7.7.
933 server tests (+23), 123 client tests (+14). routes.manifest.json unchanged
at 230 routes; the OpenAPI spec regenerates byte-identical.
Co-Authored-By: Claude <noreply@anthropic.com>
|
|||
| b517d7b2df |
feat(theming): dropdown sections and added links in the public header
Phase 10 of docs/website/THEMING_AND_NAV.md, asked for before the edge -> main
cutover. An admin can now create dropdown sections in the public header, organise
the coded entries into them, and add links of their own.
This deliberately amends §7, which said the override layer "cannot introduce a
`to` that is not already in the hardcoded NAV array". That stays true of every
CODED entry; an admin may now also add a link, restricted to a same-origin path —
no scheme, no protocol-relative //host. A link carries no gate of its own and
needs none: the page behind it enforces its own access, so an added link
advertises a route and never grants one.
The invariant is kept structurally rather than by vigilance. Coded entries live
in an `items` map whose keys must be routes the base array declares, so that map
cannot invent a route; everything that CAN name an arbitrary path lives in
`links`, which is the one place the path rule is applied — on both the write and
the read path.
nav_public therefore grew a { items, sections, links } wrapper. A bare map still
reads as the items map, and a nav with no sections still stores one, so this
changed nothing for a nav that does not use it. Free to do now because nothing
has shipped; after the cutover it would have needed a migration.
The Public tab gets its own editor. A public section is an entry in the
top-level order that the admin created and can drag among the pills, unlike the
admin sidebar's four coded sections, where only membership moves — that is a tree
rather than a list of groups. Deleting a section returns its entries to the top
level rather than removing them, which is the one destructive act this screen
could otherwise commit.
The dropdown opens on click and never on hover, and its trigger is not a link: a
hover menu is unusable on touch, and a trigger that navigates means tapping to
open takes you somewhere instead. Escape closes and returns focus, an outside
press closes, navigating closes, and Arrow Up/Down walk the items.
pruneNav applies the shard-feature gate inside a section and drops one it leaves
empty, so a dropdown never opens onto nothing.
Also fixes a bug this surfaced in the phase 6-8 code: the save path judged "does
this route still exist?" against the palette — the base array already filtered to
what the editing admin can see — so on the public header a feature-gated row's
override could never be carried through and would have been silently reset.
Membership is now judged against the full coded nav while the rows still come
from the palette.
Co-Authored-By: Claude <noreply@anthropic.com>
|
|||
| 32a3ff104a |
feat(theming): wire the three navs and add the admin nav builder
Phases 6-8 of docs/website/THEMING_AND_NAV.md. The public header, the admin sidebar and the player portal now read their override row, and /admin/navigation writes them: rename, reorder by drag, hide, and — on the admin sidebar — move a row into another existing section. The merge always runs BEFORE the role and shard-feature filters in the layouts, which are unchanged and remain the boundary. An override is presentation: it cannot introduce a route, cannot touch a `roles` or `feature` gate, and a stored `hidden: false` on a gated item shows nobody anything. The design scoped these phases as client work, but the server had no way to store a nav row: updateSettings validates and stringifies theme_visual and brand_assets and lets everything else through, so a nav object would have been written as "[object Object]" and read as absent for ever. utils/navOverrides.js mirrors utils/brandAssets.js — strict on write with the offending key named, forgiving on read. It validates shape only; whether a `to` exists is settled client-side at merge time, because the base NAV arrays are client constants and a server-side copy would be a second source of truth that drifts. The nav editor cannot be hidden — its own toggle is disabled, the write path drops `hidden` on that one `to`, and AdminLayout strips it again before merging, which also covers a row edited straight in the database. Orders are written only when the sequence actually differs from the code's, and the comparison is restricted to the rows the editing admin can see, so renaming one item does not pin the position of every other one and a role- or feature-gated item missing from their palette is not mistaken for a reorder. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| 3d6b2e23a7 |
feat(theming): server-resolved theme engine and admin appearance UI
Phases 3-4 of docs/website/THEMING_AND_NAV.md. Three presets, the curated font shortlist, and /admin/appearance to drive them. The design put the presets in theme.css as [data-theme] blocks. That does not work: SiteContext writes --accent as an inline style on <html>, which beats any attribute-selector block, so a preset's accent would have been painted over by BRAND_ACCENT_COLOR while getPublic().brand.accent -- the value the Android app themes itself from -- reported the other one. Presets now live in server/src/config/themePresets.js. themeResolve.js layers :root <- preset <- custom per field into a token map, getPublic() returns it as `theme`, and the client writes it onto <html>. One authority for the merge, and brand.accent is by construction the accent the site paints. theme.css's :root is untouched, so an instance with no row gets no theme block and renders as today. Also: presets carry the full 15-token palette (eight would have left Fantasy with blue-grey borders); the option catalog is served from GET /settings/theme/options so the form cannot offer what the server rejects; validation is strict on write and forgiving on read; and the Discord bot now fetches the effective accent instead of its boot-time env copy. Fixes a Phase 0 bug in passing: settings/nav.controller.js imported the logger factory rather than calling it, so a DB fault would have thrown a TypeError inside the catch instead of returning 500. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| ec0036ce6d |
feat(theming): settings-store, nav merge util and radius tokens
Phases 0-2 of docs/website/THEMING_AND_NAV.md. Groundwork only: no admin UI, no consumer wiring, and an instance that never touches the new settings keys renders exactly as it does today. Phase 0 - settings store: - settingsDb.remove() and DELETE /api/v1/admin/settings/:key, the "reset to default" primitive. Defaults for these keys live in BRAND_* env, theme.css and the hardcoded NAV arrays, so reset has to delete the row rather than store a copy of the default. Allowlisted to the five theming/nav keys plus hero_layout_draft, admin-only, idempotent. - GET /api/v1/settings/nav behind requireAuth with no role gate. AdminLayout renders for editors and moderators and PlayerPortalLayout for players, and none of them can read GET /admin/settings, so without this their nav override would silently never apply. - A fifth router group for it: /public is anonymous, /admin/settings is adminOnly, /player is self-scoped data. This is configuration that needs a login. - parseJsonSetting() in utils/settingsJson.js. settings.value is TEXT, so every JSON key arrives as a string; malformed or wrong-shaped reads as absent, never as an error and never half-applied. - theme_visual / brand_assets / nav_public join PUBLIC_KEYS; nav_admin and nav_player deliberately do not. Phase 1 - client/src/lib/navOverrides.js, the pure merge util. Presentation only: it can set label/order/hidden and (grouped navs) group, and nothing else. It cannot introduce a `to`, cannot touch roles/feature, and hidden:false cannot un-hide anything - the existing filters run afterward, unchanged, and remain the boundary. Phase 2 - promoted 23 border-radius literals in theme.css to four tokens at today's values (14x8px, 4x999px, 4x10px, 1x12px). The 7px/6px editor chrome and the two 50% circles stay literal. --shadow-card and --panel-grad were already tokens. Tests: 16 new server tests, 20 new client tests. The route-manifest guard now also asserts /settings/** sits behind requireAuth. Swagger and both route artifacts regenerated. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| 7c769ea8fd |
feat(atlas): serve the spawn atlas and give operators a panel for it
Protocol 3.0 order 3 (Part C), second of two website PRs. #112 built the data pipeline; this makes it reachable — six public routes, five admin ones, two public pages and an admin panel. Still website-only: no plugin, no sidecar, no new event kinds, no wire change. The API sits at /api/v1/public/atlas, not under /public/shard. Nothing here touches the sidecar, so the pages stay complete while the shard is down, and a /shard prefix would imply a dependency the atlas does not have. Unlike /shard/* it IS site-mode gated, like /posts and /wiki: a bestiary is site content. Every route carries requireFeature('atlas') and projects its response. The atlas feature declares no sensitive fields, so the projection is a no-op today — the call is there because v3.md 3.6.1's rule is that the FIRST field needing a gate should be covered by construction rather than by a retrofit. Two bugs the UI surfaced, both fixed here: Respawn delays were stored in the wrong unit, sometimes. XmlSpawner writes MinDelay/MaxDelay in minutes and switches to seconds only when a delay does not divide into whole minutes, flagging it per record with DelayInSec. A `5` means five minutes on one spawner and five seconds on the next, both plausible, and the pipeline stored the raw number. 170 of 6,455 stock spawners are second flagged. The parser normalises to seconds; the API and UI carry seconds. That exposed the hash gate as a trap. "Has the tree changed?" is the wrong question on its own: an install whose maps never change would have kept serving the old readings forever, because the only thing compared was the tree. PARSER_VERSION is now stored beside the source hashes and a mismatch counts as drift, so any future parse correction lands on the next boot. Also renamed the detail route's spawn-point array to `spawners` — it was `points`, which is the COUNT on the search route, so one key meant a number in one place and an array in the other. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U7CBg11prhLimL9iHSX1bP |
|||
| 886a504152 |
test(client): unit-test the pure-logic modules + wire coverage into CI/Sonar
Stand up a client test suite on Node's built-in runner (no vitest/jsdom — the targeted modules are plain ESM with no browser/DOM deps) and cover the meaningful client logic, not presentational components: - api/client.js: the fetch wrapper — always sends the session cookie, maps a non-2xx response to a thrown ApiError (body.message → statusText fallback), resolves an empty body to null, sets Content-Type for JSON but NOT for raw FormData uploads, and builds/encodes query strings + path params. - lib/shardEvents.js: describe() across event kinds (payload vs live frame, actor name→acct→"Someone" fallback, sale pluralization, champ.update branches) and the categoryOf table-consistency check. - data/regionBuckets.js: the presence roll-up, incl. the first-match-wins ordering and the "buckets always reconcile to the total" invariant. - lib/heroLayout.js: parseLayout's version/shape guard and heroBackground's default-vs-custom branch. - lib/format.js: the date/label formatters + relative-time buckets. Wiring so these actually count: - client/package.json gains a `test` script (node --test); - pr-checks.yml runs the client tests as a PR gate; - sonarqube.yml generates a client LCOV report and sonar-project.properties feeds it alongside the server report (SF paths resolve to client/src/...). 43 client tests; coverage on the tested modules: format/regionBuckets/ heroLayout 100%, api client 86%, shardEvents 80%. Co-Authored-By: Claude <noreply@anthropic.com> |