diff --git a/.planning/phases/17-blog-companions/17-04-SUMMARY.md b/.planning/phases/17-blog-companions/17-04-SUMMARY.md new file mode 100644 index 0000000..e399fd4 --- /dev/null +++ b/.planning/phases/17-blog-companions/17-04-SUMMARY.md @@ -0,0 +1,336 @@ +--- +phase: 17-blog-companions +plan: 04 +subsystem: plugins +tags: [go, faq, menu, signpost, seaglass, cabana, surf, postgres, php-parity, public-api] + +requires: + - phase: 17-01 + provides: cabana repeater, richeditor with seaglass sanitizing and save notices, dependsOn with DependentOptionsProvider, config_reorder.yaml with partition, signpost registry and ReferenceOptions + - phase: 17-02 + provides: admin SPA for repeater, rich editor, dependent dropdown and reorder; summercms v0.1.23 + - phase: 17-03 + provides: Journal menu item types (all-journal-categories target of the Winter type map), sitemap plugin layout to copy +provides: + - new local repository sm-faq-plugin (main, no remote), plugin golem15.faq (Requires golem15.translate) + - new local repository sm-menu-plugin (main, no remote), plugin golem15.menu (no Requires) + - GET /_faq/api/v1/faqs/{slug}?locale=xx (bucket faq-public-api), approved shape D-17 + - GET /_menu/api/v1/menus/{code}?locale=xx (bucket menu-public-api), approved shape D-16 + - faq:import-winter and menu:import-winter with read-only Winter export SQL + - admin screens FAQs (golem15.faq.faqs) and Menus / Menu entries with tree reorder (golem15.menu.manage_menu) +affects: [17-05 site integration (remotes, submodules, go.mod bump, cors.paths _menu/api/* and _faq/api/*, Nuxt layers consume the two shapes), 17-06 unit tests] + +actuals: + tokens: 49970 + tasks: 5 + commits: 6 +plan_head_before: b1213e1250bc59bd353b0906fcecb21e6d44f118 +plan_head_after: b1213e1250bc59bd353b0906fcecb21e6d44f118 +repo_heads: + sm-faq-plugin: {before: "(new repository)", after: 1585f06, commits: 3} + sm-menu-plugin: {before: "(new repository)", after: 33a65ad, commits: 3} + +tech-stack: + added: [] + patterns: + - "Public read APIs check the path parameter in the handler (regexp) instead of a route Where, so every miss is the plugin's JSON 404" + - "HTML served from rows that may bypass the admin save gate is passed through seaglass.Sanitize on output" + - "Menu tree resolution behind a small classes.Resolver interface that *signpost.Registry satisfies; list vs single decided by the resolved item having a URL (pact.ResolvedMenuItem contract)" + - "Required belongsTo relations checked in FormAfterCreate/FormAfterUpdate with a 422 and rollback, because cabana writes relation keys after the model rules" + +key-files: + created: + - ../sm-faq-plugin/plugin.go + - ../sm-faq-plugin/routes.go + - ../sm-faq-plugin/models/faq.go + - ../sm-faq-plugin/models/faq/fields.yaml + - ../sm-faq-plugin/controllers/faqs.go + - ../sm-faq-plugin/controllers/api/faqs.go + - ../sm-faq-plugin/console/import_winter.go + - ../sm-faq-plugin/scripts/winter-export.sql + - ../sm-faq-plugin/updates/202610110001_create_golem15_faq_faqs.go + - ../sm-faq-plugin/faq_smoke_test.go + - ../sm-faq-plugin/README.md + - ../sm-menu-plugin/plugin.go + - ../sm-menu-plugin/routes.go + - ../sm-menu-plugin/models/menu.go + - ../sm-menu-plugin/models/menu_entry.go + - ../sm-menu-plugin/classes/links.go + - ../sm-menu-plugin/classes/tree.go + - ../sm-menu-plugin/controllers/menuentries.go + - ../sm-menu-plugin/controllers/helpers.go + - ../sm-menu-plugin/controllers/menuentries/config_reorder.yaml + - ../sm-menu-plugin/controllers/api/menus.go + - ../sm-menu-plugin/console/import_winter.go + - ../sm-menu-plugin/scripts/winter-export-menus.sql + - ../sm-menu-plugin/scripts/winter-export-entries.sql + - ../sm-menu-plugin/updates/202610110003_add_url_pattern_to_golem15_menu_menu_entries.go + - ../sm-menu-plugin/menu_smoke_test.go + - ../sm-menu-plugin/README.md + modified: + - .planning/phases/17-blog-companions/deferred-items.md + +key-decisions: + - "Task 4 (D-16, D-17) answered \"approve\": both public API shapes built exactly as proposed, including open details (a) and (b)" + - "Menu list vs single registry type is decided by the resolved item: no URL means a list type (keeps stored URL and title), per the pact.ResolvedMenuItem contract; resolved sub-items of any registry entry follow its stored children as generated children" + - "Registry entries are resolved with the type's Nesting and UnprefixedLocale \"\"; ?locale only picks among the returned alternates (the menu plugin does not depend on translate)" + - "Resolved and generated URLs pass the same AllowedURL check as stored URLs (defence in depth)" + - "css_class, icon and code serialize as null when empty; items is always an array; menu css_class null when empty" + - "Path parameters (menu code, FAQ slug) are validated in the handlers, not with route Where, so malformed values get the JSON 404 body" + - "FAQ default locale = enabled is_default translate locale; an unknown or disabled ?locale is 404" + - "Buckets live in plugin.go (acceptance grep) and routes in routes.go; constants faq.BucketPublicAPI and menu.BucketPublicAPI" + +patterns-established: + - "Plugin public API smoke tests insert rows with raw SQL to prove output sanitizing and scheme filtering independent of the admin save gate, and exhaust the throttle bucket from one client address" + +requirements-completed: [SC-1, SC-2, SC-3, D-02, D-05, D-07, D-08, D-09, D-10, D-16, D-17] + +coverage: + - id: D1 + description: "FAQ admin: repeater with richeditor answers, script and onclick stripped on save with a notice under content.1.answer, locale column in the list, locale required (422, nothing written), 403 without golem15.faq.faqs" + requirement: "SC-1" + verification: + - kind: integration + ref: "../sm-faq-plugin/faq_smoke_test.go#TestFaqAdminSmoke" + status: pass + human_judgment: false + - id: D2 + description: "Menu admin: PHP beforeSave rules, D-05 type set, dependent reference recheck, url_scheme and target 422s, parent cycle, menu-scoped reorder (409 for a foreign entry), menu filter" + requirement: "D-09" + verification: + - kind: integration + ref: "../sm-menu-plugin/menu_smoke_test.go#TestMenuAdminSmoke" + status: pass + human_judgment: false + - id: D3 + description: "Winter imports: FAQ locale remap by code (unknown aborts), ids kept, sequence bumped, --force/--dry-run/empty file; menu link->url and blog_categories->all-journal-categories, page subtree dropped and reported, ids and parent_id kept, sequences bumped, dry run writes nothing" + requirement: "D-05" + verification: + - kind: integration + ref: "../sm-faq-plugin/faq_smoke_test.go#TestFaqImportSmoke" + status: pass + - kind: integration + ref: "../sm-menu-plugin/menu_smoke_test.go#TestMenuImportSmoke" + status: pass + human_judgment: false + - id: D4 + description: "FAQ API: default locale, ?locale=en/pl, raw-SQL unsafe HTML served sanitized, null description, duplicate slug serves lowest id, 404 JSON for inactive, unknown slug, malformed slug, unknown and disabled locale, 429 after 120 requests per client" + requirement: "D-17" + verification: + - kind: integration + ref: "../sm-faq-plugin/faq_smoke_test.go#TestFaqAPISmoke" + status: pass + human_judgment: false + - id: D5 + description: "Menu API: 404 JSON for unknown, inactive and malformed codes; unpublished subtree absent; sort_order then id; javascript: URL and bad target null; single type resolved title/URL; url_pattern override; list type keeps stored title/URL with stored child first then generated children (id null, parent type); ?locale=en alternates; unregistered and unresolvable entries left out; 429 after 120 requests" + requirement: "D-16" + verification: + - kind: integration + ref: "../sm-menu-plugin/menu_smoke_test.go#TestMenuAPISmoke" + status: pass + human_judgment: false + - id: D6 + description: "FAQ and menu admin screens in the SPA (repeater, rich editor, dependent reference, reorder tree) render and read well, including the hook validation messages" + requirement: "SC-1" + verification: [] + human_judgment: true + rationale: "No browser test in this plan; hook ValidationError messages show as phrase keys (framework gap below)" + - id: D7 + description: "READMEs document API paths, example bodies, error bodies, buckets, host CORS paths, import workflow, Winter adaptations, and name no consuming application" + requirement: "SC-1" + verification: + - kind: other + ref: "grep -ci 'grzyby|horoskopia|fonoteka' ../sm-menu-plugin/README.md ../sm-faq-plugin/README.md -> 0 and 0" + status: pass + human_judgment: true + rationale: "The framework docs checker does not run on sibling plugin repos; identifiers were checked by hand" + +duration: 32min +completed: 2026-10-12 +status: complete +--- + +# Phase 17 Plan 04: FAQ and menu plugins Summary + +**Two new local plugin repositories: sm-faq-plugin (FAQ per slug and locale with a sanitized question repeater, Winter import, and an anonymous `/_faq/api/v1/faqs/{slug}` that re-sanitizes on output) and sm-menu-plugin (menus with link, divider and registry entries, a per-menu reorder tree, Winter import with the D-05 type map, and an anonymous `/_menu/api/v1/menus/{code}` that serves the published tree resolved through signpost), both with the API shapes a human approved** + +## Performance + +- **Duration:** about 32 min of execution (two agents across the Task 4 checkpoint) +- **Started:** 2026-10-12T15:20:26Z +- **Completed:** 2026-10-12T15:52:34Z +- **Tasks:** 5 (Task 1 tracer verified end-to-end before expansion; Task 4 blocking-human decision) +- **Files:** 28 tracked in sm-faq-plugin, 42 in sm-menu-plugin + +## Accomplishments + +- **sm-faq-plugin** (`git init -b main`, no remote, module `git.golem15.com/golem15/sm-faq-plugin`, plugin `golem15.faq`, Requires `golem15.translate`): table `golem15_faq_faqs` with the PHP columns and a (slug, locale_id) index; `models.FAQ` with jsonable content and the translate Locale relation; admin FAQs (order 500, permission `golem15.faq.faqs`) with a richeditor description and a content repeater whose answers are sanitized on save with notices; `faq:import-winter` (locale remap by code, ids kept, setval, `--force`, `--dry-run`) with `scripts/winter-export.sql`; public API `GET /_faq/api/v1/faqs/{slug}` (Task 5); README. +- **sm-menu-plugin** (`git init -b main`, no remote, module `git.golem15.com/golem15/sm-menu-plugin`, plugin `golem15.menu`): tables `golem15_menu_menus` and `golem15_menu_menu_entries` with the PHP columns plus nullable `url_pattern`; `models.MenuEntry.BeforeSave` with the PHP rules; admin Menus and Menu entries (type dropdown url, divider and registry types; reference `dependsOn: type` through `signpost.ReferenceOptions`; URL scheme and target checks; reorder tree partitioned by menu); `menu:import-winter` (type map, `--map`, page subtree drop, stale reference drop, report, setval) with two export SQL files; public API `GET /_menu/api/v1/menus/{code}` built by `classes.BuildTree` (Task 5); README. +- **Task 5 specifics:** routes in `routes.go` (anonymous groups `/_faq/api/v1` and `/_menu/api/v1` with `throttle:faq-public-api` / `throttle:menu-public-api`), buckets in `plugin.go` (120 per minute, key `faq-public|` / `menu-public|` plus `surf.ClientIP` behind trusted proxies), handlers write with `wire.WriteJSON`, no CORS in the plugins. + +## Task Commits + +**sm-faq-plugin** +1. **Task 1 (tracer): FAQ plugin, admin FAQ with sanitized question repeater** - `1c54912` (feat) +3. **Task 3: faq:import-winter and the read-only Winter export SQL** - `7413c69` (feat) +5. **Task 5: public FAQ API with output sanitizing, throttle bucket and README** - `1585f06` (feat) + +**sm-menu-plugin** +2. **Task 2: menu admin with registry types, URL checks and tree reorder** - `08f99b8` (feat) +3. **Task 3: menu:import-winter with the D-05 type map and read-only export SQL** - `e044bc7` (feat) +5. **Task 5: public menu API with registry resolution, throttle bucket and README** - `33a65ad` (feat) + +**Task 4:** checkpoint:decision, no commit. + +**summercms.go:** no code change (plan rule). Plan metadata: the docs commit with this SUMMARY and the deferred items, then the STATE/ROADMAP commit. Nothing was pushed or tagged. + +## Task 4 checkpoint: approved API shapes (D-16, D-17) + +**Proposal as presented (from the checkpoint state):** + +Menu: `GET /_menu/api/v1/menus/{code}?locale=xx` (code `[A-Za-z0-9_-]`). 200 `{"data":{"code","name","css_class","items":[Item]}}`; Item `{id (null for generated children), title, type (stored type; generated children carry parent's registry type key), url (string|null), target ("_self"|"_blank"|null), css_class, icon, code, items:[Item]}`. 404 `{"error":"Menu not found"}` for unknown/inactive. Only active menus, published entries (unpublished subtree hidden); order sort_order then id per parent; url entries return stored URL only if scheme allowlisted (http, https, mailto, tel, /, #) else null; dividers null url; single registry types resolved via signpost with entry url_pattern override, ?locale picks matching alternate else default; list registry types keep stored url/title with resolved children in items; unregistered type or unresolvable reference omitted. +- Open detail (a): single registry entry title = stored title if non-empty, else resolved title for locale. +- Open detail (b): generated child title = locale alternate's title if present, else resolved title; css_class/icon/code/target null. + +FAQ: `GET /_faq/api/v1/faqs/{slug}?locale=xx` (slug `[a-z0-9-]`). 200 `{"data":{"name","slug","locale","description","content":[{"question","answer"}]}}`; description and answers sanitized via seaglass.Sanitize on output, description may be null, questions plain; duplicate slugs → active row with lowest id; locale defaults to translate default locale. 404 `{"error":"FAQ not found"}` for unknown slug, inactive row, unknown/disabled locale. + +Both: anonymous, never behind backend middleware; throttled per client IP buckets menu-public-api / faq-public-api 120/min (429 surf `{"message":"Too Many Attempts."}`); wire.WriteJSON; no CORS in plugins (plan 05 host adds `_menu/api/*` and `_faq/api/*` to cors.paths). + +**Human reply (verbatim):** "approve" + +**Example bodies, produced by the shipped handlers from the smoke fixtures:** + +```json +GET /_menu/api/v1/menus/main?locale=en +{"data":{"code":"main","name":"Main","css_class":"nav-main","items":[{"id":1,"title":"Home","type":"url","url":"/","target":"_self","css_class":null,"icon":null,"code":"home","items":[]},{"id":2,"title":"about (en)","type":"acme-page","url":"/en/pages/about","target":null,"css_class":null,"icon":null,"code":null,"items":[]},{"id":3,"title":"Pages","type":"acme-list","url":"/pages","target":null,"css_class":null,"icon":null,"code":null,"items":[{"id":4,"title":"Child","type":"url","url":"https://example.test/x","target":"_blank","css_class":null,"icon":null,"code":null,"items":[]},{"id":null,"title":"home (en)","type":"acme-list","url":"/en/pages/home","target":null,"css_class":null,"icon":null,"code":null,"items":[]},{"id":null,"title":"about (en)","type":"acme-list","url":"/en/pages/about","target":null,"css_class":null,"icon":null,"code":null,"items":[]}]},{"id":5,"title":"","type":"divider","url":null,"target":null,"css_class":null,"icon":null,"code":null,"items":[]}]}} + +GET /_faq/api/v1/faqs/shipping (stored description had an onerror attribute, answers a script element and an onclick) +{"data":{"name":"Shipping","slug":"shipping","locale":"en","description":"

About shipping.

","content":[{"question":"How long?","answer":"

Two days.

"},{"question":"Where?","answer":"

Everywhere.

"}]}} + +GET /_faq/api/v1/faqs/nope +{"error":"FAQ not found"} +``` + +## Docker smoke tests + +All six ran on the local Docker daemon and passed after the Task 5 commits: `TestFaqAdminSmoke`, `TestFaqImportSmoke`, `TestFaqAPISmoke` (sm-faq-plugin), `TestMenuAdminSmoke`, `TestMenuImportSmoke`, `TestMenuAPISmoke` (sm-menu-plugin). `go vet ./...` and `go test -short ./...` were green in both repos at every commit; `GOWORK=off go vet ./...` also passes after `go mod tidy`. + +## Decisions Made + +See `key-decisions` in the front matter. + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 1 - Consistency] Timestamps are TIMESTAMPTZ** +- **Found during:** Task 1 +- **Issue:** The plan named `TIMESTAMP NULL`; journal and sitemap use `TIMESTAMPTZ`. +- **Fix:** `created_at` and `updated_at` are `TIMESTAMPTZ` on all three tables. +- **Committed in:** 1c54912, 08f99b8 + +**2. [Rule 1 - Parity] FAQ class string uses the real PHP namespace** +- **Found during:** Task 1 +- **Issue:** The plan wrote `Golem15\Faq\Models\FAQ`; the PHP class is `Golem15\FAQ\Models\FAQ`. +- **Fix:** MorphName, ModelName and modelClass use `Golem15\FAQ\Models\FAQ`. +- **Committed in:** 1c54912 + +**3. [Rule 3 - Blocking] Required relations checked after the write** +- **Found during:** Tasks 1 and 2 +- **Issue:** cabana runs model rules before it writes belongsTo keys from relation fields, so a `locale` or `menu` rule cannot see the value. +- **Fix:** `FormAfterCreate` / `FormAfterUpdate` answer 422 and roll back: FAQ `locale` → `errors.locale_required`, menu entry `menu` → `errors.menu_required`. +- **Committed in:** 1c54912, 08f99b8 + +**4. [Rule 3 - Blocking] Menu entry `menu` is a relation field** +- **Found during:** Task 2 +- **Issue:** The plan's menu dropdown served by `DropdownOptionsProvider`; the reorder `partition.relation` must name a belongsTo relation field. +- **Fix:** `menu` is `type: relation` (nameFrom name); `DropdownOptions` serves only `type`; menu and parent choices are ordered by name and title through `RelationExtendOptionsQuery`; all menus are offered. +- **Committed in:** 08f99b8 + +**5. [Rule 2 - Missing critical] Unknown type and parent cycle refused** +- **Found during:** Task 2 +- **Fix:** Saves refuse an unknown type (`errors.type_unknown`); updates refuse a parent that is the entry or a descendant (`errors.parent_cycle`). +- **Committed in:** 08f99b8 + +**6. [Rule 2 - Missing critical] Menu code uniqueness is a 422** +- **Found during:** Task 2 +- **Issue:** The unique index alone made a duplicate code a 500. +- **Fix:** Rule `unique:golem15_menu_menus` on `code`. +- **Committed in:** 08f99b8 + +**7. [Adaptation] Entries list shows every entry, filtered by menu** +- **Found during:** Task 2 +- **Issue:** PHP listed the top-level entries of one menu chosen in a toolbar select. +- **Fix:** The list shows every entry with the `menu_id` scope filter (named like the reorder partition column); the reorder page edits the tree. Documented in the menu README "Winter adaptations" and `config_list.yaml`. +- **Committed in:** 08f99b8 (README in 33a65ad) + +**8. [Rule 1 - Consistency] Parent always sets the menu** +- **Found during:** Task 2 +- **Issue:** PHP copied the parent's `menu_id` only when empty, which lets an entry sit in another menu than its parent. +- **Fix:** `BeforeSave` always takes `menu_id` from the parent when a parent is set. +- **Committed in:** 08f99b8 + +**9. [Rule 3 - Blocking] Extra file `sm-menu-plugin/controllers/helpers.go`** +- **Found during:** Task 2 +- **Fix:** A shared id formatting helper outside the plan's file list. +- **Committed in:** 08f99b8 + +**10. [Rule 1 - Pitfall 10] Imports keep PHP values** +- **Found during:** Task 3 +- **Fix:** The menu import writes with `SkipHooks` (keeps nest columns, sort_order, menu_id); the FAQ import does not sanitize on input, and the API sanitizes on output. +- **Committed in:** 7413c69, e044bc7 + +**11. [Rule 1 - Contract] Path parameters checked in the handler, not with Where** +- **Found during:** Task 5 +- **Issue:** The plan asked for a route `Where` on `code` and `slug`; a `Where` mismatch makes surf answer `http.NotFound` (text), so a malformed code would not get the approved `{"error":"Menu not found"}` / `{"error":"FAQ not found"}` body. Journal's author routes resolve in the handler for the same reason. +- **Fix:** `api.Show` in each plugin matches `^[A-Za-z0-9_-]+$` / `^[a-z0-9][a-z0-9\-]*$` and answers the JSON 404; tested with `main.json` and `Shipping`. +- **Committed in:** 1585f06, 33a65ad + +**12. [Rule 3 - Blocking] go.mod listed no test dependencies** +- **Found during:** Task 5 +- **Issue:** `go mod tidy` (recipe `GOPROXY=off GONOSUMDB=git.golem15.com GOFLAGS=-mod=mod`) added the testcontainers and pgx requires the smoke tests import. +- **Fix:** Tidied go.mod and go.sum committed with Task 5 in both repos. +- **Committed in:** 1585f06, 33a65ad + +### Plan notes + +- **Key link `controllers/api/menus.go` → `Resolve\(`:** the call `reg.Resolve(` lives in `classes/tree.go` (as the plan's Task 5 action describes, "classes/tree.go builds the nested tree … call the signpost Registry.Resolve"); `controllers/api/menus.go` looks up the `*signpost.Registry` and passes it in. +- **List vs single:** the plan did not say how to tell a list type; the tree uses the `pact.ResolvedMenuItem` contract ("URL is empty for a pure list type"). + +--- + +**Total deviations:** 12 auto-fixed (1 adaptation, 4 consistency/parity/contract, 2 missing critical, 4 blocking, 1 pitfall). **Impact:** all inside the two plugin repositories; no framework file changed. + +## Framework gaps (reported, not patched) + +- **Hook ValidationError details are not translated.** Checked in summercms.go: `writeCRUDError` (`modules/cabana/crud.go`) writes `cabana.ValidationError.Details` as given, while `cabana.ForbiddenError` message and details are translated in the request locale (cabana README). The admin SPA does not translate field errors either: `admin/src/components/form/FormField.vue` renders each error as `{{ text }}` and `FormErrorBanner.vue` lists orphan messages raw. So the FAQ and menu hook refusals (`golem15.faq::lang.errors.locale_required`, `golem15.menu::lang.errors.url_scheme`, `errors.target`, `errors.type_unknown`, `errors.parent_cycle`, `errors.menu_required`) show as phrase keys to the editor. Model-rule messages are unaffected. Suggested fix: translate `ValidationError.Details` in the request locale the way `ForbiddenError` is. Logged as deferred item 1 (17-04). +- Earlier framework items still apply: the reorder button label cannot be overridden per controller (17-02 item 2), so the menu entries list button reads "Reorder records". + +## Issues Encountered + +- None blocking in Task 5; both API smoke tests passed on the first run. + +## Known Stubs + +None. + +## Threat Flags + +None beyond the plan's threat register: T-17-23 (seaglass on output, raw-SQL test), T-17-24 (AllowedURL on save and in the API, target enum), T-17-25 (is_active and is_published filters, 404s), T-17-26 (throttle buckets, tested to 429) and T-17-31 (approved shape) are implemented as planned. The menu API logs left-out entries per request (deferred item 2, 17-04). + +## User Setup Required + +None. Remotes for `sm-faq-plugin` and `sm-menu-plugin`, the push, the go.mod bump and the host `cors.paths` entries are plan 05's human-approved steps. + +## Next Phase Readiness + +- Plan 05 creates the two remotes, mounts both repositories as submodules, bumps their summercms require, adds `_menu/api/*` and `_faq/api/*` to the host's `http.cors.paths`, runs the Winter exports and imports, and builds the Nuxt layers against the shapes above. +- Plan 06 adds unit tests; `classes.BuildTree` and the FAQ serializer are pure enough to unit test without Postgres. + +--- +*Phase: 17-blog-companions* +*Completed: 2026-10-12* + +## Self-Check: PASSED + +All 27 listed key files exist; commits 1c54912, 7413c69, 1585f06 (sm-faq-plugin) and 08f99b8, e044bc7, 33a65ad (sm-menu-plugin) are in their repositories; `commits` (6) is measured per repository (`git rev-list --count HEAD` = 3 in each new repository); summercms.go has no code commit in this plan (HEAD before and after the code work: b1213e1). Task 5's verify commands (vet, short suites, README path greps, the six Docker smokes with three passes per plugin) and acceptance greps were re-run after the commits and passed. diff --git a/.planning/phases/17-blog-companions/deferred-items.md b/.planning/phases/17-blog-companions/deferred-items.md index 377d296..f0708d6 100644 --- a/.planning/phases/17-blog-companions/deferred-items.md +++ b/.planning/phases/17-blog-companions/deferred-items.md @@ -16,3 +16,8 @@ Out-of-scope findings logged during execution. None blocks the plan that found i 2. **gofmt drift in `sm-journal-plugin/classes/blog_editor_test.go`.** `gofmt -l` lists the file; pre-existing, not touched in 17-03. 3. **Singleton subtitle link (17-02 item 1).** The sitemap definition form ships the `definition.view` phrase, but the "View sitemap.xml" subtitle link needs the framework form schema key that 17-02 item 1 describes; the sitemap README says so. 4. **PHP journal-category nesting returned every category.** The frozen PHP `Category::resolveMenuItem` calls `$category->getNested()`, which in Winter runs the scope on a new query and returns the whole category tree, not the category's subtree. 17-03 implements the subtree, as the plan's D-02/D-03 truth states. A site whose PHP sitemap or menu used `journal-category` with nesting gets fewer URLs from Go; plan 05's live parity check should look for such items. + +## From 17-04 (FAQ and menu plugins) + +1. **Hook validation messages reach the admin as phrase keys.** A `cabana.ValidationError` returned from a form hook (`FormBeforeCreate`, `FormAfterCreate`, …) is written as-is by `writeCRUDError` (`modules/cabana/crud.go`), while `cabana.ForbiddenError` messages and details are translated in the request locale. The admin SPA prints field errors raw (`FormField.vue` renders `{{ text }}`; `FormErrorBanner.vue` lists orphans raw), so an editor sees `golem15.menu::lang.errors.url_scheme` instead of the sentence. Affects the FAQ `errors.locale_required` and the menu `errors.url_scheme`, `errors.target`, `errors.type_unknown`, `errors.parent_cycle`, `errors.menu_required` refusals. Follow-up in the framework: translate `ValidationError.Details` phrase keys in the request locale like `ForbiddenError` (server side), so every client gets text. +2. **Public API misses log once per request.** The menu API logs each left-out entry (unregistered type, unresolvable reference) at warn level on every request; with a stale entry in a busy menu that repeats per request (bounded by the 120 per minute throttle per client). Acceptable for now; a later change could log once per entry and boot.