From 9489bd124bb0ae73498e9a8a02b9418eb629ec2d Mon Sep 17 00:00:00 2001 From: lilleman Date: Tue, 16 Jun 2026 16:31:57 +0200 Subject: [PATCH] =?UTF-8?q?Tighten=20code=20comments=20+=20README=20(todo?= =?UTF-8?q?=20=C2=A72);=20trim=20verbose=20=C2=A72=20headers,=20drop=20sta?= =?UTF-8?q?le=20planned/next-item=20markers,=20correct=20README=20status?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 31 +++++++++++++++---------------- src/dashboard.ts | 9 ++++----- src/discovery.ts | 9 ++++----- src/menu-config.ts | 2 +- src/plugin.ts | 21 ++++++++------------- src/router.ts | 10 ++++------ todo.md | 2 +- 7 files changed, 37 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 23f9bb4..084bb28 100644 --- a/README.md +++ b/README.md @@ -46,11 +46,12 @@ makes **semantic, accessible markup** a priority: real landmarks, one `

` per lists and tables with proper headers, a skip link, and ARIA (`aria-current`/`aria-sort`) only where the platform leaves a gap (see [AGENTS.md](AGENTS.md)). -> **Status.** This README describes the target architecture. What exists today is the -> **scaffold** — a Node 24 + EJS HTTP server with static serving — plus the **design -> foundation** in `html-css-foundation/` (a complete zero-JS app shell + auth screens). -> The plugin host and Ory integration (Kratos/Keto/Hydra + their Postgres) are the -> roadmap below. Sections marked _(planned)_ are not built yet. +> **Status.** This README describes the target architecture. Built today (see `todo.md`): +> the Node 24 + EJS server, the zero-JS **design system** (app shell, nav tree, data table, +> filters, pagination, forms — extracted from `html-css-foundation/`), and the **plugin host** +> (discovery, router, per-plugin views + static, the `config/menu.ts` override + branding). The +> **Ory integration** (Kratos/Keto/Hydra + Postgres) and **auth** are the roadmap; sections marked +> _(planned)_ are not built yet. ## The MVP — "clone, one command, hack on a plugin" _(planned)_ @@ -162,7 +163,7 @@ Screenshots + an HTML report land in `e2e/artifacts/` (git-ignored). Every user- is covered end-to-end; tests are independent and run **fully in parallel** for speed ([AGENTS.md](AGENTS.md) §6) — keep new tests side-effect-free so the suite stays fast. -## Building a plugin _(planned)_ +## Building a plugin A plugin is a folder under `plugins/`. The host discovers it at boot — no registration step, no central wiring. The full, authoritative API surface — @@ -276,20 +277,18 @@ nav tree from the design foundation (header/leaf × clickable/static, counts, arbitrary depth). Branding (name, logo, default theme) renders in the app shell — the sidebar brand shows the configured logo (else a default mark), and the theme sets the theme-switch default. -## Building blocks _(partly designed, planned to extract)_ +## Building blocks -Plainpages is a **component library, not a page generator** — you assemble pages -from partials and helpers rather than declaring a schema and getting magic. The -vocabulary already exists, fully styled and zero-JS, in `html-css-foundation/`; -the work is extracting it into reusable EJS partials + TS helpers: +Plainpages is a **component library, not a page generator** — you assemble pages from partials +and helpers rather than declaring a schema and getting magic. The vocabulary is extracted from +`html-css-foundation/` into reusable EJS partials + TS helpers, fully styled and zero-JS: - **Partials:** app shell, nav tree, filter bar, data table (sort / select / row actions), pagination, form fields, badges, menus, auth cards. -- **Helpers:** compose nav from config, parse a list-page query - (`?q=…&status=…&sort=…&page=…`) into filter/sort/pagination, pagination math, - guards — `requireSession` (validate the JWT), `can(role)` (read a claim, - in-process), and `check(relation, object)` (a live Keto call, for the rare - fine-grained case). +- **Helpers:** `composeNav` (menu from config), `parseListQuery` + (`?q=…&status=…&sort=…&page=…` → filter/sort/pagination), `paginate` (page math). Auth + guards — `requireSession` (validate the JWT), `can(role)` (read a claim, in-process), + `check(relation, object)` (a live Keto call) — land with §4. ## Interactivity: zero-JS spine, opt-in enhancement diff --git a/src/dashboard.ts b/src/dashboard.ts index 6e99274..1212fe0 100644 --- a/src/dashboard.ts +++ b/src/dashboard.ts @@ -1,8 +1,7 @@ -// Dashboard view model (todo §1): the app-shell "People" list that replaces the placeholder -// index. Pure — turns a request URL into the data the building-block partials render, wiring -// the §1 helpers end-to-end: parseListQuery → filter/sort/paginate the mock dataset → -// composeNav. The dataset stands in for upstream data until plugins/§4 land; everything below -// is real, so the filter form, sortable headers and pager round-trip through the URL (zero-JS). +// Dashboard view model (todo §1): the home "/" 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). import { DEFAULT_MENU, type MenuConfig } from "./menu-config.ts"; import { composeNav, type NavNode, type NavOverride } from "./nav.ts"; diff --git a/src/discovery.ts b/src/discovery.ts index 870b13e..739c573 100644 --- a/src/discovery.ts +++ b/src/discovery.ts @@ -1,9 +1,8 @@ // Plugin discovery (todo §2): scan plugins/, import each folder's plugin.ts default export, -// validate it, assemble the loaded Plugin[]. The imperative shell over the pure rules in -// plugin.ts (isValidPluginId, checkApiVersion, findConflicts). Fails loud: every per-plugin -// problem and every error-level conflict is collected and thrown as one boot-stopping Error; -// warn-level diagnostics (older-minor apiVersion, shared permission token) are logged, load -// continues. The folder name is the id; mount/router wiring is the next §2 item. +// validate it, assemble the loaded Plugin[]. The imperative shell over plugin.ts's pure rules +// (isValidPluginId, checkApiVersion, findConflicts). Fails loud: every per-plugin problem and +// error-level conflict is collected into one boot-stopping Error; warn-level diagnostics +// (older-minor apiVersion, shared permission token) log and load continues. Folder name = id. import { existsSync, readdirSync } from "node:fs"; import { dirname, join } from "node:path"; diff --git a/src/menu-config.ts b/src/menu-config.ts index 051bf4d..4da5a7a 100644 --- a/src/menu-config.ts +++ b/src/menu-config.ts @@ -12,7 +12,7 @@ import type { NavOverride } from "./nav.ts"; export type Theme = "auto" | "dark" | "light"; export interface Branding { - logo?: string; // optional logo asset path/URL (rendered in the shell — next §2 branding item) + logo?: string; // optional logo asset path/URL, rendered in the sidebar brand name: string; // app name shown in the sidebar brand sub?: string; // optional brand subtitle theme?: Theme; // default color theme for the theme-switch diff --git a/src/plugin.ts b/src/plugin.ts index e2461c3..4b1587d 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,12 +1,9 @@ -// The plugin contract (todo §2) — the product's main API surface. This module is the -// authoritative, machine-readable shape; `docs/plugin-contract.md` is the prose reference. -// It only declares types + pure rules; the §2 discovery/router wire them to the filesystem -// and HTTP. Philosophy: a powerful, predictable, overload-friendly API that fails loud at -// boot/discovery rather than sandboxing at runtime. +// The plugin contract (todo §2) — the product's main API surface: the machine-readable types + +// pure rules; `docs/plugin-contract.md` is the prose reference, discovery/router wire it to FS+HTTP. +// Powerful, predictable, fails loud at boot/discovery rather than sandboxing at runtime. // -// A plugin's identity comes from its folder under plugins/: the folder name is the `id` -// (validated by isValidPluginId) and the mount path is `/`. Neither is written in the -// manifest — the host derives them at discovery, so they can't drift or be claimed twice. +// A plugin's identity is its folder under plugins/: folder name = `id` (isValidPluginId), mount = +// `/`. Neither is in the manifest — the host derives them, so they can't drift or be claimed twice. import type { RequestContext } from "./context.ts"; import type { NavNode } from "./nav.ts"; @@ -106,11 +103,9 @@ export interface VersionCheck { message: string; } -// The versioning rule (provider/consumer semver): the host provides a contract version, the -// plugin pins the one it targets. Different major → refuse (breaking either way). Same major, -// plugin minor > host → refuse (needs a newer host). Same major, plugin minor < host → warn -// (additive, still runs — nudge to update). Equal major/minor (patch ignored) → ok. Malformed → -// refuse. Discovery maps refuse→throw, warn→log. +// Provider/consumer semver check (full table in docs/plugin-contract.md): same major+minor → ok, +// plugin minor < host → warn, else (newer minor, major mismatch, malformed) → refuse. Patch is +// ignored. Discovery maps refuse→throw, warn→log. export function checkApiVersion(pluginVersion: unknown, hostVersion: string = HOST_API_VERSION): VersionCheck { const plugin = parseSemver(pluginVersion); const host = parseSemver(hostVersion); diff --git a/src/router.ts b/src/router.ts index 6cef83d..78b4277 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1,9 +1,7 @@ -// Router (todo §2): the pure core that maps an incoming method + pathname to a discovered -// plugin route. I/O-free — app.ts is the imperative shell that builds the context, runs the -// gate, calls the handler, and turns its RouteResult into an HTTP response. A route is mounted -// at `/` + its path (fullPath, shared with conflict detection); `:name` segments become -// path params. Specificity: a literal segment beats a `:param`, so /users/new wins over -// /users/:id regardless of declaration order. +// Router (todo §2): pure core mapping method + pathname → a discovered plugin route. I/O-free; +// app.ts is the shell (build context, gate, call handler, render RouteResult). A route mounts at +// `/` + its path (fullPath, shared with conflict detection); `:name` segments → path params. +// Specificity: a literal segment beats a `:param` (/users/new wins /users/:id), order-independent. import { fullPath, type Plugin, type Route } from "./plugin.ts"; diff --git a/todo.md b/todo.md index 59c855b..eb5352a 100644 --- a/todo.md +++ b/todo.md @@ -53,7 +53,7 @@ everything via Docker. - [x] `config/menu.ts` central override: reorder/rename/hide/group + branding (app name, logo, default theme). → `src/menu-config.ts` (`MenuConfig`/`Branding`/`MenuConfigInput`, `defineMenu()` identity helper, `DEFAULT_MENU`, `loadMenuConfig()`) + the operator file `config/menu.ts`. The override is `composeNav`'s existing `NavOverride` (reorder/rename/group/hide by node id, applied before the per-user filter); branding = `{ name, logo?, sub?, theme? }`. `loadMenuConfig` (imperative shell) dynamically imports `config/menu.ts` if present, validates the authored shape fail-loud (branding field types + `theme` enum, override `hide`/`order` string-arrays / `groups` array / `rename` object), merges branding over defaults; **absent file ⇒ `DEFAULT_MENU`** (clean clone). Wired: `server.ts` loads it at boot → `createApp({ menu })` → `buildDashboardModel(url, roles, menu)` feeds `menu.override` into `composeNav` and `menu.branding` (name/sub) into the shell brand. `config/menu.ts` ships defaults matching prior behaviour (name "Plainpages"/sub "Console", empty override), so a clean clone is unchanged. Added `config` to tsconfig `include` so the authored file is type-checked (Dockerfile `COPY . .` already bakes it). Tests-first: `menu-config.test.ts` (absent⇒defaults / read+merge / malformed⇒throws) + a `dashboard.test.ts` case asserting rename+hide+branding take effect; typecheck (incl. `config/`) + 107 units green; smoke-loaded the real file at boot. **Rendering branding (logo, default theme) into the app shell is the next §2 item.** - [x] Wire branding into the app shell. → Completes the §2 branding chain (name/sub already flowed). `shell.ejs` now renders `brand.logo` as `` when set, else the default `#i-box` brand-mark; the `theme` local (already forwarded to the theme-switch) is now supplied. `buildDashboardModel` puts `menu.branding.logo` into `shell.brand` and `menu.branding.theme` into `shell.theme` (both omitted when unset, so a clean clone is unchanged → brand-mark + auto theme); `views/index.ejs` forwards `theme` to the shell. Added a `.brand-logo` CSS rule (22px, matches `.brand-mark` sizing). Tests-first: `shell.test.ts` (logo replaces the mark + default theme checked; no-logo ⇒ mark + auto) + extended `dashboard.test.ts` (logo→brand, theme→shell.theme) + an `app.test.ts` integration rendering `createApp({ menu })` end-to-end (logo `` + `theme-dark` checked on `/`). Default-app shell rendering is byte-equivalent, so the visual E2E is unaffected; typecheck + 109 units green. The §2 plugin host is feature-complete (remaining §2 items are the project-wide review + comment/test cleanup). - [x] Run the architecture _and_ the stability reviewer agents on the _whole_ project, not just the latest changes, and address their issues. → Ran both on all of `src/`, `views/`, `config/`, Docker/tsconfig. Verdict: architecture sound + disciplined, no crash/security defect in the current path (fail-loud, traversal guards, JWT/cookie defenses all confirmed). **Fixed now:** (1) HIGH — `PluginHooks` was typed+documented but never invoked; wired it (`src/hooks.ts`: `runBootHooks`/`runRequestHooks`/`runResponseHooks`) — `server.ts` runs `onBoot` after discovery before listen, `app.ts` runs `onRequest` (before routing, first non-void short-circuits, renders against its plugin) + `onResponse` (after handler, observer, throw→500); skipped entirely when no plugin declares a hook (hot path free); `hooks.test.ts` + an `app.test.ts` integration. (2) `discovery.ts` `fail` helper retyped `: void`. (3) Documented the template trust boundary in `docs/plugin-contract.md` (raw `html`/`*.html` fields; URL sinks escaped but not scheme-checked) + tightened the Hooks prose to the wired semantics. **Deferred (reviewer-scoped, not §2):** extract a shared `buildShellContext` out of `dashboard.ts` and route the built-in screens through `matchRoute`/`isAuthorized` → §5 (premature at one call site); a `safeUrl()` helper for href sinks → §4 (no untrusted URLs until upstream data flows); doc/type-duplication + non-local `§N` refs → the §2 comment-cleanup item; HEAD-render cost + dev empty-secret fallback → negligible. typecheck + 113 units green; boot smoke-tested. -- [ ] Go over all comments in the code and the README and try to make it shorter and more information dense. Remove not strictly needed stuff. +- [x] Go over all comments in the code and the README and try to make it shorter and more information dense. Remove not strictly needed stuff. → Pass over the §2 accretion (the §0/§1 cleanup at line 21 stands). Tightened the verbose module-header blocks (`plugin.ts`, `discovery.ts`, `router.ts`, `dashboard.ts`) and collapsed the `checkApiVersion` rule comment to a one-liner that points at the contract doc (the if-chain + messages already document it). Removed now-stale forward-refs ("router wiring is the next §2 item", "rendered in the shell — next §2 item"). README: corrected the **Status** note (it undersold — §1 design system + the whole §2 plugin host are built, not just a scaffold), dropped the stale `_(planned)_`/"planned to extract" markers on **Building a plugin** and **Building blocks** (both shipped; auth guards still flagged §4), and named the real helpers. Left the security-rationale comments (jwt/cookie/static/paginate) and the EJS partials' config-doc headers intact — they carry vital info / are the only schema for untyped locals. No anchor links broke; typecheck + 113 units green. - [ ] Go over all tests and combine/unify ones that cover the same stuff or are very related and could be combined in a good way. Remove tests that aren't helping, we only want tests that are actually helpful to us. ## 3. Ory stack — compose + config