docs(12): capture phase context
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
# Phase 12: Płytarium API — Collections and Albums - Context
|
||||
|
||||
**Gathered:** 2026-09-28
|
||||
**Status:** Ready for planning
|
||||
|
||||
<domain>
|
||||
## 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.
|
||||
|
||||
</domain>
|
||||
|
||||
<decisions>
|
||||
## 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).
|
||||
|
||||
</decisions>
|
||||
|
||||
<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>
|
||||
|
||||
<specifics>
|
||||
## 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.
|
||||
|
||||
</specifics>
|
||||
|
||||
<deferred>
|
||||
## 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.
|
||||
|
||||
</deferred>
|
||||
|
||||
---
|
||||
|
||||
*Phase: 12-p-ytarium-api-collections-and-albums*
|
||||
*Context gathered: 2026-09-28*
|
||||
@@ -0,0 +1,66 @@
|
||||
# Phase 12: Płytarium API — Collections and Albums - Discussion Log
|
||||
|
||||
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
|
||||
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
|
||||
|
||||
**Date:** 2026-09-28
|
||||
**Phase:** 12-p-ytarium-api-collections-and-albums
|
||||
**Areas discussed:** Route boundary, Parity evidence, Invitations & mail, Search & security test
|
||||
|
||||
---
|
||||
|
||||
## Todo cross-reference
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Fold only if Discogs lands here | Decide with the route boundary | ✓ |
|
||||
| Leave for Phase 14 | Keep pending | |
|
||||
|
||||
**Outcome:** Discogs cover-price deferred → todo not folded.
|
||||
|
||||
## Route boundary
|
||||
|
||||
| Question | Options | Selected |
|
||||
|----------|---------|----------|
|
||||
| Reservations (wishlist-only in PHP) | Move to Phase 13 / Keep in 12 | Move to Phase 13 |
|
||||
| Discogs cover-price | Pull minimal Discogs in / Defer to 14 | Defer to 14 |
|
||||
| sync/stats/value/missing/bulk | All five / Only what Nuxt calls | All five |
|
||||
| match/apply-release/recognize/import | Phase 14 / Phase 12 | Phase 14 |
|
||||
| cover_urls on create | Seam wired in 14 / Port CoverImporter now | Port CoverImporter now |
|
||||
| Public share surface | Collection side in 12 / All public routes in 13 | All public routes in 13 |
|
||||
|
||||
**Notes:** API-01 and success criterion 1 need rewording; CoverImporter uses the existing fetchguard GET.
|
||||
|
||||
## Parity evidence
|
||||
|
||||
| Question | Options | Selected |
|
||||
|----------|---------|----------|
|
||||
| New recordings | Nuxt flows + error cases / Flows only / Existing only | Nuxt flows + error cases |
|
||||
| Centrifugo publications | Record for album flows / Reuse P11 | Record for album flows |
|
||||
| Token-group twins | Same handler, own fixtures / Route-table only | Same handler, own fixtures |
|
||||
| Uploads | Replay + blob assertions / Go tests only | Replay + blob assertions |
|
||||
|
||||
## Invitations & mail
|
||||
|
||||
| Question | Options | Selected |
|
||||
|----------|---------|----------|
|
||||
| Mail sending | River job in transaction / Inline after commit | River job |
|
||||
| Accept notification | Port write now / Seam until 13 | Port write now |
|
||||
| No-account invitations | Existing-account path only / Full path now | Existing-account path only |
|
||||
|
||||
## Search & security test
|
||||
|
||||
| Question | Options | Selected |
|
||||
|----------|---------|----------|
|
||||
| Re-gate shape | Mirror Scout exactly / Candidates then SQL paginate | Mirror Scout exactly |
|
||||
| SQL LIKE on Postgres | ILIKE escaped / Research decides | ILIKE escaped |
|
||||
| Leak test cases (multi) | Stale doc; Mis-scoped doc; Deleted/revoked; Token frozen | All four |
|
||||
| Typesense in tests | Fake engine driver / Real container | Fake engine driver |
|
||||
|
||||
## Claude's Discretion
|
||||
|
||||
Handler layout, DTO structs and fuzz enumeration, job kind names, fake engine shape, record-vs-Go-test for impractical error cases, plan count and split.
|
||||
|
||||
## Deferred Ideas
|
||||
|
||||
Reservations and public routes → Phase 13; Discogs/AI routes → Phase 14; register-via-invitation → Phase 13; notifications API → Phase 13.
|
||||
Reference in New Issue
Block a user