docs(13): capture phase context
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
# Phase 13: Płytarium API — wishlist, notifications, CSV, credentials, public routes - Context
|
||||
|
||||
**Gathered:** 2026-10-02
|
||||
**Status:** Ready for planning
|
||||
|
||||
<domain>
|
||||
## Phase Boundary
|
||||
|
||||
Port the remaining core Płytarium API surface to `fonoteka.go` with byte-compatible request/response shapes: wishlist, notifications, CSV import session and export, per-user/org credentials CRUD, onboarding, invitation inspection and the anonymous public-share views (collection and wishlist) with their rate-limit buckets.
|
||||
|
||||
The manifest lists 62 pending routes in this area, every one with a recorded PHP fixture. After the decisions below, **58 are ported in this phase** and 4 move to Phase 14 (wishlist `match`/`apply-release`, `ai-credential/test`, `discogs-credential/test`). Breakdown of the 62: wishlist 36 (28 JWT, 5 personal-token group, 3 public), notifications 4, CSV 7 (6 import + export), credentials 9, onboarding/invitation/public collection 6.
|
||||
|
||||
Repos: `fonoteka.go`, plus an additive change to `sm-user-plugin` (mounted at `fonoteka.go/plugins/golem15/user`). `summercms.go` only if a framework gap turns up.
|
||||
|
||||
</domain>
|
||||
|
||||
<decisions>
|
||||
## Implementation Decisions
|
||||
|
||||
### Phase 13/14 boundary
|
||||
- **D-01:** `POST wishlist/albums/{id}/match` and `apply-release` (WishlistReleaseMatchController) move to Phase 14 with the Discogs client (INTG-01), next to the album match/apply-release routes. Their manifest entries stay `pending` (P6 D-15, no 501 shells). Success criterion 1 and API-03 are reworded at plan time.
|
||||
- **D-02:** Credentials CRUD lands in Phase 13: `GET/POST ai-credential`, `GET/POST/DELETE org-ai-credential`, `GET/POST discogs-credential` (including the `shared=true|false` mirror into the org credential, owner/admin only, else 403), encrypted storage, `FONOTEKA_AI_ORG_LOCK` org-lock and the PHP resolution order (AI: admin → backend Settings global model, then org-lock tier, user, org, else error; Discogs: admin → env `DISCOGS_TOKEN`, then user, org). The two live-call routes `ai-credential/test` and `discogs-credential/test` stay `pending` for Phase 14 (Discogs client and AI adapters). Success criterion 4 and API-06 are reworded at plan time.
|
||||
- **D-03:** All 7 CSV routes are ported in Phase 13 (`POST import/csv` 202 with throttle:10,1, `GET import/csv/{id}`, `PATCH .../mapping`, `PATCH .../rows/{rowId}`, `POST .../commit`, `POST .../cancel`, `GET export/csv` on both groups). `commit` (after the preview→importing compare-and-swap, idempotent 202 on replay) and non-canonical `mapping` enqueue real River jobs whose kinds, queues (`fonoteka.csv.import`, `fonoteka.csv.match`) and args mirror PHP's `AlbumCsvImportJob`/`AlbumCsvMatchJob`. The job bodies are JOBS-02 in Phase 14. `cancel` marks the job rows canceled as PHP does; `show` reads progress from the job table. — **Reversibility:** costly — the job kind names and args become the contract that Phase 14 workers consume and that queued rows in a live DB already carry.
|
||||
- **D-04:** Until Phase 14, no worker is registered for the CSV import/match kinds. The queued rows simply wait and `show` reports the state PHP reports before its worker picks the job up. No fake success, no stub worker.
|
||||
- **D-05:** The row-edit branch with `selected_discogs_id` (PHP calls `DiscogsClient::getRelease` inline) is held behind a narrow seam that Phase 14 fills. The rest of the row-edit route is ported. How that branch behaves in the meantime (and which recorded case stays pending) is decided at planning, with the constraint that it never fabricates release data.
|
||||
- **D-06:** Notifications: PHP has no prune route; pruning is the `fonoteka:prune-notifications` console command, already in Phase 14. Success criterion 2 and API-04 drop "prune" at plan time. The 4 routes (`notifications` capped at 50 newest-first, `unread-count`, `read-all`, `{id}/read`) are ported.
|
||||
|
||||
### Wishlist mail and digest
|
||||
- **D-07:** `wishlist/albums/{id}/purchase` keeps PHP's DB effects (move via `collection_id` update, `releaseForAlbum`, bell notifications, reserver notification) but subscriber mail is sent by a River job enqueued in the purchase transaction, sent only after commit (P12 D-13 pattern). PHP sends it inline; response bodies and DB state still match.
|
||||
- **D-08:** Adding a wishlist item writes bell notifications immediately and upserts the `wishlist_digest_queue` row exactly as `WishlistDigestQueue::enqueue` does (first enqueue in a window creates the row and enqueues the 1800 s delayed digest job; later ones bump `item_count`). The delayed River job is enqueued with the PHP-equivalent kind and args but has no worker until Phase 14, which adds the worker, the digest mail and the queue-row delete (JOBS-03).
|
||||
|
||||
### User plugin register hook (core plugin change, signed off)
|
||||
- **D-09:** `sm-user-plugin`'s `RegisterEvent` gains a generic `Payload map[string]any` carrying the raw register input, mirroring Winter's `golem15.user.register` payload. Additive and non-breaking: existing listeners compile unchanged, the user plugin learns nothing about invitations, and its README is updated in the same change. The user signed off on this core-plugin change on 2026-10-02. — **Reversibility:** costly — once other plugins read `Payload`, removing or reshaping it breaks them across every project using the user plugin.
|
||||
- **D-10:** fonoteka registers a listener that ports `Plugin.php:180`: hash `Payload["invitation_token"]`; if it matches a valid invitation for the user's email, upsert `PendingInvitationRegistration` (consumed later by the existing `guardPendingInvitation`); otherwise provision a collection.
|
||||
- **D-11:** `onboarding/bootstrap` registers the first user through the user plugin's exported registration path so password hashing, the register event and JWT issuance are identical to `/register`. If a needed function is not exported, it is exported additively (same sign-off as D-09). Bootstrap keeps PHP's validation (`org_name`, `email`, `password`), the cache-lock race guard and the 409 on replay.
|
||||
|
||||
### Parity evidence
|
||||
- **D-12:** New recordings against the isolated PHP instance with the Phase 2 `tide` capture rules (private 0600 vars, no live tokens in git):
|
||||
- `nuxt-wishlist`: create item → share show/update/regenerate → subscribe by token and by collection → reserve as a second user → reveal → purchase → settings → household → peer albums.
|
||||
- `nuxt-csv`: store → show → mapping → row edit → commit (to its 202 and the queued job row) → cancel, then CSV export on both groups. Jobs do not run (D-04).
|
||||
- `public-anonymous`: `public/{token}` and `public-wishlist/{token}` resolve, albums, album detail, a bad token, and the `pubfail:<ip>` anti-enumeration 429 after 10 failures.
|
||||
- `mcp-wishlist`: the MCP token-group wishlist CRUD (`fonoteka-mcp/src/client.ts:295-307`).
|
||||
- `onboarding`: status and bootstrap on an empty DB, then register with `invitation_token`.
|
||||
- Plus every distinct error status/body per route, with envelopes reproduced per endpoint (P7 D-13).
|
||||
- **D-13:** Side effects of purchase and item-added are verified by diffing recorded `notifications` and `wishlist_digest_queue` rows and the Centrifugo publications (P11 D-10 tooling, `timestamp`/`actor` normalised). Mail recipients, locale and template are asserted in Go tests through postcard's `memory` driver.
|
||||
|
||||
### Fixes without a decision (parity rule)
|
||||
- **D-14:** The public group's `resolve` routes (`public/{token}`, `public-wishlist/{token}`) get PHP's inline `throttle:10,1` only. `fonoteka-public-token` + `fonoteka-public-ip` apply only to the albums index/show routes. Currently `routes.go` applies both buckets at group level.
|
||||
- **D-15:** The manifest case for `GET invitations/{token}` expects 404 while its recorded fixture is 200 `{"data":{"state":"unavailable"}}`. The manifest is corrected to the fixture.
|
||||
|
||||
### Carried forward (locked earlier)
|
||||
- **C-01:** One handler per route, mounted on both groups where PHP mirrors it, with PHP's exact `->where()` constraints, inline throttles and exactly one `inv.scope:read|write` per token-group route (P12 D-01, D-10, D-26).
|
||||
- **C-02:** Error bodies as production PHP serves them (`APP_DEBUG=false`), including Winter HTML 404 pages where PHP throws `HttpException` (notifications `{id}/read`, reserve DELETE, reveal, subscriptions) (P12 D-21). CSV errors keep `{"result":"error","code","message"}`; public 404/429 keep `{"error":"Not found"}` / `{"error":"Too many requests"}`.
|
||||
- **C-03:** Response DTOs from ported `Serialize*` functions, `[]` not `null`, Carbon `+00:00`, pagination envelope without `links` (P5 D-08, P6 D-17).
|
||||
- **C-04:** Write paths go through ported fill boundaries; the request-DTO fuzz covers every write endpoint of this phase (P12 C-02).
|
||||
- **C-05:** Mail via River job in the write transaction; raw tokens in job args encrypted with the app key (P12 D-13, D-23).
|
||||
- **C-06:** Public tokens: 16 chars `[0-9a-zA-Z]`, `LOWER()` lookup plus constant-time compare, no route regex, `PublicShareHeaders` (`X-Robots-Tag: noindex, nofollow`, `Cache-Control: no-store, private`). public-wishlist mirrors public collection with `kind='wishlist'` and no reservations.
|
||||
- **C-07:** Unported routes stay `pending`; pending never counts as passing (P2, P6 D-15).
|
||||
|
||||
### Claude's Discretion
|
||||
- Whether the wishlist item-added branch extends the existing `albumAddedCallback` GORM hook or is an explicit write-service call, provided every create path (JWT, token-group POST, any bulk path) triggers it exactly once and only after commit.
|
||||
- Handler file layout and how the ~1,480 lines of wishlist controllers and the 578-line CSV controller are split across Go files.
|
||||
- Job kind names for purchase mail and digest (must be stable once chosen, see D-03 reversibility).
|
||||
- The interim behaviour of the `selected_discogs_id` row-edit branch (D-05), within its constraint.
|
||||
- Which error cases are recorded versus Go-tested with bodies from PHP source where recording is impractical. 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 public tokens, credentials encryption and authorization).
|
||||
|
||||
</decisions>
|
||||
|
||||
<canonical_refs>
|
||||
## Canonical References
|
||||
|
||||
**Downstream agents MUST read these before planning or implementing.**
|
||||
|
||||
### Contract source (PHP)
|
||||
- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/routes.php` — every Phase 13 route, group, middleware and throttle
|
||||
- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/Plugin.php` — `eloquent.created` wishlist listener (lines ~87-111), `golem15.user.register` listener (~180)
|
||||
- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/` controllers and classes: `NotificationApiController`, `Wishlist*Controller`, `CsvImportApiController`, `CsvExportApiController`, `AiCredentialController`, `OrgAiCredentialController`, `DiscogsCredentialController`, `OnboardingController`, `InvitationApiController@inspect`, `PublicShareApiController`, `PublicShareHeaders`; services `WishlistSubscriptionService`, `AlbumReservationService`, `ActiveWishlistResolver`, `WishlistProvisioner`, `CollectionShareService`, `NotificationService`, `WishlistDigestQueue`, `AiConfigResolver`, `DiscogsConfigResolver`, `AiGate`, `DiscogsGate`, `OrgAccess`, `classes/csv/*`, `config/fonoteka.php`
|
||||
- `/media/nvme/dev/golem15/fonoteka/vue-fonoteka-app/app/composables/` and `stores/fonoteka.ts`, `stores/auth.ts:374` — Nuxt callers; their requests define the contract
|
||||
- `fonoteka-mcp/src/client.ts:295-307` — MCP wishlist calls
|
||||
|
||||
### Go port
|
||||
- `../fonoteka.go/parity/manifest.yaml` and `../fonoteka.go/parity/fixtures/` — route status and recorded fixtures
|
||||
- `../fonoteka.go/plugins/golem15/fonoteka/routes.go` — empty onboarding/public_invitation/public groups (~207-212)
|
||||
- `../fonoteka.go/plugins/golem15/fonoteka/plugin.go` (~275) — rate buckets
|
||||
- `../fonoteka.go/plugins/golem15/user/classes/events.go` — `RegisterEvent`
|
||||
|
||||
### Planning
|
||||
- `.planning/ROADMAP.md` — Phase 13 and Phase 14 sections (criteria to reword per D-01, D-02, D-06)
|
||||
- `.planning/REQUIREMENTS.md` — API-03..API-07, JOBS-02, JOBS-03
|
||||
- `.planning/phases/12-p-ytarium-api-collections-and-albums/12-CONTEXT.md` — D-01, D-08, D-10, D-13, D-21, D-23, D-25, D-26 carried forward
|
||||
|
||||
</canonical_refs>
|
||||
|
||||
<code_context>
|
||||
## Existing Code Insights
|
||||
|
||||
### Reusable Assets
|
||||
- Models in `fonoteka.go/plugins/golem15/fonoteka/models/`: `notification`, `wishlist_subscription`, `wishlist_digest_queue`, `album_reservation`, `csv_import`, `csv_import_row`, the four credential models (`lagoon.Encrypted`, `json:"-"`), `pending_invitation_registration`.
|
||||
- `classes/notification_service.go`: `WriteNotification` (write + realtime publish), the five type constants, `NotifyAlbumAdded`, and `albumAddedCallback`, which currently returns early for wishlists. Purchase, reveal and digest paths are missing.
|
||||
- `classes/share_service.go`: token generation, enable/regenerate, `/w/` paths. `ResolvePublic` is missing.
|
||||
- `classes/gates.go`: `IsSiteAdmin`, `CanManageOrg`, `AIAllowed`, `aiOrgLock`, `ResolveDiscogsConfig`, `DiscogsAllowed`. No full AI config resolver yet.
|
||||
- `classes/credential_write_service.go`: only the `CredentialFillFields` allow-list.
|
||||
- `classes/active_collection.go`: `guardPendingInvitation` (consume side) and `ProvisionCollection`.
|
||||
- `middleware/public_share_headers.go`; `jobs.go` (invitation-mail job pattern to copy for purchase mail).
|
||||
|
||||
### Established Patterns
|
||||
- Mail as a River job enqueued in the write transaction, encrypted secrets in args (P12).
|
||||
- Route-table tests for group membership and `inv.scope` per route (P12 D-10).
|
||||
- Parity flows recorded under `parity/fixtures/nuxt/*.yaml`; currently only `notifications`, `unread-count`, `discogs-credential` and `onboarding/status` exist for this scope.
|
||||
|
||||
### Integration Points
|
||||
- `sm-user-plugin` `RegisterEvent` (D-09) and its registration service (D-11).
|
||||
- `schedule.go` already schedules `fonoteka:prune-notifications`, whose command ships in Phase 14.
|
||||
- Phase 14 workers consume the CSV and digest job kinds defined here (D-03, D-08).
|
||||
|
||||
</code_context>
|
||||
|
||||
<specifics>
|
||||
## Specific Ideas
|
||||
|
||||
- Interim honesty: queued jobs without workers are acceptable; fabricated results are not (D-04, D-05).
|
||||
- Reserve semantics: 422 `cannot_reserve_own_wishlist`, 409 `reservations_disabled`, 201 on success. Reveal is owner-only and one-way, bell only, a repeat is a no-op, no reservation returns `{"data":{"reserved":false}}`.
|
||||
- Onboarding `status` returns `needs_onboarding` true when there are zero users.
|
||||
|
||||
</specifics>
|
||||
|
||||
<deferred>
|
||||
## Deferred Ideas
|
||||
|
||||
- Wishlist `match`/`apply-release`, credential `/test` routes, CSV job workers, the `selected_discogs_id` row-edit branch, the digest worker and mail, and `fonoteka:prune-notifications` — all Phase 14.
|
||||
|
||||
### Reviewed Todos (not folded)
|
||||
- `redacting-slog-handler.md` — relevant to credentials but a framework logging concern; stays a standalone todo.
|
||||
- `backend-admin-api-tokens.md`, `refresh-fonoteka-readme.md`, `rewrite-summercms-readme.md`, `per-module-readmes-after-nest.md`, `readme-go-fences-src.md` — keyword matches only, unrelated to this phase's routes.
|
||||
|
||||
</deferred>
|
||||
|
||||
---
|
||||
|
||||
*Phase: 13-p-ytarium-api-wishlist-notifications-csv-credentials-public*
|
||||
*Context gathered: 2026-10-02*
|
||||
@@ -0,0 +1,58 @@
|
||||
# Phase 13: Płytarium API — wishlist, notifications, CSV, credentials, public routes - 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-10-02
|
||||
**Phase:** 13-p-ytarium-api-wishlist-notifications-csv-credentials-public
|
||||
**Areas discussed:** Phase 13/14 boundary, Wishlist mail & digest, User plugin register hook, Parity evidence
|
||||
|
||||
---
|
||||
|
||||
## Phase 13/14 boundary
|
||||
|
||||
| Question | Options | Selected |
|
||||
|----------|---------|----------|
|
||||
| Wishlist match/apply-release | Move to Phase 14 / Port with Discogs client in 13 | Move to Phase 14 |
|
||||
| Credential /test endpoints | CRUD in 13, /test in 14 / All in 13 behind an interface | CRUD in 13, /test in 14 |
|
||||
| CSV split | Routes in 13, job bodies in 14 / Whole CSV in 14 / Whole CSV in 13 | Routes in 13, job bodies in 14 |
|
||||
| Interim job behaviour | Register no worker; rows wait / You decide | Register no worker; rows wait |
|
||||
|
||||
---
|
||||
|
||||
## Wishlist mail & digest
|
||||
|
||||
| Question | Options | Selected |
|
||||
|----------|---------|----------|
|
||||
| Purchase mail | River job in the same tx / Inline like PHP | River job in the same tx |
|
||||
| Digest split | Bell + queue row in 13, job in 14 / Whole digest in 13 / Whole digest in 14 | Bell + queue row in 13, job in 14 |
|
||||
| Item-added trigger location | You decide / Extend GORM hook / Explicit service call | You decide |
|
||||
|
||||
---
|
||||
|
||||
## User plugin register hook
|
||||
|
||||
| Question | Options | Selected |
|
||||
|----------|---------|----------|
|
||||
| How invitation_token reaches fonoteka | Generic Payload field / Typed InvitationToken field / Fonoteka-side wrapper | Generic Payload field |
|
||||
| Approve additive sm-user-plugin change | Yes, additive only / No | Yes, additive only |
|
||||
| Bootstrap registration | Reuse user plugin's register service / You decide | Reuse user plugin's register service |
|
||||
|
||||
---
|
||||
|
||||
## Parity evidence
|
||||
|
||||
| Question | Options | Selected |
|
||||
|----------|---------|----------|
|
||||
| New flow recordings (multi) | nuxt-wishlist / nuxt-csv + export / public anonymous / mcp-wishlist + onboarding | All four |
|
||||
| Side-effect verification | DB + realtime goldens, mail in Go tests / Go tests only | DB + realtime goldens, mail in Go tests |
|
||||
|
||||
---
|
||||
|
||||
## Claude's Discretion
|
||||
|
||||
- Item-added trigger location (hook vs explicit call), handler file layout, job kind names, interim behaviour of the `selected_discogs_id` branch, recorded-vs-Go-tested error cases, plan split.
|
||||
|
||||
## Deferred Ideas
|
||||
|
||||
- Phase 14: wishlist match/apply-release, credential /test, CSV and digest workers, `selected_discogs_id` branch, prune-notifications command.
|
||||
Reference in New Issue
Block a user