feat(shell): Settings hierarchy, Discover, and /settings/<module-id> (Stage 1, ADR 0061) #108

Merged
Zayin_Admin merged 1 commits from feat/settings-shell into main 2026-10-15 03:43:00 +00:00
Member

Stage 1 PR 5: the Settings shell and routing hierarchy, including the corrected module-management model (Discover). Decisions are recorded in ADR 0061.

This PR covers routing, navigation, permission-aware visibility, and honest placeholders only. No section's real controls are built: no Discover catalog, installation, lifecycle or updates; no Displays, Branding or System pages; no module configuration screens.

Settings hierarchy

/settings                         Settings landing page (a real page, not a redirect)
/settings/general                 General
/settings/branding                Branding
/settings/displays                Displays
/settings/discover                → respelled in place to Browse (one canonical address)
/settings/discover/browse         Discover modules
/settings/discover/installed      Installed modules
/settings/discover/updates        Module updates
/settings/system                  System
/settings/<module-id>             that module's settings, e.g. /settings/signage
/settings/<module-id>/…           reserved for deeper module settings (not found until built)
  • /settings/discover/… is module lifecycle. /settings/<module-id> is configuring that module. There is no /settings/modules, and no /settings/modules/<id>.
  • "Modules" stays the user-facing word. "Discover" is a navigation destination; its pages say "Discover modules", "Installed modules", and "Module updates".

