Files
summercms/.planning/phases/12-p-ytarium-api-collections-and-albums/12-CONTEXT.md
2026-09-28 01:28:25 +02:00

16 KiB
Raw Blame History

Phase 12: Płytarium API — Collections and Albums - Context

Gathered: 2026-09-28 Status: Ready for planning

## Phase Boundary

The collection and album endpoints of golem15.fonoteka, ported to fonoteka.go with byte-compatible request and response shapes. They are mounted on the JWT group (/_fonoteka/api/v1) and, where PHP mirrors them, on the personal-token group (/api/v1/fonoteka, inv.scope:*).

In scope:

  • Collections: collections CRUD, collections/{id}/photos (upload and delete), collections/{id}/image (upload and delete), collections/{id}/switch, me/context (returns an opaque channel name), and collection/share (GET, PUT, regenerate).
  • Household: household/invitations (index, store, cancel, resend), household/members (index, destroy), and invitations/{token}/accept.
  • Albums:
    • albums CRUD, albums/{id}/rating (PUT, DELETE), albums/{id}/photos (upload, including the manual cover_url branch, and delete), and albums/search.
    • The side routes albums/sync, stats, value, missing and bulk.
    • The cover_urls import on create and bulk.
  • Lookups: artists, styles (GET, POST) and genres (POST; GET was ported in Phase 3).
  • Fuzz: a request-DTO-level fuzz over every write endpoint of this phase.

Moved out of this phase (roadmap and requirement wording to be corrected at plan time, see D-03, D-04 and D-06):

  • wishlist/albums/{id}/reserve|reveal (reservations) → Phase 13.
  • All anonymous public routes (public/{token}, public/{token}/albums, public/{token}/albums/{id}) → Phase 13.
  • albums/{id}/cover-price/discogs, albums/match, albums/{id}/match, albums/{id}/apply-release, albums/recognize, albums/import/discogs → Phase 14 (INTG-01/02).
  • Register-via-invitation (PendingInvitationRegistration consumed on register) and public invitation inspection → Phase 13 (API-07).

Repo: fonoteka.go. Framework changes in summercms.go only where a gap is found; no Nuxt or fonoteka-mcp change.

## Implementation Decisions

Route boundary

  • D-01: Collections, household and album routes listed in the domain are ported whole. Each handler is mounted once and reused on both groups wherever PHP mirrors it (the P6 D-15 genres pattern), with the exact ->where() constraints and inline throttles (throttle:10,1 on switch, share regenerate, invitation store and resend; throttle:20,1 on album photo upload; throttle:60,1 on albums/sync).
  • D-02: albums/sync, stats, value, missing and bulk are ported here: same controllers, no external service, and the Nuxt Albums UI calls them. bulk is where the single collection.bulk_updated broadcast under WithoutBroadcasting[Album] (P11 D-08) is exercised on a real endpoint.
  • D-03: Reservations move to Phase 13. In PHP they only exist as wishlist routes (AlbumReservationService, WishlistAlbumReservationController) and need the wishlist resolver. Fix the Phase 12 success criterion 2 and API-02 wording, and add them to Phase 13 / API-03.
  • D-04: The Discogs cover-price route moves to Phase 14 with INTG-01. The Discogs routes that match or use AI stay in Phase 14. Their manifest entries stay pending (P6 D-15: no 501 shells). Fix success criterion 2 and API-02 wording.
  • D-05: CoverImporter (the host-locked cover fetch behind cover_urls on album create and bulk) is ported now on the existing fetchguard AllowHosts GET helper (P6 D-11), so POST albums with cover_urls is parity-true in this phase. The pending todo fetchguard-guarded-http-client.md (POST, multipart, bearer) is not folded. It stays for Phase 14. If research finds that CoverImporter depends on DiscogsClient or needs more than a guarded GET, it reports that before planning.
  • D-06: All anonymous public-share routes move to Phase 13. Phase 12 ports only the owner-only JWT collection/share surface: show, update and regenerate, which emits the public_token. API-01 and success criterion 1 ("public token views") are reworded at plan time, and public/{token}* added to Phase 13 alongside public-wishlist/{token}*.
  • D-07: The manual cover URL branch of album photo upload ("exactly one of file or cover_url") uses fetchguard PublicOnly, as in PHP (P6 D-11/D-13).

