docs(14.2-01): complete Go-side local stack glue plan

This commit is contained in:
2026-10-11 17:48:18 +02:00
parent 2d68eab3c0
commit d0eeb4d291

View File

@@ -0,0 +1,229 @@
---
phase: 14.2-local-fonoteka-frontend-on-local-summercms-backend
plan: 01
subsystem: infra
tags: [dev-seed, surf, uploads, access-log, slog, console, fonoteka]
requires:
- phase: 14.1
provides: fonoteka plugin console commands, classes write services, parity corpus 175/175
provides:
- fonoteka:dev-seed (local/dev only), idempotent D-02 seed shape with generated PNG covers
- console.DevEnvironment and console.DevSeedCommand
- fonoteka plugin commands open and publish their own database (G1)
- GET /_fonoteka/api/v1/_dev/backend marker, local/dev only, outside the parity manifest
- framework config key storage.uploads.serve (uploads route on attach.StaticHandlerPublic, nosniff)
- framework config key http.access_log (one redacted "http request" record per request)
affects: [14.2-02 start script, 14.2-03 e2e specs, 14.2-04 unit tests, phase 20 cutover]
actuals:
tokens: 15212
tasks: 3
commits: 3
plan_head_before: 3408742fb0e64a57ac3dcaad5a4b31001cffc849
plan_head_after: 2d68eab3c0f82b55bf4c7e6f56c46f98b9c124f6
fonoteka_head_before: df7876bfa9933fa68d28a2ff3f80b1c33453398c
fonoteka_head_after: 464ba475eff0f5282e4c68da53b9eab4b3b688e7
fonoteka_commits: 2
tech-stack:
added: []
patterns:
- "Console command database: published handle, else lagoon.OpenFromApp + lagoon.Publish (OnDatabase hooks run) with a closer"
- "Framework opt-in routes resolve published services per request so route:list and boot need no database"
- "Request-scoped pointer slot in the context carries the matched route pattern to an outer middleware"
key-files:
created:
- modules/surf/uploads.go
- modules/surf/accesslog.go
- modules/surf/accesslog_test.go
- .planning/todos/pending/user-plugin-commands-open-database.md
- ../fonoteka.go/plugins/golem15/fonoteka/console/database.go
- ../fonoteka.go/plugins/golem15/fonoteka/console/dev_seed.go
- ../fonoteka.go/plugins/golem15/fonoteka/dev_marker.go
- ../fonoteka.go/plugins/golem15/fonoteka/dev_stack_smoke_test.go
modified:
- modules/surf/router.go
- modules/surf/README.md
- docs/services/storage.md
- docs/services/logging.md
- docs/setup/configuration.md
- ../fonoteka.go/plugins/golem15/fonoteka/plugin.go
- ../fonoteka.go/plugins/golem15/fonoteka/routes.go
- ../fonoteka.go/plugins/golem15/fonoteka/console/reindex.go
- ../fonoteka.go/plugins/golem15/fonoteka/console/oauth_client.go
- ../fonoteka.go/plugins/golem15/fonoteka/console/prune_notifications.go
key-decisions:
- "commandDatabase/commandBucket closers close the handles they opened, so a later command in the same process must not reuse that app; TestDevStackSmoke models seed and serve as separate fresh processes, like the start script"
- "The uploads route is owned by plugin id summercms.surf, prefix falls back to /storage/uploads (attach default); a plugin route with the same pattern fails boot as a duplicate"
- "Seed output is printed only after the transaction commits, so a failed run prints no created lines"
- "With DEV_SEED_API_TOKEN set the seed never prints a secret; an existing row with that hash is reassigned to the seed user and name and un-revoked (api token: updated)"
- "An existing rating on the first album is left as it is (keyed by album_id, user_id), even when its value differs from 4"
- "Seeded albums are created with name, artist_display, year and format LP only (fields the PHP create form accepts)"
patterns-established:
- "Seed lines: '<entity>: created|updated|unchanged', cover lines append the public URL, token line shows a minted secret only on creation"
requirements-completed: [QA-05]
coverage:
- id: D1
description: "fonoteka:dev-seed seeds user, owned collection as context, three albums with PNG covers, wishlist entry, rating, API token and flips search_use_typesense last, in a fresh process with no published database"
requirement: QA-05
verification:
- kind: integration
ref: "../fonoteka.go/plugins/golem15/fonoteka/dev_stack_smoke_test.go#TestDevStackSmoke"
status: pass
human_judgment: false
- id: D2
description: "Seed is idempotent (rerun prints unchanged, adds no row in users, collections, albums, system_files, ratings, user_api_tokens) and refuses production, development and staging before opening the database"
requirement: QA-05
verification:
- kind: integration
ref: "../fonoteka.go/plugins/golem15/fonoteka/dev_stack_smoke_test.go#TestDevStackSmoke"
status: pass
human_judgment: false
- id: D3
description: "storage.uploads.serve serves a seeded cover from the assembled router: 200 image/png with X-Content-Type-Options: nosniff"
requirement: QA-05
verification:
- kind: integration
ref: "../fonoteka.go/plugins/golem15/fonoteka/dev_stack_smoke_test.go#TestDevStackSmoke"
status: pass
- kind: other
ref: "go run ./cmd/summer docs:build --check"
status: pass
human_judgment: false
- id: D4
description: "Marker GET /_fonoteka/api/v1/_dev/backend answers 200 with exactly backend/app/env and Cache-Control: no-store in local; parity corpus unchanged"
requirement: QA-05
verification:
- kind: integration
ref: "../fonoteka.go/plugins/golem15/fonoteka/dev_stack_smoke_test.go#TestDevStackSmoke"
status: pass
- kind: integration
ref: "go -C ../fonoteka.go test ./parity -run TestParityCorpus"
status: pass
human_judgment: false
- id: D5
description: "fonoteka:reindex, fonoteka:oauth-client and fonoteka:prune-notifications use commandDatabase; existing console tests unchanged and passing"
requirement: QA-05
verification:
- kind: unit
ref: "../fonoteka.go/plugins/golem15/fonoteka/console#TestReindexCommand,TestPruneNotifications,TestOAuthClientCommandListPrintsClientIDAndURIsNeverTheSecret"
status: pass
human_judgment: false
- id: D6
description: "http.access_log logs one 'http request' record with method, pattern, status, duration_ms; path without query only for unmatched requests; nothing when off"
requirement: QA-05
verification:
- kind: unit
ref: "modules/surf/accesslog_test.go#TestAccessLogSmoke"
status: pass
human_judgment: false
duration: 16min
completed: 2026-10-11
status: complete
---
# Phase 14.2 Plan 01: Go-side local stack glue Summary
**A fresh fonoteka process can now seed itself with `fonoteka:dev-seed` (local/dev only, idempotent, PNG covers through StoreAlbumPhoto). The framework serves those covers under the WinterCMS upload URL through the opt-in `storage.uploads.serve` route, the plugin answers a local-only backend marker, the fonoteka commands open their own database, and surf has an opt-in redacted request log (`http.access_log`).**
## Performance
- **Duration:** 16 min
- **Started:** 2026-10-11T15:31:28Z
- **Completed:** 2026-10-11T15:47:12Z
- **Tasks:** 3
- **Files modified:** 18 (8 created, 10 modified) across summercms.go and fonoteka.go
## Accomplishments
- `fonoteka:dev-seed`: refuses outside `SUMMER_ENV` local/dev before opening anything, then seeds in one `lagoon.Transaction` inside `lighthouse.WithoutBroadcasting[models.Album]`: the user (E2E_EMAIL/E2E_PASSWORD, default demo@example.com/changeme), the owned collection as context, Kind of Blue, Blue Train and Mingus Ah Um with 64x64 PNG covers, the wishlist with A Love Supreme, a rating of 4, the `dev-seed-mcp` token, and `search_use_typesense` flipped last.
- `console.commandDatabase` / `commandBucket` (G1): reuse the published handle, else open and publish it. Used by the seed, `fonoteka:reindex`, `fonoteka:oauth-client` and `fonoteka:prune-notifications`.
- `GET /_fonoteka/api/v1/_dev/backend` (D-09/D-10): `{"backend":"summercms","app":"fonoteka","env":"local"}`, `Cache-Control: no-store`, `throttle:60,1`, registered only in local/dev, not in the manifest.
- Framework `storage.uploads.serve` (D-19): `GET {public_path_prefix}/{path...}` on `attach.StaticHandlerPublic` with nosniff, documented in the surf README, docs/services/storage.md and docs/setup/configuration.md.
- Framework `http.access_log` (D-12): one Info `http request` record per request, documented in the surf README, docs/services/logging.md (new "Request log" section) and docs/setup/configuration.md.
- Planning todo for the same database gap in the shared sm-user-plugin (not modified).
## Seed output (TestDevStackSmoke)
First run:
```
user: created
collection: created
album Kind of Blue: created
cover Kind of Blue: created /storage/app/uploads/public/25d/b37/451/25db37451a4705c659ecfb.png
album Blue Train: created
cover Blue Train: created /storage/app/uploads/public/16f/a0b/4b0/16fa0b4b09666aeca51b6c.png
album Mingus Ah Um: created
cover Mingus Ah Um: created /storage/app/uploads/public/940/b2b/d6a/940b2bd6a1cb2b3d7d4ba4.png
wishlist: created
wishlist album A Love Supreme: created
rating Kind of Blue: created
api token: created inv_<secret> (shown once)
search: created
```
The second run prints the same 13 lines with `unchanged`; the cover lines keep the same URLs and the token line reads `api token: unchanged (secret not shown)`. A third run with a different `E2E_PASSWORD` prints `user: updated` and 12 `unchanged` lines.
Parity: `recorded 175/175 passing 175 failing 0 unrecorded 0 pending 0`.
## Task Commits
1. **Task 1 (tracer): seeded cover served end to end**
- summercms.go `0b6d757` feat(14.2-01): opt-in storage.uploads.serve route for public uploads
- fonoteka.go `89b703a` feat(14.2-01): fonoteka:dev-seed with a served cover in a fresh process
- Tracer gate: verify re-run passed (automated only, end-of-phase mode), then expanded.
2. **Task 2: full seed shape, marker, commands open their database**
- fonoteka.go `464ba47` feat(14.2-01): full dev-seed shape, local backend marker, commands open their database
- summercms.go `ad1fa5a` docs(14.2-01): todo for sm-user-plugin commands that need a published database
3. **Task 3: opt-in request log**
- summercms.go `2d68eab` feat(14.2-01): opt-in http.access_log request log in surf
## Files Created/Modified
- `modules/surf/uploads.go`: `mountUploads`, per-request bucket/DB lookup, nosniff
- `modules/surf/accesslog.go`: `accessLog`, `recordPattern`, `statusRecorder` (Flush/Unwrap kept)
- `modules/surf/accesslog_test.go`: `TestAccessLogSmoke` (acme.blog fixture)
- `modules/surf/router.go`: `mountUploads` after the plugin route loop; access log wrap in `compile`; `recordPattern` outermost in `wrap`
- `modules/surf/README.md`, `docs/services/storage.md`, `docs/services/logging.md`, `docs/setup/configuration.md`: both keys documented
- `fonoteka.go/plugins/golem15/fonoteka/console/{database,dev_seed}.go`: helpers and the seed
- `fonoteka.go/plugins/golem15/fonoteka/console/{reindex,oauth_client,prune_notifications}.go`: `commandDatabase`
- `fonoteka.go/plugins/golem15/fonoteka/{dev_marker,routes,plugin}.go`: marker and command registration
- `fonoteka.go/plugins/golem15/fonoteka/dev_stack_smoke_test.go`: `TestDevStackSmoke`
## Decisions Made
See `key-decisions` in the frontmatter. The main one is that the command closers close what they opened. A process that ran the seed therefore must not serve from the same app, so the smoke test models migrate, seed and serve as three fresh processes, which is what the plan 02 start script does.
## Deviations from Plan
1. **[Rule 2 - Missing coverage] Refusal and password-change checks added to TestDevStackSmoke.** The plan put the production-refusal test in plan 04, but threat T-14.2-01 cites TestDevStackSmoke for this plan. The smoke test now checks that an unset SUMMER_ENV, `development` and `staging` are refused and that no database gets published. It also checks that a changed `E2E_PASSWORD` prints `user: updated`. Files: dev_stack_smoke_test.go. Commit: 464ba47.
2. **[Rule 1 - Test isolation] The smoke test restores `search_use_typesense`.** The seed flips the shared container's settings row, so `keepSearchSetting` makes sure row 1 exists with the flag off and restores its earlier state when the test ends. Without it, later tests in the package would run with search on. Commit: 464ba47.
3. **Mid-plan in-progress SUMMARY skipped.** The context was well under half after Task 2, so Task 3 ran in the same executor and only this final SUMMARY was written. No committed artifact is affected.
**Total deviations:** 2 auto-fixed (1 missing coverage, 1 test isolation), 1 procedural. **Impact:** no scope change. The production code follows the plan.
## Issues Encountered
None. Pre-existing gofmt drift in `fonoteka.go/plugins/golem15/fonoteka/household_smoke_test.go` was left alone (out of scope).
## Verification
- summercms.go: `go vet ./...` clean, `go test -short ./...` and full `go test ./...` (Docker) green (38 packages), `TestDocsTree` and `docs:build --check` print no problems.
- fonoteka.go: `go vet ./...` clean, `go test ./plugins/golem15/fonoteka/... -count=1` green (10 packages), `TestParityCorpus` 175/175/0.
- `git -C ../fonoteka.go status --porcelain` shows only the existing submodule pointer drift (feedback, golem, user), with nothing staged. `plugins/golem15/user` has a clean worktree. `_dev/backend` does not appear in `parity/manifest.yaml`.
## Next Phase Readiness
Plan 02's start script can run `migrate`, `fonoteka:dev-seed`, `fonoteka:reindex` and `serve --addr 127.0.0.1:8423` with `SUMMER_ENV=local`, `SUMMER_STORAGE__UPLOADS__SERVE=true` and `SUMMER_HTTP__ACCESS_LOG=true`, and can probe the marker. `/storage/app/uploads/public/` should be added to `fonoteka.go/.gitignore` (G11, plan 02 scope) before seeding with the real config.
## Self-Check: PASSED
All 8 created files are present. Commits 0b6d757, ad1fa5a and 2d68eab exist in summercms.go, and 89b703a and 464ba47 exist in fonoteka.go.