Fix the Stage 1 Dashboard UX defects found in assembled review
CI / verify (pull_request) Successful in 1m13s
CI / infrastructure-smoke (pull_request) Successful in 34s
CI / package-release (pull_request) Successful in 48s
CI / migration-smoke (pull_request) Successful in 2m22s
CI / image-build (pull_request) Successful in 1m8s
CI / deploy-smoke (same-origin) (pull_request) Successful in 49s
CI / package-publication (pull_request) Successful in 57s
CI / deploy-smoke (split) (pull_request) Successful in 3m38s

The assembled Stage 1 Dashboard was reviewed by hand on a desktop, with
the sidebar collapsed, and on an iPhone. This fixes what that review
found, and records the decisions in ADR 0064:

- Discover is its own page at /settings/discover, with Installed and
  Updates beneath it as children. There is no Browse item any more.
  /settings/discover/browse is still read, as an alias, and is respelled
  in place. Parents and children now read as a hierarchy: a guide line,
  text-only children, a disclosure chevron, and parent and current
  states that look different.
- The sidebar is navigation only. Display pairing moves from the sidebar
  and drawer to Settings -> Displays: the paired displays, a deliberate
  Add display dialog, and revocation. Nothing takes focus on arrival;
  the code field is focused only once Add display is chosen.
- Collapsed icons are centred. Nav-link layout moves from inline styles
  to the shell stylesheet, and every collapsed item is one square
  centred on the rail.
- /settings is an overview of the installation: displays paired,
  modules installed and running, and a line for each other area. It
  reads only existing endpoints.
- Phones get a sticky, safe-area-aware header holding the menu trigger
  and the page name. New pages start at the top, and focus no longer
  scrolls content under the header.
- @ekklesia/ui draws checkbox and radio indicators from tokens rather
  than accent-color, so the selected state is the strong accent in
  Light and Dark alike.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ekklesia
