diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 0beb6d2..a051c90 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -883,7 +883,7 @@ Plans: 3. Plugin README and docs stay application-neutral (`the application`, example names such as `blog`). 4. The new code has unit tests, delivered in the phase's last plan. -**Plans:** 2/4 plans executed +**Plans:** 3/4 plans executed Plans: @@ -894,7 +894,7 @@ Plans: - [x] 15-02-PLAN.md — Admin YAML (`mlmarkdown`), FormatHTML, permissions/nav SVG, import/export **Wave 3** *(blocked on Wave 2 completion)* -- [ ] 15-03-PLAN.md — `/_journal/api/v1`, buckets, backend Bearer writes, media, Typesense gate off +- [x] 15-03-PLAN.md — `/_journal/api/v1`, buckets, backend Bearer writes, media, Typesense gate off **Wave 4** *(blocked on Wave 3 completion)* - [ ] 15-04-PLAN.md — Unit/integration tests last, PHPUnit map, phase gate, security review diff --git a/.planning/STATE.md b/.planning/STATE.md index 60d70ea..f026838 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -4,16 +4,16 @@ milestone: v1.0 current_phase: 15 current_phase_name: Journal plugin status: executing -stopped_at: Completed 15-02-PLAN.md -last_updated: "2026-10-06T16:48:34Z" +stopped_at: Completed 15-03-PLAN.md +last_updated: "2026-10-06T17:12:00Z" last_activity: 2026-10-06 -last_activity_desc: Completed 15-02 journal admin YAML and import/export +last_activity_desc: Completed 15-03 journal public API, writes, media, RSS, Typesense gate state_head: 685837a3c8e23c2f05e2b367f42bd8d53e9b432b progress: total_phases: 26 completed_phases: 12 total_plans: 135 - completed_plans: 133 + completed_plans: 134 milestone_name: milestone --- @@ -29,11 +29,11 @@ See: .planning/PROJECT.md (updated 2026-09-16) ## Current Position Phase: 15 (Journal plugin) — EXECUTING -Plan: 3 of 4 +Plan: 4 of 4 Status: Ready to execute -Last activity: 2026-10-06 — Completed 15-02 journal admin YAML and import/export +Last activity: 2026-10-06 — Completed 15-03 journal public API, writes, media, RSS, Typesense gate -Progress: [█████░░░░░] 50% +Progress: [████████░░] 75% ## Performance Metrics @@ -188,6 +188,7 @@ Progress: [█████░░░░░] 50% | Phase 14.2.1 P06 | 41min | 3 tasks | 15 files | | Phase 15 P15-01 | 11 | 3 tasks | 31 files | | Phase 15 P15-02 | 75 min | 3 tasks | 39 files | +| Phase 15 P15-03 | 80 min | 3 tasks | 22 files | ## Accumulated Context @@ -215,6 +216,7 @@ Progress: [█████░░░░░] 50% Decisions are logged in PROJECT.md Key Decisions table. Recent decisions affecting current work: +- [Phase 15]: 15-03: `/_journal/api/v1` public GETs; writes require backend-audience JWT and PHP `{error}` strings; JOURNAL-005 draft 404; Typesense Gate off by default (D-12/D-14/D-15/D-17). Do not add Journal to fonoteka.go tide (D-16). - [Phase 14.2.1]: D-10 Go tables are `golem15_translate_*` (execute Task 2 `golem15-prefix`, 2026-10-06). PHP pin still uses `winter_translate_*`; do not copy those names into Go DDL. - [Phase 14.1]: 14.1-01 Task 1 `ship-shared-full` — create `golem15_user_oauth_identities` in the shared sm-user-plugin with the full PHP column set (locked D-01 and D-07). - [Phase 14.1]: Identity Index/Destroy are host-mounted on the JWT group; the user plugin does not register them under `/_user`. `/me` unrestricted `collection_ids` is JSON null (D-09); `scopes` stay `[]`. @@ -651,6 +653,6 @@ Items acknowledged and carried forward from previous milestone close: ## Session Continuity -Last session: 2026-10-06T16:30:40.090Z -Stopped at: Completed 15-01-PLAN.md +Last session: 2026-10-06T17:12:00Z +Stopped at: Completed 15-03-PLAN.md Resume file: None diff --git a/.planning/phases/15-journal-plugin/15-03-SUMMARY.md b/.planning/phases/15-journal-plugin/15-03-SUMMARY.md new file mode 100644 index 0000000..232c31c --- /dev/null +++ b/.planning/phases/15-journal-plugin/15-03-SUMMARY.md @@ -0,0 +1,238 @@ +--- +phase: 15-journal-plugin +plan: 03 +subsystem: plugins +tags: [journal, api, jwt, surf, beachcomber, rss, typesense, rate-limit] + +requires: + - phase: 15-02 + provides: "Posts/Categories/Tags admin, FormatHTML, HasPermissions, newPostsEnv postgres harness, host TestBootUserTranslateJournal" +provides: + - "Anonymous GET /_journal/api/v1/posts, posts/{slug}, categories, tags, rss with PHP shapes" + - "Backend-audience JWT writes under the same prefix; PHP {error} strings; JOURNAL-005 draft 404" + - "journal-public-api / journal-api buckets Max 120; media upload under journal/; Typesense gate off" +affects: [15-04, 16] + +actuals: + tokens: 32000 + tasks: 3 + commits: 5 +plan_head_before: db6243c8c2b9dbe594c7c3e538fe2581a5a10411 +plugin_repo_head_after: bdd1c1be618658374f42e6a643e8d34b6c350c28 +host_repo_head_after: 716aa44cf58e278251a3cff71f8ae99ed5e316a7 + +tech-stack: + added: [] + patterns: + - "Public group throttle:journal-public-api only; never cabana middleware name backend on anonymous GET" + - "Writes authenticate in-handler with bouncer.NewBackendJWTGuard (aud=backend) and PHP {error} JSON, not cabana writeUnauthenticated" + - "Where() applies to the last declared route; register /media/upload before posts/{id} constraints" + - "Host gitlink is a nested clone; bump via file:// fetch with protocol.file.allow=always" + +key-files: + created: + - ../sm-journal-plugin/routes.go + - ../sm-journal-plugin/search.go + - ../sm-journal-plugin/controllers/api/auth.go + - ../sm-journal-plugin/controllers/api/posts.go + - ../sm-journal-plugin/controllers/api/posts_helpers.go + - ../sm-journal-plugin/controllers/api/posts_search.go + - ../sm-journal-plugin/controllers/api/media.go + - ../sm-journal-plugin/controllers/api/rss.go + - ../sm-journal-plugin/models/post_search.go + - ../sm-journal-plugin/models/search_gate.go + - ../sm-journal-plugin/journal_public_list_smoke_test.go + - ../sm-journal-plugin/journal_api_writes_test.go + - ../sm-journal-plugin/journal_api_task3_test.go + modified: + - ../sm-journal-plugin/plugin.go + - ../sm-journal-plugin/plugin_test.go + - ../sm-journal-plugin/README.md + - ../sm-journal-plugin/posts_admin_smoke_test.go + - ../sm-grzybyfunkcjonalne-app/boot_test.go + - ../sm-grzybyfunkcjonalne-app/plugins/golem15/journal + +key-decisions: + - "D-14 public prefix is /_journal/api/v1; show is GET posts/{slug} with ctype_digit fallback to id; list per_page default 9 max 30" + - "D-15 writes require cabana backend JWT audience backend; missing Bearer is JSON {error:Authentication required}; frontend-audience tokens fail the same 401" + - "JOURNAL-005 unpublished or future published_at show is 404 with no data key unless owner or access_other_posts; anonymous never 403" + - "D-17 buckets journal-public-api and journal-api Max 120 Decay 1m; 429 body stays surf Too Many Attempts" + - "D-12 search_use_typesense defaults false; ShouldBeSearchable is false when unpublished or the Gate is off; a fresh save makes zero Typesense HTTP" + - "D-16 Journal routes are not added to fonoteka.go tide/parity files" + +patterns-established: + - "optionalBackendPrincipal on GET (Bearer present only; failure is anonymous); requireBackendPrincipal on writes with PHP-shaped writer" + - "Store/update assign field-by-field; never lagoon.Fill the whole body onto Post; FormatHTML regenerates content_html" + - "beachcomber.From in Plugin.Boot; Gate reads golem15_journal_settings.search_use_typesense for ID=1; read errors count as off" + +requirements-completed: [D-12, D-14, D-15, D-16, D-17] + +coverage: + - id: D1 + description: "Anonymous GET /_journal/api/v1/posts returns published posts only; buckets Max 120; public group has no cabana backend middleware" + requirement: D-14 + verification: + - kind: integration + ref: "../sm-journal-plugin#TestJournalPublicList" + status: pass + - kind: unit + ref: "../sm-journal-plugin#TestJournalBuckets" + status: pass + human_judgment: false + - id: D2 + description: "POST without Bearer is 401 Authentication required (not cabana envelope); JOURNAL-005 draft 404; frontend JWT cannot write; publish without access_publish is 403" + requirement: D-15 + verification: + - kind: integration + ref: "../sm-journal-plugin#TestJournalWriteUnauthenticated" + status: pass + - kind: integration + ref: "../sm-journal-plugin#TestJournal005DraftShow" + status: pass + human_judgment: false + - id: D3 + description: "Anonymous GET categories and tags return PHP {data} keys; featured-image writes without Bearer are 401; non-editor backend Bearer is 403" + requirement: D-14 + verification: + - kind: integration + ref: "../sm-journal-plugin#TestJournalPublicCategories" + status: pass + - kind: integration + ref: "../sm-journal-plugin#TestJournalPublicTags" + status: pass + - kind: integration + ref: "../sm-journal-plugin#TestJournalFeaturedImageUnauthenticated" + status: pass + human_judgment: false + - id: D4 + description: "Media upload without access_posts is 403; with permission path is under journal/; folder .. is 422" + requirement: D-15 + verification: + - kind: integration + ref: "../sm-journal-plugin#TestJournal006MediaUpload" + status: pass + human_judgment: false + - id: D5 + description: "Default search_use_typesense is off; a published save with Typesense configured records zero outbound HTTP; unpublished ShouldBeSearchable is false" + requirement: D-12 + verification: + - kind: integration + ref: "../sm-journal-plugin#TestSearchGateOff" + status: pass + human_judgment: false + - id: D6 + description: "Anonymous GET rss is 200 application/rss+xml, RSS 2.0, rss_title and rss_posts_per_feed honored, unpublished omitted" + requirement: D-14 + verification: + - kind: integration + ref: "../sm-journal-plugin#TestJournalRSS" + status: pass + human_judgment: false + - id: D7 + description: "Host assembled routes include GET and POST /_journal/api/v1/posts" + requirement: D-14 + verification: + - kind: integration + ref: "../sm-grzybyfunkcjonalne-app#TestBootUserTranslateJournal" + status: pass + human_judgment: false + +duration: 80min +completed: 2026-10-06 +status: complete +--- + +# Phase 15: Journal plugin — Plan 03 Summary + +**Anonymous `/_journal/api/v1` list/show/categories/tags/rss with PHP shapes, backend-Bearer writes, JOURNAL-005 draft 404, media under `journal/`, and Typesense gated off by default** + +## Performance + +- **Duration:** 80 min +- **Started:** 2026-10-06T16:51:02Z +- **Completed:** 2026-10-06T17:12:00Z +- **Tasks:** 3 +- **Files modified:** 22 + +## Accomplishments + +- Public GETs under `/_journal/api/v1` work without Authorization; list omits drafts; `per_page` default 9 max 30. +- Writes require a cabana backend JWT (`aud=backend`) and return PHP `{error}` strings, not the cabana admin envelope. +- Unpublished show is 404 with no `data` key unless the caller is the owner or holds `access_other_posts`. +- Media upload stays under `journal/`; RSS is well-formed 2.0; `search_use_typesense` default false makes zero Typesense HTTP on a fresh save. + +## Task Commits + +Each task was committed atomically: + +1. **Task 1: Serve anonymous GET /_journal/api/v1/posts as a published list** — `f2e5b3d072d21a7865d30cd504330bcc5164e0a1` (feat, sm-journal-plugin) +2. **Task 2: Slug show, draft 404, and backend-Bearer writes** — `3a1dda45ec32c47438f5a159d57194a5bb783ce6` (feat, sm-journal-plugin) +3. **Task 3: Media upload, RSS, Typesense gate, and host route assertion** — `bdd1c1be618658374f42e6a643e8d34b6c350c28` (feat, sm-journal-plugin) and `716aa44cf58e278251a3cff71f8ae99ed5e316a7` (feat, sm-grzybyfunkcjonalne-app gitlink + boot_test) + +**Plan metadata:** (this commit) + +## Files Created/Modified + +- `../sm-journal-plugin/plugin.go` — `HasRoutes`, `BucketProvider`, `wireSearch` in Boot +- `../sm-journal-plugin/routes.go` — public and write groups under `/_journal/api/v1` +- `../sm-journal-plugin/controllers/api/auth.go` — PHP `{error}` writer, optional/require backend principal +- `../sm-journal-plugin/controllers/api/posts.go` — Index/Show/Store/Update/Destroy/featured-images +- `../sm-journal-plugin/controllers/api/posts_helpers.go` — JOURNAL-005, categories/tags trees, field-by-field writes +- `../sm-journal-plugin/controllers/api/media.go` — JOURNAL-006 media upload +- `../sm-journal-plugin/controllers/api/rss.go` — RSS 2.0 +- `../sm-journal-plugin/search.go` — beachcomber Gate on `search_use_typesense` +- `../sm-journal-plugin/models/post_search.go` — `SearchableAs` / `ShouldBeSearchable` / `ToSearchableArray` +- `../sm-journal-plugin/README.md` — public prefix with blog examples +- `../sm-grzybyfunkcjonalne-app/boot_test.go` — assembled GET and POST `/_journal/api/v1/posts` +- `../sm-grzybyfunkcjonalne-app/plugins/golem15/journal` — gitlink `bdd1c1b` + +## Decisions Made + +- Followed D-14/D-15/D-16/D-17/D-12 as specified. Show is slug-or-numeric-id. Writes are backend audience only; Apparatus personal tokens are not parsed. +- `ShouldBeSearchable` is false when unpublished **or** the Gate is off (plan, not PHP's index-drafts-when-enabled). +- RSS `rss_enabled` false still returns well-formed XML. + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 2 - Blocking] `Where("id")` after `/media/upload` failed Assemble** +- **Found during:** Task 3 (`TestJournal006MediaUpload` env boot) +- **Issue:** surf `Where` applies to the last declared route; `/media/upload` has no `{id}` parameter. +- **Fix:** Register `POST /media/upload` before the `{id}` / `{fileId}` routes so `Where` still targets featured-image delete. +- **Files modified:** `../sm-journal-plugin/routes.go` +- **Verification:** Task 3 named tests PASS +- **Committed in:** `bdd1c1b` (Task 3) + +**2. [Rule 3 - Test layout] Task 2 tests live next to `newPostsEnv`, not `controllers/api/posts_test.go`** +- **Found during:** Task 2 +- **Issue:** `newPostsEnv` and `TestMain` are package `journal_test` at the plugin root; `controllers/api` cannot share that harness without a second Postgres TestMain. +- **Fix:** Named tests in `journal_api_writes_test.go` (same 15-01/15-02 smoke layout). Verify `-run` still finds them via `./...`. +- **Files modified:** `../sm-journal-plugin/journal_api_writes_test.go` +- **Verification:** All five Task 2 named tests PASS +- **Committed in:** `3a1dda4` (Task 2) + +--- + +**Total deviations:** 2 auto-fixed (1 assemble, 1 test-file location) +**Impact on plan:** Required for boot and harness reuse. No scope creep. Contract tests are the plan names. + +## Issues Encountered + +- Plan verify `go test ./... -run '^(…)$'` prints `[no tests to run]` / `[no test files]` for `models`, `updates`, `classes`, `console`, `controllers`, `controllers/api`. Named tests still `--- PASS`. Same leaf-package shape as 15-01/15-02; stubs were not added. +- Host `go test` must run with `GOWORK` unset so it does not inherit the plugin workspace. +- Nested host gitlink is a separate clone from sibling `sm-journal-plugin`; bump with `git -c protocol.file.allow=always fetch file://…`. +- Journal remote still has no refs; gitlink records local SHA `bdd1c1b`. Do not push unless asked. +- fonoteka.go tide/parity files were not modified (D-16). + +## User Setup Required + +None - no external service configuration required. + +## Next Phase Readiness + +Ready for 15-04 (unit/integration tests last, PHPUnit map, phase gate, security review). Public and write HTTP surface is in. Typesense query path exists behind the Gate; a fresh install never dials Typesense. + +--- +*Phase: 15-journal-plugin* +*Completed: 2026-10-06*