Wire built-in admin screens into the global menu (todo §5); extract adminSection() = one permission-gated 'Admin' header (Users/Groups/Roles), reused by both the home dashboard menu and the in-screen adminNav so they can't drift. composeNav drops the whole gated header+subtree for a non-admin/anonymous (cosmetic — the admin routes stay independently GuardError(403)-gated); narrowed AdminScreen to groups|roles|users. Reuses existing sprite icons (no icon-guard change); default anonymous / render byte-equivalent so visual E2E unaffected. Tests-first: dashboard model gating (admin→3 hrefs, non-admin→absent) + app HTTP (admin JWT→/admin/users link, anon→absent). Stability-reviewer run as a local PR: APPROVE, no Critical/High/Medium. README Layout updated. 242→244 units + typecheck green

This commit is contained in:
2026-06-18 18:33:19 +02:00
parent a016a0131e
commit 6920751cb8
6 changed files with 50 additions and 13 deletions
+13
View File
@@ -72,6 +72,19 @@ test("dashboard applies the central menu config: branding + nav override (rename
assert.ok(!labels.includes("Teams")); // "Teams" hidden
});
test("dashboard menu wires in the permission-gated Admin section (only for admins)", () => {
// An admin sees the Admin section with the three built-in screens.
const admin = buildDashboardModel(new URL("http://x/"), ["admin"]);
const adminNode = admin.nav.find((n) => n.label === "Admin");
assert.ok(adminNode, "admin role → Admin section present");
assert.deepEqual(adminNode!.children?.map((c) => c.href), ["/admin/users", "/admin/groups", "/admin/roles"]);
// A non-admin (default []) never sees it — composeNav drops the gated header + its subtree.
const plain = buildDashboardModel(new URL("http://x/"));
assert.equal(plain.nav.find((n) => n.label === "Admin"), undefined);
assert.ok(!plain.nav.some((n) => n.children?.some((c) => c.href === "/admin/users")));
});
test("dashboard paginates: page 2 slices the next rows and preserves state in links", () => {
const p2 = buildDashboardModel(new URL("http://x/?sort=-name&page=2"));
assert.equal(p2.pagination.summary.from, 13); // 30 rows / 12 per page → page 2 starts at 13