feat(theming): nav wiring and the admin nav builder (phases 6-8) #124
Reference in New Issue
Block a user
No description provided.
Delete Branch "feat/theming-nav-phase-6-8"
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?
What & why
Phases 6–8 of
docs/website/THEMING_AND_NAV.md, the nav half of the feature. Targetsedge; docs pair: RunicGateway/docs#107.Phase 1 landed
navOverrides.jsand phase 0 landed the settings keys andGET /settings/nav, but nothing consumed any of it and there was no UI that could write a row. This closes the loop: the public header, the admin sidebar and the player portal all read their override, and/admin/navigationwrites them — rename, reorder by drag, hide, and on the admin sidebar move a row into another existing section.An instance where no admin opens the screen renders exactly as before: with no row
applyNavOverridesreturns the base array itself.The merge is presentation; the filters are still the boundary
The override merge runs before the role and shard-feature filters in
SiteHeader.jsxandAdminLayout.jsx, which are unchanged. Verified live rather than only in tests, because this is the criterion §9 names:champsdisabled in Shard Visibility + a hand-written{"/site/champs":{"hidden":false,"label":"SHOULD NOT SHOW"}}→ Champions absent from the header, label nowhere on the page.{"/admin/users":{"hidden":false,"label":"LEAKED USERS","order":0}}viewed as a moderator → sidebar is still exactlyMOD_PATHS, noLEAKED USERS, and no orphaned "System" header from the emptied group.The gap the design did not see: the server could not store a nav row
§8 scopes 6–8 as client work, and for the merge that is right. But
updateSettingsvalidates and stringifiestheme_visualandbrand_assetsand lets everything else fall through tosettingsDb.set— so anav_publicobject would have been stored as the string"[object Object]", whichparseJsonSettingreads as absent. The save returns 200 and does nothing, for ever.server/src/utils/navOverrides.jsmirrorsutils/brandAssets.js:validateNavOverridesstrict on write and naming the offending key,resolveNavOverridesforgiving and dropping fields that would do nothing.It deliberately does not check that a
toexists. The baseNAVarrays are client constants; shipping a copy to the server would create a second source of truth for navigation that drifts the first time a route is added, and buys nothing —applyNavOverridesalready drops an unknowntoat merge time, which is what makes deleting a route in code safe with no migration. So: the server validates shape, the client owns membership. A field that is not one of the four (label/order/hidden/group) is a 400, so arolesortoin the body is refused rather than quietly stored.hidden: falseis accepted and never stored — the editor sends it mid-edit, so rejecting it would be hostile, but storing it would leave a row that reads like an instruction to force something visible, which this layer must never be able to express.The nav editor cannot be hidden
An admin who hid
/admin/navigationwould lose the only screen that can un-hide it. Three guards: the row's eye toggle is disabled with a note saying why;resolveNavOverridesdropshiddenon thattofornav_admin; andAdminLayoutstrips it again before merging, which is the one that also covers a row edited straight in the database (confirmed live — the row saidhidden: trueand the sidebar entry stayed).Orders are written only when something moved
§7.1 says the editor writes an order for every item "the way drag-and-drop does", and it does — but only for a nav whose sequence actually differs from the code's. An admin who renames one item stores one field (
{"/player":{"label":"My Heroes"}}, live), so a route added toNAVlater still lands where the code puts it. The comparison is against the base restricted to the rows that admin can see, or filtering the palette by role/feature would look like a reorder and pin everything — a bug this had, caught by the test for it.An override for an item outside the editing admin's palette is carried through their save rather than silently reset.
How it was tested
cd server && DB_HOST=127.0.0.1 DB_PORT=59999 npm test— 782 pass, 0 fail (760 before): 19 new innavOverrides.test.js, 3 insettingsTheming.test.js(a nav row is stored stringified and not"[object Object]"; an invalid one is rejected with nothing written; the write path dropshiddenon the editor and never storeshidden: false).cd client && node --test— 94 pass, 0 fail (75 before), incl. the editor round trip: an untouched editor saves nothing, a rename writes no orders, reordering writes one per row, save→reload→save is stable, a group move round-trips and clears when moved back.npm run buildclean.npm run swaggerregenerated — a one-line diff on the PUT description.npm run routes:manifestregenerated: no new routes, zero diff, sodocs/website/api-route-inventory.jsonis unchanged too.My Heroes, no Appeals) → reset each nav → zero rows, code order back everywhere.Notes
@dnd-kit/core+/sortable+/utilities(~13 kB gz), as §8 specified. Pointer, keyboard and touch sensors with screen-reader announcements is the part worth not hand-rolling. Dragging reorders within a section only; moving between sections is the dropdown, since that is what an override can express.useNavOverrideskeeps one module-level copy of the two authenticated rows — that is what lets a save update the sidebar the admin is looking at, and stops the second layout to mount from flashing the coded nav.AppearanceAdmin's two inlinetry { JSON.parse }blocks now use the newclient/src/lib/settingsJson.js, the client counterpart §4.4 promised "with its first consumer".Checklist
AI-assisted contributions (required)
Claude Code (Opus 5). I have reviewed and understandevery change, and take responsibility for it. AI-authored commits are
marked with a
Co-Authored-By/Assisted-Bytrailer.License
(GNU GPL v3.0 or later), and I have the right to contribute it.