feat(shell): Settings hierarchy, Discover, and /settings/<module-id>
CI / verify (pull_request) Successful in 1m12s
CI / infrastructure-smoke (pull_request) Successful in 37s
CI / package-release (pull_request) Successful in 51s
CI / migration-smoke (pull_request) Successful in 2m29s
CI / image-build (pull_request) Successful in 1m4s
CI / deploy-smoke (same-origin) (pull_request) Successful in 54s
CI / package-publication (pull_request) Successful in 58s
CI / deploy-smoke (split) (pull_request) Successful in 2m56s
CI / verify (pull_request) Successful in 1m12s
CI / infrastructure-smoke (pull_request) Successful in 37s
CI / package-release (pull_request) Successful in 51s
CI / migration-smoke (pull_request) Successful in 2m29s
CI / image-build (pull_request) Successful in 1m4s
CI / deploy-smoke (same-origin) (pull_request) Successful in 54s
CI / package-publication (pull_request) Successful in 58s
CI / deploy-smoke (split) (pull_request) Successful in 2m56s
The Settings foundation (ADR 0061):
- URL model. /settings is a real landing page. The platform sections
are /settings/general, /branding, /displays, and /system. Module
lifecycle is Discover: /settings/discover/browse, /installed, and
/updates. /settings/discover is respelled in place to Browse's one
canonical address. A module's settings are /settings/<module-id>,
never nested under Discover. Deeper Settings addresses are
reserved. "Modules" stays the user-facing word: "Discover modules",
"Installed modules", "Module updates".
- Collisions. Core's one reserved list gains the Settings section
names (branding, discover, displays, general, system) as a second
documented group, so no module id can shadow a section; `modules` is
an ordinary id. apps/api tests hold it equal to the shell's sections.
- Navigation. One Settings item in the same sidebar expands in place
into Platform and Modules (and Discover's three views) while in
Settings, and the phone drawer has the same hierarchy. The rest of
the navigation stays.
- Permissions. GET /api/v1/dashboard/settings evaluates, through
Core's evaluator, which destinations the viewer may see:
- Displays: core.devices.manage
- Discover: core.modules.lifecycle.manage
- a module's settings: core.modules.configuration.manage, for each
module that declares configuration (ADR 0029)
- General, Branding, System: the platform owner, until each has its
own permission
These are presentation hints; every data route keeps its own
enforcement. With nothing visible, there is no Settings link.
- Pages are honest placeholders with Back to Settings. A disabled
module's settings say it is turned off; a section or module the
person cannot see says so without detail; an unknown name is not
found; a failed load withholds the link and offers Retry.
- The deploy smoke opens Settings by deep link, refreshes a section,
enters Discover, opens the module's settings, uses the phone
drawer, and returns to the Dashboard.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
34692ac05f
commit
89ef4b5ff6
@@ -221,6 +221,10 @@ verification and reset links still land on `/` with a query (ADR 0038).
|
||||
|
||||
## Deferred
|
||||
|
||||
> **Cross-reference — see
|
||||
> [ADR 0061](0061-settings-hierarchy-and-module-settings-routes.md).** The Settings pages and their
|
||||
> addresses have since been defined; the rest of this list is unchanged.
|
||||
|
||||
Each of these needs its own decision. This ADR reserves the addresses for them and decides none of
|
||||
them:
|
||||
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
# 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
|
||||
- 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.
|
||||
@@ -69,3 +69,4 @@ ADRs capture durable choices that affect more than one package or phase.
|
||||
- [0058: The Dashboard shell's navigation, and Lucide as Ekklesia's icon vocabulary](0058-dashboard-shell-navigation-and-lucide-icons.md) — Accepted
|
||||
- [0059: Shared interaction primitives — confirmation, toasts, errors, loading, Back, and no native dialogs](0059-shared-interaction-primitives.md) — Accepted
|
||||
- [0060: Light, Dark, and System appearance; reduced motion; and one radius scale](0060-light-dark-system-theme-and-motion.md) — Accepted
|
||||
- [0061: Settings is one host-owned hierarchy; module lifecycle is Discover, and a module's settings live at `/settings/<module-id>`](0061-settings-hierarchy-and-module-settings-routes.md) — Accepted
|
||||
|
||||
Reference in New Issue
Block a user