Parity evidence

  • D-08: New recordings against the isolated PHP instance, using the Phase 2 tide capture rules (private 0600 vars, no live tokens in git):

    • nuxt-collections flow: create → switch → me/context → share show/update/regenerate → invite → accept as a second user → members → remove member.
    • nuxt-albums flow: create (with and without cover_urls) → update → rate / unrate → photo upload and delete → search → stats/value/missing/sync → bulk → delete.
    • Plus every distinct error status and body per route: 404 for foreign or missing ids, 403/404 non-owner, 410 on an unavailable invitation, 422 validation, and the personal-token 404 on owner-only household actions.

    Envelopes are reproduced per endpoint (P7 D-13).

  • D-09: Centrifugo publications are recorded from PHP during the album flows (create, update, delete, bulk) with the P11 D-10 capture tooling. The Go side, on the memory driver or a fake Centrifugo, is diffed against them with timestamp/actor normalised.

  • D-10: Token-group twins run the same handler as the JWT route and each replays its own recorded fixture. A route-table test asserts that every twin carries the right inv.scope:read|write and that JWT-only routes (switch, share, household, invitations) are absent from the token group.

  • D-11: Uploads get both recorded multipart replays (same file bytes, body diff including thumb_url) and Go tests. The Go tests assert the system_files row, the blob and the thumb exist; that the ImageContentGuard port rejects non-images; and that the upload-group MaxBytesReader cap holds (P7 D-04 pattern).

  • D-12: Search replays run with search disabled, as PHP was recorded (SCOUT_DRIVER=null). Typesense behaviour is covered by Go tests (D-16..D-18).

Invitations and mail

  • D-13: Invitation mail is sent by a River job enqueued in the same transaction as the invitation write, using the P11 job machinery. It goes out only if the invitation commits, which matches PHP's commit-before-side-effect ordering without the inline send.
    • The raw token (64 hex chars, sha256 at rest, 7-day expiry, as PHP) is present only in the job args and the mail. It is never logged and never stored in summer_jobs.metadata.
    • Locale and link follow PHP: inviter preferred_locale picks collection_invitation / collection_invitation-en and /zaproszenia/{token} / /en/invitations/{token} under app.url.
    • Asserted through postcard's memory driver.
  • D-14: Accepting an invitation ports NotificationService's write path now: the notifyInvitationAccepted row, plus any mail it sends, through the same job mechanism. The recorded accept flow then matches PHP DB state. Phase 13 adds the notifications list, read and prune API on top.
  • D-15: Only the existing-account path is ported. That means invite, resend and cancel for any email; accept by a logged-in user; and removing an editor with PHP's CollectionProvisioner context repair and organisation-membership rules. PendingInvitationRegistration rows are cleared on accept as PHP does, but consuming them on register belongs to Phase 13.

Search and the security test

  • D-16: Scout semantics are mirrored exactly. When Typesense is on and the query takes the Typesense path:

    1. Typesense returns one page of candidate ids.
    2. SQL applies whereIn(ids) + accessibleBy(user) + active collection (or token-frozen collection) + filters.
    3. meta.total/last_page come from Typesense, as Scout's paginator does. A stale document therefore yields a short page, never a leaked row. The inflated total is a documented, tested quirk, not a bug to fix.

    Rating, name and artist sorts force the SQL path, as PHP does. A Typesense error logs a warning and falls back to the SQL search.

  • D-17: The SQL text search uses ILIKE with % and _ escaped in user input, over PHP's column lists (TEXT_FIELDS_AUTHENTICATED: name, notes, artist_display, track_titles, label, catalog_number, plus artists.name). Escaping is a small, deliberate hardening: PHP passes %/_ through, so results differ only for queries containing them. Polish ordering relies on the pl-PL ICU database (P3 D-06).

  • D-18: The leak security test (success criterion 3) proves, with a scripted fake engine driver returning poisoned ids through the P11 search interface, that none of these return a row:

    1. A stale document whose album moved to a collection the caller cannot access.
    2. A mis-scoped document carrying the caller's collection_id for a foreign album.
    3. A soft-deleted album still in the index.
    4. An editor removed from a collection whose documents remain.
    5. A personal token frozen to its creation-time collection receiving hits from the user's other collections.

    No Typesense container is used.

