diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 2d11853..13bde89 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -375,7 +375,7 @@ Plans: 4. The relation-manager schema supports search/link/unlink/manage-or-view lists for Collections' editors tab, replacing the `partial` field entirely. 5. Admin CRUD endpoints expose `listExtendQuery`/`formExtendQuery`/`formBeforeCreate`/`formBeforeUpdate`/`relationExtendManageQuery` hooks, bulk delete runs each record's lifecycle hooks, and the Settings model binds to a settings screen through the same schema pipeline. -**Plans**: 5/12 plans executed +**Plans**: 6/12 plans executed **Research flag:** yes Plans: @@ -396,7 +396,7 @@ Plans: - [x] 09-05-PLAN.md — Deliver schema-projected CRUD and transactional bulk deletion **Wave 6** *(blocked on Wave 5 completion)* -- [ ] 09-06-PLAN.md — Port the Albums admin surface and the collection boundary +- [x] 09-06-PLAN.md — Port the Albums admin surface and the collection boundary **Wave 7** *(blocked on Wave 6 completion)* - [ ] 09-07-PLAN.md — Port the Artists backend controller @@ -529,7 +529,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → | 6. HTTP routing, auth groups and rate limiting | 14/14 | Complete | 2026-09-21 | | 7. User plugin and authentication | 8/8 | Complete | 2026-09-23 | | 8. OAuth2.1 authorization server | 10/10 | Complete | 2026-09-23 | -| 9. Backend admin authentication and schema pipeline | 5/12 | In Progress| | +| 9. Backend admin authentication and schema pipeline | 6/12 | In Progress| | | 10. Admin Vue SPA | 0/TBD | Not started | - | | 11. Jobs, realtime and search infrastructure | 0/TBD | Not started | - | | 12. Płytarium API — Collections and Albums | 0/TBD | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index baeabab..a6559fd 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -4,16 +4,16 @@ milestone: v1.0 current_phase: 09 current_phase_name: Backend admin authentication and schema pipeline status: executing -stopped_at: Completed 09-05-PLAN.md -last_updated: "2026-09-24T17:46:54.525Z" +stopped_at: Completed 09-06-PLAN.md +last_updated: "2026-09-24T18:21:51.771Z" last_activity: 2026-09-24 last_activity_desc: Phase 09 execution started -state_head: 50754808f61071e23746f3f36dde8a292a18360b +state_head: 0caa86ec0b6d255dd22a55ee5923a40d3a821977 progress: total_phases: 15 completed_phases: 8 total_plans: 67 - completed_plans: 59 + completed_plans: 61 milestone_name: milestone --- @@ -29,7 +29,7 @@ See: .planning/PROJECT.md (updated 2026-09-16) ## Current Position Phase: 09 (Backend admin authentication and schema pipeline) — EXECUTING -Plan: 6 of 12 +Plan: 7 of 12 Status: Ready to execute Last activity: 2026-09-24 — Phase 09 execution started @@ -114,6 +114,7 @@ Progress: [██████████] 100% | Phase 09 P03 | 25min | 3 tasks | 10 files | | Phase 09 P04 | 36min | 3 tasks | 13 files | | Phase 09 P05 | 25min | 3 tasks | 8 files | +| Phase 09 P06 | 29min | 3 tasks | 15 files | ## Accumulated Context @@ -292,6 +293,10 @@ Recent decisions affecting current work: - [Phase 09]: Show and update use one not-found body for missing and out-of-scope rows; delete of an absent row is deleted 0 and does not run hooks - [Phase 09]: A bulk selection that matches no scoped row is a no-op; a mixed present and absent selection is a 409 and rolls back - [Phase 09]: Controller hook failures return an opaque lifecycle error and do not echo the hook text +- [Phase 09]: Album admin D-14 stores collection_id of the one active frontend user matched by normalized backend email; no album user_id column was added +- [Phase 09]: Winter relation keys match exported Go fields case-insensitively and the served JSON keeps the YAML spelling +- [Phase 09]: Required relation fields stay on the admin form schema and are not save-time column rules +- [Phase 09]: Genre and style admin option values are decimal strings because pact.Option.Value is a string ### Pending Todos @@ -314,6 +319,6 @@ Items acknowledged and carried forward from previous milestone close: ## Session Continuity -Last session: 2026-09-24T17:46:43.371Z -Stopped at: Completed 09-05-PLAN.md +Last session: 2026-09-24T18:21:51.437Z +Stopped at: Completed 09-06-PLAN.md Resume file: None diff --git a/.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-06-SUMMARY.md b/.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-06-SUMMARY.md new file mode 100644 index 0000000..3655142 --- /dev/null +++ b/.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-06-SUMMARY.md @@ -0,0 +1,261 @@ +--- +phase: 09-backend-admin-authentication-and-schema-pipeline +plan: 06 +subsystem: admin +tags: [cabana, albums, winter-yaml, collection-scope, dropdowns, postgres] + +requires: + - phase: 09-backend-admin-authentication-and-schema-pipeline + provides: compiled form and list schemas, permission gate, and schema-projected CRUD +provides: + - Registered Albums admin controller with embedded Winter form and list YAML + - Format, genre, and style dropdown options without cross-field leakage + - Exact active-collection binding for album create, update, list, and bulk delete +affects: [09-backend-admin-authentication-and-schema-pipeline, admin-api, phase-10-spa] + +actuals: + tokens: 14974 + tasks: 3 + commits: 2 + +tech-stack: + added: [] + patterns: + - "Winter relation keys match exported Go fields case-insensitively; served JSON keeps the YAML spelling" + - "Album admin scope is the active collection of the one active frontend user with the backend email" + - "Non-scalar required flags stay on the form schema and are not save-time column rules" + +key-files: + created: + - ../fonoteka.go/plugins/golem15/fonoteka/controllers/albums_admin_controller.go + - ../fonoteka.go/plugins/golem15/fonoteka/controllers/albums/config_form.yaml + - ../fonoteka.go/plugins/golem15/fonoteka/controllers/albums/config_list.yaml + - ../fonoteka.go/plugins/golem15/fonoteka/models/album/fields.yaml + - ../fonoteka.go/plugins/golem15/fonoteka/models/album/columns.yaml + - ../fonoteka.go/plugins/golem15/fonoteka/classes/backend_album_collection.go + - ../fonoteka.go/plugins/golem15/fonoteka/admin_albums_test.go + modified: + - cabana/http.go + - cabana/query.go + - cabana/list_schema.go + - cabana/crud.go + - ../fonoteka.go/plugins/golem15/fonoteka/models/album.go + - ../fonoteka.go/plugins/golem15/fonoteka/admin.go + +key-decisions: + - "The album foreign key written by D-14 is collection_id of the matched frontend user's active collection; albums have no user_id column" + - "Genre and style option values are decimal strings because pact.Option.Value is a string" + - "relation: genre resolves to field Genre while the schema JSON keeps relation genre" + - "Required relation fields remain in the form schema and are not applied as save validation" + +patterns-established: + - "Pattern: albumsAdminController resolves *gorm.DB per call from the booted app, never a process-global collection id" + - "Pattern: zero, duplicate, inactive, and no-active-collection email matches share one 422 and name no candidate rows" + +requirements-completed: [ADMIN-01, ADMIN-02, ADMIN-04] + +coverage: + - id: D1 + description: Albums form schema serves every Winter field, locale key, and format scalar in source order for pl and en. + requirement: ADMIN-01 + verification: + - kind: integration + ref: plugins/golem15/fonoteka/admin_albums_test.go#TestAlbumsAdminForm + status: pass + human_judgment: false + - id: D2 + description: Albums registration resolves the album model and embedded assets and rejects a mismatched model or missing directory. + requirement: ADMIN-01 + verification: + - kind: integration + ref: plugins/golem15/fonoteka/admin_albums_test.go#TestAlbumsAdminRegistration + status: pass + human_judgment: false + - id: D3 + description: Denied form, create, and update requests return 403 before album, genre, style, or frontend-user SQL. + requirement: ADMIN-01 + verification: + - kind: integration + ref: plugins/golem15/fonoteka/admin_albums_test.go#TestAlbumsAdminPermissionOrder + status: pass + human_judgment: false + - id: D4 + description: Albums list schema keeps columns, toolbar actions, filters, and locale keys in declaration order. + requirement: ADMIN-02 + verification: + - kind: integration + ref: plugins/golem15/fonoteka/admin_albums_test.go#TestAlbumsAdminList + status: pass + human_judgment: false + - id: D5 + description: Format, genre, and style dropdowns are ordered and do not leak choices across fields. + requirement: ADMIN-01 + verification: + - kind: integration + ref: plugins/golem15/fonoteka/admin_albums_test.go#TestAlbumsAdminDropdowns + status: pass + human_judgment: false + - id: D6 + description: Permitted album lists keep empty, single, equal-value, and adjacent page behavior, and unknown sorts return 422. + requirement: ADMIN-02 + verification: + - kind: integration + ref: plugins/golem15/fonoteka/admin_albums_test.go#TestAlbumsAdminListEdges + status: pass + human_judgment: false + - id: D7 + description: One active normalized email persists that frontend user's active collection id. + requirement: ADMIN-04 + verification: + - kind: integration + ref: plugins/golem15/fonoteka/admin_albums_test.go#TestAlbumsAdminCollectionMatch + status: pass + human_judgment: false + - id: D8 + description: Duplicate, inactive, and no-active-collection email matches return 422 and persist nothing. + requirement: ADMIN-04 + verification: + - kind: integration + ref: plugins/golem15/fonoteka/admin_albums_test.go#TestAlbumsAdminAmbiguousEmail + status: pass + - kind: integration + ref: plugins/golem15/fonoteka/admin_albums_test.go#TestAlbumsAdminInactiveUser + status: pass + - kind: integration + ref: plugins/golem15/fonoteka/admin_albums_test.go#TestAlbumsAdminCrossCollection + status: pass + human_judgment: false + - id: D9 + description: Client user_id and collection_id cannot override the resolved collection, and responses do not echo those keys. + requirement: ADMIN-04 + verification: + - kind: integration + ref: plugins/golem15/fonoteka/admin_albums_test.go#TestAlbumsAdminProtectedAssociation + status: pass + human_judgment: false + - id: D10 + description: Scoped update, show, and atomic bulk delete keep foreign albums unchanged. + requirement: ADMIN-04 + verification: + - kind: integration + ref: plugins/golem15/fonoteka/admin_albums_test.go#TestAlbumsAdminCRUD + status: pass + human_judgment: false + +duration: 29min +completed: 2026-09-24 +status: complete +plan_head_before: 3e7738ff32012699bccc4e3f53ea347c87b45f8b +plan_head_after: 0caa86ec0b6d255dd22a55ee5923a40d3a821977 +--- + +# Phase 9 Plan 06: Albums Admin Collection Boundary Summary + +**Albums admin form, list, and writes bind only the active collection of the one active frontend user matched by the backend account email.** + +## Performance + +- **Duration:** 29 min +- **Started:** 2026-09-24T17:50:44Z +- **Completed:** 2026-09-24T18:20:13Z +- **Tasks:** 3 +- **Files modified:** 15 + +## Accomplishments +- Embedded the Winter Albums form and list YAML and served the localized form at `GET /_admin/api/v1/golem15/fonoteka/albums/schema/form`. +- Format options keep Album::FORMATS scalars and phrase keys; genre and style options are active rows ordered by name then id. +- Create and update set `collection_id` from `ResolveBackendAlbumCollectionUser`. Lists, shows, updates, and bulk deletes cannot see another collection. + +## TDD Gate Compliance + +Each task has a RED `test(09-06)` commit before its GREEN `feat(09-06)` commit. RED evidence checks returned `RED_EVIDENCE_OK` for `TestAlbumsAdminForm`, `TestAlbumsAdminDropdowns`, and `TestAlbumsAdminCollectionMatch`. + +## Task Commits + +1. **Task 1: Port complete Albums form and controller registration** - `60a4b10` (test, fonoteka.go), `c63146a` (feat, summercms.go), `652bfda` (feat, fonoteka.go) +2. **Task 2: Port Albums list and typed option providers** - `81f1aa4` (test, fonoteka.go), `5de760d` (feat, fonoteka.go) +3. **Task 3: Enforce exact active-collection user resolution** - `6004c6a` (test, fonoteka.go), `0caa86e` (feat, summercms.go), `15dfefa` (feat, fonoteka.go) + +The summercms.go ledger `3e7738ff..0caa86e` contains the two framework commits above. Fonoteka commits are in `fonoteka.go` `60a4b10..15dfefa`. + +## Files Created/Modified +- `fonoteka.go/plugins/golem15/fonoteka/controllers/albums_admin_controller.go` - Albums controller, permissions, dropdowns, and collection hooks +- `fonoteka.go/plugins/golem15/fonoteka/classes/backend_album_collection.go` - exact normalized email resolution +- `fonoteka.go/plugins/golem15/fonoteka/models/album/fields.yaml` - Winter album form, copied unchanged +- `fonoteka.go/plugins/golem15/fonoteka/models/album/columns.yaml` - Winter album columns, copied unchanged +- `fonoteka.go/plugins/golem15/fonoteka/models/album.go` - `Genre` association and `getFormatOptions` +- `cabana/http.go` - `GET .../schema/form` behind the existing permission check +- `cabana/query.go` and `cabana/list_schema.go` - case-insensitive relation field match +- `cabana/crud.go` - required validation limited to scalar writable fields +- `fonoteka.go/plugins/golem15/fonoteka/admin_albums_test.go` - assembled PostgreSQL proof + +## Decisions Made +- D-14 stores `collection_id`, the active collection of the matched frontend user. The albums table has no `user_id` column, so none was added. +- `pact.Option.Value` is a string, so genre and style ids are decimal strings. Format values stay the Winter scalars (`LP`, `EP 7"`, and the rest). +- Served list JSON keeps `relation: genre`. Joins and preloads use the exported Go field `Genre`. +- `artists` stays `required: true` on the form schema. Save validation does not require it, because the admin CRUD path cannot bind a relation. + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 3 - Blocking] Embedded the list YAML while registering the controller** +- **Found during:** Task 1 +- **Issue:** Activation compiles `config_list.yaml` for every admin controller. Registering Albums with only the form files failed boot. +- **Fix:** Copied the Winter list and columns YAML in the same commit as the form. +- **Files modified:** `controllers/albums/config_list.yaml`, `models/album/columns.yaml` +- **Verification:** `TestAlbumsAdminRegistration` and `TestAlbumsAdminList` +- **Committed in:** `652bfda` + +**2. [Rule 2 - Missing Critical] Served the form schema over HTTP** +- **Found during:** Task 1 +- **Issue:** Form compilation existed, but no route served it, so permission-denied form requests could not be proven. +- **Fix:** Added `GET /{vendor}/{plugin}/{controller}/schema/form` using the same `protect` gate as the list schema. +- **Files modified:** `cabana/http.go` +- **Verification:** `TestAlbumsAdminForm` and `TestAlbumsAdminPermissionOrder` +- **Committed in:** `c63146a` + +**3. [Rule 1 - Bug] Matched Winter relation names to exported Go fields** +- **Found during:** Task 1 +- **Issue:** `relation: genre` did not match field `Genre`, so the unchanged columns YAML could not activate. +- **Fix:** Accept an exact field name, otherwise a case-insensitive relation match. JSON still says `genre`. +- **Files modified:** `cabana/list_schema.go`, `cabana/query.go`, `models/album.go` +- **Verification:** `go test ./cabana` and `TestAlbumsAdminList` +- **Committed in:** `c63146a`, `652bfda` + +**4. [Rule 2 - Missing Critical] Did not save-validate unbound relation required flags** +- **Found during:** Task 3 +- **Issue:** `artists.required` made every album create 422 before the collection hook ran. The field is not a writable column. +- **Fix:** Merge `required` into save rules only for scalar writable field types. The schema JSON is unchanged. +- **Files modified:** `cabana/crud.go` +- **Verification:** `go test ./cabana` and `TestAlbumsAdminCollectionMatch` +- **Committed in:** `0caa86e` + +--- + +**Total deviations:** 4 auto-fixed (1 bug, 2 missing critical, 1 blocking) +**Impact on plan:** Required for unchanged Winter YAML to boot and for album creates to reach the collection hook. No new album columns and no extra controllers. + +## Issues Encountered + +`TestPhase8RedMCPMe` and `TestOAuthToolsMeRouteIsolation` fail because `GET /_admin/api/v1/auth/me` ends in `/me`. That route was already mounted with the admin API before this plan. It was left unchanged. + +`ADMIN-01`, `ADMIN-02`, and `ADMIN-04` stay pending in `REQUIREMENTS.md`. Later plans in this phase still declare them. + +## User Setup Required + +None - no external service configuration required. + +## Next Phase Readiness + +Ready for 09-07. Albums is the only new admin controller. Artists, Styles, and Collections controllers were not added. Genre remains the existing list tracer. + +## Self-Check: PASSED + +- FOUND: `fonoteka.go` `60a4b10`, `652bfda`, `81f1aa4`, `5de760d`, `6004c6a`, `15dfefa` +- FOUND: `summercms.go` `c63146a`, `0caa86e` +- FOUND: albums controller, form YAML, columns YAML, and `backend_album_collection.go` + +--- +*Phase: 09-backend-admin-authentication-and-schema-pipeline* +*Completed: 2026-09-24*