diff --git a/.planning/STATE.md b/.planning/STATE.md index ff1f0f4..1edc547 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -5,10 +5,10 @@ current_phase: "19.3" current_phase_name: Framework transport and infrastructure for WavePath (INSERTED) status: Phase 19.3 plans complete; release and whole-phase docs pending in the phase 19.3 main session stopped_at: Completed 19.3-06-PLAN.md (gap closure; release handed off to the phase 19.3 main session) -last_updated: "2026-10-15T22:07:28.573Z" -last_activity: 2026-10-15 -last_activity_desc: 19.3-06 gap closure complete, release handed off -state_head: 853ea1fa6e6c34a84d066b22a0b34a8e3648260e +last_updated: "2026-10-16T09:58:01.923Z" +last_activity: 2026-10-16 +last_activity_desc: "Completed quick task 261016-0fm: Admin minors for v0.2.1" +state_head: 66fd4ae4b702373adc818324270337796f32d71a progress: total_phases: 40 completed_phases: 31 @@ -32,7 +32,7 @@ See: .planning/PROJECT.md (updated 2026-10-12) Phase: 19.3 (Framework transport and infrastructure for WavePath (INSERTED)) — EXECUTING Plan: 6 of 6 Status: Phase 19.3 plans complete; release and whole-phase docs pending in the phase 19.3 main session -Last activity: 2026-10-15 — 19.3-06 gap closure complete, release handed off +Last activity: 2026-10-16 - Completed quick task 261016-0fm: Admin minors for v0.2.1 Progress: [████░░░░░░] 44% @@ -899,6 +899,7 @@ Recent decisions affecting current work: | 261013-0wa | Close T-17-15: validate sitemap frontend_url and escape it in Rewrite | 2026-10-12 | 6e5a8e7 | [261013-0wa-close-t-17-15-validate-sitemap-frontend-](./quick/261013-0wa-close-t-17-15-validate-sitemap-frontend-/) | | 261015-w7f | Framework docs since v0.1.14 + Upgrading to 0.2, release v0.2.0 (plugins, layers), deploy summercms.io | 2026-10-15 | a204d38 | [261015-w7f-update-framework-docs-for-phase-19-3-rel](./quick/261015-w7f-update-framework-docs-for-phase-19-3-rel/) | | 36 | summercms.io copy Alpha 0.1 → 0.2 (vue b5d6dbb, app 6a13a87), deployed | 2026-10-15 | 853ea1f | — | +| 261016-0fm | Admin minors for v0.2.1: theme switch, language switch, settings nav on form pages | 2026-10-16 | 66fd4ae | [261016-0fm-admin-minors-for-v0-2-1-theme-switch-lan](./quick/261016-0fm-admin-minors-for-v0-2-1-theme-switch-lan/) | ## Deferred Items diff --git a/.planning/quick/261016-0fm-admin-minors-for-v0-2-1-theme-switch-lan/261016-0fm-PLAN.md b/.planning/quick/261016-0fm-admin-minors-for-v0-2-1-theme-switch-lan/261016-0fm-PLAN.md new file mode 100644 index 0000000..6fde475 --- /dev/null +++ b/.planning/quick/261016-0fm-admin-minors-for-v0-2-1-theme-switch-lan/261016-0fm-PLAN.md @@ -0,0 +1,328 @@ +--- +phase: quick-261016-0fm +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - modules/phrasebook/translator.go + - modules/phrasebook/translator_test.go + - modules/phrasebook/README.md + - modules/phrasebook/backend/lang/en/lang.yaml + - modules/phrasebook/backend/lang/pl/lang.yaml + - modules/cabana/account_preferences.go + - modules/cabana/account_preferences_test.go + - modules/cabana/account_preferences_internal_test.go + - modules/cabana/account.go + - modules/cabana/admin_openapi.go + - modules/cabana/http.go + - modules/cabana/lang.go + - modules/cabana/auth.go + - modules/cabana/token_guard.go + - modules/cabana/security_coverage_test.go + - modules/cabana/token_routes_test.go + - modules/cabana/phase10_coverage_test.go + - modules/cabana/openapi_conformance_test.go + - modules/cabana/README.md + - docs/backend/users-and-permissions.md + - docs/backend/admin-spa.md + - docs/services/localization.md + - admin/openapi/admin.json + - admin/src/api/schema.d.ts + - admin/src/api/types.ts + - admin/src/app/theme.ts + - admin/src/app/i18n.ts + - admin/src/state/usePreferences.ts + - admin/src/state/useAuth.ts + - admin/src/state/useSidebar.ts + - admin/src/main.ts + - admin/src/components/shell/PluginRail.vue + - admin/src/components/shell/UserMenu.vue + - admin/src/components/shell/AppShell.vue + - admin/src/views/FormView.vue + - admin/src/views/SettingsFormView.vue + - admin/src/views/AccountView.vue + - admin/src/views/PreviewView.vue + - admin/tests/helpers.ts + - admin/tests/fixtures/lang.json + - admin/tests/app/theme.test.ts + - admin/tests/app/i18n.test.ts + - admin/tests/state/usePreferences.test.ts + - admin/tests/shell/PluginRail.test.ts + - admin/tests/shell/UserMenu.test.ts + - admin/tests/shell/AppShell.test.ts + - admin/tests/form/FormView.test.ts + - modules/boardwalk/dist/ +autonomous: true +requirements: [QUICK-261016-0fm] + +estimate: + tokens: 190000 + raw_tokens: 190000 + tasks: 3 + confidence: low + +must_haves: + truths: + - 'The top item of the left rail (the Sun mark, now a button with data-theme-toggle) cycles the admin theme system -> light -> dark -> system on click. light never sets the .dark class on , dark always sets it, system follows prefers-color-scheme live (the pre-change behaviour). Its icon and aria-label/title name the current choice in the admin language.' + - 'The theme choice is stored server-side for the signed-in admin only: PUT /api/v1/account/preferences {"theme": ...} writes backend_users.metadata key admin_theme of the token subject, and GET /api/v1/auth/me and GET /api/v1/account/preferences return it as preferences.theme ("system" when unset). A second browser signed in as the same admin gets the same theme on load; the SPA applies the last choice from localStorage summer-admin.theme before the first await of boot, so the shell never renders in the wrong theme.' + - 'The user menu shows a Language radio group listing every admin locale from GET /lang meta.locales (en and pl in the framework), each named in its own language. Choosing one saves preferences.locale through PUT /account/preferences, caches it in localStorage summer-admin.locale and reloads the page; after the reload the SPA strings (GET /lang?locale=), the navigation, settings, list/form schemas and validation messages are in that language even when the request Accept-Language or a translate-plugin locale cookie names another one.' + - 'GET and PUT /account/preferences act only on the row of the authenticated admin (bouncer principal id; no id in the path or body). PUT accepts only the keys theme and locale (any other key, including id or backend_user_id, is a 422 on that key and nothing is written); theme must be one of system, light, dark; locale must be "" (clear) or a code in phrasebook.Translator.Locales("backend::lang."); a value of the wrong type is a 422. Personal API tokens get 403 interactive_only on both routes, and a cookie PUT without X-Requested-With gets 403. Every other metadata key (subtitle, keys copied from WinterCMS) is kept on save, and metadata that is not a JSON object is never overwritten (422).' + - 'Requests authenticated with a personal API token keep the request locale exactly as before: the stored admin_locale never becomes their bouncer.Principal.PreferredLocale.' + - 'On form, settings form, My account and preview pages the bottom save bar begins at the right edge of the sidebar (the rail, plus the section panel when it is shown), so the Settings rail item at the bottom of the rail stays visible and clickable; list pages are unchanged.' + - 'No backend_user_preferences table is created (backend_admin_migrations_test.go keeps asserting it is absent) and no migration is added: preferences live in the existing backend_users.metadata jsonable column.' + - 'modules/boardwalk/dist is rebuilt and committed; scripts/check-admin-dist.sh, scripts/check-admin-openapi.sh --check, go vet ./..., go test -short ./..., the Docker-backed cabana preference tests, npm --prefix admin run test and typecheck, and go run ./cmd/summer docs:build --check all pass. Nothing is tagged, pushed or deployed; every commit is staged by explicit path and carries no co-author tag.' + artifacts: + - path: "modules/cabana/account_preferences.go" + provides: "metadata keys admin_theme/admin_locale, preferencesOf, GET and PUT /account/preferences handlers" + contains: "admin_theme" + - path: "modules/phrasebook/translator.go" + provides: "Translator.Locales(prefix): the locales that define keys under a prefix (supported admin locales)" + contains: "func (t *Translator) Locales(" + - path: "modules/cabana/admin_openapi.go" + provides: "AdminPreferences, AdminPreferencesRequest, AdminProfile.Preferences, LangMeta/LangEnvelope and swag docs of the new routes" + contains: "type AdminPreferences struct" + - path: "modules/cabana/lang.go" + provides: "GET /lang honours a supported ?locale= and reports meta.locales" + contains: "Locales(adminLangPrefix)" + - path: "admin/src/state/usePreferences.ts" + provides: "theme and locale preference state, localStorage cache, PUT /account/preferences, apply-on-load" + contains: "summer-admin.theme" + - path: "admin/src/components/shell/PluginRail.vue" + provides: "the rail's top button cycles the theme" + contains: "data-theme-toggle" + - path: "admin/src/components/shell/UserMenu.vue" + provides: "Language radio group in the user menu" + contains: "DropdownMenuRadioGroup" + - path: "admin/src/components/shell/AppShell.vue" + provides: "--shell-offset CSS variable: the width the sidebar occupies" + contains: "--shell-offset" + - path: "modules/cabana/account_preferences_test.go" + provides: "Docker-backed round trip, authz and locale-override tests" + contains: "func TestAccountPreferences" + - path: "admin/tests/state/usePreferences.test.ts" + provides: "vitest coverage of cache validation, cycling, failure revert, locale switch reload" + contains: "usePreferences" + key_links: + - from: "admin/src/components/shell/PluginRail.vue (theme button)" + to: "modules/cabana/account_preferences.go (PUT /account/preferences)" + via: "usePreferences.cycleTheme -> setTheme -> api.PUT('/account/preferences', { body: { theme } })" + pattern: "cycleTheme" + - from: "modules/cabana/auth.go (principalFrom)" + to: "cabana guarded admin group (http.go service.mount)" + via: "Principal.PreferredLocale from metadata admin_locale; the group lists surf's locale.from-principal after the backend guard, so towel.Locale and schemaLocale see the preference" + pattern: "locale.from-principal" + - from: "admin/src/main.ts (boot)" + to: "modules/cabana/lang.go (langBundle)" + via: "loadStrings(cachedLocale()) sends GET /lang?locale=; langBundle uses it only when it is in Translator.Locales(backend::lang.)" + pattern: "loadStrings\\(" + - from: "admin/src/components/shell/AppShell.vue (--shell-offset)" + to: "FormView/SettingsFormView/AccountView/PreviewView footers" + via: "footer class left-[var(--shell-offset,0px)] right-0" + pattern: "shell-offset" + - from: "modules/cabana/token_guard.go (personal token principal)" + to: "surf LocaleFromPrincipal" + via: "PreferredLocale cleared on token principals, so the override is a no-op for API tokens" + pattern: "PreferredLocale" +--- + + +Three admin minors for v0.2.1, all in the embedded admin SPA (`admin/`) and cabana: + +1. A manual theme switch (system / light / dark) on the rail's top icon, persisted per backend admin on the server and applied on load without a wrong-theme flash. +2. An admin language switcher (in the user menu) that persists in the same per-admin storage and makes the SPA bundle and the server-side admin locale agree. +3. The Settings rail item stays reachable on pages with the fixed bottom save bar. + +Then rebuild and commit the embedded dist and run every release gate. Release target v0.2.1, but nothing is tagged, pushed or deployed: that waits for the user. + +Purpose: the deployed v0.2.0 admin has a theme-looking icon that does nothing, no way to change the backend language, and Settings is covered on every form page. +Output: local commits on `master` (code+docs commits per logical change, a test commit or two, one dist rebuild commit) and the quick SUMMARY. + +Hard limits: no tag, no push, no deploy. Other Claude sessions work in this repository: stage by explicit path, never `git add -A`; do not touch `.planning/STATE.md`, `.planning/state.json`, `.msd/` or `.planning/phases/17-blog-companions/17-VERIFICATION.md`. No co-author tags. One logical change per commit. Framework code, docs and tests never name a consuming application (use neutral names like `acme`). + + + +@~/.claude/msd-core/workflows/execute-plan.md +@~/.claude/msd-core/templates/summary.md + + + +@CLAUDE.md +@modules/cabana/account.go +@modules/cabana/auth.go +@modules/cabana/lang.go +@modules/cabana/http.go +@modules/cabana/admin_openapi.go +@modules/cabana/token_guard.go +@modules/phrasebook/translator.go +@modules/surf/locale_from_principal.go +@admin/src/app/theme.ts +@admin/src/app/i18n.ts +@admin/src/main.ts +@admin/src/state/useAuth.ts +@admin/src/state/useSidebar.ts +@admin/src/components/shell/AppShell.vue +@admin/src/components/shell/PluginRail.vue +@admin/src/components/shell/UserMenu.vue + +## Planning-time observations (2026-10-16, live, HEAD 1a03399) + +Edit scope rests on these facts. Re-check the ones a task's precondition names before editing. + +- Working tree: only `.planning/state.json` (modified), `.msd/` and `.planning/phases/17-blog-companions/17-VERIFICATION.md` (untracked), all other sessions' files. Nothing uncommitted under `modules/`, `admin/` or `docs/`. +- **Per-admin storage already exists:** `backend_users.metadata` is WinterCMS's nullable jsonable TEXT column (`lagoon/backend_admin_migrations.go` migration `202610090001_backend_users_metadata`), mapped as `cabana.BackendUser.Metadata *string`. `modules/cabana/account.go` has `decodeMetadata`, `metadataString` and `setMetadataString` (keeps every other key, NULL/blank/`null`/`[]` = empty object, refuses non-object text with `errMetadataNotObject`), used today for the `subtitle` key. `lockAccount(ctx, tx, id)` (account.go:227) loads the admin's own row `FOR UPDATE`; `accountPrincipal(w, r)` (account.go:216) returns the backend principal or writes 401; `refusedAccountKeys` (account.go:445) is the unknown-key 422 pattern; `writeAccountError` maps `errAccountGone` to 401; `s.decodeCappedObject(w, r)` (relation_child.go:709) decodes a capped JSON object; `fieldMessage(ctx, tr, rule, field, params)` (field_file.go:885) renders `lagoon::validation.`. +- **WinterCMS's preference table is deliberately not ported:** `modules/lagoon/backend_admin_migrations_test.go:116` fails if `backend_user_preferences` exists (Phase 9 decision, re-confirmed in 16.1 research). So: no new table, no migration; store under metadata keys `admin_theme` and `admin_locale`. +- **Locale today:** surf's `Router.locale` (surf/router.go:861) wraps every request before any guard: a published `surf.LocaleResolver` (the translate plugin publishes one in deployed apps; its `Resolve` prefers URL prefix, then principal, then a remembered cookie, then Accept-Language) or else raw Accept-Language goes into `towel.WithLocale`. `cabana.schemaLocale` reads `towel.Locale`. `bouncer.Principal.PreferredLocale` exists ("empty means no override"), and surf registers the named middleware `locale.from-principal` (`surf.LocaleFromPrincipal`, router.go:534) that overrides the towel locale with it. cabana's `principalFrom` (auth.go:103) never sets it today. Because the resolver runs before the guard and a remembered cookie beats Accept-Language, sending a header from the SPA alone cannot fix the admin language: cabana must apply the preference after its guard. +- cabana's guarded admin group is `r.GroupRaw(api, []string{"backend"}, ...)` (http.go:264). Account routes sit at http.go:266-275 wrapped in `interactiveOnly(...)` and, for writes, `requireAjax(...)`. The personal-token guard builds its principal with `g.users.FindByID` (token_guard.go:101), i.e. through `principalFrom`, then sets `principal.Scope = limit` (token_guard.go:119). +- `GET /lang` (`cabana.langBundle`, lang.go) is public (outside the guard), resolves `schemaLocale` then `tr.Resolved(locale, "backend::lang.")`, answers `meta.locale`. Swag: `AdminLang` documents `Envelope[LangBundle]` (admin_openapi.go:167-175); `SuccessMeta` is shared by every envelope (admin_openapi.go:33), so new /lang meta needs its own type. +- `phrasebook.Translator` has `Bundle`, `Resolved`, `Forms`, `Has` but no way to list the locales a prefix exists in. Framework admin strings: `modules/phrasebook/backend/lang/{en,pl}/lang.yaml` (groups `nav:` and `myaccount:` exist). +- Route tables that enumerate cabana's mounted routes and must list new ones in the same commit (else `go test -short` fails): `security_coverage_test.go` `phase09Routes` (~L33-48), `token_routes_test.go` `tokenRouteClasses` (~L37-43) and `interactivePaths` (~L169-171), `phase10_coverage_test.go` unsafe-route `want` list (~L85-120; its method switch accepts only GET/POST/PUT/DELETE, so use PUT, not PATCH), `openapi_conformance_test.go` cases (account cases ~L716; `GET /lang` expects schema `cabana.Envelope-cabana_LangBundle` at ~L406). +- Docker works for the current user (`docker info` = 29.7.2). Docker-backed cabana tests use `adminGorm(t)`, `adminHandler(t, gdb, nil)`, `insertAdmin`, `signIn`, `getAuth`, `adminAPI`, `profileOf` (see `TestAccountProfileSmoke`, account_test.go:266). `summer` is not on PATH: use `go run ./cmd/summer docs:build --check`. +- **SPA theme today:** `admin/src/app/theme.ts` `applyColorScheme(win)` toggles `.dark` on `` from `(prefers-color-scheme: dark)` and follows changes; `main.ts` calls it first in `boot()`. `main.css` uses `@custom-variant dark (&:where(.dark, .dark *))` and `.dark { color-scheme: dark; }`, so a class switch is the whole mechanism. The "theme-looking icon" is the decorative `` with a lucide `Sun` at the top of the `