Stage 1 PR 7 (ADR 0063). A module becomes a first-class part of the Dashboard. - Manifest v9 = v8 plus optional `navigation`: a Lucide icon from the curated MODULE_NAVIGATION_ICONS set, and up to 12 primary pages (stable id, safe label, kebab-case path relative to the module root; no traversal, schemes, queries, duplicates, or nesting). Validated before activation. - Version 8 stays accepted beside version 9, each read strictly as itself (discriminated union); no released manifest is rewritten. The template moves to v9; the released Signage 0.1.5 manifest is a test fixture. - Real module routes: /<id> is the landing view, /<id>/<page> a declared page, anything beneath a page a module-owned detail route; anything else is not-found and mounts nothing. api/assets/display are never pages. - Sidebar: declared icons, pages nested under an open module, one aria-current, collapsed and drawer behaviour; v8 modules unchanged. - module-ui-sdk: ModuleUiHostContext.navigation (location, subscribe, navigate, link, settings), refusing anything outside the module's routes; undefined on the display host. Mount ABI unchanged. - Return-to-origin: one `?return=` carrier on Settings URLs, validated by parseReturnTarget and against running modules' declared routes; Settings Back returns to exactly the originating module page, across refreshes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
179 lines
11 KiB
Markdown
179 lines
11 KiB
Markdown
# 0061: Settings is one host-owned hierarchy; module lifecycle is Discover, and a module's settings live at `/settings/<module-id>`
|
|
|
|
- Status: Accepted
|
|
- Date: 2026-10-15
|
|
- Module → Settings return link decided by: [0063](0063-module-navigation-contract-manifest-v9.md)
|
|
- Extends: [0027](0027-dashboard-navigation-and-flat-modules.md),
|
|
[0028](0028-module-operational-lifecycle.md),
|
|
[0029](0029-module-configuration-and-secrets.md),
|
|
[0057](0057-dashboard-urls-are-host-owned-routes.md),
|
|
[0058](0058-dashboard-shell-navigation-and-lucide-icons.md),
|
|
[0059](0059-shared-interaction-primitives.md)
|
|
|
|
## Context
|
|
|
|
DESIGN.md makes Settings the one home for shared administration (§9): church-wide configuration in
|
|
Settings, personal configuration under the account area (§9.2), every module's settings reachable
|
|
through Settings rather than an independent system inside the module (§9.3), and people without
|
|
management permission not shown administration they cannot carry out (§9.2). ROADMAP §5.3 asks that
|
|
the navigation and ownership model exist before every category is functional.
|
|
|
|
ADR 0057 reserved `/settings` and served the Dashboard page throughout it, but defined no page there.
|
|
Three things had to be decided before any section could be built: the address of each page, how the
|
|
Settings hierarchy sits in the shell's one sidebar (ADR 0058), and how the browser learns which
|
|
Settings destinations a person may see — it knew only that they were signed in, plus each module's
|
|
own permission hints (ADR 0026).
|
|
|
|
## Decision
|
|
|
|
### The URL model
|
|
|
|
```text
|
|
/settings the Settings landing page
|
|
/settings/general General: how this installation works for everyone
|
|
/settings/branding Branding: the church's name, logo, and colours
|
|
/settings/displays Displays: paired screens
|
|
/settings/discover enters Discover at Browse (respelled in place, below)
|
|
/settings/discover/browse Browse: find and install modules
|
|
/settings/discover/installed Installed: see and manage installed modules
|
|
/settings/discover/updates Updates: module and platform updates
|
|
/settings/system System: version, health, and diagnostics
|
|
/settings/<module-id> that module's settings
|
|
/settings/<module-id>/… reserved for a module's deeper settings pages
|
|
```
|
|
|
|
The distinction is the decision:
|
|
|
|
- **`/settings/discover/…` is module lifecycle** — finding, installing, turning on and off, inspecting,
|
|
updating, and eventually removing modules.
|
|
- **`/settings/<module-id>` is configuring that module.** It is never nested under Discover or under a
|
|
`modules` segment: a module's settings are a first-class Settings destination.
|
|
|
|
**"Modules" is the product's word**, in the interface as in the architecture — never "features",
|
|
"apps", "extensions", or "add-ons". "Discover" is the name of a navigation destination, not a
|
|
replacement for it: inside Discover the pages say "Discover modules", "Installed modules", and "Module
|
|
updates". Implementation terms — Core, a platform module — never appear.
|
|
|
|
**Entering Discover opens Browse, at one address.** `/settings/discover` reads as Browse, whose
|
|
canonical spelling is `/settings/discover/browse`; the shell respells it in place (`replace`, no new
|
|
history entry), exactly as it corrects a trailing slash (ADR 0057). There is one meaningful Browse
|
|
address rather than two, and Discover's own item in the navigation links to it.
|
|
|
|
**`/settings` is a real landing page**, never a redirect to the first section: the two groups the
|
|
person may open, each item named and described in a line. It is an index, not a dashboard.
|
|
|
|
Anything deeper than these addresses is `not-found` in the shell until its page exists. The API already
|
|
serves the page throughout `/settings/*`, and the proxies already forward it (ADR 0057), so deeper pages
|
|
need no server or deployment change.
|
|
|
|
### Module ids cannot collide with a Settings section
|
|
|
|
A module id is a **stable URL identifier**: its page is `/<module-id>` and its settings
|
|
`/settings/<module-id>`. So the platform's own Settings section names join the top-level names in Core's
|
|
one list, `RESERVED_MODULE_IDS` (`core/src/modules/reserved-ids.ts`), now in two documented groups:
|
|
|
|
| Group | Names |
|
|
| --- | --- |
|
|
| Top-level namespaces (`/<name>`) | `account`, `api`, `assets`, `display`, `settings` |
|
|
| Settings sections (`/settings/<name>`) | `branding`, `discover`, `displays`, `general`, `system` |
|
|
|
|
Discovery refuses a module that declares any of them (`MODULE_ID_RESERVED`), at startup, in
|
|
`pnpm modules:validate`, and in package verification. Look-alikes stay valid — `general-info`,
|
|
`discovery`, `display-board`, `systems` — and so does `modules`, since module management is Discover.
|
|
Discover's views live beneath `/settings/discover/`, so they cannot collide with a module and are not
|
|
reserved. `apps/api` tests hold Core's Settings group equal to the shell's `SETTINGS_SECTIONS`, so a
|
|
section cannot be added to one without the other.
|
|
|
|
### Settings sits in the same sidebar
|
|
|
|
Settings is one item in the shell's one navigation (ADR 0058), after the modules and above the account
|
|
area. While a Settings page is open it expands in place into two labelled groups — **Platform**
|
|
(General, Branding, Displays, Discover, System) and **Modules** (each module's settings) — and inside
|
|
Discover, Browse, Installed, and Updates beneath it. The rest of the navigation stays where it is, so
|
|
leaving Settings is one click on any other item; it is not a second application with its own shell.
|
|
The phone drawer shows the identical hierarchy. Exactly one item carries `aria-current="page"`.
|
|
|
|
### Which destinations a person sees is evaluated by the server
|
|
|
|
`GET /api/v1/dashboard/settings` (`apps/api/src/dashboard-settings-routes.ts`), readable by any signed-in
|
|
person (`core.modules.ui.read`, like the navigation beside it), answers which sections and which modules'
|
|
settings this person may see, through Core's one authorization evaluator:
|
|
|
|
| Destination | Shown to |
|
|
| --- | --- |
|
|
| Displays | `core.devices.manage` |
|
|
| Discover | `core.modules.lifecycle.manage` — deciding which modules run |
|
|
| a module's settings | `core.modules.configuration.manage` (ADR 0029), for every module that declares configuration |
|
|
| General, Branding, System | the **platform owner** only, until each defines its own permission |
|
|
|
|
General, Branding, and System have no permission yet because nothing in them exists to authorize.
|
|
Rather than invent three speculative permissions, they are shown only to the platform owner — the one
|
|
principal who already holds every permission — and each gains a delegable permission in the work that
|
|
builds it. Platform updates in Discover → Updates will likewise get their own permission then.
|
|
|
|
A module **has settings** when its manifest declares configuration (ADR 0029). That is the existing,
|
|
authoritative signal; no manifest change is needed. A disabled or blocked module with settings is listed
|
|
with `available: false`.
|
|
|
|
These are **presentation hints, not authority**, as ADR 0026's are. Every route behind a section keeps
|
|
its own enforcement, unchanged, and a test proves the data behind each is still refused without its
|
|
permission. An evaluation failure is a `503`, never an empty answer that would look like a permissions
|
|
problem. The browser decides nothing about permissions itself.
|
|
|
|
### What each Settings address shows
|
|
|
|
- A section or module the person may see shows its page.
|
|
- A section the person may not see, or a module they know of but cannot configure (or that has no
|
|
settings), shows *These settings aren't available* — without saying which.
|
|
- A disabled module's settings show *This module is turned off*. Never a silent redirect.
|
|
- A name that is no module the person knows of is *Page not found*.
|
|
- When the Settings view cannot be loaded, the Settings link is withheld and its pages say so, with
|
|
Retry; the rest of the Dashboard keeps working.
|
|
- When a person can see nothing in Settings, the Settings link is not shown at all.
|
|
|
|
### Pages are honest placeholders
|
|
|
|
Every section, Discover view, and module's settings page in this decision is a placeholder: Back to
|
|
Settings (`PageHeader`, ADR 0059), a title and one-line description, and *Not available yet* with what
|
|
will happen there. No fake controls, no dead fields. The work that builds each replaces its placeholder.
|
|
|
|
Back goes to the page's parent, Settings. A validated return target (ADR 0059) takes precedence once
|
|
something carries one, so `/signage` → `/settings/signage` can return to `/signage`; how a module links
|
|
into its settings with a return target is the module navigation contract's to define.
|
|
|
|
## Alternatives
|
|
|
|
- **`/settings/modules` for module management, and `/settings/modules/<module-id>` for a module's
|
|
settings.** Rejected: it nests a module's configuration under its lifecycle and makes "configure
|
|
Signage" two levels deep. Discover is lifecycle; `/settings/<module-id>` is configuration.
|
|
- **`/settings/discover` as a page of its own beside `/settings/discover/browse`.** Rejected: two
|
|
meaningful addresses for one view.
|
|
- **Redirecting `/settings` to the first section.** Rejected: a landing page says what Settings holds,
|
|
and the first section differs per person.
|
|
- **Replacing the module navigation with the Settings hierarchy while in Settings.** Rejected: DESIGN.md
|
|
keeps one sidebar with Settings in it (§4.2), and leaving Settings should not require a second
|
|
navigation model.
|
|
- **Probing each section's API for a `403` from the browser.** Rejected: several requests per page view,
|
|
and it confuses "may not" with "failed".
|
|
- **Defining General, Branding, and System permissions now.** Rejected as speculative; the owner-only
|
|
interim rule is deny-by-default with the owner's existing bypass.
|
|
- **A manifest field declaring "has settings".** Unnecessary: declared configuration already says so;
|
|
module-owned settings pages arrive with the module navigation contract.
|
|
- **Reserving Discover's view names.** Unnecessary: they live under `/settings/discover/`.
|
|
|
|
## Consequences
|
|
|
|
- Every later Settings section has an address, a navigation entry, a permission rule, and a placeholder
|
|
to replace.
|
|
- A module may not be called `branding`, `discover`, `displays`, `general`, or `system`. No known module
|
|
is; one that was would fail discovery and must be renamed.
|
|
- The shell asks one more endpoint at bootstrap, alongside the navigation.
|
|
- A person without any administrative permission sees no Settings link.
|
|
|
|
## Deferred
|
|
|
|
The real General, Branding, Displays, Discover (catalog, installation, lifecycle, updates), and System
|
|
pages, and each one's permission; module settings screens and their deeper pages; how a module links to
|
|
its settings with a validated return target (the module navigation contract, manifest v9); moving the
|
|
transitional device panel into Settings → Displays; and the account area.
|