Record the reading cost of the previous chunk

This commit is contained in:
2026-10-31 08:37:45 +01:00
parent a8a9744744
commit de4eb7d9ee
5 changed files with 17 additions and 159 deletions
-28
View File
@@ -1,28 +0,0 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { buildDashboardModel } from "./dashboard.ts";
import type { NavNode } from "./nav.ts";
// The default /dashboard is an instructional starter: no mock data, just the unified
// menu + shell. The host passes the one global menu (ctx.chrome.nav); the model passes it through.
const NAV: NavNode[] = [{ href: "/dashboard", label: "Dashboard" }, { children: [{ href: "/admin/users", label: "Users" }], label: "Admin" }];
test("dashboard model: titled shell, passes the unified nav + csrf + user through", () => {
const m = buildDashboardModel({ csrfToken: "tok.sig", user: { email: "ada@x.io", id: "u1", permissions: ["admin"] }, nav: NAV });
assert.equal(m.shell.title, "Dashboard");
assert.equal(m.shell.csrfToken, "tok.sig");
assert.equal(m.shell.user.name, "ada"); // real signed-in identity, not a demo profile
assert.deepEqual(m.nav, NAV); // the host's menu is used verbatim — the dashboard builds no nav of its own
});
test("dashboard model: sensible defaults (empty nav, no token, Guest) and branding from menu", () => {
const m = buildDashboardModel();
assert.deepEqual(m.nav, []);
assert.equal(m.shell.csrfToken, "");
assert.equal(m.shell.user.name, "Guest");
assert.equal(m.shell.brand.name, "Plainpages");
const branded = buildDashboardModel({ menu: { branding: { name: "Acme Ops", sub: "Admin", theme: "dark" }, override: {} } });
assert.equal(branded.shell.brand.name, "Acme Ops");
assert.equal(branded.shell.theme, "dark");
});
-29
View File
@@ -1,29 +0,0 @@
// Dashboard view model: the gated "/dashboard" app home. By default a short instructional
// starter — what the dashboard is and how to replace it by exporting a `dashboard` handler from a
// plugin (views/index.ejs holds the prose). A plugin owns the real one (PluginManifest.dashboard);
// this placeholder renders until then. Pure: `nav` is the one global menu (ctx.chrome.nav), built
// once per request by the host, so the dashboard shows the exact same menu as every other page.
import type { User } from "../http/context.ts";
import { ENGLISH } from "../i18n/english.ts";
import type { Translate } from "../i18n/translate.ts";
import { DEFAULT_MENU, type MenuConfig } from "./menu-config.ts";
import type { NavNode } from "./nav.ts";
import { buildShellContext } from "./shell-context.ts";
export function buildDashboardModel(opts: { csrfToken?: string; menu?: MenuConfig; nav?: NavNode[]; t?: Translate; user?: User | null } = {}) {
const t = opts.t ?? ENGLISH;
return {
nav: opts.nav ?? [],
shell: buildShellContext({
breadcrumbs: [{ label: t("dashboard.title") }],
csrfToken: opts.csrfToken ?? "",
menu: opts.menu ?? DEFAULT_MENU,
t,
title: t("dashboard.title"),
user: opts.user ?? null,
}),
};
}
export type DashboardModel = ReturnType<typeof buildDashboardModel>;
-32
View File
@@ -1,32 +0,0 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { buildShellContext, shellUser } from "./shell-context.ts";
test("shellUser derives the profile from the real user; anonymous → Guest", () => {
assert.deepEqual(shellUser(null), { email: "", initials: "G", name: "Guest" });
// Real user: name = email local part, email kept, initials = first two letters of the local part.
assert.deepEqual(shellUser({ email: "ada@example.com", id: "u1", permissions: [] }), { email: "ada@example.com", initials: "AD", name: "ada" });
});
test("buildShellContext maps branding + breadcrumbs, omitting unset optional fields", () => {
const bare = buildShellContext({ menu: { branding: { name: "Plainpages" }, override: {} }, title: "Users" });
assert.deepEqual(bare.brand, { name: "Plainpages" }); // no logo/sub when unset
assert.equal(bare.theme, undefined);
assert.equal(bare.csrfToken, "");
assert.equal(bare.user.name, "Guest");
assert.equal(bare.signInHref, undefined); // omitted unless supplied (a public built-in screen would set it)
const full = buildShellContext({
breadcrumbs: [{ href: "/", label: "Home" }, { label: "Users" }],
csrfToken: "tok.sig",
menu: { branding: { logo: "/l.svg", name: "Acme", sub: "Ops", theme: "dark" }, override: {} },
signInHref: "/login?return_to=%2Fx",
title: "Users",
user: { email: "a@b.c", id: "u1", permissions: ["admin"] },
});
assert.deepEqual(full.brand, { logo: "/l.svg", name: "Acme", sub: "Ops" });
assert.equal(full.theme, "dark");
assert.equal(full.csrfToken, "tok.sig");
assert.equal(full.breadcrumbs?.length, 2);
assert.equal(full.signInHref, "/login?return_to=%2Fx");
});
-70
View File
@@ -1,70 +0,0 @@
// Shell view-model builder: the brand/theme/user/title block every app-shell page
// (the home dashboard, the built-in admin screens) hands to shell.ejs. Pure. Extracted so the
// shell user is the *real* signed-in identity — no hardcoded demo profile — and branding is
// read from one place. The User carries no display name (the JWT holds only id/email/permissions), so
// the profile shows the email's local part as the name with the full email beneath, initials from
// the local part; anonymous ⇒ "Guest".
import type { User } from "../http/context.ts";
import { ENGLISH } from "../i18n/english.ts";
import type { Translate } from "../i18n/translate.ts";
import { type MenuConfig } from "./menu-config.ts";
// The brand block both the chrome and the shell model carry. `name`/`sub` pass through `t`, so a
// catalog key is translated and an operator's own wording renders as written.
export function branding(menu: MenuConfig, t: Translate): { logo?: string; name: string; sub?: string; theme?: string } {
const b = menu.branding;
return {
...(b.logo != null ? { logo: b.logo } : {}),
name: t(b.name),
...(b.sub != null ? { sub: t(b.sub) } : {}),
...(b.theme != null ? { theme: b.theme } : {}),
};
}
export interface ShellUser {
email: string;
initials: string;
name: string;
}
export interface ShellModel {
brand: { logo?: string; name: string; sub?: string };
breadcrumbs?: { href?: string; label: string }[];
csrfToken: string;
signInHref?: string; // anonymous "Sign in" target (mirrors PageChrome.signInHref); a gated screen omits it
theme?: string;
title: string;
user: ShellUser;
}
export function shellUser(user: User | null | undefined, t: Translate = ENGLISH): ShellUser {
if (!user) {
const guest = t("shell.guest");
return { email: "", initials: ([...guest][0] ?? "?").toUpperCase(), name: guest }; // by character: the word is translated
}
const local = user.email.split("@")[0] || user.email;
return { email: user.email, initials: (local.slice(0, 2) || "U").toUpperCase(), name: local };
}
export function buildShellContext(opts: {
breadcrumbs?: { href?: string; label: string }[];
csrfToken?: string;
menu: MenuConfig;
signInHref?: string;
t?: Translate;
title: string;
user?: User | null;
}): ShellModel {
const t = opts.t ?? ENGLISH;
const { theme, ...brand } = branding(opts.menu, t);
return {
brand,
...(opts.breadcrumbs ? { breadcrumbs: opts.breadcrumbs } : {}),
csrfToken: opts.csrfToken ?? "",
...(opts.signInHref != null ? { signInHref: opts.signInHref } : {}),
...(theme != null ? { theme } : {}),
title: opts.title,
user: shellUser(opts.user, t),
};
}