2026-10-15 20:37:32 -05:00
co-authored by Claude Opus 5.5
parent 186668915b
commit 405e509bcb
31 changed files with 2346 additions and 611 deletions
@@ -3,6 +3,7 @@
- Status: Accepted
- Date: 2026-10-14
- Module-declared icons and pages decided by: [0063](0063-module-navigation-contract-manifest-v9.md)
- Device panel placement and the phone menu placement superseded by: [0064](0064-stage-1-shell-review-corrections.md)
- Extends: [0026](0026-minimal-ekklesia-module-ui-host.md),
[0027](0027-dashboard-navigation-and-flat-modules.md),
[0039](0039-shared-design-tokens-and-elements.md),
@@ -3,6 +3,7 @@
- Status: Accepted
- Date: 2026-10-15
- Module → Settings return link decided by: [0063](0063-module-navigation-contract-manifest-v9.md)
- Discover's address, the landing page, and the device panel's move decided by: [0064](0064-stage-1-shell-review-corrections.md)
- Extends: [0027](0027-dashboard-navigation-and-flat-modules.md),
[0028](0028-module-operational-lifecycle.md),
[0029](0029-module-configuration-and-secrets.md),
@@ -0,0 +1,241 @@
# 0064: Discover is its own page, the sidebar is navigation only, Settings opens on an overview, and phones get a sticky header
- Status: Accepted
- Date: 2026-10-15
- Supersedes, in part: [0058](0058-dashboard-shell-navigation-and-lucide-icons.md) (the device panel
in the sidebar and drawer; "no application top bar" on phones),
[0061](0061-settings-hierarchy-and-module-settings-routes.md) (Discover entered at
`/settings/discover/browse`; the landing page as an index)
- Extends: [0036](0036-managed-device-identity-and-pairing.md),
[0039](0039-shared-design-tokens-and-elements.md),
[0057](0057-dashboard-urls-are-host-owned-routes.md),
[0060](0060-light-dark-system-theme-and-motion.md),
[0063](0063-module-navigation-contract-manifest-v9.md)
## Context
The assembled Stage 1 Dashboard was reviewed by hand on a real desktop, with the sidebar expanded and
collapsed, and on a real iPhone in Safari. The review found defects that no unit test had caught,
because each only shows in the assembled product:
- **Discover and Browse were the same thing twice.** ADR 0061 made Discover a navigation item that
opened Browse, at `/settings/discover/browse`, with Browse, Installed, and Updates listed beneath it.
In use, "Discover" and "Browse" named one page, and the children looked as if they had simply
appeared, with no relationship to Discover.
- **The sidebar still held the Stage 0 device panel.** It showed a *Display devices* heading, a pairing
code field, and the list of paired screens beneath the navigation. A form in the navigation is not
navigation. The field also held focus as the page loaded on the review device, although nobody had
asked to pair anything.
- **Collapsed icons sat off centre.** `DashboardLink` set `justify-content: flex-start` inline. An
inline style outranks the stylesheet, so the collapsed rule's `justify-content: center` never
applied, and every glyph sat at the left of its 47px box, with 1px on its left and 26px on its
right. The toggle above the items had its own, different geometry.
- **The Settings landing page repeated the sidebar.** ADR 0061 called it "an index, not a dashboard":
each section was named and described in a line. It listed exactly what the sidebar beside it already
listed.
- **On the phone the menu scrolled away.** ADR 0058 put the menu button above the page as "the one
control there, not a bar". It scrolled out of reach with the page. It also did not respect the
status bar or notch. A page opened from the drawer could start part-way down, because focusing
`<main>` scrolled it to the very top of the viewport.
- **The light theme's selected radio looked wrong.** Radios were native, tinted with
`accent-color: accent.base`. Given an accent, each browser picks its own contrasting mark, and on
the light theme's pale teal (`#4fb3a8`) that mark was a near-black dot and ring. The shared
`Checkbox` in `@ekklesia/ui` used the same technique.
ROADMAP §5.3 and DESIGN.md §9.4 still name `/settings/discover/browse` as canonical. This decision
reverses that at the maintainer's direction after the review, and the product documents need the
same correction.
## Decision
### Discover is its own page, at `/settings/discover`
`/settings/discover` is where modules are found and installed. It is Discover's own page, titled
*Discover modules*, not the parent of a separate Browse page. Its children, beneath it, are:
```text
/settings/discover Discover: find and install modules
/settings/discover/installed Installed: see and manage installed modules
/settings/discover/updates Updates: module and platform updates
```
- **No Browse item.** The navigation names Installed and Updates beneath Discover, and nothing else.
- **The old address is an alias, never a link.** `/settings/discover/browse` still opens Discover's
page, so a bookmark from before this decision keeps working. The shell then respells it in place to
`/settings/discover` (`replace`, keeping any `?return=`), exactly as it corrects a trailing slash
(ADR 0057). Nothing links to it.
- **Internal name.** The route keeps `view: "browse"` as the internal name of Discover's own page.
Only its address changes. The API already serves the Dashboard throughout `/settings/*`, so no server
or proxy change is needed.
### Parents and children read as a hierarchy
The same treatment applies to every parent in the navigation: Discover with Installed and Updates,
Settings with its sections, and a module with its declared pages (ADR 0063).
- **Children are text only**, marked `data-nav-level="child"`. On desktop they use the small control
height and regular weight. In the drawer they keep the large touch target.
- **They hang from a guide line** that runs down from beneath the parent's icon. Their text starts past
the parent's label.
- **Exactly one item is current** (`aria-current="page"`, selected fill, and weight). The parent of the
open child is marked `data-nav-ancestor`: primary text, bold, and the strong accent on its icon, but
no fill. A parent and its child are therefore never both "selected", and the relationship reads at a
glance.
- **An item that opens in place carries a disclosure chevron.** It points right while closed and down
while open, and it rotates over `motion.duration-short`, which is zero when motion is reduced
(ADR 0060). It is decorative: the nested list, named by its parent, carries the structure for
assistive technology.
- **Collapsed, the rail shows parents only.** Discover is current for every Discover page, and its
children, which need the labels the rail has no room for, are not drawn. A module's pages were
already hidden in the rail.
### The sidebar is navigation only; displays are paired in Settings → Displays
The device panel leaves the sidebar and the drawer entirely. Its working functionality moves, rather
than being rebuilt, to **Settings → Displays** (`/settings/displays`). This is the move ADR 0061
deferred, and the first platform Settings page that does real work. The page has:
- a page header with **Add display**;
- **Paired displays:** one card per screen, with its name, a *Paired* or *Revoked* badge, when it was
paired, when it was last seen, and **Revoke**;
- an **Add display** dialog: the code shown on the screen, then **Find display**; then the display's
one name, then **Pair display**;
- the existing in-product confirmation before revoking;
- empty, loading, failed (with Retry), and "not available to you" states.
It uses the same platform-owned display identity, the same endpoints (`GET /api/v1/devices`,
`POST /api/v1/device-pairing/lookups` and `/approvals`, `POST /api/v1/devices/<id>/revoke`), and the
same permission (`core.devices.manage`) as before (ADR 0036). No display concept is added.
The device list already returned `createdAt` and `lastSeenAt`, and the client now reads them.
`lastSeenAt` is written at most every few minutes (`DEVICE_LAST_SEEN_THROTTLE_MS`), so the page says
when a screen was last in touch, coarsely ("within the last hour", "3 days ago"). It never says a
screen is online.
**Nothing takes focus by itself.** No page load or navigation focuses a form field. Focus moves into
the code field only once someone has chosen **Add display**, because typing the code is then plainly
the next thing to do. It moves to the name field once a display is found. The dialog's focus trap
returns focus to **Add display** when the dialog closes. The button focuses itself before opening the
dialog, because Safari does not focus a button that is clicked.
### `/settings` opens on an overview of the installation
`/settings` stays a real landing page, never a redirect (ADR 0061). It now answers "what state is this
installation in?" instead of repeating the sidebar:
- **Displays:** how many are paired, and which was most recently seen. The link is *Manage displays*,
or *Add a display* when none are paired.
- **Modules:** how many are installed, and whether all are running or how many are turned off or
cannot start. The links are *Manage modules* and each running module's *<name> settings*.
- **General, Branding, and System:** one restrained line each about what is true today, such as
*Using the default name and colours*, with the one link that changes it. There are no large
*Not available yet* boxes.
The overview reads only existing data, and only for what this person may see:
- the Displays summary uses `GET /api/v1/devices`, read only when Displays is visible;
- the Modules summary uses `GET /api/v1/module-lifecycle`, read only when Discover is visible. That
endpoint requires `core.modules.lifecycle.manage`, the same permission Discover does.
A refusal or failure shows as a quiet *Couldn't check …* line. No backend API was added.
### Phones get a restrained sticky header
Below the compact breakpoint, a `<header>` holds the menu trigger and the open page's name. It is
`position: sticky` at the top while the page scrolls, and the drawer opens from that stable trigger.
It is not a desktop top bar and holds nothing else. The page name is a visual echo of the page's own
`<h1>`, so it is `aria-hidden`. Wide screens still have no top bar.
- **Safe areas.** The Dashboard page declares `viewport-fit=cover`, so the shell lays itself out with
`env(safe-area-inset-*)`:
- the header clears the status bar or notch;
- the page, the drawer, and a sidebar shown on a wide landscape phone clear the notch and the home
indicator.
- **Viewport height.** The shell's minimum height is `100dvh`, falling back to `100vh`, so Safari's
collapsing toolbar never leaves the page a toolbar's height too tall.
- **Scrolling and focus.**
- A new page starts at its top, as a full page load would. Focus moves to `<main>` with
`preventScroll`, so the page's top is never scrolled under the header.
- Focus that returns to the trigger after the drawer closes does not scroll either.
- `scroll-padding-top` keeps anything else scrolled into view, such as a focused field, below the
header.
- **Page margin.** A phone's margin is `spacing.lg` instead of `spacing.xl`.
### Collapsed geometry belongs to the stylesheet
A navigation link's layout and its open-page look are classes in the shell stylesheet: `ek-shell-link`,
plus `[aria-current="page"]`. They are never inline styles. Collapsed, every navigation item has the
same geometry, whatever its level:
- a `control.height-md` square;
- `display: flex`, centred on the rail with auto margins;
- no padding and no gap;
- its glyph centred.
The rail scrolls without a visible scrollbar, which would otherwise take its width from one side. The
tooltip, the keyboard order, and each item's visually hidden name are unchanged. No icon is nudged by
a one-off offset.
### Choice indicators are drawn from tokens, in `@ekklesia/ui`
`@ekklesia/ui`'s stylesheet draws the checkbox (`ek-checkbox__input`) and a radio (`ek-radio__input`)
itself, as native inputs with `appearance: none`. They are never tinted with `accent-color`.
- **Unselected:** a `border.control` ring on `background.surface`.
- **Selected:** `accent.strong` in both themes. The radio is a ring, a gap of the surface, and a dot.
The checkbox is a filled box with a surface-coloured check.
- **Other states:** hover strengthens the ring to `border.strong`, disabled uses
`state.disabled-opacity`, and forced colours use the system's own selection colours.
- **Size:** a larger control sets `--ek-choice-size`.
The Appearance page's radios use this shared indicator. Its rows keep their own focus ring, so the
radio inside draws none of its own.
No `Radio` component is added: ADR 0039 adds an element to `@ekklesia/ui` once two independent
consumers need it, and the shell is the only one. The change is to the existing stylesheet and the
existing `Checkbox`, whose selected state now matches. That is additive for modules: a module using
`Checkbox` gets the same, now consistent, selected state.
## Alternatives
- **Keep Browse and make it look more like a child.** Rejected: the review's finding was that
Discover and Browse are one concept. Two names for one page is the defect, not its styling.
- **Remove `/settings/discover/browse` outright.** Rejected: it shipped, and a bookmark should not
become *Page not found*. An alias respelled in place costs nothing and is never linked.
- **Pair displays from a route of its own (`/settings/displays/add`).** Deferred, not rejected. A dialog
keeps the routing contract unchanged and gets focus handling from the shared `Dialog`. A deep link
to *Add display*, for example from Signage, can add a route later without changing this page.
- **Keep the device panel, collapsed by default.** Rejected: the sidebar is navigation, and a form in
it is the defect.
- **Redirect `/settings` to General, or keep the index.** Rejected: the redirect, as in ADR 0061. The
index repeated the sidebar.
- **New backend endpoints for richer overview cards** (version, health, update counts). Rejected for
now: the overview uses what exists, and System, Updates, and Branding gain real summaries with the
work that builds them.
- **A full top bar on every screen size.** Rejected: DESIGN.md §4.2 keeps navigation in the sidebar.
Phones need only the menu kept within reach.
- **Fix the radio by changing `accent.base`, or with page-specific colours.** Rejected: a browser's
mark on an accent-tinted control is its own choice and differs between engines. Drawing the
indicator from tokens is the only way to get one result in every browser and both themes.
- **Offset individual icons to centre them.** Rejected: the cause was an inline style outranking the
collapsed rule, and one geometry for every item removes it.
## Consequences
- Discover's canonical address is `/settings/discover`. Nothing links to `/browse`, and it respells
itself away.
- The shell no longer asks for the device list on every page. Displays are read on Settings →
Displays and the Settings overview, for people allowed to see them.
- Released Signage 0.1.5 tells administrators to enter a screen's code "under Display devices in the
Dashboard sidebar". That text is now stale and must change in Signage. The pairing itself, and every
paired display, is unaffected.
- `@ekklesia/ui`'s checkbox looks different when checked: the strong accent instead of the browser's
accent-tinted control. Its behaviour and markup are unchanged.
- ROADMAP §5.3 and DESIGN.md §9.4, §12.1, and decision 98 still describe Browse at
`/settings/discover/browse` and need the same correction.
## Deferred
The real General, Branding, System, Installed, and Updates pages, and richer overview summaries with
them. Display rooms or locations, rename, and history (DESIGN.md §11). A routable *Add display* for
modules to link to. Signage consuming platform displays (ROADMAP §5.8).
+3 -2
View File
@@ -66,9 +66,10 @@ ADRs capture durable choices that affect more than one package or phase.
- [0055: Native redistributable packages carry corresponding-source artifacts](0055-corresponding-source-artifacts.md) — Accepted
- [0056: A device credential lasts while the device is in use](0056-device-credentials-last-while-in-use.md) — Accepted
- [0057: Dashboard URLs are real, host-owned routes](0057-dashboard-urls-are-host-owned-routes.md) — Accepted
- [0058: The Dashboard shell's navigation, and Lucide as Ekklesia's icon vocabulary](0058-dashboard-shell-navigation-and-lucide-icons.md) — Accepted
- [0058: The Dashboard shell's navigation, and Lucide as Ekklesia's icon vocabulary](0058-dashboard-shell-navigation-and-lucide-icons.md) — Accepted (device panel and phone menu placement superseded by 0064)
- [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
- [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 (Discover's address and the landing page superseded by 0064)
- [0062: The account area, and appearance preferences that belong to the account](0062-account-area-and-synced-appearance-preferences.md) — Accepted
- [0063: Module navigation — manifest v9 icons and pages, real module routes, and return-to-origin](0063-module-navigation-contract-manifest-v9.md) — Accepted
- [0064: Discover is its own page, the sidebar is navigation only, Settings opens on an overview, and phones get a sticky header](0064-stage-1-shell-review-corrections.md) — Accepted
+5 -3
View File
@@ -602,9 +602,11 @@ durations when motion is reduced — in CSS alone, so the Dashboard and every mo
operating system live; the host chooses an explicit appearance through a small controller, applied
before React renders. Contrast is tested for every pairing the elements use, in both schemes
([ADR 0060](decisions/0060-light-dark-system-theme-and-motion.md)). Settings is one host-owned
hierarchy in the same sidebar: a landing page, the platform's General, Branding, Displays, Discover
(module lifecycle: Browse, Installed, Updates), and System, and each module's settings at
`/settings/<module-id>`. Which of them a person sees is evaluated by the server
hierarchy in the same sidebar: a landing page that summarises the installation, the platform's
General, Branding, Displays (where display screens are paired and revoked), Discover (module
lifecycle: Discover's own page at `/settings/discover`, with Installed and Updates beneath it), and
System, and each module's settings at `/settings/<module-id>`; the sidebar itself is navigation only
([ADR 0064](decisions/0064-stage-1-shell-review-corrections.md)). Which of them a person sees is evaluated by the server
(`GET /api/v1/dashboard/settings`) as presentation hints, while every route behind them keeps its own
enforcement; the Settings section names are reserved module ids
([ADR 0061](decisions/0061-settings-hierarchy-and-module-settings-routes.md)). Personal configuration