From bbb804961ec8d7625d59a933d37b6431f9e4cfa1 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Fri, 2 Oct 2026 18:17:03 +0200 Subject: [PATCH] docs(13): capture phase context --- .../13-CONTEXT.md | 142 ++++++++++++++++++ .../13-DISCUSSION-LOG.md | 58 +++++++ 2 files changed, 200 insertions(+) create mode 100644 .planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-CONTEXT.md create mode 100644 .planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-DISCUSSION-LOG.md diff --git a/.planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-CONTEXT.md b/.planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-CONTEXT.md new file mode 100644 index 0000000..d5e70c5 --- /dev/null +++ b/.planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-CONTEXT.md @@ -0,0 +1,142 @@ +# Phase 13: Płytarium API — wishlist, notifications, CSV, credentials, public routes - Context + +**Gathered:** 2026-10-02 +**Status:** Ready for planning + + +## 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. + + + + +## 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:` 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). + + + + +## 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 + + + + +## 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). + + + + +## 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. + + + + +## 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. + + + +--- + +*Phase: 13-p-ytarium-api-wishlist-notifications-csv-credentials-public* +*Context gathered: 2026-10-02* diff --git a/.planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-DISCUSSION-LOG.md b/.planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-DISCUSSION-LOG.md new file mode 100644 index 0000000..6830e9c --- /dev/null +++ b/.planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-DISCUSSION-LOG.md @@ -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.