§10 gate the dashboard + make "/" replaceable by a plugin (todo §10); "/" is now gated to a signed-in session (anonymous → /login via loginRedirect, query preserved as return_to) and fully replaceable via a new optional home?: RouteHandler on PluginManifest — a handler with the same signature as any route (the most ergonomic shape). The app.ts "/" branch gates first, then renders the single home plugin's handler against its own views/ with the native shell via ctx.chrome (HEAD / void-return / response-hook parity with a plugin route), else the built-in mock-data People list. home mounts at the root above the /<id> namespace, so it can't shadow or be shadowed by a built-in route. Single-slot + loud: findConflicts errors on >1 home (new "home" kind), discovery rejects a non-function home — never last-write-wins. Tests-first (338 → 344 units): app.test.ts gate + home-override; plugin.test.ts home conflict; discovery.test.ts home validation. Docs: plugin-contract.md (manifest table + "The dashboard (home)" section + conflict row), README. E2E: visual.spec plants a dev-signed session (the anonymous plugin-gate probe uses the cookie-free request fixture); all e2e web/gateway healthchecks repointed from the gated "/" to /public/css/styles.css. stability-reviewer: APPROVE, no Critical/High/Medium. typecheck + 344 units + visual(9) + full-flow(7) E2E green.
This commit is contained in:
@@ -98,6 +98,7 @@ there is **no `id` or `basePath`** in the manifest — both come from the folder
|
||||
| Field | Required | Notes |
|
||||
| --- | --- | --- |
|
||||
| `apiVersion` | yes | Semver the plugin was built against — a **literal**, not `HOST_API_VERSION`. See [Versioning](#contract-versioning). |
|
||||
| `home` | no | A `RouteHandler` that owns the dashboard `/` (the post-login landing page). At most one plugin may declare it. See [The dashboard](#the-dashboard-home). |
|
||||
| `nav` | no | `NavNode[]` fragment (same shape `composeNav` consumes). `icon` is a Lucide sprite id (`src/icons.ts`); node `id`s must be globally unique. |
|
||||
| `permissions` | no | Tokens this plugin introduces; declared for docs, conflict detection, and bootstrap seeding (see [Nav & permissions](#nav--permissions)). |
|
||||
| `routes` | no | See [Routes & handlers](#routes--handlers). |
|
||||
@@ -175,6 +176,33 @@ safety of the data it renders**:
|
||||
return { view: "list", data: { rows: rows.map((r) => ({ ...r, href: safeUrl(r.href) })) } };
|
||||
```
|
||||
|
||||
## The dashboard ("home")
|
||||
|
||||
`/` is the **post-login landing page**. The host gates it to a **signed-in session** (an anonymous
|
||||
visitor is redirected to `/login`) and, by default, renders a built-in mock-data dashboard. A plugin
|
||||
**fully replaces** it by exporting a `home` handler:
|
||||
|
||||
```ts
|
||||
import { definePlugin } from "../../src/plugin-api.ts";
|
||||
import { dashboard } from "./dashboard.ts";
|
||||
|
||||
export default definePlugin({
|
||||
apiVersion: "1.0.0",
|
||||
home: dashboard, // owns "/" — the post-login landing page
|
||||
});
|
||||
```
|
||||
|
||||
`home` is a `RouteHandler` like any route's — it receives the [`RequestContext`](#requestcontext)
|
||||
and returns a `RouteResult`, typically a `view` rendered from the plugin's own `views/` against the
|
||||
native app shell (`ctx.chrome`), exactly as a route handler does. The host enforces the session gate
|
||||
first, so `ctx.user` is non-null; branch on `ctx.roles` *inside* to tailor the page per role. Don't
|
||||
gate `home` itself behind a single permission — there's no second dashboard to fall back to, so a
|
||||
user lacking it would land on a 403 instead of a home page. (`GET /` also answers `HEAD`.)
|
||||
|
||||
Only **one** plugin may own the dashboard: two declaring `home` is a boot-stopping conflict
|
||||
([below](#conflict-rules)), never last-write-wins. The plugin needs no `routes` entry for `/` — the
|
||||
host mounts `home` at the root, above the `/<id>` route namespace.
|
||||
|
||||
## RequestContext
|
||||
|
||||
Every handler receives one argument, the `RequestContext` (`src/context.ts`), built once per
|
||||
@@ -278,6 +306,7 @@ with `findConflicts` and resolves them **loudly — never last-write-wins**. `er
|
||||
| `id` | error | Two plugins share an `id` (folder name). Ids must be globally unique — they namespace the mount path, views/static, and the override target. |
|
||||
| `route` | error | Two routes resolve to the same `method` + full path. Cross-plugin routes can't collide (the `/<id>` prefix is unique), so this catches a plugin duplicating one of its own. |
|
||||
| `nav-id` | error | A nav node `id` is used more than once — the central override targets ids, so they must be unique. |
|
||||
| `home` | error | More than one plugin declares `home`. The dashboard `/` is a single slot, so only one may own it ([The dashboard](#the-dashboard-home)). |
|
||||
| `permission` | warn | A permission token is declared by more than one plugin. Sharing is legitimate (shared role); namespace as `<id>:<action>` if unintended. |
|
||||
|
||||
There is **no separate `basePath` rule**: the mount path is the derived `/<id>`, so its
|
||||
|
||||
Reference in New Issue
Block a user