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

268 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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.
<objective>
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.
</objective>
<execution_context>
@~/.codex/gsd-core/workflows/execute-plan.md
@~/.codex/gsd-core/templates/summary.md
</execution_context>
<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
</context>
## 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`.
<tasks>
<task type="tracer">
<name>Task 1: Create one Post through cabana with mlmarkdown and FormatHTML</name>
<reversibility rating="costly">D-10/D-11 couple post content to cabana mlmarkdown and translate nested maps; changing the YAML type later is an admin contract change.</reversibility>
<files>../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</files>
<read_first>.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)</read_first>
<action>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.</action>
<verify>
<automated>go -C ../sm-journal-plugin vet ./... &amp;&amp; go -C ../sm-journal-plugin test ./... -count=1 -v -run '^(TestPostsFormCompiles|TestPostsAdminCreateSmoke)$'</automated>
<fails_when>Non-zero exit; output contains "--- FAIL", "--- SKIP", or "no tests to run"; either named PASS line is absent.</fails_when>
</verify>
<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>
<done>An administrator can create a translatable markdown Post through cabana, with content_html regenerated by plugin FormatHTML.</done>
</task>
<task type="auto">
<name>Task 2: Categories, Tags, Settings screens and remaining query guards</name>
<files>../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</files>
<read_first>.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</read_first>
<action>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.</action>
<verify>
<automated>go -C ../sm-journal-plugin vet ./... &amp;&amp; go -C ../sm-journal-plugin test ./... -short -count=1 &amp;&amp; go -C ../sm-grzybyfunkcjonalne-app test ./... -count=1 -v -run '^(TestBootUserTranslateJournal)$'</automated>
<fails_when>Non-zero exit; output contains "--- FAIL", "--- SKIP", or "no tests to run"; host run lacks "--- PASS: TestBootUserTranslateJournal".</fails_when>
</verify>
<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>
<done>All three Journal admin screens and the settings singleton are registered and boot in the proof host.</done>
</task>
<task type="auto">
<name>Task 3: Register journal:export-posts and journal:import-posts plus Posts toolbar</name>
<files>../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</files>
<read_first>.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</read_first>
<action>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.</action>
<verify>
<automated>go -C ../sm-journal-plugin vet ./... &amp;&amp; go -C ../sm-journal-plugin test ./... -count=1 -v -run '^(TestJournalCommands)$'</automated>
<fails_when>Non-zero exit; output contains "--- FAIL", "--- SKIP", or "no tests to run"; lacks "--- PASS: TestJournalCommands".</fails_when>
</verify>
<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>
<done>CSV import/export is available from CLI and from the Posts toolbar for operators holding access_import_export.</done>
</task>
</tasks>
<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>
<verification>
Run all three task commands, then go -C ../sm-journal-plugin vet ./... && go -C ../sm-journal-plugin test ./... -short -count=1.
</verification>
<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>
<output>
Create `.planning/phases/15-journal-plugin/15-02-SUMMARY.md` when done
</output>