Carried forward (locked earlier)

  • C-01: ActiveCollection (P3 D-03) is extended with switching and editor membership. me/context never exposes a raw collection_id, and the channel name is opaque (P11 D-11 naming rules).
  • C-02: Write paths go through the ported AlbumWriteService / CollectionWriteService fill boundaries (P5 D-05..D-07). The new request-DTO fuzz covers every write endpoint of this phase (unknown and server-owned keys never persisted). This inherits P5 criterion 3's HTTP half.
  • C-03: Response DTOs come from ported Serialize* functions, never from marshalled models. Use [] not null, Carbon +00:00 times, and the pagination envelope without links (P5 D-08, P6 D-17, DATA-10).
  • C-04: Album broadcasts and Typesense sync are already wired by Phase 11 (after-commit inline sync, broadcast job in the write transaction). This phase only calls them.

Claude's Discretion

  • Handler file layout under controllers/api and how the ~800-line AlbumApiController is split across Go files.
  • DTO structs and how the fuzz enumerates write endpoints (route-table driven preferred).
  • Job kind names and queue for invitation and notification mail.
  • Fake engine driver shape for tests.
  • Which errors per route need a recording and which a Go test with bodies from PHP source, where recording a case is impractical (e.g. a mid-transaction race). Recording is the default.
  • Plan count and split, subject to the plan-count checkpoint, "unit tests are the last plan" and the security-review agent (this phase touches authorization and public tokens).

<canonical_refs>

Canonical References

Downstream agents MUST read these before planning or implementing.

Scope

  • .planning/ROADMAP.md § Phase 12 (criteria 1 and 2 reworded per D-03, D-04, D-06), § Phase 13, § Phase 14
  • .planning/REQUIREMENTS.md: API-01, API-02 (wording fixes), API-03, API-07, INTG-01
  • .planning/PROJECT.md: API parity is the acceptance test; two-repo split

PHP reference (contract source)

  • /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/routes.php: JWT group (lines ~74–330) and personal-token group (~450–520)
  • /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/controllers/api/: CollectionApiController, CollectionShareController, MeContextController, InvitationApiController, CollectionMemberController, AlbumApiController, AlbumSyncController, RatingApiController, ArtistApiController, StyleApiController, GenreApiController
  • /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/classes/: ActiveCollectionResolver, AlbumSearchService, AlbumSyncService, CollectionFingerprint, CollectionShareService, CollectionProvisioner, InvitationService, NotificationService (write path), ImageContentGuard, ManualCoverUrlFetcher, AlbumWriteService, ArtistResolver, OrgAccess, discogs/CoverImporter.php
  • /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/traits/SerializesFonoteka.php
  • /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/views/mail/collection_invitation.htm, collection_invitation-en.htm
  • /media/nvme/dev/golem15/fonoteka/vue-fonoteka-app/app/composables/: useFonoteka.ts, useAlbumsQuery.ts, useCollectionSwitcher.ts, useCollectionSync.ts, useBulkAlbumTape.ts, useRealtimeSync.ts (client contract)