Navigation

  • One Settings item in the same sidebar, after the modules and above the account area.
  • While you are in Settings, it expands in place into Platform (General, Branding, Displays, Discover, System) and Modules (each module's settings). Discover shows Browse, Installed and Updates beneath it.
  • The rest of the navigation stays, so leaving Settings is one click.
  • The phone drawer has the identical hierarchy. Exactly one item carries aria-current="page".

Reserved ids

Core's one reserved list (core/src/modules/reserved-ids.ts) now has two documented groups:

Group Names
Top-level (/<name>) account, api, assets, display, settings
Settings sections (/settings/<name>) branding, discover, displays, general, system
  • Discovery refuses these ids (MODULE_ID_RESERVED).
  • modules is an ordinary id again.
  • Look-alikes are accepted: general-info, discovery, display-board, systems.
  • Discover's views live under /settings/discover/, so they aren't reserved.
  • apps/api tests hold Core's Settings group equal to the shell's SETTINGS_SECTIONS.

Permission behaviour

What the shell knew before this PR: signed-in plus an opaque user id, and each module's own permission hints. Nothing about platform permissions.

A new GET /api/v1/dashboard/settings evaluates, through Core's one evaluator, which destinations the viewer may see. Reading it requires core.modules.ui.read, like the navigation.

Destination Shown to
Displays core.devices.manage
Discover core.modules.lifecycle.manage
A module's settings core.modules.configuration.manage, for each module whose manifest declares configuration (ADR 0029). Disabled modules are flagged unavailable.
General, Branding, System platform owner only, until each has its own permission
  • ⚠️ Interim rule for review: General, Branding and System have no permission yet, because nothing in them exists. Rather than invent three speculative permissions, they're shown only to the platform owner, who already holds every permission. Each gains a delegable permission when it's built.
  • These are presentation hints, not authority. A test proves the data behind each section is still 403 without its permission.
  • An evaluation failure is a 503, never an empty answer.
  • With nothing visible, there's no Settings link.
  • If the Settings view fails to load, the link is withheld and Settings pages say so, with Retry. The rest of the Dashboard keeps working.
  • Has settings? Use the manifest's existing configuration declaration (Signage declares 10 keys). No manifest v9.

What each address shows

  • A destination the viewer may see shows its page.
  • A section or module the viewer can't see shows These settings aren't available, without saying why.
  • A disabled module shows This module is turned off. It does not redirect.
  • An unknown name shows Page not found.

Every page is a placeholder: Back to Settings (PageHeader), title, description, and Not available yet. There are no fake fields.

Copy note

You suggested "Modules add capabilities to Ekklesia…". Hard-coding the product name conflicts with DESIGN.md locked decision 1 and ADR 0046, so the Browse page says "Modules add capabilities to your church's Dashboard. Install and manage the modules your church uses."

Public API / package changes

None. Core, the API, and the shell are private, and no @ekklesia/* public package changed. The only module-visible effect is the five newly reserved ids.

Tests

  • Core: reserved groups, all ten names refused, look-alikes (including modules) accepted.
  • API (dashboard-settings-routes.test.ts, new):
    • nothing visible without permissions;
    • anonymous gets 403;
    • the owner sees every section and module;
    • Displays, Discover and module settings are each gated by exactly their permission, with disabled modules flagged;
    • General, Branding and System are owner-only;
    • evaluation failure gives 503;
    • data routes are still 403 regardless of hints.
  • API (dashboard-host-routes.test.ts): reserved-name parity.
  • Shell routes: landing page, each section, Discover views plus canonical entry, /settings/<id> (including /settings/modules as an ordinary id), deeper paths not found, canonical spelling.
  • Shell protocol: settings fetched alongside the navigation; failure tolerated; strict parsing.
  • Shell settings-navigation.test.tsx (new, 17 tests):
    • the landing page;
    • each section with Back and no fake controls;
    • Discover canonical entry with no extra history entry, and the three views;
    • no-access state;
    • module settings in all four states;
    • nav expanding only inside Settings, and leaving it;
    • permission-filtered nav;
    • no link with nothing visible;
    • load failure with Retry;
    • Back/Forward and refresh;
    • focus moving between Settings pages;
    • the phone drawer: same hierarchy, 48px targets, closes on choice;
    • page titles.
  • Existing tests updated for the extra bootstrap request and for /settings now being a page.
  • Deploy smoke (real Chromium, split topology):
    • deep link to /settings;
    • choose Displays, then refresh;
    • enter Discover and land at /settings/discover/browse;
    • open /settings/<module>;
    • phone drawer, choose System;
    • widen, then return to the Overview with the Settings sub-navigation gone.

Results (on the pushed commit)

  • TURBO_FORCE=true pnpm verify (no Turbo cache, see #105): passed. shell 337, api 532, core 1161, root 165, every package.
  • pnpm infrastructure:config and pnpm deploy:config: passed.
  • pnpm test:migrations:integration: passed, 401 tests.
  • scripts/smoke-deploy.sh, both same-origin and split: passed. The split run includes the new Settings browser steps in all five browser passes.
  • Not run: test:package-release and test:package-publication. No public package changed; CI runs them regardless.

Visual check

Headless Chromium, light and dark, 1280×860:

  • Settings landing page
  • Discover → Browse, with nested views and the active state
  • module settings

Also checked at 390×820 in dark: the phone drawer on Settings → Displays.

Deferred

  • The real General, Branding, Displays, Discover (catalog, install, lifecycle, updates) and System pages, each with its own permission.
  • Platform-update permission.
  • Module settings screens and deeper pages.
  • A module linking to its settings with a validated return target (module navigation contract / manifest v9). Back currently goes to Settings, though the validator already accepts /signage-style targets.
  • Moving the transitional device panel into Settings → Displays.
  • The account area.

DESIGN.md is unchanged. §9.3's conceptual path "Settings → Modules → Signage → Settings" now reads as "Settings → Modules group → Signage". You may want to update that wording yourself.

🤖 Generated with Claude Code

Stage 1 PR 5: **the Settings shell and routing hierarchy**, including the corrected module-management model (**Discover**). Decisions are recorded in [ADR 0061](docs/architecture/decisions/0061-settings-hierarchy-and-module-settings-routes.md). This PR covers routing, navigation, permission-aware visibility, and honest placeholders only. No section's real controls are built: no Discover catalog, installation, lifecycle or updates; no Displays, Branding or System pages; no module configuration screens. ## Settings hierarchy ```text /settings Settings landing page (a real page, not a redirect) /settings/general General /settings/branding Branding /settings/displays Displays /settings/discover → respelled in place to Browse (one canonical address) /settings/discover/browse Discover modules /settings/discover/installed Installed modules /settings/discover/updates Module updates /settings/system System /settings/<module-id> that module's settings, e.g. /settings/signage /settings/<module-id>/… reserved for deeper module settings (not found until built) ``` - `/settings/discover/…` is **module lifecycle**. `/settings/<module-id>` is **configuring that module**. There is no `/settings/modules`, and no `/settings/modules/<id>`. - "Modules" stays the user-facing word. "Discover" is a navigation destination; its pages say "Discover modules", "Installed modules", and "Module updates". **Navigation** - One **Settings** item in the same sidebar, after the modules and above the account area. - While you are in Settings, it expands in place into **Platform** (General, Branding, Displays, Discover, System) and **Modules** (each module's settings). Discover shows Browse, Installed and Updates beneath it. - The rest of the navigation stays, so leaving Settings is one click. - The phone drawer has the identical hierarchy. Exactly one item carries `aria-current="page"`. ## Reserved ids Core's one reserved list (`core/src/modules/reserved-ids.ts`) now has two documented groups: | Group | Names | | --- | --- | | Top-level (`/<name>`) | `account`, `api`, `assets`, `display`, `settings` | | Settings sections (`/settings/<name>`) | `branding`, `discover`, `displays`, `general`, `system` | - Discovery refuses these ids (`MODULE_ID_RESERVED`). - `modules` is an ordinary id again. - Look-alikes are accepted: `general-info`, `discovery`, `display-board`, `systems`. - Discover's views live under `/settings/discover/`, so they aren't reserved. - `apps/api` tests hold Core's Settings group equal to the shell's `SETTINGS_SECTIONS`. ## Permission behaviour What the shell knew before this PR: signed-in plus an opaque user id, and each module's own permission hints. Nothing about platform permissions. A new **`GET /api/v1/dashboard/settings`** evaluates, through Core's one evaluator, which destinations the viewer may see. Reading it requires `core.modules.ui.read`, like the navigation. | Destination | Shown to | | --- | --- | | Displays | `core.devices.manage` | | Discover | `core.modules.lifecycle.manage` | | A module's settings | `core.modules.configuration.manage`, for each module whose manifest declares `configuration` (ADR 0029). Disabled modules are flagged unavailable. | | General, Branding, System | **platform owner only**, until each has its own permission | - **⚠️ Interim rule for review:** General, Branding and System have no permission yet, because nothing in them exists. Rather than invent three speculative permissions, they're shown only to the platform owner, who already holds every permission. Each gains a delegable permission when it's built. - These are **presentation hints, not authority**. A test proves the data behind each section is still `403` without its permission. - An evaluation failure is a `503`, never an empty answer. - With nothing visible, there's **no Settings link**. - If the Settings view fails to load, the link is withheld and Settings pages say so, with Retry. The rest of the Dashboard keeps working. - **Has settings?** Use the manifest's existing `configuration` declaration (Signage declares 10 keys). No manifest v9. ## What each address shows - A destination the viewer may see shows its page. - A section or module the viewer can't see shows *These settings aren't available*, without saying why. - A disabled module shows *This module is turned off*. It does not redirect. - An unknown name shows *Page not found*. Every page is a placeholder: Back to Settings (`PageHeader`), title, description, and *Not available yet*. There are no fake fields. ## Copy note You suggested "Modules add capabilities to **Ekklesia**…". Hard-coding the product name conflicts with DESIGN.md locked decision 1 and ADR 0046, so the Browse page says "Modules add capabilities to your church's Dashboard. Install and manage the modules your church uses." ## Public API / package changes **None.** Core, the API, and the shell are private, and no `@ekklesia/*` public package changed. The only module-visible effect is the five newly reserved ids. ## Tests - **Core:** reserved groups, all ten names refused, look-alikes (including `modules`) accepted. - **API (`dashboard-settings-routes.test.ts`, new):** - nothing visible without permissions; - anonymous gets `403`; - the owner sees every section and module; - Displays, Discover and module settings are each gated by exactly their permission, with disabled modules flagged; - General, Branding and System are owner-only; - evaluation failure gives `503`; - data routes are still `403` regardless of hints. - **API (`dashboard-host-routes.test.ts`):** reserved-name parity. - **Shell routes:** landing page, each section, Discover views plus canonical entry, `/settings/<id>` (including `/settings/modules` as an ordinary id), deeper paths not found, canonical spelling. - **Shell protocol:** settings fetched alongside the navigation; failure tolerated; strict parsing. - **Shell `settings-navigation.test.tsx` (new, 17 tests):** - the landing page; - each section with Back and no fake controls; - Discover canonical entry with no extra history entry, and the three views; - no-access state; - module settings in all four states; - nav expanding only inside Settings, and leaving it; - permission-filtered nav; - no link with nothing visible; - load failure with Retry; - Back/Forward and refresh; - focus moving between Settings pages; - the phone drawer: same hierarchy, 48px targets, closes on choice; - page titles. - **Existing tests** updated for the extra bootstrap request and for `/settings` now being a page. - **Deploy smoke (real Chromium, split topology):** - deep link to `/settings`; - choose Displays, then refresh; - enter Discover and land at `/settings/discover/browse`; - open `/settings/<module>`; - phone drawer, choose System; - widen, then return to the Overview with the Settings sub-navigation gone. ### Results (on the pushed commit) - `TURBO_FORCE=true pnpm verify` (no Turbo cache, see #105): passed. shell 337, api 532, core 1161, root 165, every package. - `pnpm infrastructure:config` and `pnpm deploy:config`: passed. - `pnpm test:migrations:integration`: passed, 401 tests. - `scripts/smoke-deploy.sh`, both same-origin and `split`: passed. The split run includes the new Settings browser steps in all five browser passes. - Not run: `test:package-release` and `test:package-publication`. No public package changed; CI runs them regardless. ### Visual check Headless Chromium, light and dark, 1280×860: - Settings landing page - Discover → Browse, with nested views and the active state - module settings Also checked at 390×820 in dark: the phone drawer on Settings → Displays. ## Deferred - The real General, Branding, Displays, Discover (catalog, install, lifecycle, updates) and System pages, each with its own permission. - Platform-update permission. - Module settings screens and deeper pages. - A module linking to its settings with a validated return target (module navigation contract / manifest v9). Back currently goes to Settings, though the validator already accepts `/signage`-style targets. - Moving the transitional device panel into Settings → Displays. - The account area. `DESIGN.md` is unchanged. §9.3's conceptual path "Settings → Modules → Signage → Settings" now reads as "Settings → Modules group → Signage". You may want to update that wording yourself. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
ekklesia-bot added 1 commit 2026-10-15 03:32:06 +00:00
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
89ef4b5ff6
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]>
Zayin_Admin merged commit 8571e6854c into main 2026-10-15 03:43:00 +00:00
Zayin_Admin deleted branch feat/settings-shell 2026-10-15 03:43:00 +00:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Zayin/Ekklesia#108