docs(14.2-01): complete Go-side local stack glue plan
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user