diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index c303142..c1d0844 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -982,10 +982,17 @@ Plans: **Goal:** The Journal posts list and post form in the admin work on real imported data, found on UAT (uat.grzybyfunkcjonalne.pl/gfadmin/golem15/journal/posts): (1) list search no longer fails with "Server error" — the Go log shows `ERROR: column Categories.post_id does not exist (SQLSTATE 42703)` from summercms@v0.1.9 modules/cabana/query.go:129, i.e. search across a belongsToMany (pivot) relation builds a hasMany join; (2) list sort and filters are remembered after opening a record and going back, as WinterCMS persisted list state; (3) the post form shows the stored content (DB has content/content_html on 104/104 posts, the form shows it empty); (4) the post form shows the stored excerpt (100/104 posts have it in the DB). Fixes span summercms.go core (modules/cabana — coordinate with the concurrent session working in modules/) and sm-journal-plugin; release a summercms tag, bump the host, redeploy to UAT and re-run the staging smoke. **Requirements**: TBD **Depends on:** Phase 16 -**Plans:** 0 plans +**Plans:** 3 plans Plans: -- [ ] TBD (run /msd-plan-phase 16.1 to break down) +**Wave 1** +- [ ] 16.1-01-PLAN.md — All fixes: cabana relation search and to-many sort through EXISTS/correlated subqueries (D-02), default locale first in form enabledLocales (D-03), Journal FormatHTML keeps raw HTML behind the gate (D-03), admin list state in the URL and browser storage (D-01) + +**Wave 2** *(blocked on Wave 1 completion)* +- [ ] 16.1-02-PLAN.md — Release gate (human), summercms tag, journal and host bumps, UAT redeploy and on-server smoke, re-save posts 53 and 64 (D-04) + +**Wave 3** *(blocked on Wave 2 completion)* +- [ ] 16.1-03-PLAN.md — Unit tests: DryRun SQL matrix, locale order, FormatHTML keep/refuse, list-state vitest; Postgres tests for a Docker host; phase gate ### Phase 16.2: Plugin-provided field types and the Blog Editor formwidget (INSERTED) diff --git a/.planning/STATE.md b/.planning/STATE.md index 12473f3..6ed3e19 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -1,18 +1,18 @@ --- msd_state_version: "1.0" milestone: v1.0 -current_phase: 16 -current_phase_name: grzybyfunkcjonalne.pl on reusable blog views -status: verifying +current_phase: "16.1" +current_phase_name: "Journal admin urgent fixes: relation search, list state, post form content and excerpt" +status: executing stopped_at: Phase 19.1 context gathered -last_updated: "2026-10-09T18:18:13.179Z" +last_updated: "2026-10-09T18:51:01.702Z" last_activity: 2026-10-09 last_activity_desc: Phase 16 execution started -state_head: a8c65bc71a8d45839fb8c7fd49c761df2830bfd4 +state_head: ef6feb3441380eb0814d9a082b0a755c027b91a7 progress: total_phases: 34 completed_phases: 13 - total_plans: 148 + total_plans: 151 completed_plans: 144 gsd_state_version: "1.0" milestone_name: milestone @@ -29,9 +29,9 @@ See: .planning/PROJECT.md (updated 2026-09-16) ## Current Position -Phase: 16 (grzybyfunkcjonalne.pl on reusable blog views) — EXECUTING +Phase: 16.1 (Journal admin urgent fixes: relation search, list state, post form content and excerpt) — READY TO EXECUTE Plan: 5 of 5 -Status: Phase complete — ready for verification +Status: Ready to execute Last activity: 2026-10-09 - Completed 16-04: staging deploy at uat.grzybyfunkcjonalne.pl, smoke ok (98 urls) Progress: [██████████] 100% diff --git a/.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-01-PLAN.md b/.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-01-PLAN.md new file mode 100644 index 0000000..60b51fe --- /dev/null +++ b/.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-01-PLAN.md @@ -0,0 +1,311 @@ +--- +phase: 16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - modules/cabana/query.go + - modules/cabana/query_relation_test.go + - docs/backend/lists-and-filters.md + - modules/cabana/http.go + - modules/cabana/locale_order_test.go + - modules/cabana/README.md + - docs/backend/forms.md + - ../sm-journal-plugin/classes/format_html.go + - ../sm-journal-plugin/classes/format_html_test.go + - ../sm-journal-plugin/README.md + - admin/src/state/listState.ts + - admin/src/views/ListView.vue + - admin/src/state/useAuth.ts + - admin/src/state/useSidebar.ts + - admin/tests/list/ListView.test.ts + - scripts/check-phase10.sh + - modules/boardwalk/dist/ +autonomous: true +requirements: [D-01, D-02, D-03] +estimate: + tokens: 85000 + raw_tokens: 85000 + tasks: 4 + confidence: low +must_haves: + truths: + - "D-02: a list search over a belongsToMany relation column answers 200 instead of 'Server error': the count and page queries match through EXISTS (SELECT 1 FROM the pivot JOIN the related table ...) whose pivot, key and table names come from GORM's parsed relationship (gorm.Statement.Parse, Schema.Relationships.Relations), never from a naming convention." + - "D-02: search over hasMany, hasOne and belongsTo relation columns also goes through a correlated EXISTS subquery, so the outer list query gains no JOIN during the count: a record matching through several related rows is counted and listed once, and unqualified ListExtendQuery or scope predicates (id IN (...), user_id = ?) stay unambiguous." + - "D-02: sorting by a hasMany or belongsToMany column orders by a correlated string_agg subquery followed by the primary-key tiebreak; sorting by a belongsTo or hasOne column keeps the existing association join." + - "D-02: polymorphic relations and columns missing from the related schema are refused with a 422 validation_failed on search (or sort), never with a 500." + - "D-03: the form schema's meta.enabledLocales lists the default locale first and the other enabled codes in the writer's order, so every ML control opens on the default locale where the stored host content, excerpt, title and slug live." + - "D-03: classes.FormatHTML keeps raw HTML in content (imported HTML bodies round-trip unchanged) and still refuses script, iframe, object and embed tags, event handler attributes and javascript, vbscript and data URLs on every write path (admin save, import, Journal API)." + - "D-01: list search, sort, dir, page, per_page and filter[name] stay in the list URL; every list query the view loads is also stored in browser storage under the key summer-admin.list: followed by the controller id; an empty query is never stored." + - "D-01: a list opened without list-state params (navigation rail, breadcrumb, form back, cancel, save-and-close, delete) restores the stored query into the URL before its first list request; a URL with list-state params wins and overwrites the stored query; logout removes every stored list query." + - "D-02: every cabana and admin commit of this plan is staged by explicit path and holds only this plan's files; sm-journal-plugin changes are committed in their own repository." + - "go vet stays clean in summercms.go and sm-journal-plugin, and the named smoke tests, the vitest files, TestDocsTree and scripts/check-admin-dist.sh are green at every commit." + artifacts: + - path: "modules/cabana/query.go" + provides: "EXISTS relation search and correlated to-many sort built from the parsed GORM relationship" + contains: "Relationships.Relations" + - path: "modules/cabana/query_relation_test.go" + provides: "DryRun harness, relation fixtures and the tracer smoke test" + contains: "TestListRelationSearchSmoke" + - path: "modules/cabana/locale_order_test.go" + provides: "default-locale-first smoke test" + contains: "TestEnabledLocalesDefaultFirstSmoke" + - path: "../sm-journal-plugin/classes/format_html.go" + provides: "goldmark with unsafe HTML rendering behind the unchanged safety gate" + contains: "WithUnsafe" + - path: "admin/src/state/listState.ts" + provides: "per-list browser storage of the list query" + contains: "summer-admin.list:" + key_links: + - from: "modules/cabana/query.go applyListSearch / applyListOrder" + to: "gorm.io/gorm/schema Relationship (Type, References, JoinTable, FieldSchema)" + via: "gorm.Statement{DB: db}.Parse(model) then Schema.Relationships.Relations[field]" + pattern: "Relationships.Relations" + - from: "modules/cabana/http.go enabledContentLocales" + to: "TranslationWriter.DefaultLocale" + via: "default code moved to index 0 of meta.enabledLocales" + pattern: "DefaultLocale" + - from: "admin/src/views/ListView.vue watch on listQueryKey" + to: "admin/src/state/listState.ts readListState / writeListState" + via: "restore before the first list request when the URL has no list params" + pattern: "readListState" + - from: "../sm-journal-plugin/controllers/posts.go preparePostWrite" + to: "../sm-journal-plugin/classes/format_html.go FormatHTML" + via: "content to content_html on every admin save" + pattern: "FormatHTML" + prohibitions: + - "[FLAGGED-UNVERIFIED] The relation-search fix must not be a Journal-specific workaround, and framework code, READMEs and docs must not name a consuming application (D-02, CLAUDE.md)." + - "[FLAGGED-UNVERIFIED] List state must not be stored on the server: no table, no migration, no new API route (revised D-01)." + - "[FLAGGED-UNVERIFIED] The Journal API content and content_html response shapes must not change; only the stored HTML stops losing raw HTML (API parity)." + - "[FLAGGED-UNVERIFIED] cabana.RenderMarkdown, whose output the admin preview renders with v-html, must not switch to unsafe HTML." +--- + +## Phase Goal + +ROADMAP Goal (not in user-story format): the Journal posts list and post form in the admin work on real imported data found on UAT: (1) list search no longer fails with "Server error" (search across a belongsToMany relation built a hasMany join); (2) list sort and filters are remembered after opening a record and going back; (3) the post form shows the stored content; (4) the post form shows the stored excerpt. Then release a summercms tag, bump the host, redeploy to UAT and re-run the staging smoke. + +This plan's slice (D-05: "01 all fixes"): every code fix, each with a smoke test. Release and redeploy are plan 02; full unit coverage is plan 03. + +## Spec-less probe fallback + +Visible skip: the phase has no REQUIREMENTS.md IDs to probe (Requirements: TBD), so no probe predicates are generated this run. CONTEXT D-01..D-05 and the RESEARCH root causes are the source of truth. The edge cases RESEARCH names (polymorphic and soft-deleting relations, self-referencing relations, stale stored list state, out-of-range restored page, the user clearing every list control, same-path navigation) are authored into the task behaviors and `must_haves.truths`; the kept must-NOTs are in `must_haves.prohibitions`. + +## Artifacts this phase produces + +- summercms.go `modules/cabana`: EXISTS-based relation search and correlated to-many sort in `query.go`; default-locale-first `meta.enabledLocales` in `http.go`; new test files `query_relation_test.go` and `locale_order_test.go`; README and `docs/backend/{lists-and-filters,forms}.md` sentences. +- summercms.go admin SPA: new `admin/src/state/listState.ts`; list-state entry logic in `admin/src/views/ListView.vue`; logout clears stored list queries (`admin/src/state/useAuth.ts`); rebuilt `modules/boardwalk/dist/`; the `scripts/check-phase10.sh` browser-storage allowlist names `listState.ts`. +- sm-journal-plugin: `classes/format_html.go` renders raw HTML behind the unchanged gate; a smoke test; README sentence. Committed on `main`, not pushed (plan 02 pushes). +- No server-side table, migration, route or OpenAPI change (revised D-01). + + +Fix the four UAT defects in code (D-05 plan 01): relation search and to-many sort through subqueries built from GORM's parsed relationship (D-02), the default content locale first in form locales so the form shows stored content and excerpt (D-03), raw HTML kept in content_html so an admin save no longer destroys imported posts (D-03), and list state kept in the URL and browser storage (D-01). + +Purpose: UAT admins can search, filter and return to the Journal posts list and edit imported posts without losing data. +Output: four commits in summercms.go (cabana search/sort, cabana locales, admin list state with rebuilt dist, nothing else) and one commit in sm-journal-plugin. + + + +@~/.claude/msd-core/workflows/execute-plan.md +@~/.claude/msd-core/templates/summary.md + + + +@.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-CONTEXT.md +@.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-RESEARCH.md +@.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-PATTERNS.md +@modules/cabana/query.go + +RESEARCH caveat: its list-state design (a backend_list_states table, a GET list-state route, a migration, an OpenAPI regeneration) is superseded by the revised D-01 (URL plus browser storage). Ignore those parts of RESEARCH and PATTERNS. + +Commit rules: no co-author tags; one logical change per commit; planning docs and code in separate commits; stage every file by explicit path (never `git add -A` or `git add .`; summercms.go has untracked `.msd/` and a modified `.planning/state.json`, sm-journal-plugin has an untracked `go.work.sum`, none of which belong in these commits). + +Docker is not usable on this machine, so `go test ./modules/cabana` without `-run` fails on the Postgres-backed tests for reasons unrelated to this plan. Every gate below runs the named pure tests. + + + + + + Task 1: Tracer: a list search over a belongsToMany column answers 200 through an EXISTS subquery (GET list, ExecuteList, SQL) + git status --porcelain -- modules/cabana docs/backend prints nothing, i.e. the concurrent session has no in-flight edit in the files this task touches (D-02); otherwise stop and report the conflicting paths. + modules/cabana/query.go, modules/cabana/query_relation_test.go, docs/backend/lists-and-filters.md + modules/cabana/query.go (whole file: ExecuteList, applyListOrder, applyListSearch, qualifiedColumn, quotedIdent, tableName, relationFieldName, listRelationColumn, relatedSelectMany), modules/cabana/query_test.go (queryRow, queryArtist, queryController, newListService, getList, decodeList, listEnvelope), modules/cabana/list_schema_test.go (filterFS), modules/cabana/security_test.go (controllerRequest), modules/cabana/README.md (the rule that framework code never guesses table, pivot or foreign-key names), docs/backend/lists-and-filters.md (column options paragraph near line 62), .planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-RESEARCH.md (Root-cause evidence Defect 1, Pattern 1, Pitfall 1, Code Examples "DryRun SQL test") + + - GET list with search=fungi on a controller whose list has a searchable belongsToMany column (tags, select name) answers 200; the recorded count statement contains an EXISTS subquery over the pivot table aliased cabana_p0 joined to the related table aliased cabana_r0, correlated by the pivot owner key to the main table primary key, and contains no LEFT JOIN. + - GET list with sort=tags and dir=desc records an ORDER BY that holds a string_agg subquery with DESC followed by the main table primary-key tiebreak. + + Run the precondition check first (D-02). Then edit modules/cabana/query.go: + +Add one unexported builder (for example relationSubquery) used by both search and sort. It takes the db, the model, the Go field name from relationFieldName, the physical column from listRelationColumn and an alias index n. It parses the model with a fresh gorm.Statement{DB: db} and Parse(model), reads Schema.Relationships.Relations[field] and never derives a table, pivot or key name from a naming convention (D-02, README rule). It refuses with listInvalid (field "search" or "sort", the caller passes it) when the relation is missing, its FieldSchema lacks the column (FieldSchema.LookUpField), the column fails identifier(), any reference is polymorphic (non-empty PrimaryValue), or the type is not one of schema.Many2Many, schema.HasMany, schema.HasOne, schema.BelongsTo. The related table is aliased cabana_r(n) and the pivot cabana_p(n), so a self-referencing relation stays correct; the main table is the parsed Schema.Table, left unaliased so it names the outer FROM. Correlation by type: Many2Many joins rel.JoinTable.Table AS the pivot alias to the related alias on the references whose OwnPrimaryKey is false (pivot foreign key equals related primary key) and correlates the references whose OwnPrimaryKey is true (pivot foreign key equals main primary key); HasMany and HasOne (OwnPrimaryKey true) correlate related foreign key to main primary key; BelongsTo correlates related primary key to main foreign key. When the related schema has a field of type gorm.DeletedAt, add that column IS NULL on the related alias (Winter whereHas applies the related soft-delete scope). Quote every identifier with quotedIdent and qualifiedColumn. The builder returns the FROM part and the WHERE correlation and carries no bound values. + +applyListSearch: for a column with relation, append EXISTS (SELECT 1 FROM the builder's FROM WHERE the correlation AND LOWER(CAST(related alias.column AS TEXT)) LIKE ? ESCAPE backslash) to the existing OR group with the same bound pattern own columns use; drop the association join and its bookkeeping map. This applies to every relation type, so the count query never gains an outer JOIN (RESEARCH Pitfall 1). Own-column search is unchanged. + +applyListOrder: change it to return (*gorm.DB, error) and propagate the error from ExecuteList. Keep the existing association join and column order for BelongsTo and HasOne columns (covered by the existing artist sort). For HasMany and Many2Many columns order by the correlated scalar subquery (SELECT string_agg(CAST(related alias.column AS TEXT), ', ' ORDER BY CAST(related alias.column AS TEXT)) FROM ... WHERE correlation), which is Winter's group_concat sort and matches what relatedSelectMany displays. Emit it as a clause.OrderByColumn whose clause.Column has Raw true and whose Desc follows the request, so the existing primary-key tiebreak OrderByColumn merges into the same ORDER BY (a clause.OrderBy carrying an Expression is replaced when the tiebreak column merges into it). + +Create modules/cabana/query_relation_test.go (package cabana, no Docker). Fixtures: relPost (table cabana_rel_posts: ID, Title, AuthorID pointer, Author relAuthor belongsTo through AuthorID, Tags []relTag with gorm tag many2many:cabana_rel_post_tags, Notes []relNote has-many through PostID), relTag (cabana_rel_tags: ID, Name), relNote (cabana_rel_notes: ID, PostID, Body), relAuthor (cabana_rel_authors: ID, Name); a relController with ConfigDir controllers/widgets so filterFS applies and NewRecord returning a relPost pointer; columns.yaml with title (searchable), tags (relation tags, select name, searchable, sortable), notes (relation notes, select body, searchable), author (relation author, select name, searchable, sortable); config_list.yaml with showSorting true. A helper opens a DryRun GORM db with the postgres dialector on a DSN that is never dialed (host 127.0.0.1 port 1; gorm.Config DryRun true, DisableAutomaticPing true) and a recording logger (a gorm logger.Interface whose Trace appends the SQL returned by its callback and whose LogMode returns itself). A second helper builds a *service like newListService does, but with backpack.New(nil), Publish of the DryRun db and the compiled list registered as acme.demo.widgets (the id controllerRequest targets); plan 03 and Task 2 reuse both helpers. TestListRelationSearchSmoke implements the behavior above through getList. + +docs/backend/lists-and-filters.md: one sentence after the column options paragraph: a searchable relation column matches a record when any related record matches (belongsTo, hasOne, hasMany and belongsToMany, each through a subquery, so a record is listed and counted once), and a sortable hasMany or belongsToMany column sorts by its comma-joined values. Use acme/blog names only. The module README needs no change (no exported identifier, config key or command changed). + +Commit in summercms.go, staging exactly modules/cabana/query.go, modules/cabana/query_relation_test.go and docs/backend/lists-and-filters.md: fix(cabana): search and sort to-many relation columns through subqueries. + + go vet ./modules/cabana && go test -count=1 -run '^TestListRelationSearchSmoke$' -v ./modules/cabana 2>&1 | grep -- '^--- PASS: TestListRelationSearchSmoke' + Non-zero exit: go vet reports a problem, the test fails or does not exist, or the line "--- PASS: TestListRelationSearchSmoke" is absent from the output. + go test -count=1 ./cmd/summer -run TestDocsTree + Non-zero exit or "FAIL" in the output: the docs checker rejects an identifier, link or application name in the edited docs page. + + + - `go test -count=1 -run '^TestListRelationSearchSmoke$' -v ./modules/cabana` prints `--- PASS: TestListRelationSearchSmoke` on a machine without Docker. + - The test asserts the recorded count SQL contains `EXISTS (SELECT 1 FROM "cabana_rel_post_tags"`, the pivot owner key correlated to `"cabana_rel_posts"."id"`, and no `LEFT JOIN`; and that the sorted query holds `string_agg` followed by the `"cabana_rel_posts"."id"` tiebreak. + - `grep -c 'Relationships.Relations' modules/cabana/query.go` is at least 1; `grep -c 'string_agg' modules/cabana/query.go` is at least 1; `grep -c 'EXISTS (SELECT 1 FROM' modules/cabana/query.go` is at least 1. + - `go vet ./modules/cabana` exits 0 and `go test -count=1 ./cmd/summer -run TestDocsTree` passes. + - `git show --name-only --format= HEAD` lists exactly modules/cabana/query.go, modules/cabana/query_relation_test.go and docs/backend/lists-and-filters.md. + + A search over a belongsToMany column produces valid SQL through the pivot named by GORM's relationship and the list answers 200; to-many sort uses a correlated aggregate; the change is one cabana commit. + + + + Task 2: The post form opens on the default locale: meta.enabledLocales lists the default locale first + git status --porcelain -- modules/cabana docs/backend prints nothing besides files this plan already committed (D-02). + modules/cabana/http.go, modules/cabana/locale_order_test.go, modules/cabana/README.md, docs/backend/forms.md + modules/cabana/http.go (enabledContentLocales near line 966 and its callers near lines 536 and 727), modules/cabana/field_ml.go (TranslationWriter, hydrateMLRecord), modules/cabana/ml_smoke_test.go (recordingWriter), modules/backpack/app.go (Publish, Lookup), modules/cabana/query_relation_test.go (the DryRun db helper from Task 1), modules/cabana/README.md (ML paragraph near line 27 and the cabana.FormMeta row near line 232), docs/backend/forms.md (ML paragraph near line 333), .planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-RESEARCH.md (Defects 3 and 4, Pattern 3) + + - With a published TranslationWriter whose EnabledLocales returns en, pl and whose DefaultLocale returns pl, enabledContentLocales returns pl, en. + + In service.enabledContentLocales (modules/cabana/http.go), after writer.EnabledLocales succeeds, call writer.DefaultLocale(ctx, db). When it returns a non-empty code found at an index above 0, move that code to index 0 and keep the other codes in the writer's order (work on a clone; slices.Index and slices.Delete). When DefaultLocale fails, log slog.Error with the message "cabana: default locale" and the error, and return the writer's order unchanged. This is the D-03 fix: the SPA's MLField and FormView already open on enabledLocales[0], and hydrateMLRecord already puts the host column (content, excerpt, title, slug) under the default locale, so the form shows the stored values. Do not change the SPA, liftMLValues or hydrateMLRecord. + +Create modules/cabana/locale_order_test.go with TestEnabledLocalesDefaultFirstSmoke: a service whose backpack app publishes the Task 1 DryRun db and, with Publish typed as TranslationWriter, a recordingWriter with defaultLocale pl and enabled en, pl; enabledContentLocales returns exactly pl, en. + +Docs (exported field semantics, CLAUDE.md README rule): in modules/cabana/README.md say that cabana.FormMeta.EnabledLocales lists the default locale first, then the other enabled codes in the writer's order (the FormMeta table row and the ML paragraph); in docs/backend/forms.md add that the selector options list the default locale first, so every ML control starts on it. Both files use the words default locale first. No consuming application names. + +Commit in summercms.go, staging exactly those four paths: fix(cabana): send the default content locale first in enabledLocales. + + go vet ./modules/cabana && go test -count=1 -run '^TestEnabledLocalesDefaultFirstSmoke$' -v ./modules/cabana 2>&1 | grep -- '^--- PASS: TestEnabledLocalesDefaultFirstSmoke' + Non-zero exit: go vet reports a problem, the test fails or does not exist, or the line "--- PASS: TestEnabledLocalesDefaultFirstSmoke" is absent. + go test -count=1 ./cmd/summer -run TestDocsTree + Non-zero exit or "FAIL" in the output. + + + - `go test -count=1 -run '^TestEnabledLocalesDefaultFirstSmoke$' -v ./modules/cabana` prints `--- PASS: TestEnabledLocalesDefaultFirstSmoke`. + - `grep -c 'DefaultLocale' modules/cabana/http.go` is greater than before the task (enabledContentLocales calls it). + - `grep -ic 'default locale first' modules/cabana/README.md docs/backend/forms.md` reports at least 1 for each file (both were 0 before the task). + - `go test -count=1 ./cmd/summer -run TestDocsTree` passes. + - `git show --name-only --format= HEAD` lists exactly modules/cabana/http.go, modules/cabana/locale_order_test.go, modules/cabana/README.md and docs/backend/forms.md. + + Every form and relation-child schema sends the default locale first, so ML fields open on the stored host values. + + + + Task 3: Saving an imported post keeps its HTML: FormatHTML renders raw HTML behind the unchanged safety gate + After this change admin saves, imports and Journal API writes store raw HTML in content_html; reverting the renderer does not clean rows already saved, each would need a re-render. + ../sm-journal-plugin/classes/format_html.go, ../sm-journal-plugin/classes/format_html_test.go, ../sm-journal-plugin/README.md + ../sm-journal-plugin/classes/format_html.go, ../sm-journal-plugin/classes/format_html_test.go, ../sm-journal-plugin/classes/check_html_test.go, ../sm-journal-plugin/controllers/posts.go (preparePostWrite near line 204), ../sm-journal-plugin/README.md (Posts bullet near line 21, API table row for classes.CheckHTML near line 180), .planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-RESEARCH.md (Data loss on save, Pattern 4, Pitfall 5) + + - FormatHTML of two HTML paragraphs separated by a newline returns them unchanged after trimming. + - FormatHTML of a markdown heading followed by a raw footer element keeps the footer element. + - Every TestFormatHTMLRejectsUnsafeHTML case still passes (script, iframe, event handler, javascript, vbscript and data URLs are refused by the gate). + + In ../sm-journal-plugin/classes/format_html.go import github.com/yuin/goldmark/renderer/html (a package of the already-required goldmark v1.8.6; no go.mod change) and add goldmark.WithRendererOptions(html.WithUnsafe()) to markdownEngine, with a comment that Winter's Markdown::parse passes raw HTML through and rejectUnsafe gates the result (D-03, PHP parity). Rename the local result variable in FormatHTML and the parameters of CheckHTML and rejectUnsafe that share the imported package's name (for example out and doc) so nothing shadows the import. Keep the rejectUnsafe call after the render, the three gate patterns and CheckHTML unchanged. Do not touch cabana.RenderMarkdown in summercms.go (the admin preview stays safe-mode). + +Add TestFormatHTMLKeepsRawHTML to classes/format_html_test.go with the two keep cases from the behavior list. + +../sm-journal-plugin/README.md: extend the Posts bullet: FormatHTML passes raw HTML in content through unchanged (Winter parity), and the gate still refuses script, iframe, object and embed tags, event handlers and javascript, vbscript and data URLs on every write path (admin save, journal:import-posts, Journal API writes). The API response shapes are unchanged. + +Commit in ../sm-journal-plugin on main, staging exactly classes/format_html.go, classes/format_html_test.go and README.md (go.work.sum stays untracked): fix(classes): keep raw HTML in content_html behind the safety gate. Do not push; plan 02 pushes. + + go -C ../sm-journal-plugin vet ./... && go -C ../sm-journal-plugin test -count=1 ./classes/ && go -C ../sm-journal-plugin test -count=1 -run '^TestFormatHTMLKeepsRawHTML$' -v ./classes/ 2>&1 | grep -- '^--- PASS: TestFormatHTMLKeepsRawHTML' + Non-zero exit: vet fails, any classes test fails (including TestFormatHTMLRejectsUnsafeHTML), or the line "--- PASS: TestFormatHTMLKeepsRawHTML" is absent. + + + - `go -C ../sm-journal-plugin test -count=1 ./classes/` prints `ok` for the classes package. + - `grep -c 'WithUnsafe' ../sm-journal-plugin/classes/format_html.go` is 1 and `grep -c 'rejectUnsafe(' ../sm-journal-plugin/classes/format_html.go` is unchanged from before the task. + - `go -C ../sm-journal-plugin vet ./...` exits 0. + - `git -C ../sm-journal-plugin show --name-only --format= HEAD` lists exactly classes/format_html.go, classes/format_html_test.go and README.md. + - `git -C ../sm-journal-plugin status --porcelain -- go.mod go.sum` prints nothing (no dependency change). + + Imported HTML survives an admin save, import or API write, and the XSS gate still refuses the dangerous patterns. + + + + Task 4: The posts list remembers its state: URL plus browser storage, restored on entry before the first list request + git status --porcelain -- admin scripts/check-phase10.sh modules/boardwalk prints nothing (no in-flight edit by the concurrent session, D-02). + admin/src/state/listState.ts, admin/src/views/ListView.vue, admin/src/state/useAuth.ts, admin/src/state/useSidebar.ts, admin/tests/list/ListView.test.ts, scripts/check-phase10.sh, modules/boardwalk/dist/ + admin/src/views/ListView.vue (whole script block, especially replaceQuery, loadList and the watch on listQueryKey), admin/src/app/listQuery.ts (parseListQuery, toListQuery, listQueryKey), admin/src/app/controllerRoutes.ts (controllerIdFromParams, controllerPath), admin/src/state/useSidebar.ts (storage access pattern and header comment), admin/src/state/useAuth.ts (logout), admin/src/views/FormView.vue (listPath, backPath, go, onLeave, onDelete, redirectTarget: read only, it is not changed), admin/tests/list/ListView.test.ts (routes, mountApp, requestsTo, queryOf), admin/tests/helpers.ts (resetState clears storage), admin/tests/smoke/shell.smoke.test.ts (asserts only the sidebar key is stored after mounting a list), scripts/check-phase10.sh (hygiene storage rule near line 370), scripts/check-admin-dist.sh, .planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-CONTEXT.md (D-01) + + - Mounting /acme/demo/widgets with no query after writeListState stored sort code desc and filter active true: the first list GET carries sort=code, dir=desc and filter[active]=true, and the router URL carries them. + - Mounting /acme/demo/widgets?search=blue: the stored state for acme.demo.widgets holds search blue (the URL wins). + + Implements D-01 as revised: no server call, table, migration or route. + +Create admin/src/state/listState.ts, the only module besides useSidebar.ts that touches browser storage. Exports: LIST_STATE_PREFIX equal to summer-admin.list: (the key is the prefix plus the controller id, one key per list; Claude's discretion on naming, following SIDEBAR_KEY); readListState(controllerId) returning a ListQuery: it reads the key, JSON-parses it, accepts only a plain object whose values are strings, and passes it through parseListQuery so malformed stored values are dropped exactly like a hand-edited URL; anything else removes the key and returns an empty query; writeListState(controllerId, query) stores JSON of toListQuery(query) when listQueryKey(query) differs from the empty query's key and removes the key otherwise, so an empty list never leaves an entry; clearListStates() removes every key with the prefix. Wrap every storage access in try/catch like useSidebar.ts: disabled or full storage is a no-op and the URL still carries the state. + +admin/src/views/ListView.vue: keep the immediate watch on listQueryKey(query) but make its handler decide before any list request. When the current query is empty and readListState(controllerId) is non-empty, call router.replace with path route.path and query toListQuery(stored) and return without loading (the watch runs again with the restored query). Otherwise call writeListState(controllerId, query.value) (the URL wins), then clear the selection, sync searchText and call loadList as today. replaceQuery calls writeListState with the next query before router.replace, so a user who clears every control stores the empty state and the following empty-query run loads unfiltered instead of restoring. The same handler covers navigating to the same list while it is filtered: the query empties without replaceQuery and the stored query comes back. In loadList, after the generation check: when the response status is 422, writeListState(controllerId, an empty query) so a stale stored filter or sort never sticks; when meta.last_page is at least 1 and meta.page is above it, replaceQuery with the current query and page set to meta.last_page. FormView is not changed: its back, cancel, save-and-close, delete and redirect navigation goes to the bare list path, and this entry restore puts the stored query into the URL before the first list request, which is how the form and the navigation return to the list with its query (D-01). + +admin/src/state/useAuth.ts: logout calls clearListStates() in its finally block next to clearUser(), so the next admin on the same browser sees none of the previous searches. + +admin/src/state/useSidebar.ts: rewrite the header sentence that says nothing else is ever written to browser storage so it names listState.ts as the only other writer (list query parameters only, never tokens or record data). + +scripts/check-phase10.sh: in the hygiene storage rule allow admin/src/state/listState.ts in addition to useSidebar.ts (two grep -v path prefixes) and update its comment and message to name both files; the self-test plant under admin/src/__plant must still be refused. + +admin/tests/list/ListView.test.ts: add the two smoke cases from the behavior list, importing writeListState and readListState from ../../src/state/listState (this also satisfies the hygiene rule that every source file is imported by a test). Full coverage is plan 03. + +Rebuild the embedded SPA with npm --prefix admin run build and commit modules/boardwalk/dist in the same commit as the SPA change. Commit in summercms.go, staging exactly admin/src/state/listState.ts, admin/src/views/ListView.vue, admin/src/state/useAuth.ts, admin/src/state/useSidebar.ts, admin/tests/list/ListView.test.ts, scripts/check-phase10.sh and modules/boardwalk/dist: fix(admin): keep list state in the URL and browser storage. + + + npm --prefix admin run typecheck && npm --prefix admin test -- tests/list/ListView.test.ts tests/form/FormView.test.ts tests/smoke tests/state + Non-zero exit, or the vitest summary line reports any "failed" test or file. + bash scripts/check-admin-dist.sh + Non-zero exit or "modules/boardwalk/dist is stale" on stderr. + test -z "$(grep -rnE 'localStorage|sessionStorage|indexedDB|document\.cookie' admin/src | grep -v -e '^admin/src/state/useSidebar.ts:' -e '^admin/src/state/listState.ts:')" + Non-zero exit: a browser-storage reference exists in admin/src outside useSidebar.ts and listState.ts. + + + - The vitest run passes, including the two new list-state smoke cases and the unchanged shell smoke test that expects only the sidebar key after mounting a list without query. + - `bash scripts/check-admin-dist.sh` prints "modules/boardwalk/dist matches a fresh build". + - `grep -c 'summer-admin.list:' admin/src/state/listState.ts` is 1; `grep -c 'clearListStates' admin/src/state/useAuth.ts` is at least 1; `grep -c 'admin/src/state/listState.ts' scripts/check-phase10.sh` is at least 1; `grep -c 'readListState' admin/src/views/ListView.vue` is at least 1. + - Browser-storage references in admin/src appear only in useSidebar.ts and listState.ts (third verify command). + - `git show --name-only --format= HEAD` lists only the seven staged paths (dist files under modules/boardwalk/dist). + + The posts list keeps its search, sort, filters and page in the URL and per list in browser storage, restores them when entered without params, lets a URL win, and forgets them on logout; the embedded build matches. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| Admin SPA → cabana list API | search, sort, dir, page, per_page and filter[name] come from the URL or browser storage and are untrusted | +| Browser storage → ListView | a stored list query may be stale, hand-edited or written by another script on the admin origin | +| Admin author → content_html → public site | post bodies with raw HTML are stored and served through the Journal API to the public Nuxt site | +| Shared browser between admins | stored list queries outlive a session unless cleared | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-16.1-01 | Tampering (SQL injection) | modules/cabana/query.go relation subquery builder | high | mitigate | Relation and column names come only from the compiled list schema and GORM's parsed relationship (LookUpField, identifier()); every identifier is quoted through the dialector (quotedIdent/qualifiedColumn); the search pattern is a bound parameter with LIKE escaping; unknown or polymorphic relations are refused with 422 before any SQL runs; permission is still checked by protect() before ExecuteList | +| T-16.1-02 | Tampering / Elevation (stored XSS) | ../sm-journal-plugin/classes/format_html.go | medium | mitigate | rejectUnsafe stays after the now-unsafe render on every write path (admin, import, API) and refuses script/iframe/object/embed, on* handlers and javascript/vbscript/data URLs; authors are authenticated admins with the journal posts permission; the admin preview (cabana.RenderMarkdown, v-html) stays safe-mode; the public layer keeps its own HTML gate (T-16-01). Residual (accepted, PHP parity): inert tags such as style, footer or form pass, which PHP's Html::clean would have stripped for admins without unsafe-markdown rights | +| T-16.1-03 | Information Disclosure | admin/src/state/listState.ts | low | mitigate | Only list query parameters are stored (no tokens, no record data) under summer-admin.list: keys; logout clears them; the check-phase10.sh hygiene allowlist confines browser storage to useSidebar.ts and listState.ts | +| T-16.1-04 | Tampering | restored list state | low | mitigate | Stored values pass through parseListQuery (drops malformed page, dir, empty filters); the server re-validates every parameter against the compiled schema and answers 422; a 422 removes the stored state so it cannot stick | +| T-16.1-05 | Denial of Service | correlated EXISTS / string_agg subqueries | low | accept | One subquery per searchable relation column per row, bounded by the page and by pivot primary keys; admin-only route | +| T-16.1-SC | Tampering | npm/go installs | high | mitigate | No package is installed: github.com/yuin/goldmark/renderer/html is a package of the already-required goldmark v1.8.6 (RESEARCH Package Legitimacy Audit: no packages); no npm dependency is added; go.mod and package.json stay unchanged (Task 3 acceptance) | + + + +- Task 1: `--- PASS: TestListRelationSearchSmoke`; Task 2: `--- PASS: TestEnabledLocalesDefaultFirstSmoke`; Task 3: classes tests `ok` and `--- PASS: TestFormatHTMLKeepsRawHTML`; Task 4: vitest green, dist matches, storage confined. +- `go vet ./...` (summercms.go) and `go -C ../sm-journal-plugin vet ./...` exit 0. +- `go test -count=1 ./cmd/summer -run TestDocsTree` passes. +- summercms.go has three new commits from this plan (cabana search and sort, cabana locales, admin list state), sm-journal-plugin one; nothing is pushed. + + + +- D-02: relation search over belongsToMany, hasMany, hasOne and belongsTo columns and to-many sort produce valid SQL from GORM's relationship metadata. +- D-03: forms open on the default locale; FormatHTML keeps raw HTML behind the gate. +- D-01: list state lives in the URL and per list in browser storage, restored on entry, URL wins, cleared on logout. +- No server-side storage, migration, route or OpenAPI change; no dependency added. + + + +Create `.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-01-SUMMARY.md` when done, listing the commit hashes per repository (plan 02 tags and pushes them). + diff --git a/.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-02-PLAN.md b/.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-02-PLAN.md new file mode 100644 index 0000000..124b304 --- /dev/null +++ b/.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-02-PLAN.md @@ -0,0 +1,240 @@ +--- +phase: 16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f +plan: 02 +type: execute +wave: 2 +depends_on: ["16.1-01"] +files_modified: + - ../sm-journal-plugin/go.mod + - ../sm-grzybyfunkcjonalne-app/plugins/golem15/journal + - ../sm-grzybyfunkcjonalne-app/README.md + - ../sm-grzybyfunkcjonalne-app/DEPLOY.md +autonomous: false +requirements: [D-04, D-03] +user_setup: + - service: UAT admin (https://uat.grzybyfunkcjonalne.pl/gfadmin) + why: "The executor holds no UAT basic-auth or admin credentials; a human runs the posts-list search and re-saves posts 53 and 64 (D-03, D-04)" + dashboard_config: + - task: "Log in to the admin through the UAT basic-auth realm with an admin account" + location: "browser, https://uat.grzybyfunkcjonalne.pl/gfadmin" +estimate: + tokens: 50000 + raw_tokens: 50000 + tasks: 3 + confidence: low +must_haves: + truths: + - "D-04: an annotated summercms tag (v0.1.16, or the next free v0.1.x if the concurrent session tagged first) contains every 16.1-01 cabana and admin commit and is on origin; when an existing tag already contains them, no new tag is cut and that tag is recorded instead." + - "D-04: sm-journal-plugin main, holding the FormatHTML fix and a go.mod require of the release tag, is on origin, and sm-grzybyfunkcjonalne-app's plugins/golem15/journal gitlink points at it; the host README and DEPLOY.md no longer tell admins to avoid re-saving imported posts." + - "D-04: UAT runs a binary built by scripts/build.sh from the tagged framework working tree and the bumped journal gitlink; its sha256 on the server equals the local build, both supervisor programs are RUNNING, the previous binary is kept as bin/grzybyfunkcjonalne.prev, and the on-server smoke.sh --url run ends with 'smoke: ok ('." + - "D-02: after the restart, a human search in the UAT Journal posts list returns rows, and the Go log shows no SQLSTATE 42703 line since the restart." + - "D-03: after a human re-saves posts 53 and 64 in the admin, no post's content_html holds goldmark's raw-HTML placeholder comment, and post 64's content_html contains its footer element again." + - "D-01 and D-03 on UAT (end-of-phase human check): sort and a category filter survive open post and back, save-and-close and rail navigation, and stay in the URL; an imported post's form shows title, slug, content and excerpt with pl first." + artifacts: + - path: "../sm-grzybyfunkcjonalne-app/DEPLOY.md" + provides: "release and rollback procedure, without the obsolete admin re-save warning" + contains: "## Release" + - path: "../sm-journal-plugin/go.mod" + provides: "nominal require of the release tag (the replace to ../summercms.go stays)" + contains: "git.golem15.com/golem15/summercms v0.1." + key_links: + - from: "../sm-grzybyfunkcjonalne-app/plugins/golem15/journal (gitlink)" + to: "sm-journal-plugin origin/main" + via: "submodule pointer bump committed and pushed in the host" + pattern: "plugins/golem15/journal" + - from: "../sm-grzybyfunkcjonalne-app/scripts/build.sh" + to: "../summercms.go at the release tag" + via: "replace git.golem15.com/golem15/summercms => ../summercms.go in the host go.mod" + pattern: "summercms.go" + prohibitions: + - "[FLAGGED-UNVERIFIED] The release must not run write SQL on the UAT database or bulk-rewrite content_html; only posts 53 and 64 are re-saved, by a human, in the admin." + - "[FLAGGED-UNVERIFIED] The live Winter site, its vhost, php-fpm and the PHP database must not be touched by the redeploy (Phase 16 D-10 stays in force)." + - "[FLAGGED-UNVERIFIED] The executor must not request, print, store or commit any credential: UAT basic auth, admin password or a server .env secret." +--- + +## Phase Goal + +ROADMAP Goal (not in user-story format): the Journal posts list and post form in the admin work on real imported data found on UAT, and the fixes are released as a summercms tag, the host is bumped, UAT is redeployed and the staging smoke re-runs. + +This plan's slice (D-05: "02 release and redeploy to UAT, including re-saving posts 53 and 64"): the 16.1-01 fixes reach UAT behind a human release gate, and the four defects are checked on the real imported data. + +Wave order (stated per the orchestrator's request): 01 (wave 1) then this plan (wave 2) then 03, the unit tests (wave 3). CLAUDE.md lean rule 3 says earlier plans "must not be blocked on coverage" and the unit-test plan is the last plan of the phase, so the release does not wait for plan 03, as Phase 16 shipped 16-04 (deploy) before 16-05 (tests). The release gate runs the 16.1-01 smoke tests; plan 03's tests land on master after the tag and ship with the next tag. Plan 03 runs after this plan so the two never commit or push in the same repositories at the same time. + +## Spec-less probe fallback + +Visible skip: no REQUIREMENTS.md IDs to probe. CONTEXT D-04 (release mechanics), D-03 (re-save of posts 53 and 64) and the RESEARCH "Release / deploy mechanics" and "Runtime State Inventory" sections are the source of truth. Claude's discretion (CONTEXT): the unused `en` locale on UAT is not disabled; the framework fix (default locale first) is enough for D-03, and changing UAT data was not asked for. + +## Artifacts this phase produces + +- A summercms annotated tag `v0.1.x` on origin (or the recorded existing tag that already contains the fixes). +- sm-journal-plugin `main` on origin: the 16.1-01 FormatHTML commit and `chore(deps): require summercms v0.1.x`. +- sm-grzybyfunkcjonalne-app `master` on origin: journal gitlink bump, README and DEPLOY.md without the admin re-save warning. +- UAT server: new `/srv/grzybyfunkcjonalne/go/bin/grzybyfunkcjonalne`, previous one at `bin/grzybyfunkcjonalne.prev`, both programs restarted, posts 53 and 64 re-rendered by an admin save. + + +Release and redeploy (D-04): pass a human-approved release gate, tag summercms, push the plugin and host bumps, build, deploy to uat.grzybyfunkcjonalne.pl per DEPLOY.md, smoke on the server, then confirm the four defects on UAT and restore posts 53 and 64 (D-03). + +Purpose: the fixes from 16.1-01 serve the real imported data on UAT. +Output: pushed tag and branches, a running UAT release, a passing on-server smoke and read-only proof that the damaged posts are repaired. + + + +@~/.claude/msd-core/workflows/execute-plan.md +@~/.claude/msd-core/templates/summary.md + + + +@.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-CONTEXT.md +@.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-RESEARCH.md +@.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-01-SUMMARY.md +@.planning/phases/16-grzybyfunkcjonalne-pl-on-reusable-blog-views/16-04-SUMMARY.md +@../sm-grzybyfunkcjonalne-app/DEPLOY.md + +Server facts (16-04-SUMMARY, DEPLOY.md "Server facts"): ssh alias `grzybyfunkcjonalne-uat` (user grzybyfunkcjonalne), install root `/srv/grzybyfunkcjonalne`, programs `grzybyfunkcjonalne-go` and `grzybyfunkcjonalne-nuxt`, sudo only for nginx -t, nginx reload and supervisorctl, PostgreSQL 17.11 reachable by peer psql as the deploy user. The UAT realm answers 401 off-server, so every HTTP check runs on the server with curl resolving the host to 127.0.0.1 (DEPLOY.md "Smoke"). + +The host builds the framework from the `../summercms.go` working tree through a replace (`require ... summercms v0.0.0`), so there is no framework version to bump in the host; the host bump is the journal gitlink. Commits: no co-author tags, stage by explicit path, one logical change per commit. + + + + + + Task 1: Release gate: local checks green, tag chosen, a human approves the one-way tag push and the UAT deploy + ../sm-grzybyfunkcjonalne-app/DEPLOY.md (Build and upload, Release, Smoke, Rollback), .planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-01-SUMMARY.md (commit hashes per repository), .planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-RESEARCH.md (Release / deploy mechanics, Open Question 1), .planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-VALIDATION.md (Docker-host suites) + Before stopping, the executor does everything automatable and prints a release sheet. + +Concurrency (D-02): in summercms.go, git status --porcelain -- modules admin docs scripts must print nothing; git fetch origin --tags; git log --oneline HEAD..origin/master must print nothing. If origin/master has commits this checkout lacks, the concurrent session pushed: stop and report; do not merge or rebase on your own. + +Tag (D-04): if git tag --contains on the first 16.1-01 cabana commit lists a tag, the concurrent session's release already includes the fixes: propose reusing that tag and cutting none. Otherwise propose the highest tag from git tag -l 'v0.1.*' --sort=-v:refname plus one (v0.1.16 if nobody tagged after v0.1.15) and confirm with git ls-remote --tags origin that it does not exist yet. + +Run the release gate (the verify command). Then print the release sheet: the proposed tag; the commits that will be pushed from summercms.go (git log --oneline origin/master..HEAD, which also carries earlier planning-doc commits) and from ../sm-journal-plugin (git log --oneline origin/main..HEAD); the Postgres-backed suites that cannot run on this machine (TestListQueryContract and the rest of modules/cabana, the sm-journal-plugin admin harness); the deploy steps from DEPLOY.md "Release"; and the rollback (move bin/grzybyfunkcjonalne.prev back and restart grzybyfunkcjonalne-go). Stop for the human. + Push summercms master and the proposed annotated tag, push the plugin and host bumps, and deploy to UAT now? + A pushed tag is read by other repositories' go.mod and the Go module proxy and cannot be moved, only superseded. The cabana changes are covered by DryRun smoke tests only on this machine; the real-database proof is the UAT check in Task 3, with the previous binary kept for rollback. Plan 03 adds full unit coverage after the release (lean rule 3). + + + + + + Reply "release vX" (release-now), "docker passed, release vX" (docker-first, after the human ran the suites), or "hold". + + go vet ./... && go -C ../sm-journal-plugin vet ./... && go test -count=1 -run '^(TestListRelationSearchSmoke|TestEnabledLocalesDefaultFirstSmoke)$' -v ./modules/cabana 2>&1 | grep -c '^--- PASS: Test' | grep -qx 2 && go -C ../sm-journal-plugin test -count=1 ./classes/ && go test -count=1 ./cmd/summer -run TestDocsTree && npm --prefix admin test && bash scripts/check-admin-dist.sh && echo release-gate-ok + Non-zero exit or "release-gate-ok" absent: vet fails in either repository, fewer than two "--- PASS: Test" lines for the two cabana smoke tests, a classes or docs test fails, a vitest file fails, or the dist drift gate reports modules/boardwalk/dist as stale. + + + - The gate printed release-gate-ok. + - `git status --porcelain -- modules admin docs scripts` printed nothing and `git log --oneline HEAD..origin/master` printed nothing at the time of the gate. + - The SUMMARY records the release sheet (proposed or reused tag, commits to push per repository, suites not run) and the human's reply verbatim. + + A human chose release-now or docker-first with a named tag, or hold (the plan stops). + + + + Task 2: Tracer: tag, push, bump, build and deploy; the on-server smoke passes on UAT + A pushed annotated summercms tag is consumed by other repositories' go.mod and the Go module proxy; it cannot be moved, only superseded by a later tag. + Task 1 was answered "release vX" or "docker passed, release vX", RELEASE_TAG is set to that tag in the session, and ssh grzybyfunkcjonalne-uat true exits 0. + ../sm-journal-plugin/go.mod, ../sm-grzybyfunkcjonalne-app/plugins/golem15/journal, ../sm-grzybyfunkcjonalne-app/README.md, ../sm-grzybyfunkcjonalne-app/DEPLOY.md + ../sm-grzybyfunkcjonalne-app/DEPLOY.md (Build and upload, Release, Smoke, the admin re-save warning section before "Production cutover"), ../sm-grzybyfunkcjonalne-app/README.md (Admin section, the bold warning paragraph near line 114), ../sm-grzybyfunkcjonalne-app/scripts/build.sh, ../sm-grzybyfunkcjonalne-app/.gitmodules, ../sm-journal-plugin/go.mod, the Task 1 release sheet in this plan's SUMMARY + Framework (D-04): unless Task 1 recorded an existing tag that already contains the fixes, re-run git log --oneline HEAD..origin/master (must print nothing), create the annotated tag on summercms.go master HEAD with git tag -a "$RELEASE_TAG" and the message "$RELEASE_TAG: cabana relation search and sort through subqueries, default content locale first, admin list state in the URL and browser storage", then git push origin master and git push origin "$RELEASE_TAG". + +sm-journal-plugin: go -C ../sm-journal-plugin mod edit -require=git.golem15.com/golem15/summercms@$RELEASE_TAG (the replace to ../summercms.go stays, so the require is nominal; precedent: "chore(deps): require summercms v0.1.10" in sm-translate-plugin); go -C ../sm-journal-plugin vet ./...; commit go.mod only with "chore(deps): require summercms $RELEASE_TAG"; git -C ../sm-journal-plugin push origin main (this also publishes the 16.1-01 FormatHTML commit). + +Host (../sm-grzybyfunkcjonalne-app): git -C plugins/golem15/journal fetch origin and check out the sm-journal-plugin main commit just pushed. Delete the bold README.md paragraph in the Admin section that tells admins not to edit imported posts, and the DEPLOY.md section between "Rollback" and "Production cutover" that tells them not to re-save imported posts: since the FormatHTML fix an admin save keeps imported HTML. Commit the gitlink, README.md and DEPLOY.md together with "chore: bump journal plugin; admin saves keep imported HTML" and push origin master. + +Build: bash ../sm-grzybyfunkcjonalne-app/scripts/check-deploy.sh, then bash ../sm-grzybyfunkcjonalne-app/scripts/build.sh (it compiles the ../summercms.go working tree, which Task 1 found clean at the tagged HEAD). Record the local sha256sum of ../sm-grzybyfunkcjonalne-app/bin/grzybyfunkcjonalne. + +Release per DEPLOY.md "Release": rsync the binary to /srv/grzybyfunkcjonalne/go/bin/grzybyfunkcjonalne.new and config/, scripts/ and build/nuxt/.output/ as listed in "Build and upload" (never a local .env). On the server in /srv/grzybyfunkcjonalne/go: compare the sha256sum of bin/grzybyfunkcjonalne.new with the local one; cp -p bin/grzybyfunkcjonalne bin/grzybyfunkcjonalne.prev and mv bin/grzybyfunkcjonalne.new bin/grzybyfunkcjonalne; run bin/grzybyfunkcjonalne migrate (this phase adds no migration, it must still exit 0); record RESTART_TS as the server's date in the Go log format (date +'%Y/%m/%d %H:%M:%S') in the SUMMARY and the session; sudo supervisorctl restart grzybyfunkcjonalne-go grzybyfunkcjonalne-nuxt. + +Smoke on the server with the DEPLOY.md recipe (first verify command). On any failure: move bin/grzybyfunkcjonalne.prev back, restart grzybyfunkcjonalne-go, and report. + + ssh grzybyfunkcjonalne-uat 'C=$(mktemp -d); printf "resolve = uat.grzybyfunkcjonalne.pl:443:127.0.0.1\n" > "$C/.curlrc"; cd /srv/grzybyfunkcjonalne/go && CURL_HOME="$C" scripts/smoke.sh --url https://uat.grzybyfunkcjonalne.pl; rc=$?; rm -rf "$C"; exit $rc' | grep '^smoke: ok (' + Non-zero exit: smoke.sh failed on the server, the output contains "smoke: FAIL", or no line starts with "smoke: ok (". + test "$(sha256sum ../sm-grzybyfunkcjonalne-app/bin/grzybyfunkcjonalne | cut -d' ' -f1)" = "$(ssh grzybyfunkcjonalne-uat 'sha256sum /srv/grzybyfunkcjonalne/go/bin/grzybyfunkcjonalne' | cut -d' ' -f1)" && ssh grzybyfunkcjonalne-uat 'sudo supervisorctl status grzybyfunkcjonalne-go grzybyfunkcjonalne-nuxt' | grep -c RUNNING | grep -qx 2 && test -n "$(git ls-remote --tags origin "refs/tags/$RELEASE_TAG")" && echo deploy-ok + Non-zero exit or "deploy-ok" absent: the server binary differs from the local build, fewer than two programs are RUNNING, or the tag is missing on origin. + + + - `git ls-remote --tags origin` lists the release tag, and `git tag --contains` on the first 16.1-01 cabana commit lists it (or the SUMMARY names the existing tag Task 1 reused). + - `git -C ../sm-journal-plugin rev-parse main` equals `git -C ../sm-journal-plugin rev-parse origin/main`, and ../sm-journal-plugin/go.mod requires summercms at the release tag. + - `git -C ../sm-grzybyfunkcjonalne-app ls-files -s plugins/golem15/journal` shows the pushed sm-journal-plugin main commit, and the host master equals origin/master. + - `grep -ci omitted ../sm-grzybyfunkcjonalne-app/README.md ../sm-grzybyfunkcjonalne-app/DEPLOY.md` reports 0 for both files (both warnings removed). + - The on-server smoke prints a line starting with "smoke: ok (" and the second verify prints deploy-ok. + - The SUMMARY records the tag, the three pushed heads, the local and server sha256 and RESTART_TS; no credential appears in it. + + UAT runs the released fixes from the pushed tag and branches, and every smoke URL passes on the server. + + + + Task 3: UAT acceptance: a human searches the posts list and re-saves posts 53 and 64; the executor confirms with read-only checks + Task 2 printed deploy-ok and RESTART_TS is set in the session. + .planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-RESEARCH.md (Runtime State Inventory: posts 53 and 64; Validation Architecture, Manual), .planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-VALIDATION.md (Manual-Only Verifications), ../sm-grzybyfunkcjonalne-app/DEPLOY.md (Smoke, Rollback) + Print the steps for the human and stop. The executor never asks for, receives or stores a credential. Required human steps in https://uat.grzybyfunkcjonalne.pl/gfadmin/golem15/journal/posts: (1) search "cordyceps" and confirm rows appear instead of "The list could not be loaded" (this runs the new relation subquery on the real categories pivot, D-02); (2) open post 53 (czy-amanita-muscaria-jest-legalna) and post 64 (ekstrakt-z-turkey-tail) and press "Zapisz" on each without editing, which re-renders their content_html through the fixed FormatHTML (D-03; RESEARCH: both lost their HTML at import). Optional now, otherwise collected at end of phase: the human-check list below. After the reply, run both verify commands. If either fails, report it and offer the DEPLOY.md rollback (move bin/grzybyfunkcjonalne.prev back and restart grzybyfunkcjonalne-go). Known and accepted (RESEARCH Pitfall 9): each save also stores empty en translations; public output is unchanged. + + Claude deployed the release and passed the on-server smoke in Task 2. + The human logs in to the UAT admin (basic-auth realm plus admin account), runs the search and re-saves posts 53 and 64, then replies. + + Read-only psql on the server: no post holds the placeholder comment, post 64 has its footer element; the Go log since RESTART_TS has no SQLSTATE 42703. + Type "resaved" once the search returned rows and both posts were saved, or describe what went wrong. + + ssh grzybyfunkcjonalne-uat "psql -X -d grzybyfunkcjonalne -tAc \"SELECT (SELECT count(*) FROM golem15_journal_posts WHERE content_html LIKE '%raw HTML omitted%') || ':' || (SELECT count(*) FROM golem15_journal_posts WHERE id = 64 AND content_html LIKE '%<footer%')\"" | grep -qx '0:1' + Non-zero exit: the output is not exactly "0:1" (a post still holds the placeholder comment, post 64 lacks its footer element, or psql could not connect). + ssh grzybyfunkcjonalne-uat 'sudo supervisorctl tail -65536 grzybyfunkcjonalne-go' | awk -v since="$RESTART_TS" 'substr($0,1,19) >= since' | grep -c 'SQLSTATE 42703' | grep -qx 0 + Non-zero exit: a line with "SQLSTATE 42703" was logged after RESTART_TS (with RESTART_TS unset every line is kept, so the earlier failure makes the check fail closed). + On https://uat.grzybyfunkcjonalne.pl/gfadmin/golem15/journal/posts (D-01): set a sort and a category filter, open a post and press back; open another post and use "Zapisz i zamknij"; leave through the plugin rail and come back: each time the list keeps the sort and the filter and the URL shows them. Paste that URL into a new tab: same list. Log out and in again: the list opens unfiltered. (D-03): open any imported post: the locale selector shows pl first, and title, slug, content and excerpt are filled. /blog/ekstrakt-z-turkey-tail shows the author footer. Reply "UAT approved" or list the issues. + + + - The SQL check prints exactly 0:1 on the server. + - The log check finds 0 "SQLSTATE 42703" lines after RESTART_TS. + - The SUMMARY records the human's reply and the human-check answer (or marks it pending for /msd-verify-work). + + Search works on the real pivot, posts 53 and 64 have their HTML back, and the remaining UAT checks are answered or queued for verify-work. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| Executor → git origin (git.golem15.com) | tags and branches pushed here are read by other repositories | +| Executor → UAT server (ssh) | the release runs next to the live Winter site on the same server | +| Human admin → UAT admin | the only path that writes post data in this plan | +| Server .env files → processes | secrets live only on the server | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-16.1-06 | Tampering (false-green release) | release gate | medium | mitigate | Task 1 runs vet in both Go repositories, the named smoke tests with a counted PASS check, TestDocsTree, the full vitest suite and the dist drift gate; the Docker-only suites are named in the release sheet and the human chooses release-now or docker-first at a blocking-human checkpoint | +| T-16.1-07 | Tampering / Repudiation | annotated tag and pushes | high | mitigate | Tag only after git status is clean for modules, admin, docs and scripts and HEAD..origin/master is empty, re-checked right before the push; git tag --contains decides whether a concurrent release already includes the fixes; the uploaded binary's sha256 must equal the local build; the SUMMARY records every pushed head | +| T-16.1-08 | Information Disclosure | server .env files and credentials | high | mitigate | No .env is rsynced (DEPLOY.md); the executor never asks for, prints or stores UAT basic-auth or admin credentials; all admin steps are done by the human; the SUMMARY carries no secret | +| T-16.1-09 | Tampering | UAT database and the live Winter site | medium | mitigate | The executor runs only SELECT queries; post data changes only through two human admin saves; the Winter vhost, php-fpm and PHP database are not touched; the previous binary stays as bin/grzybyfunkcjonalne.prev for rollback | +| T-16.1-10 | Elevation of Privilege | sudo on the server | low | accept | sudo is limited by the server to nginx -t, nginx reload and supervisorctl; this plan uses only supervisorctl restart, status and tail | +| T-16.1-SC | Tampering | packages | high | mitigate | No package is installed: the binary and the Nuxt output are built locally from the committed lockfiles and go.sum by scripts/build.sh; go mod edit changes only the nominal summercms require, which resolves through the local replace | + + + +- Task 1 printed release-gate-ok and a human chose a release option. +- Task 2: the on-server smoke prints "smoke: ok (" and deploy-ok is printed. +- Task 3: the SQL check prints 0:1 and the log check finds no SQLSTATE 42703 after RESTART_TS. + + + +- D-04: tag (or recorded existing tag), plugin and host bumps pushed; UAT redeployed per DEPLOY.md; on-server smoke passes. +- D-02 and D-03 proven on the real UAT data; posts 53 and 64 repaired by an admin save. +- No credential handled by the executor; no write SQL; the Winter site untouched. + + + +Create `.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-02-SUMMARY.md` when done, with the release sheet, the human replies, the tag and pushed heads, both sha256 values, RESTART_TS, the smoke line and the two read-only check results. + diff --git a/.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-03-PLAN.md b/.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-03-PLAN.md new file mode 100644 index 0000000..1438b66 --- /dev/null +++ b/.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-03-PLAN.md @@ -0,0 +1,215 @@ +--- +phase: 16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f +plan: 03 +type: execute +wave: 3 +depends_on: ["16.1-01", "16.1-02"] +files_modified: + - admin/tests/form/FormView.test.ts + - admin/tests/list/ListView.test.ts + - admin/tests/state/listState.test.ts + - admin/tests/state/useAuth.test.ts + - modules/cabana/query_relation_test.go + - modules/cabana/locale_order_test.go + - ../sm-journal-plugin/classes/format_html_test.go + - ../sm-journal-plugin/posts_admin_smoke_test.go + - .planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-VALIDATION.md +autonomous: true +requirements: [D-01, D-02, D-03] +estimate: + tokens: 75000 + raw_tokens: 75000 + tasks: 3 + confidence: low +must_haves: + truths: + - "CLAUDE.md lean rule 3: every piece of Phase 16.1 code has unit tests in this last plan: the relation subquery builder (search and sort, every relation type, refusals), enabledContentLocales ordering, FormatHTML keep and refuse, the listState helpers, the ListView entry logic, the FormView return paths and logout clearing." + - "D-01: a form's back, cancel, save-and-close and delete each land on the list with the stored query in the URL, and the first list request after the return carries it (no list request without it)." + - "D-01: ListView restores the stored query before its first list request, lets a URL with list params win and overwrite storage, removes the stored query when the user clears every control (the next entry loads unfiltered), keeps the filters when the same list is opened again from the navigation, drops the stored query on a 422, and moves a restored page beyond last_page to last_page." + - "D-01: listState helpers use the summer-admin.list: prefix plus the controller id, drop malformed stored JSON or values (and remove that key), store nothing for an empty query, survive storage that throws, and clearListStates (called by logout) removes list keys but keeps summer-admin.sidebar." + - "D-02: DryRun tests prove belongsToMany search goes through the pivot named by the gorm tag, including custom joinForeignKey and joinReferences columns (no naming convention guessed); hasMany and belongsTo correlate on the parsed keys; a self-referencing relation gets distinct aliases; a soft-deleting related model adds deleted_at IS NULL; polymorphic relations and unknown columns answer 422; the search value is bound and LIKE-escaped, never in the SQL text; combined with a ListExtendQuery user_id predicate and a scope filter the count has no outer JOIN; to-many sort orders by string_agg with the primary-key tiebreak in both directions; to-one sort keeps the association join." + - "D-03: enabledContentLocales tests cover default not first, already first, default absent, DefaultLocale error (order kept), no writer (nil) and three locales keeping their relative order." + - "D-03: FormatHTML tests prove imported HTML round-trips (paragraphs, attributes, markdown with a trailing raw footer and style) and that every unsafe case returns an error now that the gate is the only defence (script, iframe, object, embed, on* attributes in raw HTML including an svg onload, javascript, vbscript and data URLs in links and raw attributes, mixed case)." + - "Postgres-backed tests TestListRelationSearchPostgres (exact total when one post matches through two tags) and TestPostsAdminResaveKeepsHTML (an admin update of HTML content keeps content_html) exist, pass go vet, and fail rather than skip without a database outside -short; they run on a Docker host before /msd-verify-work." + - "go vet stays clean in summercms.go and sm-journal-plugin; the named pure Go tests, the full vitest suite, TestDocsTree and scripts/check-admin-dist.sh are green; this plan changes tests only, so the embedded dist does not change." + artifacts: + - path: "admin/tests/state/listState.test.ts" + provides: "unit tests of the browser-storage helpers" + contains: "clearListStates" + - path: "modules/cabana/query_relation_test.go" + provides: "DryRun SQL matrix and the Postgres relation-search test" + contains: "TestListRelationSQL" + - path: "modules/cabana/locale_order_test.go" + provides: "default-locale ordering cases" + contains: "TestEnabledLocalesDefaultFirst" + - path: "../sm-journal-plugin/posts_admin_smoke_test.go" + provides: "admin re-save keeps imported HTML (todo journal-imported-html-admin-resave acceptance test)" + contains: "TestPostsAdminResaveKeepsHTML" + key_links: + - from: "admin/tests/form/FormView.test.ts" + to: "admin/src/views/ListView.vue entry restore" + via: "form back navigation to the bare list path, then the restored query in the URL and the first list request" + pattern: "writeListState" + - from: "modules/cabana/query_relation_test.go" + to: "modules/cabana/query.go applyListSearch and applyListOrder" + via: "DryRun db with a recording logger, through service.list and ExecuteList" + pattern: "DryRun" + prohibitions: + - "[FLAGGED-UNVERIFIED] No test may touch the UAT server, the network or a shared database: pure tests use a DryRun db on a DSN that is never dialed, Postgres tests use throwaway testcontainers databases." + - "[FLAGGED-UNVERIFIED] Tests must not be weakened to pass: the existing FormatHTML refuse cases stay and become stricter, and no existing ListView, FormView or shell smoke assertion is deleted." +--- + +## Phase Goal + +ROADMAP Goal (not in user-story format): the Journal posts list and post form in the admin work on real imported data found on UAT (relation search, remembered list state, stored content and excerpt in the form), released and redeployed. + +This plan's slice (D-05: "03 unit tests"; CLAUDE.md lean rule 3): full unit coverage of every 16.1 code change, ending the phase. It runs after plan 02 (wave 3) because the release was not to wait for coverage (lean rule 3) and the two plans commit in the same repositories; its commits ship with the next tag. + +## Spec-less probe fallback + +Visible skip: no REQUIREMENTS.md IDs to probe. The edge cases RESEARCH and plan 01 named (polymorphic, soft-deleting and self-referencing relations, custom join keys, stale or malformed stored state, cleared controls, same-path navigation, out-of-range page, disabled storage, the gate as the only XSS defence) are each authored as a test case in `must_haves.truths`. + +## Artifacts this phase produces + +- New `admin/tests/state/listState.test.ts`; extended `admin/tests/list/ListView.test.ts`, `admin/tests/form/FormView.test.ts`, `admin/tests/state/useAuth.test.ts`. +- Extended `modules/cabana/query_relation_test.go` (DryRun matrix, Postgres test) and `modules/cabana/locale_order_test.go`. +- Extended `../sm-journal-plugin/classes/format_html_test.go` and `../sm-journal-plugin/posts_admin_smoke_test.go`. +- `16.1-VALIDATION.md` statuses filled with the measured results. + + +Bring full unit coverage to the Phase 16.1 code (D-01, D-02, D-03) as the phase's last plan, and run the phase gate. + +Purpose: the fixes stay fixed: relation subqueries, locale order, lossless HTML and remembered list state are pinned by tests that run without Docker, plus Postgres tests for a Docker host. +Output: test commits in summercms.go (admin, cabana) and sm-journal-plugin, and a filled VALIDATION.md. + + + +@~/.claude/msd-core/workflows/execute-plan.md +@~/.claude/msd-core/templates/summary.md + + + +@.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-CONTEXT.md +@.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-RESEARCH.md +@.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-VALIDATION.md +@.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-01-SUMMARY.md + +Docker is not usable on this machine: run the pure Go tests by name and count their PASS lines; Postgres-backed tests are written and vetted here and run on a Docker host. Commits: tests only, staged by explicit path, no co-author tags, planning docs (VALIDATION.md) in a separate commit from code. Nothing is pushed or tagged by this plan. + + + + + + Task 1: Tracer: list, form, back round trip restores the list query; then full SPA list-state coverage + admin/tests/form/FormView.test.ts, admin/tests/list/ListView.test.ts, admin/tests/state/listState.test.ts, admin/tests/state/useAuth.test.ts + admin/src/state/listState.ts, admin/src/views/ListView.vue, admin/src/views/FormView.vue (go, onLeave, onSaveAndClose, onDelete, redirectTarget), admin/src/state/useAuth.ts (logout), admin/tests/helpers.ts (mountApp, requestsTo, queryOf, resetState), admin/tests/form/FormView.test.ts (routes and form fixtures, router.currentRoute assertions), admin/tests/list/ListView.test.ts (routes, the plan 01 smoke cases), admin/tests/state/useAuth.test.ts, admin/tests/state/useSidebar.test.ts (storage spies that throw), admin/tests/smoke/shell.smoke.test.ts (only the sidebar key after mounting a list), admin/tests/fixtures/typed.ts (form schema redirects) + Tracer first, in admin/tests/form/FormView.test.ts: seed writeListState for acme.demo.widgets with sort code, dir desc and filter active true; mount /acme/demo/widgets/1 with the form routes plus the list schema and list routes; click the back action; after flushPromises the router fullPath is the list path carrying sort=code, dir=desc and filter[active]=true, and every list GET after the click carries them (none without). Repeat for cancel, save-and-close (when the fixture's close redirect resolves to the list path; otherwise set it so in a cloned schema) and delete (confirm dialog accepted). + +admin/tests/state/listState.test.ts (new): the key is the prefix plus the controller id; read of a missing key is empty; read of invalid JSON, a non-object, or an object with non-string values returns empty and removes the key; malformed values (page 0, dir sideways, empty filter value, unknown shapes) are dropped by the strict parser; writing an empty query removes the key; a write and read round-trip keeps search, sort, dir, page above 1, per_page and filters; clearListStates removes every prefixed key and keeps summer-admin.sidebar; with Storage getItem and setItem spied to throw, read returns empty and write and clear do not throw. + +admin/tests/list/ListView.test.ts: with a stored query and an empty URL, no list GET without the stored params is made and the first carries them; a URL with params wins and overwrites storage; clearing search, sort and filters through the UI removes the key, and remounting the bare list loads unfiltered; with the list filtered, router.push to the bare list path keeps the filters and makes no unfiltered request; a 422 list response removes the stored key; a response with page above last_page replaces the URL page with last_page and requests that page. + +admin/tests/state/useAuth.test.ts: logout removes every summer-admin.list: key and keeps summer-admin.sidebar, also when the logout request fails. + +Keep every existing assertion, including the shell smoke expectation that mounting a list without query stores only the sidebar key. Commit in summercms.go, staging exactly the four test files: test(admin): list state round trip and storage coverage. + + npm --prefix admin test -- tests/state/listState.test.ts tests/state/useAuth.test.ts tests/list/ListView.test.ts tests/form/FormView.test.ts tests/smoke + Non-zero exit, a vitest summary line reporting "failed", or "No test files found" in the output. + + + - The vitest run passes and the summary counts tests in admin/tests/state/listState.test.ts (the file exists and is collected). + - `grep -c 'writeListState' admin/tests/form/FormView.test.ts` is at least 1; `grep -c 'clearListStates' admin/tests/state/listState.test.ts` is at least 1; `grep -c 'summer-admin.list:' admin/tests/state/useAuth.test.ts` is at least 1. + - `git show --name-only --format= HEAD` lists exactly the four test files. + + The list-to-form-to-list round trip and every list-state edge case are pinned by vitest. + + + + Task 2: cabana coverage: DryRun SQL matrix for relation search and sort, locale ordering, Postgres relation search + modules/cabana/query_relation_test.go, modules/cabana/locale_order_test.go + modules/cabana/query.go (the relation subquery builder, applyListSearch, applyListOrder, ExecuteList), modules/cabana/query_relation_test.go (plan 01 harness and fixtures), modules/cabana/locale_order_test.go, modules/cabana/http.go (enabledContentLocales), modules/cabana/ml_smoke_test.go (recordingWriter), modules/cabana/query_test.go (newListService, getList, decodeList, the Postgres fixture setup), modules/cabana/list_schema_test.go (filterFS) + Extend modules/cabana/query_relation_test.go, reusing the plan 01 DryRun harness (never-dialed DSN, recording logger) and the service helper: + +TestListRelationSQL, table-driven, asserting on the recorded SQL and on bound values: belongsToMany search through the pivot with the pivot owner key correlated to the main primary key and the related key joined; a belongsToMany declared with custom joinForeignKey and joinReferences tags and a non-default pivot name uses exactly those columns (proves no naming convention is guessed, D-02); hasMany search correlates the related foreign key to the main primary key; belongsTo search correlates the related primary key to the main foreign key; a self-referencing belongsToMany gets distinct aliases and still correlates to the unaliased main table; a related model with gorm.DeletedAt adds its column IS NULL on the related alias; a polymorphic hasMany and a select column missing from the related schema each answer 422 validation_failed with details.search through getList; a search term containing a quote, a percent sign and an underscore never appears in the SQL text and its bound value is lower-cased and LIKE-escaped; with a controller implementing ListExtendQuery that adds an unqualified user_id predicate and a scope filter adding an unqualified id IN predicate, the recorded count statement has no JOIN outside the subqueries; sort by the belongsToMany column asc and desc orders by the string_agg subquery with the matching direction followed by the main primary-key tiebreak; sort by the belongsTo column keeps the association join and its column order. + +Add TestListRelationSearchPostgres (Postgres through newListService's shared container; it fails rather than skips without Docker, like the package's other Postgres tests): migrate the relation fixtures, seed one post with two tags that both match "fungi" and one post with an unrelated tag; search=fungi answers total 1 with only the first post; sort=tags orders by the joined tag names; a hasMany note search returns the right post. + +Extend modules/cabana/locale_order_test.go with TestEnabledLocalesDefaultFirst, table-driven: en, pl with default pl gives pl, en; pl, en unchanged; default absent from the list leaves the order; a writer whose DefaultLocale errors (a small test double) leaves the order; no published writer returns nil; en, de, pl with default pl gives pl, en, de. + +Commit in summercms.go, staging exactly the two files: test(cabana): relation subquery and locale order coverage. + + go vet ./modules/cabana && go test -count=1 -run '^(TestListRelationSearchSmoke|TestListRelationSQL|TestEnabledLocalesDefaultFirst|TestEnabledLocalesDefaultFirstSmoke)$' -v ./modules/cabana 2>&1 | grep -c '^--- PASS: Test' | grep -qx 4 + Non-zero exit: go vet fails (including on the Postgres test), or fewer than four top-level "--- PASS: Test" lines (a named test failed, does not exist, or was filtered out). + + + - Four top-level PASS lines for TestListRelationSearchSmoke, TestListRelationSQL, TestEnabledLocalesDefaultFirst and TestEnabledLocalesDefaultFirstSmoke on a machine without Docker. + - `grep -c 'func TestListRelationSearchPostgres' modules/cabana/query_relation_test.go` is 1 and `go vet ./modules/cabana` exits 0. + - `grep -c 'joinForeignKey' modules/cabana/query_relation_test.go` is at least 1 (custom pivot keys case present). + - `git show --name-only --format= HEAD` lists exactly modules/cabana/query_relation_test.go and modules/cabana/locale_order_test.go. + + The relation subquery builder and the locale order are covered by pure tests, with a Postgres test ready for a Docker host. + + + + Task 3: Journal coverage (FormatHTML keep and refuse, admin re-save keeps HTML) and the phase gate + ../sm-journal-plugin/classes/format_html_test.go, ../sm-journal-plugin/posts_admin_smoke_test.go, .planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-VALIDATION.md + ../sm-journal-plugin/classes/format_html.go, ../sm-journal-plugin/classes/format_html_test.go, ../sm-journal-plugin/classes/check_html_test.go, ../sm-journal-plugin/posts_admin_smoke_test.go, ../sm-journal-plugin/admin_harness_test.go (TestMain, newPostsEnv, admin, expect, adminDecode), ../sm-journal-plugin/controllers/posts.go (preparePostWrite), .planning/todos/pending/journal-imported-html-admin-resave.md (its acceptance test), .planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-VALIDATION.md + ../sm-journal-plugin/classes/format_html_test.go: extend TestFormatHTMLKeepsRawHTML with imported-style bodies: paragraphs, elements with class, https href and https img src attributes, an attribute holding an entity, and markdown followed by raw footer and style elements; each output equals the input after trimming (or, for the markdown case, contains the raw elements unchanged). Make TestFormatHTMLRejectsUnsafeHTML strict: every case must return an error (with unsafe rendering the gate is the only defence), and add raw-HTML cases: an svg element with onload, an anchor whose href is a mixed-case javascript URL, object and embed elements, a script element inside an HTML block, and a raw attribute with a data URL. Keep check_html_test.go unchanged and green. + +../sm-journal-plugin/posts_admin_smoke_test.go: add TestPostsAdminResaveKeepsHTML through the admin harness: create a post whose content is two HTML paragraphs, update it through the admin update route with the same content, and assert the stored content_html equals the content after trimming and holds no goldmark placeholder comment; it runs where the harness Postgres runs (TestMain fails closed without Docker outside -short, the house rule). + +Commit in ../sm-journal-plugin on main, staging exactly the two test files: test: FormatHTML keep and refuse coverage, admin re-save keeps HTML. Do not push. + +Phase gate: run both verify commands and the plan 01 and Task 2 Go commands, then update 16.1-VALIDATION.md: each Per-Task Verification Map status (green, red or manual), wave_0_complete true, nyquist_compliant true only if every automated row is green, and the Docker-host suites and UAT items left as pending manual checks unless already answered. Commit VALIDATION.md separately from code (planning docs commit). + + go -C ../sm-journal-plugin vet ./... && go -C ../sm-journal-plugin test -count=1 ./classes/ && go -C ../sm-journal-plugin test -count=1 -run '^(TestFormatHTMLKeepsRawHTML|TestFormatHTMLRejectsUnsafeHTML)$' -v ./classes/ 2>&1 | grep -c '^--- PASS: Test' | grep -qx 2 + Non-zero exit: vet fails (including on TestPostsAdminResaveKeepsHTML), a classes test fails, or fewer than two top-level "--- PASS: Test" lines. + go vet ./... && go test -count=1 ./cmd/summer -run TestDocsTree && npm --prefix admin run typecheck && npm --prefix admin test && bash scripts/check-admin-dist.sh && echo phase-16.1-local-gate-ok + Non-zero exit or "phase-16.1-local-gate-ok" absent: vet, the docs checker, the typecheck, any vitest file or the dist drift gate failed. + On a Docker-enabled machine run go test -count=1 ./... in summercms.go and go -C ../sm-journal-plugin test -count=1 ./... and confirm both are green, including TestListQueryContract, TestListRelationSearchPostgres and TestPostsAdminResaveKeepsHTML. Reply with the two summary lines or the failures. + + + - Two top-level PASS lines for TestFormatHTMLKeepsRawHTML and TestFormatHTMLRejectsUnsafeHTML; `go -C ../sm-journal-plugin test -count=1 ./classes/` prints ok. + - `grep -c 'func TestPostsAdminResaveKeepsHTML' ../sm-journal-plugin/posts_admin_smoke_test.go` is 1 and `go -C ../sm-journal-plugin vet ./...` exits 0. + - The phase gate prints phase-16.1-local-gate-ok. + - 16.1-VALIDATION.md has no "⬜ pending" status left on an automated row; the frontmatter has wave_0_complete: true. + - `git -C ../sm-journal-plugin show --name-only --format= HEAD` lists exactly classes/format_html_test.go and posts_admin_smoke_test.go. + + Lossless HTML and the strict gate are pinned, the todo's re-save test exists, the local phase gate is green and VALIDATION.md records the results. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| Test runner → databases | Postgres-backed tests create throwaway testcontainers databases | +| Test results → phase sign-off | a filter that matches nothing or a skipped test can read as green | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-16.1-11 | Repudiation (false green) | verify commands, VALIDATION.md | medium | mitigate | Every Go gate counts the top-level "--- PASS: Test" lines of the named tests; Postgres tests fail instead of skipping outside -short; VALIDATION.md marks Docker-host suites as manual until a run is recorded; nyquist_compliant is set only when every automated row is green | +| T-16.1-12 | Tampering / Denial of Service | test fixtures | low | mitigate | Pure tests use a DryRun db with DisableAutomaticPing on a DSN that is never dialed; Postgres tests use throwaway testcontainers databases; no test reaches the UAT server or a shared database | +| T-16.1-SC | Tampering | packages | high | mitigate | No package is installed: vitest, GORM, the postgres dialector and testcontainers-go are already pinned in package.json and go.mod; go.mod, go.sum and package.json stay unchanged | + + + +- Task 1: vitest green with listState.test.ts collected. +- Task 2: four PASS lines for the named cabana tests; go vet clean. +- Task 3: two PASS lines for the FormatHTML tests; phase-16.1-local-gate-ok printed; VALIDATION.md filled. +- Docker-host suites recorded as a human-check for /msd-verify-work. + + + +- Lean rule 3 met: every 16.1 code change has unit tests in this last plan. +- The relation subquery builder, locale ordering, FormatHTML and list state are pinned by tests that run without Docker. +- The Postgres relation-search test and the admin re-save test exist for a Docker host. + + + +Create `.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-03-SUMMARY.md` when done, with the commit hashes per repository (unpushed; they ship with the next release), the gate output lines and the Docker-host result or its pending status. + diff --git a/.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-RESEARCH.md b/.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-RESEARCH.md index 305602f..2c95f4e 100644 --- a/.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-RESEARCH.md +++ b/.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-RESEARCH.md @@ -544,18 +544,21 @@ sql := db.ToSQL(func(tx *gorm.DB) *gorm.DB { | A5 | The PHP site's `use_rich_editor` setting value is unknown (Go UAT: `f`); 16.1 does not implement the rich-editor store-as-is branch | Pattern 4 | Low: unsafe markdown already round-trips HTML; the branch belongs with 16.2's Blog Editor | | A6 | The new route name `list-state` and table name `backend_list_states` are acceptable names | Pattern 2 | Cosmetic | -## Open Questions +## Open Questions (RESOLVED) 1. **Tag number vs the concurrent session.** - What we know: the latest tag is v0.1.15. No `modules/` edits are in flight in this checkout. - What's unclear: whether the other session will tag first. - Recommendation: the release task checks `git fetch --tags` and uses the next free `v0.1.x`. + - RESOLVED: plan 16.1-02 Task 1 picks the next free tag or reuses an existing tag that contains the fixes. 2. **Should `en` be disabled on grzybyfunkcjonalne?** - What we know: the site is Polish-only; the export has only `pl` translations. With en enabled, every admin save writes empty en rows (Pitfall 9). - What's unclear: whether any client requests `locale=en`. - Recommendation: the framework fix is enough for D-03. Ask the user whether to also set `is_enabled=false` for `en` on UAT (one SQL line, or the Locales admin) and in `migrate.sh`. + - RESOLVED: `en` stays enabled (Claude's discretion in CONTEXT.md); empty en rows on save are accepted. 3. **Repairing posts 53 and 64: admin re-save or a console command?** - Recommendation: admin re-save after the deploy. It is two posts and doubles as the D-03 acceptance test. A reusable `journal:rebuild-html` command is out of scope unless the user wants it. + - RESOLVED: plan 16.1-02 Task 3 repairs both posts by admin re-save. ## Environment Availability diff --git a/.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-VALIDATION.md b/.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-VALIDATION.md index 9f10132..3f61952 100644 --- a/.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-VALIDATION.md +++ b/.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/16.1-VALIDATION.md @@ -19,20 +19,25 @@ created: "2026-10-09" | Property | Value | |----------|-------| -| **Framework** | {pytest 7.x / jest 29.x / vitest / go test / other} | -| **Config file** | {path or "none — Wave 0 installs"} | -| **Quick run command** | `{quick command}` | -| **Full suite command** | `{full command}` | -| **Estimated runtime** | ~16.1 seconds | +| **Framework** | Go `testing` (GORM DryRun for SQL-shape tests; testcontainers-go v0.44.0 for Postgres suites on a Docker host only); vitest + @vue/test-utils + happy-dom for the admin SPA | +| **Config file** | none (Go); `admin/vite.config.*`, `admin/tests/setup.ts`, `admin/tests/helpers.ts` (vitest) | +| **Quick run command** | `go test -count=1 -run '^(TestListRelationSearchSmoke\|TestListRelationSQL\|TestEnabledLocalesDefaultFirst\|TestEnabledLocalesDefaultFirstSmoke)$' ./modules/cabana` ; `go -C ../sm-journal-plugin test -count=1 ./classes/` ; `npm --prefix admin test -- tests/list/ListView.test.ts tests/form/FormView.test.ts tests/state` | +| **Full suite command (this machine, no Docker)** | `go vet ./... && go -C ../sm-journal-plugin vet ./... && go test -count=1 ./cmd/summer -run TestDocsTree && npm --prefix admin run typecheck && npm --prefix admin test && bash scripts/check-admin-dist.sh` plus the quick Go commands | +| **Full suite command (Docker host)** | `go test -count=1 ./...` in summercms.go and `go -C ../sm-journal-plugin test -count=1 ./...` | +| **Estimated runtime** | ~30 s quick (Go pure tests + one vitest file set); ~3 min full local suite (dist rebuild dominates) | + +Docker is not usable on the planning machine (`permission denied` on the socket): `go test ./modules/cabana` and `go test -short ./...` in sm-journal-plugin fail there for every Postgres-backed test, so every per-task gate names the pure tests with `-run` and checks their `--- PASS` lines. The Postgres suites (`TestListQueryContract`, the new `TestListRelationSearchPostgres`, the new `TestPostsAdminResaveKeepsHTML`) run on a Docker host before `/msd-verify-work`. + +Pre-existing red, not caused by this phase: `scripts/check-phase10.sh --hygiene` already refuses `MLField.vue`, `mlLocale.ts` and `FileCaptionModal.vue` ("imported by no test"). This phase only extends that script's browser-storage allowlist and checks the storage rule with a direct grep. --- ## Sampling Rate -- **After every task commit:** Run `{quick run command}` -- **After every plan wave:** Run `{full suite command}` -- **Before `/msd-verify-work`:** Full suite must be green -- **Max feedback latency:** 16.1 seconds +- **After every task commit:** the task's own `` command (pure Go tests by name, vitest files, `go vet` of the touched package). +- **After every plan wave:** the full local suite above; `scripts/check-admin-dist.sh` whenever `admin/` changed; `go test ./cmd/summer -run TestDocsTree` whenever `docs/` or a module README changed. +- **Before `/msd-verify-work`:** full local suite green, Docker-host suites green, UAT smoke `smoke: ok (` on the server, and the manual UAT list below answered. +- **Max feedback latency:** 60 seconds per task gate (the dist rebuild in plan 01 Task 4 is the slowest). --- @@ -40,7 +45,16 @@ created: "2026-10-09" | Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status | |---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------| -| 16.1-01-01 | 01 | 1 | REQ-{XX} | T-16.1-01 / — | {expected secure behavior or "N/A"} | unit | `{command}` | ✅ / ❌ W0 | ⬜ pending | +| 16.1-01-01 | 01 | 1 | D-02 | T-16.1-01 | Relation and column names come from the compiled schema and GORM's parsed relationship; the search value is bound | unit (DryRun, through `service.list`) | `go test -count=1 -run '^TestListRelationSearchSmoke$' -v ./modules/cabana` | ❌ W0 (created by the task) | ⬜ pending | +| 16.1-01-02 | 01 | 1 | D-03 | — | N/A | unit | `go test -count=1 -run '^TestEnabledLocalesDefaultFirstSmoke$' -v ./modules/cabana` | ❌ W0 (created by the task) | ⬜ pending | +| 16.1-01-03 | 01 | 1 | D-03 | T-16.1-02 | Raw HTML passes; script/iframe/object/embed, on* handlers and javascript/vbscript/data URLs are still refused on every write path | unit | `go -C ../sm-journal-plugin test -count=1 ./classes/` | ✅ extend `classes/format_html_test.go` | ⬜ pending | +| 16.1-01-04 | 01 | 1 | D-01 | T-16.1-03, T-16.1-04 | Only list query params are stored, under `summer-admin.list:` keys; restored state passes the strict URL parser; logout clears it | unit (vitest) + dist drift | `npm --prefix admin test -- tests/list/ListView.test.ts tests/form/FormView.test.ts tests/smoke tests/state` ; `bash scripts/check-admin-dist.sh` | ✅ extend `admin/tests/list/ListView.test.ts` | ⬜ pending | +| 16.1-02-01 | 02 | 2 | D-04 | T-16.1-06, T-16.1-07 | Release only after the local gate is green and a human approves the one-way tag push | gate + checkpoint | full local suite (plan 02 Task 1 verify) | ✅ | ⬜ pending | +| 16.1-02-02 | 02 | 2 | D-04 | T-16.1-07, T-16.1-08 | Uploaded binary sha256 equals the local build; previous binary kept as `.prev`; no `.env` rsynced | e2e (server) | on-server `scripts/smoke.sh --url https://uat.grzybyfunkcjonalne.pl` via loopback resolve | ✅ | ⬜ pending | +| 16.1-02-03 | 02 | 2 | D-02, D-03 | T-16.1-09, T-16.1-10 | Read-only SQL and log checks; only posts 53 and 64 are re-saved, by a human | integration (server, read-only) | `ssh grzybyfunkcjonalne-uat "psql -X -d grzybyfunkcjonalne -tAc ..."` (raw-HTML marker count 0, post 64 footer present) | ✅ | ⬜ pending | +| 16.1-03-01 | 03 | 3 | D-01 | T-16.1-11 | Storage helpers survive disabled storage; logout clears list keys only | unit (vitest) | `npm --prefix admin test -- tests/state/listState.test.ts tests/state/useAuth.test.ts tests/list/ListView.test.ts tests/form/FormView.test.ts` | ❌ W0 `admin/tests/state/listState.test.ts` (created by the task) | ⬜ pending | +| 16.1-03-02 | 03 | 3 | D-02, D-03 | T-16.1-11, T-16.1-12 | Polymorphic relations and unknown columns refused (422); search value never inlined into SQL | unit (DryRun) + Postgres (Docker host) | `go test -count=1 -run '^(TestListRelationSQL\|TestEnabledLocalesDefaultFirst)$' -v ./modules/cabana` | ✅ extend files from plan 01 | ⬜ pending | +| 16.1-03-03 | 03 | 3 | D-03 | T-16.1-02, T-16.1-11 | Every unsafe case returns an error now that the gate is the only defence | unit + journal harness (Docker host) | `go -C ../sm-journal-plugin test -count=1 -run '^(TestFormatHTMLKeepsRawHTML\|TestFormatHTMLRejectsUnsafeHTML)$' -v ./classes/` | ✅ extend | ⬜ pending | *Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky* @@ -48,11 +62,10 @@ created: "2026-10-09" ## Wave 0 Requirements -- [ ] `{tests/test_file.py}` — stubs for REQ-{XX} -- [ ] `{tests/conftest.py}` — shared fixtures -- [ ] `{framework install}` — if no framework detected - -*If none: "Existing infrastructure covers all phase requirements."* +- [ ] `modules/cabana/query_relation_test.go` — DryRun harness (postgres dialector, never dialed; recording logger), relation fixtures (many2many, has_many, belongs_to) and the tracer smoke test; created by plan 01 Task 1, extended by plan 03 Task 2. +- [ ] `modules/cabana/locale_order_test.go` — created by plan 01 Task 2 (smoke), extended by plan 03 Task 2. +- [ ] `admin/tests/state/listState.test.ts` — created by plan 03 Task 1. +- [ ] No framework install: GORM, the postgres dialector, testcontainers-go and vitest are already in go.mod / package.json. --- @@ -60,19 +73,21 @@ created: "2026-10-09" | Behavior | Requirement | Why Manual | Test Instructions | |----------|-------------|------------|-------------------| -| {behavior} | REQ-{XX} | {reason} | {steps} | - -*If none: "All phase behaviors have automated verification."* +| Journal posts list search on UAT returns rows (no "The list could not be loaded") | D-02 | Needs the UAT basic-auth credentials and an admin login, which the executor does not hold | In `/gfadmin/golem15/journal/posts` search "cordyceps"; rows appear. The executor then checks the Go log has no `SQLSTATE 42703` after the restart | +| List state survives the form round trip, navigation and a shared URL | D-01 | Interactive flow on the real admin | Set a sort and a category filter, open a post, press back, then "Zapisz i zamknij" on another post: the list keeps both and the URL shows them. Leave through the rail and come back: restored. Paste the URL into a new tab: same list. Log out and in: the list opens unfiltered | +| Imported post form shows title, slug, content and excerpt | D-03 | Real imported data on UAT | Open any imported post: the locale selector shows `pl` first and every field is filled | +| Posts 53 and 64 get their HTML back | D-03 | Admin re-save needs credentials | Open each post and press "Zapisz"; the executor then runs the read-only SQL check (marker count 0, post 64 has its footer) | +| Docker-host suites | D-01, D-02, D-03 | Docker unavailable on the executor machine | On a Docker host: `go test -count=1 ./...` (summercms.go) and `go -C ../sm-journal-plugin test -count=1 ./...` are green, including `TestListQueryContract`, `TestListRelationSearchPostgres` and `TestPostsAdminResaveKeepsHTML` | --- ## Validation Sign-Off -- [ ] All tasks have `` verify or Wave 0 dependencies -- [ ] Sampling continuity: no 3 consecutive tasks without automated verify -- [ ] Wave 0 covers all MISSING references -- [ ] No watch-mode flags -- [ ] Feedback latency < 16.1s -- [ ] `nyquist_compliant: true` set in frontmatter +- [x] All tasks have `` verify or Wave 0 dependencies +- [x] Sampling continuity: no 3 consecutive tasks without automated verify +- [x] Wave 0 covers all MISSING references +- [x] No watch-mode flags (`vitest run` through `npm test`; `go test -count=1`) +- [ ] Feedback latency < 60s (confirmed during execution) +- [ ] `nyquist_compliant: true` set in frontmatter (after plan 03 records the green runs) -**Approval:** {pending / approved YYYY-MM-DD} +**Approval:** pending diff --git a/.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/COVERAGE.md b/.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/COVERAGE.md new file mode 100644 index 0000000..4a92dfe --- /dev/null +++ b/.planning/phases/16.1-journal-admin-urgent-fixes-relation-search-list-state-post-f/COVERAGE.md @@ -0,0 +1 @@ +No external API integration: internal admin list, form locale and HTML renderer fixes; the Journal API is named only to keep its response shapes unchanged. diff --git a/.planning/state.json b/.planning/state.json index 910fbb4..86af7ac 100644 --- a/.planning/state.json +++ b/.planning/state.json @@ -93,6 +93,11 @@ "name": "Local Fonoteka frontend on local SummerCMS backend (INSERTED)", "status": "pending" }, + { + "number": "14.3", + "name": "One-command app scaffolding (INSERTED)", + "status": "in_progress" + }, { "number": "15", "name": "Journal plugin", @@ -101,7 +106,7 @@ { "number": "16", "name": "grzybyfunkcjonalne.pl on reusable blog views", - "status": "pending" + "status": "in_progress" }, { "number": "17", @@ -113,6 +118,11 @@ "name": "Payment Gateway", "status": "pending" }, + { + "number": "18.1", + "name": "Payment Gateway Subscriptions", + "status": "pending" + }, { "number": "19", "name": "Chat and ChatChannels", @@ -130,9 +140,9 @@ } ], "next": { - "command": "/gsd:progress --next", + "command": "/msd:progress --next", "label": "Advance to the next step", - "reason": "Phase 14.2 of 30 · executing" + "reason": "Phase 16.1 of 34 · executing" }, - "updated_at": "2026-10-07T19:26:02.872Z" + "updated_at": "2026-10-09T18:51:01.734Z" }