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
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:
co-authored by
Claude Opus 5.5
parent
8571e6854c
commit
cd0287537e
@@ -13,7 +13,8 @@
|
||||
* /settings/<section> a platform Settings section: general, branding, displays, system
|
||||
* /settings/discover/<view> finding, installing, and managing modules: browse, installed, updates
|
||||
* /settings/<module-id> a module's settings (and later its own deeper settings pages)
|
||||
* /account[/…] the account area
|
||||
* /account your account
|
||||
* /account/<section> a personal settings page: appearance
|
||||
* /<module-id>[/…] a module's page, and later its own sub-pages
|
||||
* ```
|
||||
*
|
||||
@@ -50,6 +51,15 @@ export type SettingsSection = (typeof SETTINGS_SECTIONS)[number];
|
||||
/** The sections that are one page each. Discover is a group of views instead. */
|
||||
export type SettingsPageSection = Exclude<SettingsSection, "discover">;
|
||||
|
||||
/**
|
||||
* The account area's personal settings pages, in navigation order (ADR 0062). Personal, never
|
||||
* church administration: that is Settings. They live beneath `/account/`, so none can collide with a
|
||||
* module id and none is reserved.
|
||||
*/
|
||||
export const ACCOUNT_SECTIONS = Object.freeze(["appearance"] as const);
|
||||
|
||||
export type AccountSection = (typeof ACCOUNT_SECTIONS)[number];
|
||||
|
||||
/**
|
||||
* Discover's views, in navigation order: Browse (find and install modules), Installed (see and
|
||||
* manage installed modules), and Updates (module and platform updates). Browse is where Discover
|
||||
@@ -67,6 +77,8 @@ export type DashboardRoute =
|
||||
| { readonly kind: "settings-section"; readonly section: SettingsPageSection }
|
||||
| { readonly kind: "discover"; readonly view: DiscoverView }
|
||||
| { readonly kind: "module-settings"; readonly moduleId: string }
|
||||
| { readonly kind: "account" }
|
||||
| { readonly kind: "account-section"; readonly section: AccountSection }
|
||||
| { readonly kind: "not-found" };
|
||||
|
||||
/** A route the shell can link to. `not-found` is something the shell arrives at, never a target. */
|
||||
@@ -76,6 +88,7 @@ const platformPages: ReadonlySet<string> = new Set(DASHBOARD_PLATFORM_PAGES);
|
||||
const settingsSections: ReadonlySet<string> = new Set(SETTINGS_SECTIONS);
|
||||
|
||||
const discoverViews: ReadonlySet<string> = new Set(DISCOVER_VIEWS);
|
||||
const accountSections: ReadonlySet<string> = new Set(ACCOUNT_SECTIONS);
|
||||
|
||||
function isSettingsPageSection(value: string): value is SettingsPageSection {
|
||||
return value !== "discover" && settingsSections.has(value);
|
||||
@@ -85,6 +98,10 @@ function isDiscoverView(value: string): value is DiscoverView {
|
||||
return discoverViews.has(value);
|
||||
}
|
||||
|
||||
function isAccountSection(value: string): value is AccountSection {
|
||||
return accountSections.has(value);
|
||||
}
|
||||
|
||||
function decodeSegment(segment: string): string | undefined {
|
||||
try {
|
||||
const decoded = decodeURIComponent(segment);
|
||||
@@ -120,6 +137,9 @@ export function parseDashboardPath(pathname: string): DashboardRoute {
|
||||
if (first === "settings") {
|
||||
return parseSettingsPath(meaningful.slice(1));
|
||||
}
|
||||
if (first === "account") {
|
||||
return parseAccountPath(meaningful.slice(1));
|
||||
}
|
||||
if (first === undefined || platformPages.has(first)) {
|
||||
return { kind: "not-found" };
|
||||
}
|
||||
@@ -161,6 +181,18 @@ function parseSettingsPath(rest: readonly string[]): DashboardRoute {
|
||||
: { kind: "module-settings", moduleId: name };
|
||||
}
|
||||
|
||||
/** Reads what follows `/account`: the account page itself, or one of its personal settings pages.
|
||||
* Anything else is `not-found` until it exists. */
|
||||
function parseAccountPath(rest: readonly string[]): DashboardRoute {
|
||||
if (rest.length === 0) {
|
||||
return { kind: "account" };
|
||||
}
|
||||
const name = rest[0] === undefined ? undefined : decodeSegment(rest[0]);
|
||||
return rest.length === 1 && name !== undefined && isAccountSection(name)
|
||||
? { kind: "account-section", section: name }
|
||||
: { kind: "not-found" };
|
||||
}
|
||||
|
||||
/** The one canonical spelling of a route. A module id is percent-encoded, so no id can escape its
|
||||
* own path segment. */
|
||||
export function dashboardPath(route: DashboardDestination): string {
|
||||
@@ -177,5 +209,9 @@ export function dashboardPath(route: DashboardDestination): string {
|
||||
return `/settings/discover/${route.view}`;
|
||||
case "module-settings":
|
||||
return `/settings/${encodeURIComponent(route.moduleId)}`;
|
||||
case "account":
|
||||
return "/account";
|
||||
case "account-section":
|
||||
return `/account/${route.section}`;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user