--- 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`. **Plan metadata:** `2a955d6` (docs: complete albums admin collection boundary plan) ## 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*