§10 review pass: address the architecture + product reviewers (todo §10); hide the gated Dashboard nav node from anonymous visitors in buildPluginChrome (a no-permission link to /dashboard only dead-ended them at /login) and dedup it into a shared DASHBOARD_NAV (admin-nav.ts, reused by chrome + adminNav). New chrome.signInHref bakes the current page in as return_to for the shell's anonymous Sign-in link (shell.ejs + reference overview.ejs), mirrored as optional ShellModel.signInHref so the typed builder is complete. ctx.chrome is now a lazy, memoized getter (context.ts chrome option = a factory) so a json/redirect handler or the public "/" with a standalone home never composes the global menu — app.ts passes the app-level memoized factory at every site. Default /dashboard prints a "Starter dashboard" note framing the mock-data home as a replaceable demo (signals its inert affordances); stale "until §4" comments fixed. RESERVED_PLUGIN_IDS drift-guard test derives the built-in segments from AUTH_FLOWS + ADMIN_*_BASE + host literals (home stays deliberately unreserved). Refreshed the stale plugin-contract status blurb and documented the chrome.*→partials/shell mapping. Reviewers: architecture + product APPROVE (no addressable findings remain), stability APPROVE (no Critical/High/Medium). typecheck + 356 units + visual(10) + full-flow(7) E2E green.

This commit is contained in:
2026-06-21 01:19:40 +02:00
parent 7bdeb24b7f
commit 58398481ca
17 changed files with 117 additions and 46 deletions
+5 -4
View File
@@ -1,7 +1,8 @@
// Dashboard view model (todo §1): the gated "/dashboard" app-shell "People" list. Pure — turns a
// request URL into the data the building-block partials render, wiring the §1 helpers end-to-end:
// parseListQuery → filter/sort/paginate a mock dataset → composeNav. Mock data stands in for
// upstream until §4; the filter form, sortable headers and pager all round-trip the URL (zero-JS).
// parseListQuery → filter/sort/paginate a mock dataset → composeNav. This is the built-in *demo* home
// over mock data (a plugin owns the real one via a `dashboard` handler, §10); the filter form,
// sortable headers and pager all round-trip the URL (zero-JS).
import { adminSection } from "./admin-nav.ts";
import type { User } from "./context.ts";
@@ -122,8 +123,8 @@ export function buildDashboardModel(url: URL | URLSearchParams | string, roles:
export type DashboardModel = ReturnType<typeof buildDashboardModel>;
// Sidebar: the demo "Directory" fragment, then each discovered plugin's own nav fragment (so a
// plugin is reachable from "/"; gated nodes stay invisible to non-admins), then the gated admin
// section. composeNav applies the central override + per-user role filter.
// plugin is reachable from the dashboard; gated nodes stay invisible to non-admins), then the gated
// admin section. composeNav applies the central override + per-user role filter.
function nav(roles: string[], override: NavOverride, plugins: Plugin[]): NavNode[] {
const pluginFragments = plugins.filter((p) => p.nav?.length).map((p) => p.nav as NavNode[]);
return composeNav([[