CataSync feeds 1/6: connector 0.9.0 read endpoint /catalog
Imported from GitHub issue [El-SaMa/oma#244](https://github.com/El-SaMa/oma/issues/244) by @El-SaMa.
---
Part of the Product feeds epic. Build first.
## What
Add a signed, read-only REST route `POST /wp-json/lafu-catasync/v1/catalog` to `ops/catasync/plugins/lafu-catasync-connector` that returns **published** products with everything a feed needs. Version **0.9.0**.
## Exact spec
- Register in `includes/class-lafu-catasync-connector-rest.php` next to `/inventory`, same `permission_callback` (`Lafu_Catasync_Connector_Auth::verify`), same WooCommerce-missing guard.
- Params: `page` (>=1), `per_page` (1..100, default 100), optional `modified_after` (ISO 8601 UTC; return only products with `date_modified` after it).
- Implement `Lafu_Catasync_Connector_Products::catalog( $page, $per_page, $modified_after )` in `class-lafu-catasync-connector-products.php` using `wc_get_products` with `status => publish`, `type => simple, variable`, `orderby => ID`, `order => ASC`, `paginate => true`, plus `date_modified => '>' . timestamp` when given.
- Variable products: one row per **purchasable, published variation** (with `item_group_id` = parent id) plus no row for the parent. Simple: one row.
- Row fields (all strings or null, never PHP notices):
`id`, `parent_id`, `sku`, `ean` (`get_global_unique_id`), `mpn` (meta `_lafu_catasync_mpn` if present else null), `name`, `short_description` and `description` (raw HTML, `edit` context), `permalink` (`get_permalink`), `image` (main image URL), `gallery` (array of URLs, max 10), `regular_price`, `sale_price`, `price` (`wc_get_price_including_tax` with the shop's tax display, 2 decimals), `currency` (`get_woocommerce_currency`), `stock_status`, `stock_quantity`, `backorders`, `weight`, `shipping_class` (slug or null), `categories` (array of full paths like `Tietokoneet > Näytönohjaimet`), `brand` (first `product_brand` term name or null), `energy_class` (meta `_lafu_eu_energy_class`), `lead_text` (meta `_lafu_catasync_lead_text`), `catasync_id` (meta `_lafu_catasync_id` or null), `date_modified` (ISO UTC).
- Response: `{ items: [...], total, pages, now }` where `now` is the server time ISO UTC (engine uses it as the next `modified_after`).
- Performance: prime caches (`_prime_post_caches`, `update_meta_cache`) for the page; no per-row `get_terms` loops beyond what WooCommerce caches. A 100-row page must answer in under 2 s on niletech.
- `readme.txt` changelog + `Stable tag` + plugin header + `LAFU_CATASYNC_CONNECTOR_VERSION` all 0.9.0.
## Not in scope
No writes, no settings page changes, no Google code in the plugin.
## Acceptance
- [ ] Gate `scripts/wp-plugin-gate-remote.sh ops/catasync/plugins/lafu-catasync-connector` = 0 errors 0 warnings; security check written in the PR/commit notes (auth, escaping not needed for JSON, no unbounded queries).
- [ ] Installed on **maxfort.fi** first; signed call returns rows for simple and variable products, `modified_after` filters correctly, timing logged.
- [ ] Then installed on niletech the same way earlier versions were (see `ops/catasync/README` / handbook CataSync section "Connector"); 100-row page < 2 s measured.
- [ ] Handbook line added (connector routes list).
## Binding rules (read before coding)
- Read `CLAUDE.md` and every `.cursor/rules/*.mdc` with `alwaysApply: true` first. 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 from `src/db.mjs`; core-api = TypeScript).
- Every staff action goes through the admin MCP (`admin-mcp-first.mdc`): executor in `services/core-api/src/agent-tools/tools-catasync.ts`, confirm handler in `services/core-api/src/platform/confirm.ts` for writes, registry entry, zod schema in `services/mcp-server/src/tools.ts`, test in `tools-catasync.test.ts`. The UI calls the same tool (via `apps/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-intl` key with native **en, fi, sv** (Finnish must read native, not translated). No em/en dashes in prose. Run `node 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 `main` right after each commit (rebase on `origin/main` first).
- Deploy: engine with `bash scripts/catasync-engine-roll.sh figaro`; OMA only via the train `bash scripts/deploy-quick.sh` (never set `OMA_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-connector` with **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.