19 KiB
Phase 14: Domain jobs and external integrations - Context
Gathered: 2026-10-03 Status: Ready for planning
## Phase BoundaryPhase 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-notificationsandfonoteka:reindex(asserts zerocollection_id-0 documents before and after, flag to drop the legacy index).fonoteka:oauth-clientalready 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.Clientin 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).
AI layer (sm-golem-plugin)
- D-01: The Go port of PHP
Golem15.Golemlives in its own shared core-plugin repo,git@git.golem15.com:golem15/sm-golem-plugin.git(created empty by the user). Modulegit.golem15.com/golem15/sm-golem-plugin, mounted in fonoteka.go atplugins/golem15/golemas a git submodule, following.planning/notes/core-plugins-own-repos.md. The plugin ID staysgolem15.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
modelsrepeater (name, adapter openai|anthropic, encryptedapi_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'sclasses.AdminVisionModelseam (Phase 13), so the admin tier ofResolveAIConfigand the site-admin branch ofAIAllowedlight 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/AIResponsevalue objects andPromptFactory. 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_urloverrides 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'sSSRFGuardand 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/recognizeon 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
ReleaseFetcherseam behind the CSV row-editselected_discogs_idpick gets the real DiscogsgetRelease+DiscogsMapper::mapReleaseimplementation, so a pick resolves exactly as in PHP. - D-09:
GET/DELETE oauth-identities(social login, deferred since Phase 7) andGET /api/v1/fonoteka/me(the minimal endpoint from Phase 8 D-20) are not part of Phase 14. They staypendingand 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 andcancelwrites lock the import row or compare-and-swap on its status, ascommitalready 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.phpand the Nuxt/MCP callers for any recorded case that is still missing, for example a successfulselected_discogs_idpick, 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). Modulegit.golem15.com/golem15/sm-feedback-plugin, mounted in fonoteka.go atplugins/golem15/feedback, plugin IDgolem15.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), theOPTIONS {any}204 preflight, and JWTPUT me/hidden.embed.jsis served at the same/plugins/golem15/feedback/assets/js/embed.jspath 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 thegolem15.user.getApiArrayhook that addsfeedback_widget_hiddento the user payload. The user plugin already references this field.SyncFeedbackToG15Officebecomes 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
httptestfake 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.phpand 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_modelstable with alagoon.Encryptedapi_key(json:"-"), an admin list/form, and a one-time importer from the PHP settings row. The PHP settings code isgolem_settings, notgolem15_golem_settings(this corrects D-01), and PHP storesmodels[].api_keyin plaintext. Feedback settings use the same pattern: a typed singleton table plus an importer, because cabana rejectscolorpicker/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/testanddiscogs-credential/testare re-recorded through it. - D-20: SSRF parity: Go ports PHP
SSRFGuardexactly. That covers the https requirement, the host allowlist (default.openai.complus the DALL-E blob host, overrideGOLEM15_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
WishlistDigestJoband its PHP mail view on the existing postcard mail pipeline. prune-notificationsandreindexoptions, output and exit codes: a straight port ofPruneNotifications.phpandReindexAlbums.php, on bonfire with the existingschedule.goentry.- 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 outboundhttp.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 ApparatusRequestSender. Its consumers are the Discogs client, the Golem adapters and feedback's G15OfficeClient. Framework change in summercms.go: updatemodules/fetchguard/README.mdanddocs/. - redacting-slog-handler (
.planning/todos/pending/redacting-slog-handler.md, medium): a frameworkslog.Handlerwrapper porting ApparatusRedactCredentialsTap. 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 givesSafeExceptionResponsebehaviour. This is the first phase to send user credentials to outside services.
<canonical_refs>
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.phplines 207-246, 443-505 — the routes in scope and their groups, scopes and throttlesplugins/golem15/fonoteka/jobs/{AlbumCsvImportJob,AlbumCsvMatchJob,WishlistDigestJob}.phpplugins/golem15/fonoteka/console/{PruneNotifications,ReindexAlbums}.phpplugins/golem15/fonoteka/classes/discogs/*— DiscogsClient, DiscogsRateLimiter, AlbumCoverFetcher, AlbumReleaseApplicator, DiscogsImportResolver, DiscogsInputParser, DiscogsMapper, PriceSuggestionResolver, ReleaseMatchScorer, exceptionsplugins/golem15/fonoteka/classes/{AlbumRecognitionService,RecognitionTruncatedException,AiDefaults,AiGate,DiscogsGate}.phpplugins/golem15/fonoteka/controllers/api/{AlbumReleaseMatchController,WishlistReleaseMatchController,RecognizeApiController,DiscogsImportController,AlbumCoverFetchController}.phpand the credentialtestactionsplugins/golem15/golem/—classes/services/AIService.php,classes/providers/*,classes/security/SSRFGuard.php,classes/valueobjects/*,classes/factories/PromptFactory.php,models/Settings.php,models/settings/fields.yamlplugins/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
</canonical_refs>
<code_context>
Existing Code Insights
Reusable Assets
classes/job_contract.goplus the Phase 13 enqueue sites (csv_import_service.godispatchesCsvMatchArgs/CsvImportArgs, andwishlist_notifications.godispatchesWishlistDigestArgsat 1800 s). The workers register against these exact kinds and args.jobs.goregisters the invitation and purchase-mail workers. New workers follow the same pattern, andjob_contract_worker_test.gois where they are tested.classes.ReleaseFetcherseam (Phase 13 D-05) andclasses.AdminVisionModelseam (Phase 13): replace their defaults.ResolveAIConfig/ResolveDiscogsConfig/AIAllowedare already ported tier by tier.cover_importer.goandimage_guard.goalready use fetchguard'sAllowHostsGET, which the host-locked cover fetch reuses.schedule.goalready schedulesfonoteka:prune-notifications, so only the command is missing.console/oauth_client.gois the bonfire command pattern for the new commands.search.goholds 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).
TestRouteTablePhase13asserts that the Phase 14 routes are absent, so update it when they land. - Secrets are
lagoon.Encryptedwithjson:"-". - 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.yamlandplugins.gen.gofor the two new submodule plugins. - The fonoteka plugin requires
golem15.golemfor recognition. Feedback hooks into the user plugin'sgetApiArrayevent.
</code_context>
## Specific Ideas- The user created both repos up front:
sm-golem-plugin(the user first created it assm-ai-pluginand then renamed it so it matches the PHP plugin name) andsm-feedback-plugin. - After Płytarium, a blog project (grzybyfunkcjonalne.pl or golem15.com) is ported next and needs the sitemap plugin 1:1.
- Sitemap plugin 1:1 port for the next blog project: a new todo is written alongside this context.
oauth-identitiesGET/DELETE (social login) andGET /api/v1/fonoteka/me: the routes staypendingwithout 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