Files
summercms/.planning/phases/15-journal-plugin/15-02-PLAN.md
2026-10-06 18:02:17 +02:00

21 KiB
Raw Blame History

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, estimate, must_haves
phase plan type wave depends_on files_modified autonomous requirements estimate must_haves
15-journal-plugin 02 execute 2
15-01
../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
true
D-04
D-06
D-08
D-10
D-11
D-13
tokens raw_tokens tasks confidence
100000 100000 3 low
truths artifacts key_links prohibitions
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.
path provides contains
../sm-journal-plugin/controllers/posts.go golem15.journal.posts controller, ListExtendQuery, FormBeforeCreate, FormatHTML hook golem15.journal.access_posts
path provides contains
../sm-journal-plugin/models/post/fields.yaml adapted cabana-legal post form including mlmarkdown content mlmarkdown
path provides contains
../sm-journal-plugin/assets/images/journal-icon.svg embedded admin nav SVG (D-08) svg
path provides contains
../sm-journal-plugin/classes/format_html.go journal.FormatHTML func FormatHTML
path provides contains
../sm-journal-plugin/console/export_posts.go journal:export-posts journal:export-posts
path provides contains
../sm-journal-plugin/models/settings/fields.yaml HasSettings code journal search_use_typesense
from to via pattern
../sm-journal-plugin/controllers/posts.go ../sm-journal-plugin/classes/format_html.go FormBeforeCreate/Update regenerate content_html FormatHTML
from to via pattern
../sm-journal-plugin/admin_permissions.go ../sm-journal-plugin/controllers/posts.go RequiredPermissions golem15.journal.access_posts access_posts
from to via pattern
../sm-journal-plugin/console/export_posts.go ../sm-journal-plugin/plugin.go HasCommands registers PHP command names journal:export-posts
requirement_id category statement status verification
D-11 architecture Do not add a second cabana YAML field type and do not edit modules/cabana/field_markdown.go or cabana.RenderMarkdown resolved test
requirement_id category statement status verification
D-13 architecture Do not port Winter.Pages menu item types or an admin dashboard report widget resolved test
requirement_id category statement status verification
D-06 privacy Do not ship the remaining 19 PHP locales beyond en and pl resolved 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.

<execution_context> @/.codex/gsd-core/workflows/execute-plan.md @/.codex/gsd-core/templates/summary.md </execution_context>

@.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)$' <fails_when>Non-zero exit; output contains "--- FAIL", "--- SKIP", or "no tests to run"; either named PASS line is absent.</fails_when> <acceptance_criteria> - 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. </acceptance_criteria> 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)$' <fails_when>Non-zero exit; output contains "--- FAIL", "--- SKIP", or "no tests to run"; host run lacks "--- PASS: TestBootUserTranslateJournal".</fails_when> <acceptance_criteria> - 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. </acceptance_criteria> 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)$' <fails_when>Non-zero exit; output contains "--- FAIL", "--- SKIP", or "no tests to run"; lacks "--- PASS: TestJournalCommands".</fails_when> <acceptance_criteria> - 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. </acceptance_criteria> CSV import/export is available from CLI and from the Posts toolbar for operators holding access_import_export.

<threat_model>

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. </threat_model>

Run all three task commands, then go -C ../sm-journal-plugin vet ./... && go -C ../sm-journal-plugin test ./... -short -count=1.

<success_criteria>

  • 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. </success_criteria>
Create `.planning/phases/15-journal-plugin/15-02-SUMMARY.md` when done