docs(12-02): complete collections, me/context and share link plan
This commit is contained in:
@@ -0,0 +1,315 @@
|
||||
---
|
||||
phase: 12-p-ytarium-api-collections-and-albums
|
||||
plan: 02
|
||||
subsystem: api
|
||||
tags: [fonoteka, collections, tenancy, personal-token, uploads, attach, share-token, me-context, realtime, parity, tide]
|
||||
|
||||
requires:
|
||||
- phase: 12-p-ytarium-api-collections-and-albums
|
||||
provides: "12-01: lagoon.ValidateRequest + pl/en catalogs, attach.File.URL/PublicURL, tide multipart parts and upload URL masking, user groups (HasGroupCode)"
|
||||
- phase: 11-jobs-realtime-and-search-infrastructure
|
||||
provides: lighthouse album binding and memory driver, beachcomber after-commit sync, conga worker
|
||||
provides:
|
||||
- classes.Resolve/SwitchTo/DisplayActiveID/ErrCollectionNotFound (token-aware ActiveCollectionResolver port)
|
||||
- classes.AccessibleBy/AlbumsAccessibleBy (membership plus personal-token pin), ProvisionCollection
|
||||
- classes.CollectionDTO/CollectionIndexDTO/PhotoDTO, SerializeCollection/SerializePhoto/LoadCollectionDTO
|
||||
- classes.CollectionKey, gates (IsSiteAdmin, CanManageOrg, AIAllowed, AIInherited, ResolveDiscogsConfig, DiscogsAllowed, MarketCurrency)
|
||||
- classes share service (GenerateShareToken[From], ShareStateFor, Enable/Disable/Rename/RegenerateShare)
|
||||
- 23 collections-area routes ported on both auth groups (collections CRUD, photos, image, switch, me/context, realtime/channels, collection/share)
|
||||
- controllers/api helpers decodeInput, phpInt, laravelBoolean, requestScope, writeWinterHTTPError (winter_<status>.html), writeValidationFailed
|
||||
- parity seed hook fonoteka (reseeded per case) and parity/fonoteka_reset.php for the PHP side
|
||||
affects: [12-03, 12-04, 12-05, 13]
|
||||
|
||||
actuals:
|
||||
tokens: 61230
|
||||
tasks: 3
|
||||
commits: 3
|
||||
plan_head_before: 02e1aa94612fbfe0940d8424ae7efedb00ea0190
|
||||
plan_head_after: 6c729a708f96fe941c4791049b89a9cad321461f
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "One handler value per route, mounted on the JWT group and its token twin with exactly one inv.scope per token route"
|
||||
- "PHP HttpException answers are embedded Winter production pages (winter_<status>.html) with only the app.url stylesheet origin templated"
|
||||
- "Parity cases that mutate state are recorded from a fresh PHP reset each and replayed from a fresh Go seed each (per-case reseed)"
|
||||
- "PHP-side recording state is built by a tinker script that mirrors the Go seed hook and bumps id sequences into 8-digit ranges"
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/classes/access.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/classes/collection_provisioner.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/classes/fingerprint.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/classes/gates.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/classes/share_service.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/request.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/http_errors.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/winter_404.html
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/collections_controller.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/collection_media_controller.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/collection_share_controller.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/me_context_controller.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/realtime_channels_controller.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/collections_smoke_test.go
|
||||
- ../fonoteka.go/parity/fonoteka_seed_test.go
|
||||
- ../fonoteka.go/parity/fonoteka_reset.php
|
||||
- ../fonoteka.go/parity/fixtures/routes/files/collection-photo.png
|
||||
- ../fonoteka.go/parity/fixtures/routes/files/not-an-image.txt
|
||||
modified:
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/classes/active_collection.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/classes/serialize.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/classes/backend_album_collection.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/models/collection.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/genre_controller.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/middleware/token_scope.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/routes.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/config/config.yaml
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/routes_group_test.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/phase08_coverage_test.go
|
||||
- ../fonoteka.go/config/storage.yaml
|
||||
- ../fonoteka.go/parity/manifest.yaml
|
||||
- ../fonoteka.go/parity/parity_test.go
|
||||
- ../fonoteka.go/parity/parity_contract_test.go
|
||||
- ../fonoteka.go/parity/genres_seed_test.go
|
||||
- ../fonoteka.go/parity/user_api_seed_test.go
|
||||
- ../fonoteka.go/parity/README.md
|
||||
- ../fonoteka.go/README.md
|
||||
|
||||
key-decisions:
|
||||
- "Collection strings are trimmed on save (Collection.BeforeSave) because Winter's Model::setAttribute trims every string attribute; the recordings show ' New Shelf ' stored as 'New Shelf', so the plan's byte-for-byte encoding edge does not hold for this stack"
|
||||
- "ProvisionCollection and Resolve's fallback carry no kind filter, as PHP; a lower-id wishlist can become the active context (pinned by TestResolveFallbackHasNoKindFilter)"
|
||||
- "GET collections sorts names with PHP 8 SORT_REGULAR (two numeric names compare as numbers, else bytes), stable over id order; recorded by the ordering case"
|
||||
- "The backend admin album binding keeps a non-provisioning resolver (backendActiveCollection); only API callers provision"
|
||||
- "Uploads use the Winter layout (bucket storage/app/uploads/public, prefix /storage/app/uploads/public); the parity test config uses the same prefix"
|
||||
- "Recordings come from a tinker reset of a canonical state (parity/fonoteka_reset.php) rather than the bootstrap flow, one reset per case; id sequences are lifted to 8 digits so captured ids are scrubbed and never collide"
|
||||
- "A body over the router cap answers 413 {\"error\":\"Payload too large\"} from the handler (the router only wraps the body); not recordable against PHP"
|
||||
|
||||
patterns-established:
|
||||
- "requestScope(w, r, app) returns the gorm handle, the user row and the optional personal token for handlers shared by both auth groups"
|
||||
- "New Winter error statuses are added as winter_<status>.html files next to http_errors.go; no code change"
|
||||
|
||||
requirements-completed: [API-01]
|
||||
|
||||
coverage:
|
||||
- id: D1
|
||||
description: "Token-aware tenant core: pinned tokens resolve to their single accessible collection, JWT users get the stored or lowest accessible collection under users-then-context row locks, else exactly one provisioned Moja kolekcja"
|
||||
requirement: API-01
|
||||
verification:
|
||||
- kind: integration
|
||||
ref: "../fonoteka.go/plugins/golem15/fonoteka/collections_smoke_test.go#TestResolveProvisionsOnce"
|
||||
status: pass
|
||||
- kind: integration
|
||||
ref: "../fonoteka.go/plugins/golem15/fonoteka/collections_smoke_test.go#TestResolvePinnedToken"
|
||||
status: pass
|
||||
- kind: integration
|
||||
ref: "../fonoteka.go/plugins/golem15/fonoteka/collections_smoke_test.go#TestResolveFallbackHasNoKindFilter"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D2
|
||||
description: "GET collections on both groups (pin-only for tokens, owner_name local-part rule, PHP name order) and the token group's one-scope-per-route rule"
|
||||
requirement: API-01
|
||||
verification:
|
||||
- kind: integration
|
||||
ref: "../fonoteka.go/plugins/golem15/fonoteka/collections_smoke_test.go#TestCollectionsIndexBothGroups"
|
||||
status: pass
|
||||
- kind: integration
|
||||
ref: "../fonoteka.go/plugins/golem15/fonoteka/routes_group_test.go#TestTokenGroupOneScopePerRoute"
|
||||
status: pass
|
||||
- kind: e2e
|
||||
ref: "go -C ../fonoteka.go test ./parity -run TestParityCorpus (GET collections jwt and personal_token cases)"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D3
|
||||
description: "Collections create/show/update/delete, photo and image uploads and deletes, and switch with the Winter 404 page, on both groups where PHP mirrors them; delete removes albums one by one and repairs contexts"
|
||||
requirement: API-01
|
||||
verification:
|
||||
- kind: integration
|
||||
ref: "../fonoteka.go/plugins/golem15/fonoteka/collections_smoke_test.go#TestCollectionDeleteRemovesAlbumsOneByOne"
|
||||
status: pass
|
||||
- kind: integration
|
||||
ref: "../fonoteka.go/plugins/golem15/fonoteka/collections_smoke_test.go#TestCollectionPhotoUpload"
|
||||
status: pass
|
||||
- kind: integration
|
||||
ref: "../fonoteka.go/plugins/golem15/fonoteka/collections_smoke_test.go#TestCollectionSwitchRefusals"
|
||||
status: pass
|
||||
- kind: e2e
|
||||
ref: "go -C ../fonoteka.go test ./parity -run TestParityCorpus (16 collections routes, multipart replays)"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D4
|
||||
description: "me/context flags, realtime/channels channel name and the owner-only collection/share surface with crypto/rand share tokens"
|
||||
requirement: API-01
|
||||
verification:
|
||||
- kind: integration
|
||||
ref: "../fonoteka.go/plugins/golem15/fonoteka/collections_smoke_test.go#TestMeContextFlags"
|
||||
status: pass
|
||||
- kind: integration
|
||||
ref: "../fonoteka.go/plugins/golem15/fonoteka/collections_smoke_test.go#TestRealtimeChannelsName"
|
||||
status: pass
|
||||
- kind: unit
|
||||
ref: "../fonoteka.go/plugins/golem15/fonoteka/collections_smoke_test.go#TestShareTokenAlphabet"
|
||||
status: pass
|
||||
- kind: integration
|
||||
ref: "../fonoteka.go/plugins/golem15/fonoteka/collections_smoke_test.go#TestShareOwnerOnly"
|
||||
status: pass
|
||||
- kind: e2e
|
||||
ref: "go -C ../fonoteka.go test ./parity -run TestParityCorpus (56 ported and passing); check_corpus.go --require-recorded --check-secrets"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D5
|
||||
description: "Photo URLs and avatar URLs use the Winter /storage/app/uploads/public layout (D-22)"
|
||||
requirement: API-01
|
||||
verification:
|
||||
- kind: integration
|
||||
ref: "../fonoteka.go/parity/avatar_assembled_test.go#TestAvatarAssembled"
|
||||
status: pass
|
||||
- kind: integration
|
||||
ref: "../fonoteka.go/plugins/golem15/fonoteka/collections_smoke_test.go#TestCollectionPhotoUpload"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
|
||||
duration: 46min
|
||||
completed: 2026-10-02
|
||||
status: complete
|
||||
---
|
||||
|
||||
# Phase 12 Plan 02: Collections, me/context, realtime channel and share link Summary
|
||||
|
||||
**Token-aware tenant core (pinned tokens, row-locked resolve and provisioning, one access scope) and all 23 collections-area routes ported byte-compatibly on both auth groups, with Winter 404 pages, multipart photo/image uploads under the Winter URL layout, per-album collection deletes, me/context gates and crypto/rand share tokens, replayed from 82 fresh PHP recordings.**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** 46 min
|
||||
- **Started:** 2026-10-02T09:51:46Z
|
||||
- **Completed:** 2026-10-02T10:37:30Z
|
||||
- **Tasks:** 3 (tracer verified end to end before expansion)
|
||||
- **Files modified:** 119 in fonoteka.go (code, tests, 75 fixture files, docs)
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- `classes.Resolve` ports `ActiveCollectionResolver::resolve`: a personal token needs exactly one accessible pinned id (else `ErrCollectionNotFound`, the Winter 404 page); a JWT user gets the stored context while accessible, else the lowest-id accessible collection, else a provisioned `Moja kolekcja`, under `SELECT ... FOR UPDATE` on the users row then the context row. Four concurrent first resolves create one collection and one context row.
|
||||
- `classes.AccessibleBy` / `AlbumsAccessibleBy` are the request-facing access rule: grouped owner-or-editor membership on live rows, plus the token pin `(id IN pin OR own wishlist)`. Genres on the token group now resolve the pin, as PHP does.
|
||||
- Collections CRUD, photos, image, switch, `me/context`, `realtime/channels` and `collection/share` are mounted once each; the token group carries exactly one `inv.scope` per route, so a write-only token updates a collection and is refused on reads with PHP's 403 body (which now carries `Cache-Control: no-cache, private`).
|
||||
- Uploads write Winter-style disk names under `storage/app/uploads/public`, set `sort_order` to the id, and answer `url` and a 200x200 crop `thumb_url`; photo removal detaches the row as `AttachMany::remove` does; image upload replaces the previous image.
|
||||
- Deleting a collection soft-deletes its albums one by one (each gets its search removal and `deleted.fonoteka.album` broadcast after commit, none on rollback) and repairs the context of the owner and every editor.
|
||||
- `me/context` returns exactly the ten recorded flags (site admin from the user's `admin` group, AI org lock and inheritance, Discogs tiers, market currency); `realtime/channels` returns `collection:<id>`; the share surface is owner-only with tokens drawn from `crypto/rand` with rejection at 248.
|
||||
- 82 parity cases recorded from the isolated PHP (APP_DEBUG=false); `TestParityCorpus` reports 56 ported routes passing; `check_corpus.go --require-recorded --check-secrets` is green.
|
||||
|
||||
## Task Commits
|
||||
|
||||
All commits are in fonoteka.go (summercms.go code is untouched):
|
||||
|
||||
1. **Task 1: tenant core and GET collections on both groups (tracer)** - `574b2f1` (feat)
|
||||
2. **Task 2: collection CRUD, photos, image and switch** - `b41d5ca` (feat)
|
||||
3. **Task 3: me/context, realtime/channels and the share link** - `6c729a7` (feat)
|
||||
|
||||
Ledger: fonoteka.go `02e1aa9..6c729a7`, 3 commits (`git rev-list --count`).
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `classes/active_collection.go`, `access.go`, `collection_provisioner.go`: resolver, switch, display id, access scopes, provisioner
|
||||
- `classes/serialize.go`: CollectionDTO/CollectionIndexDTO/PhotoDTO, album counts and media loaders
|
||||
- `classes/gates.go`, `share_service.go`, `fingerprint.go`: me/context gates, share tokens and state, collection_key HMAC
|
||||
- `controllers/api/*`: request decoding helpers, Winter error pages, collections, media, share, me/context and channel handlers
|
||||
- `models/collection.go`: per-album BeforeDelete and Winter string trimming in BeforeSave
|
||||
- `routes.go`: JWT routes and token twins with per-route scopes, inline throttles, `{id}`/`{fileId}` constraints
|
||||
- `config/storage.yaml`, plugin `config/config.yaml`, `README.md`: Winter upload layout and the new `golem15.fonoteka.*` keys
|
||||
- `parity/*`: fonoteka seed hook (reseeded per case), manifest flips (56 ported), PHP reset script, recording recipe, 75 fixtures
|
||||
|
||||
## Decisions Made
|
||||
|
||||
See `key-decisions` in the frontmatter.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 1 - Bug] Winter trims string attributes, so names are not stored byte-for-byte**
|
||||
- **Found during:** Task 1 (first PHP recordings)
|
||||
- **Issue:** The plan's encoding truth says names keep leading and trailing spaces. Winter's `Model::setAttribute` trims every string (`$trimStringAttributes = true`); the recorded PHP stores `" New Shelf "` as `New Shelf`.
|
||||
- **Fix:** `Collection.BeforeSave` trims name and description with PHP's trim set; share renames trim too. Recorded cases pin it (POST, PUT, share rename).
|
||||
- **Commit:** b41d5ca, 6c729a7
|
||||
|
||||
**2. [Rule 1 - Bug] PHP's provisioner and resolve fallback have no kind filter**
|
||||
- **Found during:** Task 1
|
||||
- **Issue:** The plan said the provisioner reuses the lowest kind=collection collection; `CollectionProvisioner::provision` uses `accessibleByMembership()->orderBy('id')->first()` with no kind filter.
|
||||
- **Fix:** Followed PHP; `TestResolveFallbackHasNoKindFilter` pins the wishlist case, and the recorded owner-delete case shows the repaired context.
|
||||
- **Commit:** 574b2f1
|
||||
|
||||
**3. [Rule 1 - Bug] Display id for a token with other than one pinned id is null**
|
||||
- **Issue:** The plan said such a token falls back to the stored context; `activeCollectionIdForDisplay` returns null for any token without exactly one id.
|
||||
- **Fix:** Followed PHP (`TestCollectionsIndexBothGroups` unpinned-token rows have `is_active` null).
|
||||
- **Commit:** 574b2f1
|
||||
|
||||
**4. [Rule 1 - Bug] Name order is PHP SORT_REGULAR, not a plain byte compare**
|
||||
- **Issue:** `sortBy('name')` uses `asort(SORT_REGULAR)`: two numeric names compare as numbers ("9" before "10").
|
||||
- **Fix:** `phpCompareStrings` in a stable sort; the recorded `ordering` case covers numeric, duplicate, accented and case-differing names.
|
||||
- **Commit:** 574b2f1
|
||||
|
||||
**5. [Rule 1 - Bug] InvScope refusals lacked `Cache-Control: no-cache, private`**
|
||||
- **Issue:** The recorded PHP 403 carries the header every Laravel response has; the Go middleware omitted it.
|
||||
- **Fix:** `middleware/token_scope.go` sets it before writing 401/403.
|
||||
- **Commit:** 574b2f1
|
||||
|
||||
**6. [Rule 3 - Blocking] Shared parity database across routes**
|
||||
- **Issue:** Every ported route replays against one Postgres; the fonoteka hook's tokens and alice's organisation broke the tokens, connected-apps and user-api fixtures, and an unpinned `token:mcp-read` now 404s on the token genres route as PHP would.
|
||||
- **Fix:** The genres hook deletes the fonoteka tokens and serves `token:mcp-read` from a dedicated pinned reader (alice's "Parity MCP" token stays unpinned for the tokens fixtures; all genre counts are zero either way); the user-api hook resets the organisation fields; fonoteka routes reseed before every case.
|
||||
- **Commit:** 574b2f1
|
||||
|
||||
**7. [Rule 3 - Blocking] Backend album binding must not provision**
|
||||
- **Issue:** `ResolveActiveCollection` now has PHP resolve semantics (provisioning, no kind filter), but the admin album binding (a Go-only Phase 10 path) relied on the old non-provisioning read.
|
||||
- **Fix:** `backendActiveCollection` keeps the old behaviour for the backend binding; API callers (token store, OAuth consent) use the full resolver as PHP does.
|
||||
- **Commit:** 574b2f1
|
||||
|
||||
**8. [Rule 3 - Blocking] Upload part files live under `parity/fixtures/routes/files/`**
|
||||
- **Issue:** The plan named `parity/fixtures/files/`; tide refuses part paths that leave the fixture's directory (`../`).
|
||||
- **Fix:** Files sit in `parity/fixtures/routes/files/`, pinned by sha256.
|
||||
- **Commit:** b41d5ca
|
||||
|
||||
**9. [Rule 3 - Blocking] Deferred-scope placeholder tests and the unported-route check**
|
||||
- **Issue:** `TestOAuthTokenSurfaceIsolationCoverage` subtests fail by design once token collections, write scopes, switch and share routes exist; `assertPortedMismatch` used `me/context` as its unported route.
|
||||
- **Fix:** Replaced each placeholder with the real assertion (401 then read 200 then write-only PUT 200; read-only token 403 on every write route; switch and share JWT-only); the mismatch check now uses `GET ai-credential` (Phase 14).
|
||||
- **Commits:** 574b2f1, b41d5ca, 6c729a7
|
||||
|
||||
**10. [Rule 2 - Missing critical] Replaced and deleted image blobs are removed after commit**
|
||||
- **Issue:** PHP's `image()->delete()` leaves the old bytes on disk; the plan asked for after-commit deletion.
|
||||
- **Fix:** Rows are deleted in the request, blobs after the write succeeds; not observable in responses.
|
||||
- **Commit:** b41d5ca
|
||||
|
||||
**11. [Rule 3 - Blocking] Over-cap uploads answer 413 from the handler**
|
||||
- **Issue:** surf only wraps the body in `http.MaxBytesReader`; there is no router 413 response, and PHP's `post_max_size` path cannot be recorded.
|
||||
- **Fix:** `decodeInput` reports the cap and the handler answers 413 `{"error":"Payload too large"}`; `TestCollectionPhotoUpload` asserts it and that no row is stored.
|
||||
- **Commit:** b41d5ca
|
||||
|
||||
**12. [Recording approach] PHP state from a tinker reset per case**
|
||||
- The plan suggested the bootstrap seed plus tinker additions. A single reset script (`parity/fonoteka_reset.php`) that mirrors the Go hook was used instead, run before each case, with collection and file id sequences lifted to 8-digit ranges so captured ids are scrubbed and never collide. The README documents the recipe.
|
||||
|
||||
---
|
||||
|
||||
**Total deviations:** 11 auto-fixed (5 bugs, 1 missing critical, 5 blocking) plus 1 recording-approach change.
|
||||
**Impact on plan:** All 23 routes ported and replaying; every deviation follows the recorded PHP contract or keeps the shared corpus honest. No scope creep.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
- The org-lock-on `me/context` case was not recorded (it needs a PHP restart with `FONOTEKA_AI_ORG_LOCK`); `TestMeContextFlags` covers lock on and off.
|
||||
- A personal-token caller cannot reach `collection/share` or `switch` over HTTP (the routes are absent from the token group), so the in-handler second lock is covered by Go tests rather than recordings.
|
||||
- Recording uploads against the isolated PHP wrote 16 files into the PHP checkout's `storage/app/uploads/public/6ab/f82/`; they were removed afterwards.
|
||||
- The pending-invitation 409 guard is not part of `Resolve` yet; it lands with household and invitations in 12-03 (`invitation_acceptance_required` is always false, as PHP returns it).
|
||||
|
||||
## Known Stubs
|
||||
|
||||
None.
|
||||
|
||||
## User Setup Required
|
||||
|
||||
None. Production keeps `storage.uploads` on the Winter layout; the new `golem15.fonoteka.*` keys default to PHP's values.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
- 12-03 adds the pending-invitation guard to `Resolve`, more Winter pages (409/410 as new `winter_<status>.html` files) and reuses `ProvisionCollection` for member removal.
|
||||
- 12-04 builds album handlers on `AlbumsAccessibleBy`, `requestScope`, `decodeInput` and the media helpers; `models.marketCurrency()` still returns EUR and should read `classes.MarketCurrency`.
|
||||
- 12-05 can reuse `collectionsHarness`, the fonoteka seed hook and `GenerateShareTokenFrom` for coverage and fuzzing.
|
||||
|
||||
---
|
||||
*Phase: 12-p-ytarium-api-collections-and-albums*
|
||||
*Completed: 2026-10-02*
|
||||
Reference in New Issue
Block a user