Add the account area and account-synced Appearance preferences
CI / verify (pull_request) Successful in 1m10s
CI / infrastructure-smoke (pull_request) Successful in 34s
CI / package-release (pull_request) Successful in 50s
CI / migration-smoke (pull_request) Successful in 2m26s
CI / image-build (pull_request) Successful in 1m12s
CI / deploy-smoke (same-origin) (pull_request) Successful in 42s
CI / package-publication (pull_request) Successful in 54s
CI / deploy-smoke (split) (pull_request) Successful in 3m17s

Stage 1 PR 6 (ADR 0062). /account shows who is signed in; /account/appearance
chooses System, Light, or Dark and reduced motion (System, Reduce, Allow).

- Core: ekklesia_core_user_preference, one typed, CHECK-constrained,
  nullable column per preference (null = never chosen), migration
  0008_user_preferences (additive), @ekklesia/core/account store, and the
  authenticated-baseline permission core.account.self.manage.
- API: GET /api/v1/account (name, email, preferences) and partial
  PATCH /api/v1/account/preferences, self-only by construction: no user id
  is accepted anywhere; trusted Origin and body limit on the mutation.
- Shell: the account control shows name, email, and initials (never the
  unvetted image URL) and offers Account, Appearance, Sign out; native
  radio groups apply a choice at once through the ADR 0060 controller and
  save it; the server's answer wins and a failed save reverts.
- First paint: a classic, same-origin, render-blocking appearance.js in
  <head> applies a two-word localStorage cache before paint (no inline
  script, CSP unchanged). The account replaces the cache on every load,
  sign-out clears it, and other tabs follow via the storage event.
- Deploy smoke: real-Chromium checks for light/dark, OS emulation,
  overrides, reload persistence, pre-paint, Shadow DOM modules, reduced
  motion, the display staying light, and phone width; the browser
  container gets a 512 MiB /dev/shm so a long session no longer fails
  requests with ERR_INSUFFICIENT_RESOURCES.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ekklesia
2026-10-14 23:56:50 -05:00
co-authored by Claude Opus 5.5
parent 8571e6854c
commit cd0287537e
88 changed files with 6404 additions and 158 deletions
+30 -3
View File
@@ -404,9 +404,36 @@ no runtime assertion can make: that the identity feature names no SMTP detail, t
schema gained a token column and no identity code hashes or compares a token, that nothing
interpolates a token, link, address, or body into a log line, that a device has no email, password,
or verification concept, that no browser source imports Better Auth or Core's mail, and that the
shell's only use of browser storage is the sidebar's expanded/collapsed mode — `localStorage` named
only by the entry point, one write of one fixed key, and no storage API in any file that holds the
reset token ([ADR 0058](../architecture/decisions/0058-dashboard-shell-navigation-and-lucide-icons.md)).
shell's uses of browser storage are exactly two: the sidebar's expanded/collapsed mode
([ADR 0058](../architecture/decisions/0058-dashboard-shell-navigation-and-lucide-icons.md)) and the
pre-paint appearance cache
([ADR 0062](../architecture/decisions/0062-account-area-and-synced-appearance-preferences.md)).
`localStorage` is named once by the entry point and once, read-only, by the pre-paint script. There is
one write of each fixed key, one removal of the appearance key, and no storage API in any file that
holds the reset token.
The account area
([ADR 0062](../architecture/decisions/0062-account-area-and-synced-appearance-preferences.md)) is
proven at each layer:
- `core/tests/account-preferences.test.ts` covers the closed preference model, its strict parser, the
table's typed columns, and that the migration's `CHECK` lists equal the model.
- `core/tests/account-store.integration.ts` uses real PostgreSQL. It proves defaults without writing a
row, saving each value, partial updates, isolation between two people, persistence across a new
connection, the database refusing an out-of-model value, and the cascade on delete.
- `apps/api/tests/account-routes.test.ts` covers the routes. Anonymous and device callers are refused,
only the session's account is ever read or written whatever the request names, there is no route
that addresses another person, and validation, `Origin`, body limit, failures, and CORS preflight
all behave as specified. It also holds Core's value sets equal to the design tokens'.
- `apps/api/tests/api-http.integration.ts` runs two real accounts over real HTTP sessions, with the
same isolation proven end to end.
- `surfaces/shell/tests/appearance-preference.test.ts` covers the cache, the pre-paint script (run in
the DOM), and the one handle over the ADR 0060 controller.
- `surfaces/shell/tests/account-appearance.test.tsx` covers the account control's identity, both
pages, live switching, saving, reverting on failure, reconciling with the account, cross-tab
following, sign-out forgetting, and the phone drawer.
- The deploy smoke's real Chromium checks Light and Dark rendering, reload persistence, an emulated OS
change, reduced motion, a module's Shadow DOM following the theme, and phone width.
Unit suites (`core/tests/module-storage.test.ts`, `core/tests/module-storage-lifecycle.test.ts`) use an
injected in-memory provider and never touch the network. They prove the parts the protocol cannot: