docs(quick-261007-p4y): toggle sizing and settings controller links

This commit is contained in:
Jakub Zych
2026-10-07 18:28:54 +02:00
parent 0b09acdb0c
commit 52eef9b43b
3 changed files with 428 additions and 1 deletions

View File

@@ -31,7 +31,7 @@ See: .planning/PROJECT.md (updated 2026-09-16)
Phase: 15 (Journal plugin) — EXECUTING Phase: 15 (Journal plugin) — EXECUTING
Plan: 4 of 4 Plan: 4 of 4
Status: Ready for phase verification Status: Ready for phase verification
Last activity: 2026-10-06 — Completed quick task 261006-tnp: Add mltextarea field type Last activity: 2026-10-07 — Completed quick task 261007-p4y: toggle sizing and settings controller links
Progress: [██████████] 100% Progress: [██████████] 100%
@@ -651,6 +651,7 @@ Recent decisions affecting current work:
| 261006-tnp | Add mltextarea field type | 2026-10-06 | 3596df4 | [261006-tnp-add-mltextarea-field-type](./quick/261006-tnp-add-mltextarea-field-type/) | | 261006-tnp | Add mltextarea field type | 2026-10-06 | 3596df4 | [261006-tnp-add-mltextarea-field-type](./quick/261006-tnp-add-mltextarea-field-type/) |
| 15 | Commit accumulated planning documents properly | 2026-10-06 | c05ad09 | — | | 15 | Commit accumulated planning documents properly | 2026-10-06 | c05ad09 | — |
| 16 | Default icon shouldn't be a square but a sun. | 2026-10-06 | 4a94380 | — | | 16 | Default icon shouldn't be a square but a sun. | 2026-10-06 | 4a94380 | — |
| 261007-p4y | Switch toggle pixel sizing; settings entries linking to admin controllers; translate Locales moved to Settings | 2026-10-07 | 0b09acd | [261007-p4y-toggle-proportions-and-settings-controll](./quick/261007-p4y-toggle-proportions-and-settings-controll/) |
## Deferred Items ## Deferred Items

View File

@@ -0,0 +1,271 @@
---
phase: quick-261007-p4y
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- admin/src/components/form/fields/SwitchField.vue
- modules/boardwalk/dist/
- modules/pact/capabilities.go
- modules/pact/README.md
- modules/cabana/registry.go
- modules/cabana/settings.go
- modules/cabana/navigation.go
- modules/cabana/contracts.go
- modules/cabana/settings_link_test.go
- modules/cabana/README.md
- admin/openapi/admin.json
- admin/src/api/schema.d.ts
- admin/tests/fixtures/settings.json
- admin/src/state/useSettings.ts
- admin/src/views/SettingsIndexView.vue
- admin/src/components/shell/PluginRail.vue
- admin/src/components/shell/Breadcrumbs.vue
- admin/src/app/pageTitle.ts
- admin/tests/state/useSettings.test.ts
- admin/tests/form/Settings.test.ts
- admin/tests/shell/PluginRail.test.ts
- admin/tests/shell/Breadcrumbs.test.ts
- admin/tests/app/pageTitle.test.ts
- docs/backend/settings.md
- docs/setup/coming-from-wintercms.md
- /media/nvme/dev/golem15/summercms.io/summercms/sm-translate-plugin/admin_navigation.go
- /media/nvme/dev/golem15/summercms.io/summercms/sm-translate-plugin/admin_settings.go
- /media/nvme/dev/golem15/summercms.io/summercms/sm-translate-plugin/admin.go
- /media/nvme/dev/golem15/summercms.io/summercms/sm-translate-plugin/plugin_test.go
- /media/nvme/dev/golem15/summercms.io/summercms/sm-translate-plugin/locales_admin_test.go
- /media/nvme/dev/golem15/summercms.io/summercms/sm-translate-plugin/README.md
autonomous: true
requirements: [QUICK-261007-p4y]
estimate:
tokens: 140000
raw_tokens: 140000
tasks: 3
confidence: low
must_haves:
truths:
- "The admin switch track is 44px by 26px and its knob 20px whatever the root font size (html is 14px), so the knob travels 18px inside a 3px inset on both ends; the embedded modules/boardwalk/dist matches a fresh build of that change"
- "A pact.SettingsItem with Controller set compiles without Form, NewModel, Model or an AdminFS and is not a singleton; Controller combined with Form, NewModel or Model, or Controller naming an admin controller no plugin registered, stops start-up with a cabana error; settings codes stay unique across singleton and link entries"
- "GET /settings returns a controller-link entry with `controller` set to its controller ID and `model` empty; every singleton entry carries `controller: \"\"`; a link entry is listed only when the principal passes the item's Permissions AND can open the controller (Registry.canOpen); singleton filtering is unchanged"
- "GET /settings/{code}/schema, GET /settings/{code} and PUT /settings/{code} answer 404 not_found for a link entry's code; singleton settings endpoints behave exactly as before"
- "In the admin SPA a settings index card for a link entry navigates to that controller's route (/vendor/plugin/controller, the same derivation navigation uses); a singleton card still goes to /settings/<encoded code>. On a link controller's pages with no main-navigation entry the rail marks Settings current, breadcrumbs read Settings > <entry label> (plus the record title on record and create routes), and the page title uses the entry label"
- "The translate plugin registers no main navigation; its Locales screen appears on the Settings page under category Translate (golem15.translate::lang.plugin.name) as 'Manage languages' (golem15.translate::lang.locale.title), order 550, gated by golem15.translate.manage_locales, linking to golem15.translate.locales, exactly as the PHP plugin's registerSettings entry"
- "admin/openapi/admin.json and admin/src/api/schema.d.ts match a fresh generation; the pact and cabana READMEs and docs/backend/settings.md describe the Controller link; TestDocsTree, go vet ./... and go test ./... are green in summercms.go and in sm-translate-plugin; existing keyed pact.SettingsItem literals in other plugins compile unchanged"
artifacts:
- path: modules/pact/capabilities.go
provides: "SettingsItem.Controller string (additive field), documented as WinterCMS registerSettings 'url' => Backend::url(...)"
- path: modules/cabana/settings.go
provides: "compileSettingLink: exclusivity check (no Form, NewModel or Model) returning a CompiledSetting with nil Form and Writable"
- path: modules/cabana/registry.go
provides: "compileContributions link branch (no AdminFS requirement) and post-collection unknown-controller validation for link entries"
- path: modules/cabana/navigation.go
provides: "SettingsEntry.Controller json:\"controller\"; Metadata filters link entries through Allows and canOpen"
- path: modules/cabana/contracts.go
provides: "Registry.Setting returns false for link entries so every singleton endpoint answers 404"
- path: modules/cabana/settings_link_test.go
provides: "Compile validation, metadata output and permission filtering, and singleton-endpoint 404 tests for link entries"
- path: admin/src/state/useSettings.ts
provides: "settingsPath(entry) and settingsLinkFor(controllerId)"
- path: /media/nvme/dev/golem15/summercms.io/summercms/sm-translate-plugin/admin_settings.go
provides: "Plugin.Settings() returning the single locales link entry"
key_links:
- from: "pact.SettingsItem.Controller"
to: "cabana compileContributions link branch + unknown-controller check"
via: "reg.settings map shared with singletons (one code namespace)"
- from: "cabana.Registry.Metadata"
to: "SettingsIndexView RouterLink"
via: "SettingsEntry.controller -> admin.json -> schema.d.ts -> useSettings.settingsPath"
- from: "cabana.Registry.Setting"
to: "protectSetting in modules/cabana/http.go"
via: "link code is not found -> 404 not_found before any Form access"
- from: "translate Plugin.Settings()"
to: "GET /settings in the translate admin harness"
via: "cabana registry boot with the plugin's controllers"
---
<objective>
Two user-facing fixes in one quick task.
1. The admin toggle (SwitchField) looks too tall for its width. Root cause: admin/src/styles/main.css sets the html font size to 14px, so the Tailwind v4 rem utilities w-11 (38.5px) and size-5 (17.5px) shrink against the fixed h-[26px] track and translate-x-[18px] travel. The fix (w-[44px] and size-[20px]) is ALREADY APPLIED, uncommitted, in admin/src/components/form/fields/SwitchField.vue. It needs its test check, a dist rebuild and its own commit.
2. WinterCMS lets registerSettings point an entry at a backend controller ('url' => Backend::url('golem15/translate/locales')), so the translate plugin's Locales screen lives on the Settings page, not in the main navigation. SummerCMS core only supports singleton-model settings, so the Go translate plugin registers a main navigation item instead. This task adds an additive `Controller` field to pact.SettingsItem with registry validation, metadata output, permission filtering and singleton-endpoint 404s, plus the SPA card link and Settings-aware shell. The translate plugin then switches from Navigation() to Settings().
Purpose: restore the WinterCMS admin shape (Locales under Settings) without breaking the pact contract that many plugins and apps depend on, and fix a visibly wrong control.
Output: three summercms.go commits (switch fix with dist; Go contract with OpenAPI, READMEs and docs; SPA with tests and dist) and one sm-translate-plugin commit.
</objective>
<execution_context>
@~/.claude/gsd-core/workflows/execute-plan.md
@~/.claude/gsd-core/templates/summary.md
</execution_context>
<context>
@.planning/STATE.md
@CLAUDE.md
Facts established during planning (do not re-derive):
- The SPA builds into modules/boardwalk/dist (admin/vite.config.ts outDir, emptyOutDir). Rebuild with `npm --prefix admin run build` (vue-tsc then vite build). `scripts/check-admin-dist.sh` fails when dist differs from a fresh build. Stage dist with `git add -A modules/boardwalk/dist` so hashed asset renames and deletions are captured (see commits f15b38e and a586357).
- No admin test asserts the switch's w-11 or size-5 classes. tests/form/fields.test.ts and tests/form/registry.test.ts mount SwitchField behaviourally.
- `scripts/check-admin-openapi.sh` (no argument) regenerates admin/openapi/admin.json and admin/src/api/schema.d.ts from the cabana swag annotations. `--check` fails on drift. Every SettingsEntry property is currently `required` in admin.json, so a non-omitempty `json:"controller"` becomes a required string. That matches NavigationEntry.Controller, which is also always present.
- admin/tests/fixtures/typed.ts types settings.json as the generated SettingsEntry. Once `controller` is required, every fixture entry needs `"controller": ""` or `npm --prefix admin run typecheck` fails. Build link entries inline in tests rather than adding them to the shared fixture (existing tests count its entries).
- The registry builds `reg.byID` (compiled controllers) before compileContributions runs, so link validation can check `reg.byID` the same way validateNavigation does (modules/cabana/registry.go ~line 322).
- protectSetting (modules/cabana/http.go ~line 501) resolves the code only through Registry.Setting, so Registry.Setting returning false for a link code turns all three singleton endpoints into 404s. http.go needs no edit.
- Reusable Go fixtures: modules/cabana/metadata_settings_test.go has contributionPlugin, metadataController (requires acme.demo.access), metadataRegistry() (byID has acme.demo.widgets) and metadataPlugin(). HTTP-level settings calls exist in modules/cabana/openapi_conformance_test.go (conformEnv, "GET /settings") and modules/cabana/security_coverage_test.go. Reuse whichever harness mounts the admin API with a backend principal.
- Settings category convention: every Go settings item uses its own plugin phrase key as Category (feedback and fonoteka use lang.plugin.tab, journal uses lang.journal.menu_label). Core has no "System" category key. The PHP reference examples/golem15-wintercms-starter/plugins/golem15/translate/Plugin.php registerSettings uses category 'golem15.translate::lang.plugin.name', label 'golem15.translate::lang.locale.title', description 'golem15.translate::lang.plugin.description', order 550 and permission golem15.translate.manage_locales, with no keywords. All three keys already exist in sm-translate-plugin/lang/en and lang/pl. Per the API-parity rule, follow the PHP reference and add no core category constant.
- sm-translate-plugin is a separate git repo whose go.mod has `replace git.golem15.com/golem15/summercms => ../summercms.go` (and a go.work with the same replace), so it builds against the local framework. Do not change its go.mod require version. Its admin harness (admin_harness_test.go: newAdminEnv, e.admin(t, permissions...), e.expect(...), adminAPI("/settings")) boots cabana with the plugin and Postgres via testcontainers.
- Consumers of the translate plugin (sm-grzybyfunkcjonalne-app, sm-summercmsio-app) pin it as a submodule. Bumping them is OUT of scope.
Commit rules: one logical change per commit; planning docs never go in a code commit; NO co-author trailers (the user's global CLAUDE.md overrides any attribution reminder); `go vet ./...` and `go test ./...` green at every commit. Do not commit anything under .planning (the orchestrator does).
</context>
<tasks>
<task type="auto">
<name>Task 1: Commit the pre-applied switch proportion fix with its rebuilt dist</name>
<files>admin/src/components/form/fields/SwitchField.vue, modules/boardwalk/dist/</files>
<read_first>admin/src/components/form/fields/SwitchField.vue, scripts/check-admin-dist.sh</read_first>
<action>
This change is already in the working tree. It must land first, alone, so the later SPA commit's dist rebuild does not mix in an unrelated fix.
1. Run `git status --short` in summercms.go. The only modified file must be admin/src/components/form/fields/SwitchField.vue. If anything else is dirty, stop and report it rather than building it into dist.
2. Confirm the track uses w-[44px] with h-[26px], and the knob uses size-[20px] with top-[3px], left-[3px] and translate-x-[18px]. The geometry is 3 + 20 + 18 + 3 = 44, so it is symmetric. Leave the colour, aria and transition classes untouched.
3. Grep admin/tests for any assertion on w-11 or size-5 tied to the switch. Planning found none. If one exists, update it to the pixel classes.
4. Run the switch tests, then `npm --prefix admin run build`, then `scripts/check-admin-dist.sh`.
5. Stage SwitchField.vue and `git add -A modules/boardwalk/dist`. Commit as `fix(admin): size the switch toggle in pixels at the 14px root` with a body explaining that the rem utilities shrank the track and knob under html font-size 14px. Add no co-author trailer.
</action>
<verify>
<automated>cd /media/nvme/dev/golem15/summercms.io/summercms/summercms.go && grep -q 'w-\[44px\]' admin/src/components/form/fields/SwitchField.vue && grep -q 'size-\[20px\]' admin/src/components/form/fields/SwitchField.vue && npm --prefix admin test -- tests/form/fields.test.ts tests/form/registry.test.ts && scripts/check-admin-dist.sh && go test ./modules/boardwalk -count=1 && test -z "$(git status --short)"</automated>
</verify>
<done>SwitchField uses pixel width and knob sizes, the switch tests pass, dist matches a fresh build, boardwalk tests pass, and one commit holds SwitchField.vue plus modules/boardwalk/dist with no co-author trailer. The working tree is clean afterwards.</done>
</task>
<task type="tracer" tdd="true">
<name>Task 2: Settings entries that link to an admin controller, end to end (pact, cabana, OpenAPI, SPA, dist, docs)</name>
<files>modules/pact/capabilities.go, modules/pact/README.md, modules/cabana/settings.go, modules/cabana/registry.go, modules/cabana/navigation.go, modules/cabana/contracts.go, modules/cabana/settings_link_test.go, modules/cabana/README.md, docs/backend/settings.md, docs/setup/coming-from-wintercms.md, admin/openapi/admin.json, admin/src/api/schema.d.ts, admin/tests/fixtures/settings.json, admin/src/state/useSettings.ts, admin/src/views/SettingsIndexView.vue, admin/src/components/shell/PluginRail.vue, admin/src/components/shell/Breadcrumbs.vue, admin/src/app/pageTitle.ts, admin/tests/state/useSettings.test.ts, admin/tests/form/Settings.test.ts, admin/tests/shell/PluginRail.test.ts, admin/tests/shell/Breadcrumbs.test.ts, admin/tests/app/pageTitle.test.ts, modules/boardwalk/dist/</files>
<read_first>modules/pact/capabilities.go (SettingsItem, NavigationItem), modules/cabana/registry.go (compileContributions, validateNavigation), modules/cabana/settings.go (CompiledSetting, compileSetting), modules/cabana/navigation.go (SettingsEntry, Metadata, canOpen), modules/cabana/contracts.go (Registry.Setting), modules/cabana/metadata_settings_test.go (fixtures), admin/src/state/useSettings.ts, admin/src/state/useNavigation.ts, admin/src/app/controllerRoutes.ts, admin/src/views/SettingsIndexView.vue, admin/src/components/shell/PluginRail.vue, admin/src/components/shell/Breadcrumbs.vue, admin/src/app/pageTitle.ts, admin/tests/form/Settings.test.ts, admin/tests/shell/PluginRail.test.ts, admin/tests/shell/Breadcrumbs.test.ts, admin/tests/app/pageTitle.test.ts, docs/backend/settings.md</read_first>
<behavior>
- Go: a link item `{Code: "locales", Controller: "acme.demo.widgets", Permissions: ["acme.demo.manage_settings"]}` compiles from a plugin with no AdminFS. Registry.Setting("locales") returns false.
- Go: Controller plus Form, plus NewModel, or plus Model each fail compileContributions with an error naming the setting code. Controller "acme.demo.missing" fails with "cabana: setting locales references unknown controller acme.demo.missing". A link code equal to a singleton code fails with "duplicate setting".
- Go: Metadata for a developer holding acme.demo.access and acme.demo.manage_settings lists the link entry with Controller "acme.demo.widgets" and Model "", and the singleton entry with Controller "".
- Go: a principal granted only acme.demo.manage_settings (it passes the item but cannot open the controller, which requires acme.demo.access) does not see the link entry, but does see the singleton.
- Go: GET /settings/locales/schema, GET /settings/locales and PUT /settings/locales answer 404 not_found for an allowed backend principal. GET /settings JSON has a "controller" key on every entry.
- SPA: settingsPath returns /settings/<encoded code> for a singleton, /acme/demo/locales for controller acme.demo.locales, and /settings for a malformed controller ID. settingsLinkFor('acme.demo.locales') finds the link entry; settingsLinkFor('') and null find nothing (singleton entries with an empty controller never match).
- SPA: the settings index card for a link entry has an href ending in /acme/demo/locales. Singleton cards are unchanged.
- SPA: on route /acme/demo/locales with no navigation entry for acme.demo, data-rail-settings has aria-current="page" and no rail item is current. Breadcrumbs are Settings (link to /settings) > entry label (current), the record route adds the record title, and the document title starts with the entry label. On a navigation controller route, Settings is not current.
</behavior>
<action>
Write the failing Go tests and SPA tests from the behavior block first, then implement. This tracer wires one settings link through every layer. The behaviour-preserving rule is absolute: keyed pact.SettingsItem literals elsewhere must compile and behave unchanged, and singleton settings must not change.
Go contract, commit A:
- modules/pact/capabilities.go: add `Controller string` to SettingsItem, after Permissions and before Form, with no json tag (like the other exported fields). Its doc comment says: when set, the entry is a link to that admin controller ID (vendor.plugin.controller), the equivalent of WinterCMS registerSettings 'url' => Backend::url(...). A link entry declares no Model, Form or NewModel and is not a singleton. Extend the SettingsItem doc comment to say an item is either a singleton form or a controller link.
- modules/cabana/settings.go: add `compileSettingLink(pluginID string, item pact.SettingsItem) (*CompiledSetting, error)`. It errors when item.Form != "" or item.NewModel != nil, with message "cabana: setting %s links controller %s and declares a singleton form; declare one". It errors when item.Model != "", with message "cabana: setting %s links controller %s and names a model". Otherwise it returns &CompiledSetting{PluginID, Item} with Form and Writable nil. Rejecting Model is a planning discretion choice: a link has no model, so the SPA never sees a misleading model string. Update the CompiledSetting doc comment to say Form and Writable are nil for a link entry.
- modules/cabana/registry.go compileContributions: in the HasSettings loop, keep the identifier and duplicate checks first. If item.Controller != "", call compileSettingLink, store the result in reg.settings[item.Code] and continue. This skips the AdminFS requirement and compileSetting. Otherwise keep the existing path unchanged. In the post-collection loop over reg.settings, after validatePermissions, a link entry whose controller is not in reg.byID returns "cabana: setting %s references unknown controller %s", mirroring validateNavigation.
- modules/cabana/contracts.go Registry.Setting: return (nil, false) when the stored setting's Item.Controller is non-empty. Update the doc comment: link entries are not singletons and are not returned. This alone makes protectSetting answer 404 for all three singleton endpoints. Leave http.go unchanged.
- modules/cabana/navigation.go: add `Controller string` with tag `json:"controller"` (not omitempty, matching NavigationEntry) to SettingsEntry, after Model, with a doc comment ("the admin controller a link entry opens; empty for a singleton settings form"). In Metadata's settings loop, skip an item unless Allows(principal, item.Permissions) holds and also either item.Controller is empty or r.canOpen(principal, item.Controller) is true. Populate Controller in the appended entry.
- modules/cabana/settings_link_test.go (new, package cabana): implement the Go behavior cases, reusing contributionPlugin, metadataRegistry, metadataPlugin and metadataController from metadata_settings_test.go. Add a plugin with nil fsys for the no-AdminFS case. For the 404 and JSON-shape cases, reuse the existing admin HTTP harness (see the Context facts) instead of writing a new one.
- Run `scripts/check-admin-openapi.sh` with no argument to regenerate admin/openapi/admin.json and admin/src/api/schema.d.ts. Confirm the only diff is the new controller property, which is required on cabana.SettingsEntry.
- admin/tests/fixtures/settings.json: add `"controller": ""` to every existing list entry so `npm --prefix admin run typecheck` stays green against the regenerated schema.d.ts.
- Docs in the same commit, per the CLAUDE.md documentation rule:
- modules/pact/README.md: extend the SettingsItem row and the Features bullet to cover the controller link.
- modules/cabana/README.md: extend the Features bullet on settings, covering link entries, their validation, the 404 on singleton endpoints and the canOpen filtering. Update any Registry.Setting or SettingsEntry rows.
- docs/backend/settings.md: add a section "Linking a settings entry to an admin controller". Explain the WinterCMS 'url' equivalent and the Controller field, that a link entry declares no Model, Form or NewModel, the start-up validation, the permission plus controller-access filtering and how the SPA opens the controller, with a short Go example using neutral names such as acme.blog. A plain go fence without src= is fine unless TestDocsTree requires otherwise.
- docs/setup/coming-from-wintercms.md: extend the registerSettings row to mention 'url' mapping to the Controller field.
- Name no consuming application. If TestDocsTree rejects a field selector such as pact.SettingsItem.Controller, write "the `Controller` field of `pact.SettingsItem`" instead.
- Commit A, Go plus OpenAPI plus fixture plus docs: `feat(cabana): settings entries that link to an admin controller`. Add no co-author trailer.
SPA, commit B:
- admin/src/state/useSettings.ts: import controllerPath from ../app/controllerRoutes. Export `settingsPath(entry: SettingsEntry): string`. When entry.controller is non-empty it returns controllerPath(entry.controller), falling back to '/settings' when that is null. Otherwise it returns `/settings/` plus encodeURIComponent(entry.code). Export `settingsLinkFor(controllerId: string | null): SettingsEntry | null`. It returns null for null or an empty string, and otherwise the first entry whose non-empty controller equals the ID. Add both to useSettings().
- admin/src/views/SettingsIndexView.vue: bind the card RouterLink `:to` to settingsPath(entry). Keep data-settings. Update the header comment.
- admin/src/components/shell/PluginRail.vue: inSettings is also true when active is null and settingsLinkFor(controllerIdFromPath(route.path)) is non-null. A controller that also has a navigation entry keeps its plugin highlight. Update the comment.
- admin/src/components/shell/Breadcrumbs.vue: when activeEntry is null, look up settingsLinkFor(controllerIdFromPath(route.path)). If an entry is found, the crumbs are the Settings label (t('backend::lang.nav.settings'), to /settings), then the entry label (to its controller path), then the record title on record and create routes when non-empty, using the same rule as the plugin branch. Otherwise return [] as today.
- admin/src/app/pageTitle.ts: controllerLabel falls back to settingsLinkFor(controller)?.label after the section and plugin labels.
- Tests: implement the SPA behavior cases in useSettings.test.ts, Settings.test.ts (index), PluginRail.test.ts, Breadcrumbs.test.ts and pageTitle.test.ts. Build link entries inline (for example `setSettings([...settingsFixture.list.data, linkEntry])` with controller 'acme.demo.locales' and a vendor or plugin absent from the navigation fixture). Do not change existing assertions.
- `npm --prefix admin run build`, then `scripts/check-admin-dist.sh`.
- Commit B, SPA plus tests plus dist (`git add -A modules/boardwalk/dist`): `feat(admin): open controller-link settings entries from the settings index`. Add no co-author trailer.
</action>
<verify>
<automated>cd /media/nvme/dev/golem15/summercms.io/summercms/summercms.go && test -z "$(gofmt -l modules/pact modules/cabana)" && go vet ./modules/pact ./modules/cabana && go test ./modules/cabana -run 'TestSettingsLink' -count=1 -v && go test ./modules/pact ./modules/cabana ./modules/boardwalk -count=1 && scripts/check-admin-openapi.sh --check && grep -q '"controller"' admin/openapi/admin.json && npm --prefix admin run typecheck && npm --prefix admin test && scripts/check-admin-dist.sh && go test ./cmd/summer -run TestDocsTree -count=1</automated>
</verify>
<done>Every Go and SPA behavior case failed before the change and passes after it. Existing metadata, settings-singleton, conformance, security-coverage and SPA tests pass unchanged apart from the fixture's added controller key. admin.json and schema.d.ts match a fresh generation. Dist matches a fresh build. TestDocsTree passes. Two commits exist (feat(cabana) with docs, feat(admin) with dist), neither with a co-author trailer.</done>
</task>
<task type="auto" tdd="true">
<name>Task 3: Translate plugin lists Locales on the Settings page instead of the main navigation</name>
<files>/media/nvme/dev/golem15/summercms.io/summercms/sm-translate-plugin/admin_navigation.go, /media/nvme/dev/golem15/summercms.io/summercms/sm-translate-plugin/admin_settings.go, /media/nvme/dev/golem15/summercms.io/summercms/sm-translate-plugin/admin.go, /media/nvme/dev/golem15/summercms.io/summercms/sm-translate-plugin/plugin_test.go, /media/nvme/dev/golem15/summercms.io/summercms/sm-translate-plugin/locales_admin_test.go, /media/nvme/dev/golem15/summercms.io/summercms/sm-translate-plugin/README.md</files>
<read_first>/media/nvme/dev/golem15/summercms.io/summercms/sm-translate-plugin/admin_navigation.go, /media/nvme/dev/golem15/summercms.io/summercms/sm-translate-plugin/admin.go, /media/nvme/dev/golem15/summercms.io/summercms/sm-translate-plugin/admin_harness_test.go (newAdminEnv, admin, expect, adminAPI), /media/nvme/dev/golem15/summercms.io/summercms/sm-translate-plugin/locales_admin_test.go, /media/nvme/dev/golem15/summercms.io/summercms/sm-translate-plugin/README.md, /media/nvme/dev/golem15/summercms.io/summercms/examples/golem15-wintercms-starter/plugins/golem15/translate/Plugin.php (registerSettings)</read_first>
<behavior>
- Plugin.Settings() returns exactly one item. Its fields are: Code "locales", Label "golem15.translate::lang.locale.title", Description "golem15.translate::lang.plugin.description", Category "golem15.translate::lang.plugin.name", Icon "languages", Order 550, Permissions [controllers.PermissionManageLocales], Controller "golem15.translate.locales". Model and Form are empty, NewModel is nil and there are no Keywords.
- *Plugin does not implement pact.HasNavigation, and does implement pact.HasSettings.
- Harness, with Postgres: an admin holding golem15.translate.manage_locales gets a GET /settings entry with code locales, controller golem15.translate.locales, model "" and a translated label (English "Manage languages"). GET /navigation contains no entry with code translate. GET /settings/locales and GET /settings/locales/schema answer 404.
- Harness: an admin without manage_locales gets no locales entry from GET /settings.
</behavior>
<action>
Work in /media/nvme/dev/golem15/summercms.io/summercms/sm-translate-plugin, a separate git repo. Use absolute paths. Run `pwd` if a path error appears. Start with `git status --short` and confirm the repo is clean. It builds against the local framework through the go.mod and go.work replace, so the Task 2 Controller field is visible. Do not change the go.mod require version.
1. Write the failing tests first.
- In plugin_test.go, add a test asserting the exact Settings() item from the behavior block, compared with reflect.DeepEqual against an expected pact.SettingsItem (NewModel nil on both sides). Assert the interface facts with type assertions on any(&Plugin{}).
- In locales_admin_test.go, add a harness test covering the GET /settings, GET /navigation and 404 cases using newAdminEnv, e.admin and e.expect.
2. Create admin_settings.go (package translate). It defines `func (*Plugin) Settings() []pact.SettingsItem` returning the single item, using controllers.PermissionManageLocales. Its doc comment says it replaces registerSettings and that Locales opens from the Settings page, as the PHP plugin's 'url' => Backend::url('golem15/translate/locales') entry did. The values follow the PHP reference exactly: category is the plugin name key, and there are no keywords. Planning chose the reference over a core "System" category because core has none and every other Go settings item uses its own plugin phrase key.
3. Remove admin_navigation.go with `git rm`. In admin.go's interface-assertion block, replace the navigation assertion with `_ pact.HasSettings = (*Plugin)(nil)`.
4. All three lang keys already exist in lang/en/lang.yaml and lang/pl/lang.yaml (locale.title, plugin.description, plugin.name). Add no lang changes.
5. README.md:
- Extend the "Locales admin" Features bullet: Locales opens from the Settings page (category Translate) through a settings entry linking to the controller, not from the main navigation, matching the PHP plugin.
- Add an API reference row `translate.Plugin.Settings`: one settings entry, locales, linking to controller golem15.translate.locales and gated by golem15.translate.manage_locales.
- Note the framework requirement: the entry uses the Controller field of pact.SettingsItem, so the plugin needs a framework release that has it. Do not edit go.mod.
6. Run gofmt, go vet ./... and go test ./... -count=1. The harness tests need Docker for testcontainers.
7. Commit in sm-translate-plugin as `feat(settings): open Locales from the Settings page`. Add no co-author trailer.
8. Back in summercms.go, run the full `go vet ./...` and `go test ./... -count=1` once more to confirm both repos are green. Leave the consuming apps' translate submodules unbumped and note that in the SUMMARY as a follow-up.
</action>
<verify>
<automated>cd /media/nvme/dev/golem15/summercms.io/summercms/sm-translate-plugin && test ! -e admin_navigation.go && test -z "$(gofmt -l .)" && go vet ./... && go test ./... -count=1 && test -z "$(git status --short)" && cd /media/nvme/dev/golem15/summercms.io/summercms/summercms.go && go vet ./... && go test ./... -count=1</automated>
</verify>
<done>The translate plugin implements HasSettings and not HasNavigation. Settings() returns the single locales link entry matching the PHP registerSettings values. The harness proves the entry appears on /settings for manage_locales admins only, is absent from /navigation, and that the singleton endpoints 404 for it. The README documents it. One sm-translate-plugin commit with no co-author trailer. go vet and go test are green in both repos.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| backend principal -> GET /settings | Metadata decides which settings entries, and which controller IDs, a principal may learn about |
| backend principal -> /settings/{code} endpoints | Singleton read and write endpoints keyed by a client-supplied code |
| plugin registration -> cabana registry | Compiled plugins declare settings entries that the registry trusts at boot |
| server metadata -> SPA router | The SPA turns a server-sent controller ID into a client route |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-quick-261007-p4y-01 | Information disclosure | cabana Registry.Metadata settings loop | medium | mitigate | A link entry is listed only when Allows(principal, item.Permissions) and r.canOpen(principal, item.Controller) both hold, so a principal never learns a controller ID it cannot open; covered by the restricted-principal case in settings_link_test.go |
| T-quick-261007-p4y-02 | Tampering / Denial of service | protectSetting via Registry.Setting | medium | mitigate | Registry.Setting returns false for link entries, so GET/PUT on a link code answer 404 before any nil Form or model factory is touched (no panic, no write); covered by the three 404 cases |
| T-quick-261007-p4y-03 | Spoofing / Tampering | compileContributions link branch | low | mitigate | Boot fails when a link names an unregistered controller, combines Controller with Form, NewModel or Model, or reuses a settings code; there is no runtime path to a dangling or ambiguous entry |
| T-quick-261007-p4y-04 | Tampering | SPA settingsPath | low | mitigate | The route comes only from controllerPath, which accepts exactly three [A-Za-z0-9_-] segments and otherwise falls back to /settings; no server string is interpolated into a URL unchecked |
| T-quick-261007-p4y-05 | Elevation of privilege | controller pages opened from Settings | low | accept | Opening a controller still goes through the existing controller guard (requiredOf) on every API call; the Settings link adds no access path |
| T-quick-261007-p4y-SC | Tampering | npm/go installs | low | accept | No new npm or Go dependency; the work uses the committed lockfile, the existing Vitest setup and the pinned swag in check-admin-openapi.sh |
</threat_model>
<verification>
- summercms.go git log shows three new commits in order: fix(admin) switch with dist, feat(cabana) with OpenAPI, fixture, READMEs and docs, and feat(admin) with tests and dist. None has a co-author trailer and none touches .planning.
- sm-translate-plugin git log shows one new commit with no co-author trailer; admin_navigation.go is removed.
- scripts/check-admin-openapi.sh --check, scripts/check-admin-dist.sh and go test ./cmd/summer -run TestDocsTree pass.
- go vet ./... and go test ./... pass in both repos; npm --prefix admin test and typecheck pass.
</verification>
<success_criteria>
- The toggle renders a 44x26 track with a 20px knob and symmetric 3px insets.
- A plugin can declare a pact.SettingsItem that links to its admin controller. The Settings page shows it, permission- and controller-access-filtered, and opens the controller. The shell treats that controller as part of Settings. Singleton endpoints refuse it with 404. Existing singleton settings and other plugins are unaffected.
- The translate plugin's Locales screen is reached from Settings under Translate, as in WinterCMS, and no longer from the main navigation.
</success_criteria>
<output>
Create `.planning/quick/261007-p4y-toggle-proportions-and-settings-controll/261007-p4y-SUMMARY.md` when done. Record:
- the discretion choices: Model rejected on link entries; category follows the PHP reference rather than a core "System" constant;
- the follow-up that consuming apps must bump their translate submodule and need a framework tag that includes the Controller field.
</output>

View File

@@ -0,0 +1,155 @@
---
phase: quick-261007-p4y
plan: 01
status: complete
subsystem: admin, cabana, pact, sm-translate-plugin
tags: [admin-spa, settings, pact, cabana, translate, parity]
requires: []
provides:
- "pact.SettingsItem.Controller: a settings entry that links to an admin controller"
- "cabana link compile/validation, metadata filtering (Allows + canOpen), singleton-endpoint 404s"
- "SPA settingsPath / settingsLinkFor; Settings-aware rail, breadcrumbs and page title"
- "translate plugin: Locales on the Settings page instead of the main navigation"
affects: [sm-grzybyfunkcjonalne-app, sm-summercmsio-app]
tech-stack:
added: []
patterns:
- "Settings entries share one code namespace across singleton and link entries"
- "Registry.Setting hides link entries so protectSetting answers 404 with no http.go change"
key-files:
created:
- modules/cabana/settings_link_test.go
- modules/cabana/example_settings_test.go
- ../sm-translate-plugin/admin_settings.go
modified:
- admin/src/components/form/fields/SwitchField.vue
- modules/pact/capabilities.go
- modules/cabana/settings.go
- modules/cabana/registry.go
- modules/cabana/navigation.go
- modules/cabana/contracts.go
- modules/cabana/example_test.go
- admin/openapi/admin.json
- admin/src/api/schema.d.ts
- admin/tests/fixtures/settings.json
- admin/src/state/useSettings.ts
- admin/src/views/SettingsIndexView.vue
- admin/src/components/shell/PluginRail.vue
- admin/src/components/shell/Breadcrumbs.vue
- admin/src/app/pageTitle.ts
- modules/pact/README.md
- modules/cabana/README.md
- docs/backend/settings.md
- docs/setup/coming-from-wintercms.md
- modules/boardwalk/dist/
- ../sm-translate-plugin/admin.go
- ../sm-translate-plugin/plugin_test.go
- ../sm-translate-plugin/locales_admin_test.go
- ../sm-translate-plugin/README.md
deleted:
- ../sm-translate-plugin/admin_navigation.go
decisions:
- "A settings link entry rejects Model as well as Form/NewModel, so the SPA never sees a misleading model string"
- "Translate's settings category follows the PHP reference (golem15.translate::lang.plugin.name); no core System category constant added"
- "The docs Go example for the link entry lives in its own file (example_settings_test.go) because example_controller_test.go is embedded whole in admin-controllers.md"
metrics:
duration: "13 min"
completed: 2026-10-07
estimate:
tokens: 140000
tasks: 3
actuals:
tokens: 15600
tasks: 3
commits: 3
plan_head_before: a586357a346a66111d37bd8c1d564c769a412715
plan_head_after: 0b09acdb0cca4955a2dcba2e9a2708a06b032104
---
# Quick 261007-p4y: Toggle proportions and Settings controller links Summary
The admin switch now has pixel-based proportions: a 44x26 track and a 20px knob. Settings entries can now link to an admin controller through an additive `pact.SettingsItem.Controller` field, covered from start-up validation through to the Settings page. The translate plugin now lists Locales on the Settings page, under the Translate category, as WinterCMS did.
## Commits
summercms.go (3 commits, measured `git rev-list --count a586357..HEAD` = 3):
| Task | Commit | Message |
|------|--------|---------|
| 1 | b040ee3 | fix(admin): size the switch toggle in pixels at the 14px root |
| 2A | 4a9b089 | feat(cabana): settings entries that link to an admin controller |
| 2B | 0b09acd | feat(admin): open controller-link settings entries from the settings index |
sm-translate-plugin (separate repo, 1 commit):
| Task | Commit | Message |
|------|--------|---------|
| 3 | bde3f1a | feat(settings): open Locales from the Settings page |
No commit has a co-author trailer. No commit touches `.planning`. Nothing was pushed.
## What was built
- **Switch:** `w-[44px]` and `size-[20px]` give 3 + 20 + 18 + 3 = 44, with a symmetric inset. Dist was rebuilt and committed on its own.
- **pact:** `SettingsItem.Controller` is a new additive field placed before `Form`. Keyed literals in other code compile unchanged, and a grep of the meta repo found no unkeyed ones.
- **cabana:**
- `compileSettingLink` rejects a link that also sets Form, NewModel or Model.
- The link branch in `compileContributions` skips the AdminFS requirement.
- After every plugin is collected, start-up fails on an unknown controller with `cabana: setting <code> references unknown controller <id>`.
- `Registry.Setting` hides link entries, so all three singleton endpoints answer 404.
- `SettingsEntry.Controller` is serialised as `json:"controller"` and is always present.
- Metadata lists a link entry only when the principal passes `Allows(item.Permissions)` and `canOpen(controller)`.
- **OpenAPI:** `controller` is a required string on `cabana.SettingsEntry` in admin.json and schema.d.ts. The settings fixture entries now carry `"controller": ""`.
- **SPA:**
- Index cards link through `settingsPath`.
- The rail marks Settings current on a linked controller that has no navigation entry. A linked controller that does have a navigation entry keeps its plugin highlighted.
- Breadcrumbs on such a page read `Ustawienia > <label> [> record]`.
- The page title falls back to the entry label.
- **Translate:** `Plugin.Settings()` returns the single locales link entry, with values taken from the PHP registerSettings reference. `admin_navigation.go` is removed, and `HasNavigation` is replaced by `HasSettings` in the assertion block.
## TDD evidence
- Go RED: `settings_link_test.go` failed to build (`unknown field Controller in struct literal of type pact.SettingsItem`). After the change it passes: 4 tests plus 9 subtests.
- SPA RED: 6 new cases failed across useSettings, Settings, PluginRail, Breadcrumbs and pageTitle. After the change it passes: all 1083 tests in 74 files.
- Translate RED: the build failed (`p.Settings undefined`). After the change it passes, and the harness test ran against Postgres via testcontainers.
## Verification
- summercms.go: `go vet ./...` and `go test ./... -count=1` pass. So do `scripts/check-admin-openapi.sh --check`, `scripts/check-admin-dist.sh`, `go test ./cmd/summer -run TestDocsTree`, `npm --prefix admin run typecheck` and `npm --prefix admin test`.
- sm-translate-plugin: `gofmt -l .` is clean, and `go vet ./...` and `go test ./... -count=1` pass.
- Both working trees are clean, apart from the orchestrator-owned `.planning/quick/...` directory.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 3 - Blocking] The docs snippet needed a `src=` reference and a shorter description**
- **Found during:** Task 2, commit A
- **Issue:** TestDocsTree rejects a Go fence without `src=` and a frontmatter description over 160 characters. Adding the example to `example_controller_test.go` changed a whole-file snippet in `docs/backend/admin-controllers.md`.
- **Fix:** I added `modules/cabana/example_settings_test.go`, which holds `LinkedBlogPlugin` and a `docs:start settings-link` region. `TestDocsSettingsLink` in `example_test.go` activates the plugin through `cabana.Activate`, which proves the link compiles end to end. The settings.md description is shortened.
- **Commit:** 4a9b089
**2. [Process] The admin-behaviour paragraph moved to commit B**
- The settings.md paragraph about the rail and breadcrumbs went into the SPA commit (0b09acd) rather than commit A, so each commit's docs match its code.
**3. [Test fixture choice] Rail, breadcrumb and title tests use `acme.lang.locales`**
- The plan suggested `acme.demo.locales`, but `acme.demo` is in the navigation fixture, which would make the plugin highlight win. These tests use a plugin that is absent from navigation, `acme.lang.locales`. The path tests in useSettings still use `acme.demo.locales`.
## Follow-ups
- Consuming apps (sm-grzybyfunkcjonalne-app, sm-summercmsio-app) must bump their translate submodule to bde3f1a or later. They also need a framework tag that includes `pact.SettingsItem.Controller` (4a9b089 or later). Neither bump is done here, because it was out of scope.
- The PHP plugin also registers a `messages` settings link. There is no Messages admin controller in the Go port yet, so only `locales` is registered.
## Known Stubs
None.
## Threat Flags
None. All new surface is covered by the plan's threat register: T-01 has the metadata filter tests, T-02 the 404 tests, T-03 the compile validation tests, and T-04 the settingsPath fallback test.
## Self-Check: PASSED
- FOUND: modules/cabana/settings_link_test.go, modules/cabana/example_settings_test.go, ../sm-translate-plugin/admin_settings.go
- MISSING (intended): ../sm-translate-plugin/admin_navigation.go
- FOUND commits: b040ee3, 4a9b089, 0b09acd (summercms.go); bde3f1a (sm-translate-plugin)