CataSync feeds 2/6: engine shop catalog snapshot (shop_items) + sync
Imported from GitHub issue El-SaMa/oma#245 by @El-SaMa.
Part of the Product feeds epic. Needs issue "Connector 0.9.0 /catalog" live.
What
The engine keeps a snapshot of every published product on each connected shop, so feeds are built from what customers actually see (CataSync products and the shop's own products such as spare parts and AliExpress items).
Exact spec
- Migration
ops/catasync/engine/migrations/015_shop_items.sql:CREATE TABLE IF NOT EXISTS shop_items ( site_id CHAR(36) NOT NULL, remote_id BIGINT UNSIGNED NOT NULL, parent_id BIGINT UNSIGNED NULL, product_id CHAR(36) NULL, -- CataSync product when catasync_id matches data JSON NOT NULL, -- the connector row as returned data_hash CHAR(64) NOT NULL, seen_at DATETIME NOT NULL, modified_at DATETIME NULL, PRIMARY KEY (site_id, remote_id), KEY (site_id, seen_at) ); ALTER TABLE sites ADD COLUMN IF NOT EXISTS catalog_synced_at DATETIME NULL, ADD COLUMN IF NOT EXISTS catalog_cursor VARCHAR(40) NULL; - New module
ops/catasync/engine/src/catalog.mjs:syncShopCatalog(siteId, { full }): pages through/lafu-catasync/v1/catalogwithcallSite(same asadopt.mjsuses/inventory),per_page100. Incremental by default usingsites.catalog_cursorasmodified_after; full run every 24 h or when no cursor. Upsert rows (skip whendata_hashunchanged exceptseen_at). On a full run, delete rows not seen in that run (product unpublished or deleted). Save the responsenowas the new cursor.- Map
catasync_id->product_id(validate it exists inproducts). - Sleep 300 ms between pages so the shop is never hammered. Stop and log on connector errors; never throw out of the sweep.
server.mjs: hourly sweep for every site withpush_enabled = 1(incremental), daily full; routePOST /v1/sites/:id/catalog-sync {full}(returns counts) andGET /v1/sites/:id/catalog?q=&limit=&offset=(rows for the UI preview).- Log events
catalog_syncwith counts (seen, changed, removed, ms). - Tests in
test/for row upsert/hash/delete logic (pure functions, no network).
Acceptance
- Engine tests pass (
node --test test/*.test.mjs). - Rolled with
bash scripts/catasync-engine-roll.sh figaro; first full sync of maxfort, then niletech; counts in the engine log match the shop's published product + variation count (check with a DB count on the shop, read-only). - niletech front page TTFB unchanged during the sync (measure before/during).
- Handbook section "Shop catalog snapshot".
Binding rules (read before coding)
- Read
CLAUDE.mdand every.cursor/rules/*.mdcwithalwaysApply: truefirst. They win over your defaults. - No OpenAI / AI calls anywhere in this feature. CataSync AI switch (
ai_enabled) stays off. Never turn it on. - Smallest correct change. Match the surrounding code style (engine = plain ESM
.mjs, mysql2 pool fromsrc/db.mjs; core-api = TypeScript). - Every staff action goes through the admin MCP (
admin-mcp-first.mdc): executor inservices/core-api/src/agent-tools/tools-catasync.ts, confirm handler inservices/core-api/src/platform/confirm.tsfor writes, registry entry, zod schema inservices/mcp-server/src/tools.ts, test intools-catasync.test.ts. The UI calls the same tool (viaapps/web/lib/catasync/admin-actions.ts), never a separate path. - UI: vertical side nav only (never a second tab row), forms in modals, shadow cards, full width (
portal-chrome.mdc). - Every string:
next-intlkey with native en, fi, sv (Finnish must read native, not translated). No em/en dashes in prose. Runnode scripts/i18n-validate.mjs. - Handbook: update
apps/web/content/admin-docs/products/catasync.md(new section) for every behaviour you add. - Secrets (tokens, Google tokens, feed tokens) never in logs, commits, handbook or chat.
- Commits: author ELSAMA only, no Co-authored-by / AI attribution. Push to
mainright after each commit (rebase onorigin/mainfirst). - Deploy: engine with
bash scripts/catasync-engine-roll.sh figaro; OMA only via the trainbash scripts/deploy-quick.sh(never setOMA_DEPLOY_SERVICES, never build on the VM). Check the live SHA contains your commit. - WordPress plugin changes: bump version, pass
scripts/wp-plugin-gate-remote.sh ops/catasync/plugins/lafu-catasync-connectorwith 0 errors 0 warnings + security check (wp-plugin-dod.mdc). Install on maxfort.fi first (test shop), only then niletech. - Never change niletech prices, statuses, shipping rows or the Code Snippets / table-rate plugins as part of this work.
- Clean up test data and temp files when done.