--- phase: 15-journal-plugin plan: 02 type: execute wave: 2 depends_on: ["15-01"] files_modified: - ../sm-journal-plugin/plugin.go - ../sm-journal-plugin/admin.go - ../sm-journal-plugin/admin_permissions.go - ../sm-journal-plugin/admin_navigation.go - ../sm-journal-plugin/assets/images/journal-icon.svg - ../sm-journal-plugin/classes/format_html.go - ../sm-journal-plugin/controllers/admin_registry.go - ../sm-journal-plugin/controllers/posts.go - ../sm-journal-plugin/controllers/categories.go - ../sm-journal-plugin/controllers/tags.go - ../sm-journal-plugin/controllers/posts/config_list.yaml - ../sm-journal-plugin/controllers/posts/config_form.yaml - ../sm-journal-plugin/controllers/posts/config_filter.yaml - ../sm-journal-plugin/controllers/categories/config_list.yaml - ../sm-journal-plugin/controllers/categories/config_form.yaml - ../sm-journal-plugin/controllers/tags/config_list.yaml - ../sm-journal-plugin/controllers/tags/config_form.yaml - ../sm-journal-plugin/models/post.go - ../sm-journal-plugin/models/post/fields.yaml - ../sm-journal-plugin/models/post/columns.yaml - ../sm-journal-plugin/models/category/fields.yaml - ../sm-journal-plugin/models/category/columns.yaml - ../sm-journal-plugin/models/tag/fields.yaml - ../sm-journal-plugin/models/tag/columns.yaml - ../sm-journal-plugin/models/settings.go - ../sm-journal-plugin/models/settings/fields.yaml - ../sm-journal-plugin/console/export_posts.go - ../sm-journal-plugin/console/import_posts.go - ../sm-journal-plugin/posts_admin_smoke_test.go - ../sm-journal-plugin/go.mod - ../sm-journal-plugin/README.md - ../sm-grzybyfunkcjonalne-app/boot_test.go autonomous: true requirements: [D-04, D-06, D-08, D-10, D-11, D-13] estimate: tokens: 100000 raw_tokens: 100000 tasks: 3 confidence: low must_haves: truths: - "D-04/D-08: Posts, Categories, and Tags admin controllers are YAML-driven, permission-gated, and the nav item embeds assets/images/journal-icon.svg." - "D-11: post content YAML type is mlmarkdown; cabana markdown/mltext/mlmarkdown already shipped in 14.2.1 and are not re-added." - "D-10: title, slug, excerpt, content use mltext/mlmarkdown so translatable attributes stay translatable." - "D-13: journal:export-posts and journal:import-posts are registered, and Posts toolbar actions require golem15.journal.access_import_export." - "FormatHTML regenerates content_html on admin save using goldmark footnote/table/attribute extensions plus cabana's rejectUnsafe gate, without editing cabana.RenderMarkdown." - "Without golem15.journal.access_other_posts, list/form queries restrict to user_id of the backend principal; publish writes without golem15.journal.access_publish return ForbiddenError." artifacts: - path: "../sm-journal-plugin/controllers/posts.go" provides: "golem15.journal.posts controller, ListExtendQuery, FormBeforeCreate, FormatHTML hook" contains: "golem15.journal.access_posts" - path: "../sm-journal-plugin/models/post/fields.yaml" provides: "adapted cabana-legal post form including mlmarkdown content" contains: "mlmarkdown" - path: "../sm-journal-plugin/assets/images/journal-icon.svg" provides: "embedded admin nav SVG (D-08)" contains: "svg" - path: "../sm-journal-plugin/classes/format_html.go" provides: "journal.FormatHTML" contains: "func FormatHTML" - path: "../sm-journal-plugin/console/export_posts.go" provides: "journal:export-posts" contains: "journal:export-posts" - path: "../sm-journal-plugin/models/settings/fields.yaml" provides: "HasSettings code journal" contains: "search_use_typesense" key_links: - from: "../sm-journal-plugin/controllers/posts.go" to: "../sm-journal-plugin/classes/format_html.go" via: "FormBeforeCreate/Update regenerate content_html" pattern: "FormatHTML" - from: "../sm-journal-plugin/admin_permissions.go" to: "../sm-journal-plugin/controllers/posts.go" via: "RequiredPermissions golem15.journal.access_posts" pattern: "access_posts" - from: "../sm-journal-plugin/console/export_posts.go" to: "../sm-journal-plugin/plugin.go" via: "HasCommands registers PHP command names" pattern: "journal:export-posts" prohibitions: - requirement_id: D-11 category: architecture statement: "Do not add a second cabana YAML field type and do not edit modules/cabana/field_markdown.go or cabana.RenderMarkdown" status: resolved verification: test - requirement_id: D-13 category: architecture statement: "Do not port Winter.Pages menu item types or an admin dashboard report widget" status: resolved verification: test - requirement_id: D-06 category: privacy statement: "Do not ship the remaining 19 PHP locales beyond en and pl" status: resolved verification: test --- ## Phase Goal **As a** application developer, **I want to** mount `sm-journal-plugin` in a host the same way `sm-user-plugin` mounts, **so that** a blog can run on SummerCMS without the PHP plugin. This plan's slice: an administrator with `golem15.journal.*` can create a Post whose `mlmarkdown` content is stored and whose `content_html` is regenerated through plugin FormatHTML. Ship YAML admin for Posts, Categories, Tags, and Settings, embed the nav SVG, implement FormatHTML, and register CSV import/export CLI plus toolbar actions. Purpose: operators can manage Journal content in the existing admin SPA without PHP widgets or illegal YAML keys. Output: adapted YAML, permissions, navigation, FormatHTML, commands, Posts admin smoke. @~/.codex/gsd-core/workflows/execute-plan.md @~/.codex/gsd-core/templates/summary.md @.planning/phases/15-journal-plugin/15-CONTEXT.md @.planning/phases/15-journal-plugin/15-RESEARCH.md @.planning/phases/15-journal-plugin/15-PATTERNS.md @../sm-journal-plugin/plugin.go @../fonoteka.go/plugins/golem15/user/admin.go @../fonoteka.go/plugins/golem15/user/controllers/users_admin_controller.go @modules/cabana/form_schema.go @modules/cabana/field_markdown.go @docs/backend/forms.md ## Spec-less probe fallback Visible skip: no REQUIREMENTS.md IDs. D-04/D-06/D-08/D-10/D-11/D-13 and RESEARCH §3–§4 are the contract. D-11 is a no-op field-type addition: Journal uses existing mlmarkdown. ## Artifacts this phase produces - Controllers `golem15.journal.posts|categories|tags`, Settings code `journal`, permission codes copied from PHP Plugin.php 55-90. - Adapted fields/columns/list/filter YAML using only cabana-legal types and keys. - Embedded `assets/images/journal-icon.svg`. - `journal.FormatHTML` in the plugin; goldmark v1.8.6 required only if this file imports it. - Commands `journal:export-posts` / `journal:import-posts` and Posts toolbar actions gated by `golem15.journal.access_import_export`. Task 1: Create one Post through cabana with mlmarkdown and FormatHTML D-10/D-11 couple post content to cabana mlmarkdown and translate nested maps; changing the YAML type later is an admin contract change. ../sm-journal-plugin/plugin.go, ../sm-journal-plugin/admin.go, ../sm-journal-plugin/admin_permissions.go, ../sm-journal-plugin/admin_navigation.go, ../sm-journal-plugin/assets/images/journal-icon.svg, ../sm-journal-plugin/classes/format_html.go, ../sm-journal-plugin/controllers/admin_registry.go, ../sm-journal-plugin/controllers/posts.go, ../sm-journal-plugin/controllers/posts/config_list.yaml, ../sm-journal-plugin/controllers/posts/config_form.yaml, ../sm-journal-plugin/controllers/posts/config_filter.yaml, ../sm-journal-plugin/models/post.go, ../sm-journal-plugin/models/post/fields.yaml, ../sm-journal-plugin/models/post/columns.yaml, ../sm-journal-plugin/posts_admin_smoke_test.go, ../sm-journal-plugin/go.mod .planning/phases/15-journal-plugin/15-CONTEXT.md (D-08, D-10, D-11), .planning/phases/15-journal-plugin/15-RESEARCH.md (§3 YAML rewrite, §4 D-11 already shipped, FormatHTML, permissions), .planning/phases/15-journal-plugin/15-PATTERNS.md (admin.go, posts controller, YAML rewrite table, format_html.go), ../fonoteka.go/plugins/golem15/user/admin.go, ../fonoteka.go/plugins/golem15/user/admin_permissions.go, ../fonoteka.go/plugins/golem15/user/controllers/users_admin_controller.go, modules/cabana/form_schema.go (formFieldTypes and allowed keys), modules/cabana/filter_schema.go, modules/cabana/field_markdown.go (rejectUnsafeMarkdownHTML), docs/backend/forms.md (mltext/mlmarkdown), /media/nvme/dev/golem15/fonoteka/plugins/golem15/journal/Plugin.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/journal/models/post/fields.yaml, /media/nvme/dev/golem15/fonoteka/plugins/golem15/journal/controllers/Posts.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/journal/assets/images/journal-icon.svg, /media/nvme/dev/golem15/fonoteka/plugins/golem15/journal/models/Post.php (formatHtml, canEdit, filterFields, beforeSave) Implement HasAdminControllers, HasPermissions, HasNavigation. Embed YAML file-by-file (not the whole controllers directory). Copy permission codes exactly: golem15.journal.manage_settings, access_posts, access_categories, access_other_posts, access_import_export, access_publish, access_tags. Roles developer like the user plugin. cabana.Allows is OR. Controller ID golem15.journal.posts, ModelName Golem15\Journal\Models\Post, ConfigDir controllers/posts, RequiredPermissions golem15.journal.access_posts. ListExtendQuery and FormExtendQuery: without access_other_posts, where user_id equals bouncer.User principal ID. FormBeforeCreate stamps user_id from the backend principal when empty. Publish writes (published true or published_at set) without access_publish return cabana.ForbiddenError — hiding fields is optional UX; refuse is mandatory. Rewrite post fields.yaml to cabana-legal keys only (D-04, D-10, D-11). title mltext; slug mltext plus preset field title type slug; content mlmarkdown (not a PHP form widget class); excerpt mltext; categories relation nameFrom name; tags relation nameFrom name (create tags on the Tags admin, not an on-the-fly list widget); published switch; is_pinned checkbox; user relation nameFrom login emptyOption current user; published_at datepicker mode datetime; featured_images fileupload mode image imageWidth/imageHeight 200. Omit the sources repeater field from the admin form (keep JSONB; API still accepts sources in Plan 03). Omit metadata preview_page (Phase 16). Omit toolbar partial. Drop PHP keys cabana refuses (stretch, cssClass, commentAbove, widget class, paneCssClass). D-11 is already shipped in 14.2.1 — do not add a YAML type and do not edit modules/cabana. config_filter.yaml: no raw SQL condition keys (cabana boot-fails those). published switch via FilterPublished; published_date daterange column created_at; category scope FilterCategories including child categories as PHP does. Implement FilterScopes on Post. List columns may use type date. recordsPerPage 25. recordUrl golem15/journal/posts/update/:id. Copy PHP assets/images/journal-icon.svg bytes into the Go plugin (D-08). Navigation Code journal, Permissions golem15.journal.*, Order 300, Controller golem15.journal.posts, icon pencil or icon-pencil (existing lucide map). Side menu new_post, posts, categories, tags with PHP permission lists. Skip dashboard widget. Implement journal.FormatHTML in classes/format_html.go: goldmark with footnote, table, and parser attribute extensions from the existing github.com/yuin/goldmark module (A1 — if a separate module is required, stop and do not add it). Reuse the same rejectUnsafeMarkdownHTML checks as cabana (script, iframe, event handlers, javascript/vbscript/data schemes). Do not change cabana.RenderMarkdown (pitfall 15). If FormatHTML imports goldmark, require github.com/yuin/goldmark v1.8.6 in plugin go.mod. Call FormatHTML from FormBeforeCreate and FormBeforeUpdate so stored content_html matches admin saves, not only API writes. Ignore use_rich_editor when compiling the form (always mlmarkdown; WYSIWYG deferred). Smoke TestPostsFormCompiles / TestPostsAdminCreateSmoke: cabana.Activate compiles the posts form; a privileged backend principal creates a post with nested en/pl title and content maps; English lands on host columns; Polish reaches translate attributes; content_html is non-empty and contains no script tags. Full PHPUnit map stays Plan 04. go -C ../sm-journal-plugin vet ./... && go -C ../sm-journal-plugin test ./... -count=1 -v -run '^(TestPostsFormCompiles|TestPostsAdminCreateSmoke)$' Non-zero exit; output contains "--- FAIL", "--- SKIP", or "no tests to run"; either named PASS line is absent. - models/post/fields.yaml contains type mlmarkdown for content and type mltext for title, slug, excerpt. - models/post/fields.yaml does not use PHP widget class names, on-the-fly tag widgets, or source-repeater fields. - controllers/posts/config_filter.yaml compiles and does not use cabana-illegal filter keys. - assets/images/journal-icon.svg is embedded and byte-comparable to the PHP SVG. - FormatHTML lives in the plugin; modules/cabana/field_markdown.go is unmodified in this plan. - Creating a post without access_publish cannot persist published=true. - Host/plugin README still does not name a consuming application. An administrator can create a translatable markdown Post through cabana, with content_html regenerated by plugin FormatHTML. Task 2: Categories, Tags, Settings screens and remaining query guards ../sm-journal-plugin/controllers/categories.go, ../sm-journal-plugin/controllers/tags.go, ../sm-journal-plugin/controllers/categories/config_list.yaml, ../sm-journal-plugin/controllers/categories/config_form.yaml, ../sm-journal-plugin/controllers/tags/config_list.yaml, ../sm-journal-plugin/controllers/tags/config_form.yaml, ../sm-journal-plugin/models/category/fields.yaml, ../sm-journal-plugin/models/category/columns.yaml, ../sm-journal-plugin/models/tag/fields.yaml, ../sm-journal-plugin/models/tag/columns.yaml, ../sm-journal-plugin/models/settings.go, ../sm-journal-plugin/models/settings/fields.yaml, ../sm-journal-plugin/plugin.go, ../sm-journal-plugin/admin.go, ../sm-grzybyfunkcjonalne-app/boot_test.go .planning/phases/15-journal-plugin/15-PATTERNS.md (categories/tags controllers, settings analog, YAML), ../sm-translate-plugin/controllers/locales.go, ../sm-translate-plugin/models/locale/fields.yaml, modules/cabana/example_controller_test.go (Settings), docs/backend/settings.md, /media/nvme/dev/golem15/fonoteka/plugins/golem15/journal/controllers/Categories.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/journal/controllers/Tags.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/journal/models/category/fields.yaml, /media/nvme/dev/golem15/fonoteka/plugins/golem15/journal/models/tag/fields.yaml, /media/nvme/dev/golem15/fonoteka/plugins/golem15/journal/models/settings/fields.yaml, ../sm-grzybyfunkcjonalne-app/boot_test.go Add controllers golem15.journal.categories (access_categories) and golem15.journal.tags (access_tags). Category parent_id is a relation; keep nest_* off the form. Tag fields name, slug, description only. HasSettings item Code journal, Permissions golem15.journal.manage_settings, Form models/settings/fields.yaml, NewModel Settings. Rewrite settings YAML: drop every show/hide trigger block so search weight fields always display; drop placeholder keys (use comment). Keep use_rich_editor stored; do not switch the post editor off mlmarkdown. Extend host TestBootUserTranslateJournal to assert controller IDs golem15.journal.posts, golem15.journal.categories, golem15.journal.tags and settings code journal are registered. Still do not require /_journal/api/v1 routes (Plan 03). canEdit on Post: owner or access_other_posts. Use it in FormExtendQuery/update/delete paths so a second admin without access_other_posts cannot open another author's post. go -C ../sm-journal-plugin vet ./... && go -C ../sm-journal-plugin test ./... -short -count=1 && go -C ../sm-grzybyfunkcjonalne-app test ./... -count=1 -v -run '^(TestBootUserTranslateJournal)$' Non-zero exit; output contains "--- FAIL", "--- SKIP", or "no tests to run"; host run lacks "--- PASS: TestBootUserTranslateJournal". - AdminControllers include golem15.journal.posts, golem15.journal.categories, golem15.journal.tags. - Settings() includes Code journal with manage_settings. - models/settings/fields.yaml has search_use_typesense default off and always-visible weight fields. - Host boot test asserts the three Journal controller IDs. - Category form has no nest_left field. All three Journal admin screens and the settings singleton are registered and boot in the proof host. Task 3: Register journal:export-posts and journal:import-posts plus Posts toolbar ../sm-journal-plugin/console/export_posts.go, ../sm-journal-plugin/console/import_posts.go, ../sm-journal-plugin/plugin.go, ../sm-journal-plugin/controllers/posts.go, ../sm-journal-plugin/README.md .planning/phases/15-journal-plugin/15-CONTEXT.md (D-13), .planning/phases/15-journal-plugin/15-RESEARCH.md (Import/export), .planning/phases/15-journal-plugin/15-PATTERNS.md (console, HasAdminActions), ../fonoteka.go/plugins/golem15/user/console/require_password_change.go, modules/pact/capabilities.go (HasCommands, HasAdminActions), /media/nvme/dev/golem15/fonoteka/plugins/golem15/journal/Plugin.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/journal/console/ExportPosts.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/journal/console/ImportPosts.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/journal/models/PostExport.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/journal/models/PostImport.php Add TestJournalCommands that asserts Commands() contains journal:export-posts and journal:import-posts. Implement pact.HasCommands. Names journal:export-posts and journal:import-posts matching PHP Plugin.php 169-170 (D-13). Port PostExport/PostImport column sets; flags --path and --dry-run as PHP. Do not invent a generic CSV framework. Do not add Winter ImportExport behavior. On Posts, HasAdminActions toolbar buttons with Permissions golem15.journal.access_import_export. Names must not be create or delete (reserved). Actions call the same import/export column logic as the CLI. Document the two command names in the plugin README. Keep examples as blog. Do not implement Pages menu types or a dashboard widget. go -C ../sm-journal-plugin vet ./... && go -C ../sm-journal-plugin test ./... -count=1 -v -run '^(TestJournalCommands)$' Non-zero exit; output contains "--- FAIL", "--- SKIP", or "no tests to run"; lacks "--- PASS: TestJournalCommands". - Plugin Commands() includes journal:export-posts and journal:import-posts. - Posts AdminActions include import/export names whose Permissions contain golem15.journal.access_import_export. - README documents both command names. - No Pages menu-type registration and no dashboard widget type appear in plugin.go or admin_navigation.go. CSV import/export is available from CLI and from the Posts toolbar for operators holding access_import_export. ## Trust Boundaries | Boundary | Description | |----------|-------------| | Admin JSON → cabana CRUD | Untrusted form maps cross permission and Fillable | | Markdown source → content_html | Stored HTML can carry active markup | | Toolbar/CLI → posts table | Import must not bypass fillable or publish permission | ## STRIDE Threat Register | Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | |-----------|----------|-----------|----------|-------------|-----------------| | T-15-07 | Elevation of Privilege | Posts admin | high | mitigate | RequiredPermissions, ListExtendQuery owner scope, access_other_posts, 403 smoke in Plan 04 | | T-15-08 | Tampering | FormatHTML | high | mitigate | rejectUnsafe on stored HTML; do not enable unsafe goldmark HTML globally | | T-15-09 | Elevation of Privilege | access_publish | high | mitigate | ForbiddenError on publish writes without grant | | T-15-SC | Tampering | package installs | high | mitigate | goldmark v1.8.6 only if FormatHTML imports it; already in the framework graph | ASVS L1: high threats mitigated here; fail-closed tests in Plan 04. Run all three task commands, then go -C ../sm-journal-plugin vet ./... && go -C ../sm-journal-plugin test ./... -short -count=1. - Posts form compiles with mlmarkdown and an admin can create a post. - Categories, Tags, and Settings are registered. - FormatHTML is plugin-local; cabana.RenderMarkdown unchanged. - Import/export commands and toolbar exist. - D-11 adds no new field type. Pages/dashboard/extra locales remain absent. Create `.planning/phases/15-journal-plugin/15-02-SUMMARY.md` when done