Prior phase decisions that apply

  • .planning/phases/03-first-vertical-slice-genres-end-to-end/03-CONTEXT.md: D-03 ActiveCollection, D-06 Polish ordering, D-14/D-15 routing and typed params
  • .planning/phases/05-data-layer-full-fidelity/05-CONTEXT.md: D-05..D-09 fill/hidden/rules, D-14..D-19 attachments and thumbs
  • .planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-CONTEXT.md: D-05 parameterized middleware, D-11..D-14 fetchguard, D-15 no 501 shells, D-17 response helpers, D-18 body caps
  • .planning/phases/07-user-plugin-and-authentication/07-CONTEXT.md: D-04 upload pattern, D-11..D-14 recording rules and envelopes
  • .planning/phases/11-jobs-realtime-and-search-infrastructure/11-CONTEXT.md: D-02 job dispatch in transaction, D-06..D-10 broadcasts and capture, D-11 channel naming, D-19/D-20 search driver and sync
  • .planning/phases/02-api-parity-harness-bootstrap/02-CONTEXT.md: tide capture rules

Research

  • .planning/research/PITFALLS.md Pitfall 3 (DTOs, not models), Pitfall 4 ([] vs null), Pitfall 15 (Typesense as pre-filter)

Parity corpus

  • fonoteka.go/parity/manifest.yaml, fonoteka.go/parity/README.md, fonoteka.go/parity/fixtures/{routes,nuxt,mcp}/

</canonical_refs>

<code_context>

Existing Code Insights

Reusable Assets

  • fonoteka.go/plugins/golem15/fonoteka/classes/active_collection.go: default-path resolver to extend with switch and membership.
  • classes/album_write_service.go, collection_write_service.go: fill boundaries with service-level fuzz tests already in place.
  • classes/artist_resolver.go, serialize.go, join_tables.go (album_artists ordering, CollectionEditor pivot), backend_album_collection.go.
  • controllers/genre_controller.go: the Phase 3 handler shape. controllers/api/token_api_controller.go, me_token_controller.go: JWT/token-group handler precedents.
  • routes.go: both group builders already exist. Only genres, tokens, me, me/locale and OAuth are mounted.
  • Framework: fetchguard (AllowHosts/PublicOnly), lagoon attachments and Thumb, postcard memory driver, P11 job manager, realtime memory driver and search driver interface.

Established Patterns

  • One handler on both groups. Route-table tests for scope and isolation (routes_group_test.go, routes_isolation_test.go).
  • Manifest entries flip from pending to ported only with a real handler and a passing replay.
  • Security-relevant phases carry T-12-xx threats with failing-when-broken tests. Candidates: cross-collection id access, invitation token reuse or expiry, owner-only enforcement under a personal token, search leak, upload content guard.

Integration Points

  • fonoteka.go/plugins/golem15/fonoteka/routes.go (both groups), parity/manifest.yaml, new parity/fixtures/nuxt/nuxt-collections.yaml and nuxt-albums.yaml, and the Centrifugo publication goldens.
  • The P11 job manager (invitation and notification mail jobs) and the search driver (fake engine for tests).

</code_context>

## Specific Ideas
  • The user moved all anonymous public routes to Phase 13 so the public surface (collection and wishlist, stacked public buckets, pubfail:<ip>) is ported and security-reviewed in one place.
  • Cover import is wanted now so album create is parity-true, while the broader guarded-client work waits for Phase 14.
## Deferred Ideas
  • Reservations (wishlist/albums/{id}/reserve|reveal) → Phase 13.
  • Anonymous public-share routes public/{token}* → Phase 13.
  • Discogs cover-price, release match, apply-release, recognize, Discogs import → Phase 14.
  • Register-via-invitation and public invitation inspection → Phase 13 (API-07).
  • Notifications list, read and prune API → Phase 13 (API-04).

Reviewed Todos (not folded)

  • fetchguard-guarded-http-client.md: not folded. Discogs cover-price moved to Phase 14, and CoverImporter only needs the existing guarded GET.
  • backend-admin-api-tokens.md: unrelated to this API surface (admin auth). Stays deferred past v1.
  • redacting-slog-handler.md: Phase 14 (credentials). Worth noting that invitation tokens must not reach logs (D-13), but no handler port is needed here.

Phase: 12-p-ytarium-api-collections-and-albums Context gathered: 2026-09-28