239 lines
12 KiB
Markdown
239 lines
12 KiB
Markdown
---
|
|
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*
|