Files
summercms/.planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-CONTEXT.md
2026-10-02 18:17:03 +02:00

15 KiB

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:<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).

<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>

## 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