# Phase 14: Domain jobs and external integrations - Context **Gathered:** 2026-10-03 **Status:** Ready for planning ## Phase Boundary Phase 14 fills in the Płytarium work that talks to the outside world or runs in the background, on top of the Phase 11 jobs/realtime/search infrastructure and the Phase 13 API surface: - **Job workers** for the kinds Phase 13 already enqueues: CSV import write, CSV/Discogs match (240 s timeout, re-enqueues itself with a delay on a Discogs rate-limit error), and wishlist digest (30-minute coalescing window, digest mail, queue-row delete). - **Discogs client** (proactive rate threshold, bounded in-request wait budget, retry-after fallback, host-locked cover fetch) and every route that uses it. - **AI cover recognition** through a new shared `sm-golem-plugin` (Anthropic and OpenAI-compatible adapters, per-credential model/base-URL overrides, backend global vision model). - **Commands:** `fonoteka:prune-notifications` and `fonoteka:reindex` (asserts zero `collection_id`-0 documents before and after, flag to drop the legacy index). `fonoteka:oauth-client` already shipped in Phase 8. - **Feedback plugin**, ported in full to a new shared `sm-feedback-plugin`. - **Framework helpers** folded in from todos: a guarded outbound `http.Client` in fetchguard and a slog handler that redacts credentials. Routes that leave `pending` in this phase (12): `wishlist/albums/{id}/match`, `wishlist/albums/{id}/apply-release`, `ai-credential/test`, `discogs-credential/test`, `albums/match`, `albums/{id}/match`, `albums/{id}/apply-release`, `albums/recognize` (JWT group), `albums/import/discogs`, `/api/v1/fonoteka/albums/recognize` (token group, `inv.scope:ai`), `/api/v1/fonoteka/albums/{id}/cover-price/discogs` (token group, `inv.scope:write`, `throttle:12,1`), plus the `selected_discogs_id` success case of the CSV row-edit route. The feedback routes are new to the manifest. Out of scope: sitemap (dropped for Płytarium, D-14), `oauth-identities` GET/DELETE and `GET /api/v1/fonoteka/me` (D-09), the Golem face/CompreFace services (D-04). ## Implementation Decisions ### AI layer (sm-golem-plugin) - **D-01:** The Go port of PHP `Golem15.Golem` lives in its own shared core-plugin repo, `git@git.golem15.com:golem15/sm-golem-plugin.git` (created empty by the user). Module `git.golem15.com/golem15/sm-golem-plugin`, mounted in fonoteka.go at `plugins/golem15/golem` as a git submodule, following `.planning/notes/core-plugins-own-repos.md`. The plugin ID stays `golem15.golem`, with the same settings key (`golem15_golem_settings`) and lang namespace, so migrated PHP data loads as-is. — **Reversibility:** costly — the plugin ID, settings key and module path are what every consuming application and the migrated settings row use. - **D-02:** The provider adapters (Anthropic Messages, OpenAI-compatible chat completions) are hand-rolled JSON mappings over the guarded outbound client (folded todo fetchguard-guarded-http-client). No Anthropic or OpenAI SDK dependency. PHP's adapters are raw curl, which makes request parity easier. - **D-03:** Port the Golem Settings model in full: the `models` repeater (name, adapter openai|anthropic, encrypted `api_key`, `base_url`, `model`, `system_prompt`, `is_enabled`, `is_default`, `generates_images`, `accepts_images`, `has_files_endpoint`, `max_completion_tokens`) plus its admin settings screen, reading the existing settings row. `getVisionModel()` fills fonoteka's `classes.AdminVisionModel` seam (Phase 13), so the admin tier of `ResolveAIConfig` and the site-admin branch of `AIAllowed` light up. - **D-04:** Port all of AIService except faces: `send`, `sendStream`, `sendFile`/`sendFilePath` (files endpoint), `sendWithModel`, `sendToImageModel`, `sendToVisionModel`, `generateImage`, `ask`, the model system-prompt application, `Prompt`/`AIResponse` value objects and `PromptFactory`. CompreFace/FaceService and ChatContextCollector are not ported. The SSRFGuard is ported as part of the guarded-client work. - **D-05:** Base-URL guard: user and org credential `base_url` overrides always go through the private/reserved-IP dial guard. Admin-configured Settings models are trusted (they may point at a LAN/local endpoint such as Ollama). The researcher confirms this split against PHP's `SSRFGuard` and reports any difference before planning. - **D-06:** REQUIREMENTS INTG-02 ("Anthropic Go SDK") is reworded at plan time to "Anthropic and OpenAI-compatible adapters over the guarded client". PROJECT.md's repository paragraph gains sm-golem-plugin and sm-feedback-plugin, and drops feedback and sitemap from the application-plugin list. ### Route scope - **D-07:** All 6 album Discogs/AI routes that Phase 12 D-04 moved here are in scope (`albums/match`, `albums/{id}/match`, `albums/{id}/apply-release`, `albums/recognize` on the JWT and token groups, `albums/import/discogs`), together with the routes the criteria already name. ROADMAP SC4/SC5 are reworded at plan time to list them. They share ReleaseMatchScorer, AlbumReleaseApplicator, DiscogsImportResolver, DiscogsMapper and AlbumRecognitionService with the named routes. - **D-08:** The Phase 13 `ReleaseFetcher` seam behind the CSV row-edit `selected_discogs_id` pick gets the real Discogs `getRelease` + `DiscogsMapper::mapRelease` implementation, so a pick resolves exactly as in PHP. - **D-09:** `GET/DELETE oauth-identities` (social login, deferred since Phase 7) and `GET /api/v1/fonoteka/me` (the minimal endpoint from Phase 8 D-20) are not part of Phase 14. They stay `pending` and are flagged for the roadmap (see Deferred). - **D-10:** WR-02 from the Phase 13 review is fixed before the CSV workers go live: the `mapping`, row-edit and `cancel` writes lock the import row or compare-and-swap on its status, as `commit` already does, so none of them can race a commit and queue a second import job. Response shapes stay the same. This is a deliberate deviation from PHP, which has the same race. - **D-11:** The researcher checks `routes.php` and the Nuxt/MCP callers for any recorded case that is still missing, for example a successful `selected_discogs_id` pick, the cover-price route, and the recognize success and truncation paths. It records those against the PHP backend, together with their upstream exchanges (D-15). ### Feedback plugin (sm-feedback-plugin) - **D-12:** Feedback is a shared core plugin in its own repo, `git@git.golem15.com:golem15/sm-feedback-plugin.git` (created by the user). Module `git.golem15.com/golem15/sm-feedback-plugin`, mounted in fonoteka.go at `plugins/golem15/feedback`, plugin ID `golem15.feedback`, with the same tables (`feedback_submissions`, user preferences) and the same settings key. — **Reversibility:** costly — the plugin ID, tables and settings key become the contract other applications mount. - **D-13:** Full port. It covers `GET /_feedback/api/v1/{key}/config` (throttle 60/min/IP), `POST {key}/submit` (multipart, 10/min/IP, Origin allowlist gate, ImageContentGuard), the `OPTIONS {any}` 204 preflight, and JWT `PUT me/hidden`. `embed.js` is served at the same `/plugins/golem15/feedback/assets/js/embed.js` path that the Nuxt proxy expects. The plugin also ports the Settings model (enabled, widget_key, allow_hide, position, allowed_origins, colors, pl/en labels) with its admin screen, the submissions admin list, and the `golem15.user.getApiArray` hook that adds `feedback_widget_hidden` to the user payload. The user plugin already references this field. `SyncFeedbackToG15Office` becomes a River job on the guarded client (JSON + multipart). Feedback routes are recorded as new parity fixtures. API-08 is reworded to feedback only. - **D-14:** Sitemap is dropped for Płytarium. Nuxt builds its own sitemap with `@nuxtjs/seo`, and fonoteka registers no menu item types. A todo records a 1:1 sitemap plugin port for the next blog project (grzybyfunkcjonalne.pl or golem15.com). ### Vendor testing - **D-15:** Upstream HTTP in tests: the upstream Discogs/Anthropic/OpenAI/G15Office exchanges are recorded once as a sidecar to each parity case and served from an `httptest` fake that the guarded client points at during replay. Replay is deterministic and offline. The fake also asserts the request Go sends upstream (method, path, headers including User-Agent and auth, body) against PHP's recorded request. CI makes no live vendor calls, and G15Office is verified only through the fake. - **D-16:** Time is injected. The Discogs rate limiter, its wait budget and retry-after handling, and the match job's re-enqueue all take a clock/sleeper interface. Tests advance a fake clock without real sleeps, and assert the 240 s timeout and the re-enqueue delay as values. - **D-17:** The researcher reads `DiscogsRateLimiter.php` and decides where its state lives, either process memory or Postgres (an UNLOGGED table or an advisory lock), depending on whether PHP's cross-worker guarantee is still needed when one binary runs HTTP and River workers. The choice and its reason go in RESEARCH.md. ### Plan-time resolutions (2026-10-03, after research) - **D-18:** Golem models are stored in a dedicated `golem15_golem_models` table with a `lagoon.Encrypted` `api_key` (`json:"-"`), an admin list/form, and a one-time importer from the PHP settings row. The PHP settings code is `golem_settings`, not `golem15_golem_settings` (this corrects D-01), and PHP stores `models[].api_key` in plaintext. Feedback settings use the same pattern: a typed singleton table plus an importer, because cabana rejects `colorpicker`/`readOnly`. — **Reversibility:** costly — the table schema becomes the plugin contract. - **D-19:** Upstream sidecars (D-15) are captured with a recording HTTPS proxy in tide (`summer parity:upstream`). PHP runs through the proxy while cases are recorded, so the sidecars are the real exchanges. `ai-credential/test` and `discogs-credential/test` are re-recorded through it. - **D-20:** SSRF parity: Go ports PHP `SSRFGuard` exactly. That covers the https requirement, the host allowlist (default `.openai.com` plus the DALL-E blob host, override `GOLEM15_SSRF_ALLOWED_HOSTS`) and the uncaught-exception 500 page on guard failure. Go also adds the fetchguard dial-time private-IP guard on top. Admin Settings models stay trusted (D-05 confirmed). - **D-21:** Discogs rate-limiter state lives in Postgres: an UNLOGGED table plus one atomic `INSERT … ON CONFLICT DO UPDATE … WHERE … RETURNING`, with the clock passed in (resolves D-17, see RESEARCH). ### Claude's Discretion - Wishlist digest mail content, locale and template: a straight port of `WishlistDigestJob` and its PHP mail view on the existing postcard mail pipeline. - `prune-notifications` and `reindex` options, output and exit codes: a straight port of `PruneNotifications.php` and `ReindexAlbums.php`, on bonfire with the existing `schedule.go` entry. - Exact package layout inside sm-golem-plugin and sm-feedback-plugin, which follows `.planning/notes/plugin-layout-winter-directories.md`. - Plan split, subject to the lean-mode plan-count checkpoint. ### Folded Todos - **fetchguard-guarded-http-client** (`.planning/todos/pending/fetchguard-guarded-http-client.md`, high): extend fetchguard from a guarded HTTPS GET into a guarded outbound `http.Client`/constructor (POST, PUT, multipart upload, bearer auth), keeping the dial-time private/reserved-IP rejection, with an explicit trusted mode for admin-configured endpoints (D-05). It replaces Apparatus `RequestSender`. Its consumers are the Discogs client, the Golem adapters and feedback's G15OfficeClient. Framework change in summercms.go: update `modules/fetchguard/README.md` and `docs/`. - **redacting-slog-handler** (`.planning/todos/pending/redacting-slog-handler.md`, medium): a framework `slog.Handler` wrapper porting Apparatus `RedactCredentialsTap`. It redacts the keys api_key, apikey, authorization, bearer, password, secret, token, webhook_secret and admin_password (case-insensitive, nested groups) and scrubs messages with the PHP regex patterns. It also checks whether surf's error path already gives `SafeExceptionResponse` behaviour. This is the first phase to send user credentials to outside services. ## Canonical References **Downstream agents MUST read these before planning or implementing.** ### Planning - `.planning/ROADMAP.md` § Phase 14 — goal and SC1–SC6 (SC4/SC5/SC6 reworded per D-06, D-07, D-14) - `.planning/REQUIREMENTS.md` — JOBS-02, JOBS-03, SRCH-02, INTG-01, INTG-02 (reword per D-06), API-08 (reword per D-13/D-14), CLI-05 - `.planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-CONTEXT.md` — D-01..D-08: job kinds, args and queues; seams; route deferrals - `.planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-VERIFICATION.md` — WR-02 and the Phase 14 hand-off table - `.planning/phases/13-p-ytarium-api-wishlist-notifications-csv-credentials-public/13-REVIEW.md` — WR-02 detail - `.planning/phases/12-p-ytarium-api-collections-and-albums/12-CONTEXT.md` — D-04/D-05: album Discogs routes deferred, CoverImporter on fetchguard - `.planning/phases/11-jobs-realtime-and-search-infrastructure/11-CONTEXT.md` — conga/River, scheduling, search (reindex) decisions - `.planning/notes/core-plugins-own-repos.md` — sm-*-plugin repo, submodule and module-path workflow (D-01, D-12) - `.planning/notes/apparatus-dissolved-into-framework.md` — RequestSender and RedactCredentialsTap porting targets - `.planning/notes/plugin-layout-winter-directories.md` — plugin directory layout - `.planning/todos/pending/fetchguard-guarded-http-client.md`, `.planning/todos/pending/redacting-slog-handler.md` — folded todos ### PHP reference (Płytarium, `/media/nvme/dev/golem15/fonoteka`) - `plugins/golem15/fonoteka/routes.php` lines 207-246, 443-505 — the routes in scope and their groups, scopes and throttles - `plugins/golem15/fonoteka/jobs/{AlbumCsvImportJob,AlbumCsvMatchJob,WishlistDigestJob}.php` - `plugins/golem15/fonoteka/console/{PruneNotifications,ReindexAlbums}.php` - `plugins/golem15/fonoteka/classes/discogs/*` — DiscogsClient, DiscogsRateLimiter, AlbumCoverFetcher, AlbumReleaseApplicator, DiscogsImportResolver, DiscogsInputParser, DiscogsMapper, PriceSuggestionResolver, ReleaseMatchScorer, exceptions - `plugins/golem15/fonoteka/classes/{AlbumRecognitionService,RecognitionTruncatedException,AiDefaults,AiGate,DiscogsGate}.php` - `plugins/golem15/fonoteka/controllers/api/{AlbumReleaseMatchController,WishlistReleaseMatchController,RecognizeApiController,DiscogsImportController,AlbumCoverFetchController}.php` and the credential `test` actions - `plugins/golem15/golem/` — `classes/services/AIService.php`, `classes/providers/*`, `classes/security/SSRFGuard.php`, `classes/valueobjects/*`, `classes/factories/PromptFactory.php`, `models/Settings.php`, `models/settings/fields.yaml` - `plugins/golem15/feedback/` — `routes.php`, `controllers/api/*`, `jobs/SyncFeedbackToG15Office.php`, `classes/{G15OfficeClient,ImageContentGuard}.php`, `models/*`, `updates/*`, `assets/js/embed.js`, `Plugin.php` (getApiArray hook, settings) - `plugins/golem15/sitemap/` — reference only for the deferred todo (D-14) - `vue-fonoteka-app/nuxt.config.ts` (feedback proxy paths), `vue-fonoteka-app/app/stores/auth.ts` (`me/hidden`), `fonoteka-mcp` (recognize and cover-price callers) ### Go code - `../fonoteka.go/parity/manifest.yaml` — pending entries to flip - `../fonoteka.go/plugins/golem15/fonoteka/{jobs.go,schedule.go,search.go,routes.go}` - `../fonoteka.go/plugins/golem15/fonoteka/classes/{csv_import_service.go,job_contract.go,ai_config_resolver.go,gates.go,cover_importer.go,wishlist_notifications.go}` - `modules/fetchguard`, `modules/conga`, `modules/postcard`, `modules/bonfire` — framework modules this phase builds on ## Existing Code Insights ### Reusable Assets - `classes/job_contract.go` plus the Phase 13 enqueue sites (`csv_import_service.go` dispatches `CsvMatchArgs`/`CsvImportArgs`, and `wishlist_notifications.go` dispatches `WishlistDigestArgs` at 1800 s). The workers register against these exact kinds and args. - `jobs.go` registers the invitation and purchase-mail workers. New workers follow the same pattern, and `job_contract_worker_test.go` is where they are tested. - `classes.ReleaseFetcher` seam (Phase 13 D-05) and `classes.AdminVisionModel` seam (Phase 13): replace their defaults. - `ResolveAIConfig` / `ResolveDiscogsConfig` / `AIAllowed` are already ported tier by tier. - `cover_importer.go` and `image_guard.go` already use fetchguard's `AllowHosts` GET, which the host-locked cover fetch reuses. - `schedule.go` already schedules `fonoteka:prune-notifications`, so only the command is missing. - `console/oauth_client.go` is the bonfire command pattern for the new commands. - `search.go` holds the Typesense collection setup that the reindex command builds on. ### Established Patterns - Pending manifest routes are absent from the router: no 501 shells (P6 D-15). `TestRouteTablePhase13` asserts that the Phase 14 routes are absent, so update it when they land. - Secrets are `lagoon.Encrypted` with `json:"-"`. - Core plugins are developed inside their submodule checkout: commit and push there first, then bump the pointer in fonoteka.go as a separate commit. - Framework README/docs rules from CLAUDE.md apply to fetchguard and the new slog handler. ### Integration Points - fonoteka.go `go.work`, `go.mod` (require + local replace), `summer.yaml` and `plugins.gen.go` for the two new submodule plugins. - The fonoteka plugin requires `golem15.golem` for recognition. Feedback hooks into the user plugin's `getApiArray` event. ## Specific Ideas - The user created both repos up front: `sm-golem-plugin` (the user first created it as `sm-ai-plugin` and then renamed it so it matches the PHP plugin name) and `sm-feedback-plugin`. - After Płytarium, a blog project (grzybyfunkcjonalne.pl or golem15.com) is ported next and needs the sitemap plugin 1:1. ## Deferred Ideas - **Sitemap plugin 1:1 port** for the next blog project: a new todo is written alongside this context. - **`oauth-identities` GET/DELETE (social login) and `GET /api/v1/fonoteka/me`**: the routes stay `pending` without a phase. The roadmap needs a home for them before Phase 15, whose criterion is all routes green. A new todo is written. - Golem face detection (CompreFace/FaceService) and ChatContextCollector: not ported until a consumer needs them. ### Reviewed Todos (not folded) - `backend-admin-api-tokens`, `bonfire-duplicate-command-names`, `lagoon-readme-after-commit-callback-order`, `nest-framework-packages-under-modules`, `per-module-readmes-after-nest`, `readme-go-fences-src`, `refresh-fonoteka-readme`, `rewrite-summercms-readme`, `scaffold-*` (3), `wristband-neutral-resource-default`, `2026-10-01-benchmark-…`: these matched on keywords only and are unrelated to the phase domain. --- *Phase: 14-domain-jobs-and-external-integrations* *Context gathered: 2026-10-03*