LAFU Sales Reports
Feedback

What should we build next?

Browse boards, vote on ideas, share your own.

MCP: update_project and update_changelog

Imported from GitHub pull request [El-SaMa/hollahoop#123](https://github.com/El-SaMa/hollahoop/pull/123) by @El-SaMa. --- ## Summary - New write-scope MCP tools `update_project` (name, tagline, description, accent_color, is_public; slug or id) and `update_changelog` (title, body_md, tag, publish; entry id or slug). - REST twins: `PATCH /api/v1/projects/:projectSlug` and `PATCH /api/v1/projects/:projectSlug/changelog/:entryId`. - OpenAPI, api-docs page, MCP doc seed table and server instructions updated. ## Behaviour (mirrors the dashboard actions) - `update_project`: same patch as `updateProjectAction`, restricted to the API key's org, then layout revalidation. The dashboard applies no plan limit or audit entry to this patch, so none added. - `update_changelog`: same rules as `updateChangelogAction`. The public-safety check (`publicPublishError`, same as `publish_changelog`) runs whenever the resulting entry is published; a blocked draft stays a draft with the guard's message. Publishing a draft sets `published_at`, clears any schedule and queues email notifications. `publish=false` moves a published entry back to draft (supported by the dashboard) and keeps the original timestamp. The slug never changes. Unknown entry returns `not_found`. Note: the server actions use cookie-auth Supabase, so they can't be called from API-key context; the tools reuse the shared helpers and the same rules instead. Webhook/SMS/voter-email fan-out that the dashboard does on publish is not included (`publish_changelog` doesn't do it either). The repo's default branch is `master` (there is no `main`), so this targets `master`. ## Tests typecheck, lint (0 errors) and full vitest suite (260 files, 2017 tests) pass. Added tests: visibility flip, cross-org refusal, publish-guard block stays draft, unpublish, unknown entry, REST PATCH routes, registry and OpenAPI. --- _Generated by [Claude Code](https://claude.ai/code/session_01U27k1v4zzKcPz598E3N1t2)_ <!-- hh:closed-by:https://github.com/El-SaMa/hollahoop/pull/123 --> Closed by [MCP: update_project and update_changelog](https://github.com/El-SaMa/hollahoop/pull/123)

FeatureShippedGeneral·3 days ago·0

Bump actions/setup-node from 4 to 7

Imported from GitHub pull request [El-SaMa/hollahoop#84](https://github.com/El-SaMa/hollahoop/pull/84) by @dependabot[bot]. --- Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 7. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/actions/setup-node/releases">actions/setup-node's releases</a>.</em></p> <blockquote> <h2>v7.0.0</h2> <h2>What's Changed</h2> <h3>Enhancements:</h3> <ul> <li>Add cache-primary-key and cache-matched-key as outputs by <a href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a> in <a href="https://redirect.github.com/actions/setup-node/pull/1577">actions/setup-node#1577</a></li> <li>Migrate to ESM and upgrade dependencies by <a href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a> in <a href="https://redirect.github.com/actions/setup-node/pull/1574">actions/setup-node#1574</a></li> </ul> <h3>Bug fixes:</h3> <ul> <li>Remove dummy NODE_AUTH_TOKEN export by <a href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a> in <a href="https://redirect.github.com/actions/setup-node/pull/1558">actions/setup-node#1558</a></li> <li>Only use <code>mirrorToken</code> in <code>getManifest</code> if it's provided by <a href="https://github.com/deiga"><code>@​deiga</code></a> in <a href="https://redirect.github.com/actions/setup-node/pull/1548">actions/setup-node#1548</a></li> </ul> <h3>Documentation updates:</h3> <ul> <li>Add documentation for publishing to npm with Trusted Publisher (OIDC) by <a href="https://github.com/chiranjib-swain"><code>@​chiranjib-swain</code></a> in <a href="https://redirect.github.com/actions/setup-node/pull/1536">actions/setup-node#1536</a></li> <li>docs: Update restore-only cache documentation by <a href="https://github.com/priya-kinthali"><code>@​priya-kinthali</code></a> in <a href="https://redirect.github.com/actions/setup-node/pull/1550">actions/setup-node#1550</a></li> <li>docs: Update caching recommendations to mitigate cache poisoning risks by <a href="https://github.com/chiranjib-swain"><code>@​chiranjib-swain</code></a> in <a href="https://redirect.github.com/actions/setup-node/pull/1567">actions/setup-node#1567</a></li> </ul> <h3>Dependency update:</h3> <ul> <li>Upgrade <code>@​actions/cache</code> to 5.1.0, log cache write denied by <a href="https://github.com/jasongin"><code>@​jasongin</code></a> in <a href="https://redirect.github.com/actions/setup-node/pull/1569">actions/setup-node#1569</a></li> </ul> <h2>New Contributors</h2> <ul> <li><a href="https://github.com/chiranjib-swain"><code>@​chiranjib-swain</code></a> made their first contribution in <a href="https://redirect.github.com/actions/setup-node/pull/1536">actions/setup-node#1536</a></li> <li><a href="https://github.com/deiga"><code>@​deiga</code></a> made their first contribution in <a href="https://redirect.github.com/actions/setup-node/pull/1548">actions/setup-node#1548</a></li> <li><a href="https://github.com/jasongin"><code>@​jasongin</code></a> made their first contribution in <a href="https://redirect.github.com/actions/setup-node/pull/1569">actions/setup-node#1569</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/actions/setup-node/compare/v6...v7.0.0">https://github.com/actions/setup-node/compare/v6...v7.0.0</a></p> <h2>v6.5.0</h2> <h2>What's Changed</h2> <ul> <li>Update <code>@​actions/cache</code> to 5.1.0 and add security overrides for undici and fast-xml-parser by <a href="https://github.com/HarithaVattikuti"><code>@​HarithaVattikuti</code></a> in <a href="https://redirect.github.com/actions/setup-node/pull/1579">actions/setup-node#1579</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/actions/setup-node/compare/v6.4.0...v6.5.0">https://github.com/actions/setup-node/compare/v6.4.0...v6.5.0</a></p> <h2>v6.4.0</h2> <h2>What's Changed</h2> <h3>Dependency updates:</h3> <ul> <li>Upgrade <a href="https://github.com/actions"><code>@​actions</code></a> dependencies by <a href="https://github.com/Copilot"><code>@​Copilot</code></a> in <a href="https://redirect.github.com/actions/setup-node/pull/1525">actions/setup-node#1525</a></li> <li>Update Node.js versions in versions.yml and bump package to v6.4.0 by <a href="https://github.com/priya-kinthali"><code>@​priya-kinthali</code></a> in <a href="https://redirect.github.com/actions/setup-node/pull/1533">actions/setup-node#1533</a></li> </ul> <h2>New Contributors</h2> <ul> <li><a href="https://github.com/Copilot"><code>@​Copilot</code></a> made their first contribution in <a href="https://redirect.github.com/actions/setup-node/pull/1525">actions/setup-node#1525</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/actions/setup-node/compare/v6...v6.4.0">https://github.com/actions/setup-node/compare/v6...v6.4.0</a></p> <h2>v6.3.0</h2> <h2>What's Changed</h2> <h3>Enhancements:</h3> <ul> <li>Support parsing <code>devEngines</code> field by <a href="https://github.com/susnux"><code>@​susnux</code></a> in <a href="https://redirect.github.com/actions/setup-node/pull/1283">actions/setup-node#1283</a></li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/actions/setup-node/commit/949feb2413d6458794dcd2491c4babbbce0c15c1"><code>949feb2</code></a> Bump eslint from 10.5.0 to 10.11.0 and package.json from 7.0.0 to 7.1.0 (<a href="https://redirect.github.com/actions/setup-node/issues/1635">#1635</a>)</li> <li><a href="https://github.com/actions/setup-node/commit/7207c4f1189c7ee83ddf20c7c68bd1af6fdd751c"><code>7207c4f</code></a> chore: update dependencies and add license files (<a href="https://redirect.github.com/actions/setup-node/issues/1642">#1642</a>)</li> <li><a href="https://github.com/actions/setup-node/commit/54337e0d02de9202d0948aaff36ad556375497b9"><code>54337e0</code></a> Bump eslint-plugin-n from 18.1.0 to 18.3.0 (<a href="https://redirect.github.com/actions/setup-node/issues/1638">#1638</a>)</li> <li><a href="https://github.com/actions/setup-node/commit/fb54fa8c14c63410cc752f0ae3b7b3ca6af161fc"><code>fb54fa8</code></a> Bump jest-each from 30.4.1 to 30.5.2 (<a href="https://redirect.github.com/actions/setup-node/issues/1636">#1636</a>)</li> <li><a href="https://github.com/actions/setup-node/commit/7f84f1d52d0c56afb45c62fee31a93b843cea4d6"><code>7f84f1d</code></a> Bump pnpm/action-setup from 6.0.10 to 6.1.0 (<a href="https://redirect.github.com/actions/setup-node/issues/1631">#1631</a>)</li> <li><a href="https://github.com/actions/setup-node/commit/0c54c7b07420dcba69a7d832587f55ebdf771c9e"><code>0c54c7b</code></a> Bump <code>@​typescript-eslint/parser</code> from 8.62.0 to 8.70.0 (<a href="https://redirect.github.com/actions/setup-node/issues/1615">#1615</a>)</li> <li><a href="https://github.com/actions/setup-node/commit/a8294190049a9c296f01b9a432aff41dd83d8284"><code>a829419</code></a> Bump <code>@​types/semver</code> from 7.7.1 to 7.8.0 (<a href="https://redirect.github.com/actions/setup-node/issues/1616">#1616</a>)</li> <li><a href="https://github.com/actions/setup-node/commit/680d1e489b82f1243de15bc7665c0007e3c9a860"><code>680d1e4</code></a> Support absolute paths in node-version-file (<a href="https://redirect.github.com/actions/setup-node/issues/1633">#1633</a>)</li> <li><a href="https://github.com/actions/setup-node/commit/db0fb3a98ed314b69026b6e8c9be26a7e2fe65a8"><code>db0fb3a</code></a> Bump baseline-browser-mapping from 2.10.38 to 2.11.22 (<a href="https://redirect.github.com/actions/setup-node/issues/1629">#1629</a>)</li> <li><a href="https://github.com/actions/setup-node/commit/94196ee1d15439c1b6651cd87ef14e88ec435966"><code>94196ee</code></a> Bump pnpm/action-setup from 6.0.9 to 6.0.10 (<a href="https://redirect.github.com/actions/setup-node/issues/1602">#1602</a>)</li> <li>Additional commits viewable in <a href="https://github.com/actions/setup-node/compare/v4...v7">compare view</a></li> </ul> </details> <br /> <!-- hh:closed-by:https://github.com/El-SaMa/hollahoop/pull/84 --> Closed by [Bump actions/setup-node from 4 to 7](https://github.com/El-SaMa/hollahoop/pull/84)

FeatureShippedGeneral·1 month ago·0

Huddle feature page with a live interactive demo

Imported from GitHub pull request [El-SaMa/hollahoop#124](https://github.com/El-SaMa/hollahoop/pull/124) by @El-SaMa. --- ## What A public feature page for Huddle at `/features/huddle`. Its hero is a live demo instead of a screenshot. **Demo** (`src/components/marketing/huddle-demo/`): a mini browser (traffic lights, tabs, `acme-bakery.com/menu`) with a made-up bakery site and the Huddle toolbar inside the frame. - Comment mode toggle. Click or tap anywhere to drop a numbered pin. Each pin is anchored to the region under the click (headline, button, menu card and so on) with percent coordinates, so it stays put on resize. - Popover thread with avatar, name, role and time. Post, reply, Resolve or Reopen. Resolved pins hide unless "Show resolved" is on. - A side list of pins (click to open), 3 seeded example pins from a client and a designer, and Reset demo. - State is client only: a pure reducer plus optional sessionStorage. No network calls and nothing touches the Huddle API. - Accessibility: pins are buttons with labels, a keyboard "Comment on ..." button per region, focus is trapped in the popover, Esc closes and returns focus, Enter posts (Shift+Enter adds a line), a live region announces actions, and motion is gated behind `motion-safe`. Logical CSS is used throughout, so the frame mirrors correctly under `dir="rtl"`. - Compact variant (frame only) now sits in a new Huddle section on the landing page. **Page sections:** how it works in 3 steps (script tag or WordPress plugin, guest link with no login, comments become posts and sync to GitHub), PDF and mockup review, guest links, privacy (safety scan, publish guard, Live switch), FAQ, and a sign-up CTA. **Wiring:** nav (Product menu), footer, sitemap, cache policy, custom-domain passthrough, and `features` added to reserved slugs. Metadata plus its own OG image: the OG card is now a shared `renderOgCard` helper, and the root OG uses it too. `Faq`, `Cta` and `Feature` take optional props, and their defaults are unchanged. **i18n:** marketing copy is English only in this repo (the 7-locale packs cover product UI), so no locale keys were added. ## Checks - vitest: 262 files, 2039 tests pass (new: reducer, popover placement, DOM helpers, component flow) - `tsc --noEmit`: exit 0 - `npm run lint`: 0 errors - `check-copy-dashes`, `check-tracked-secrets`, `check-client-facing-copy`, commit attribution check: pass - `fallow audit --base origin/master`: pass (only inherited findings remain) - axe (`test:a11y`) against a local `next start`: `/` and `/features/huddle` have 0 serious or critical violations, also with the popover open in light and dark. `/features/huddle` is added to the spec. <!-- hh:closed-by:https://github.com/El-SaMa/hollahoop/pull/124 --> Closed by [Huddle feature page with a live interactive demo](https://github.com/El-SaMa/hollahoop/pull/124)

FeatureShippedGeneral·3 days ago·0

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.

FeatureOpenGeneral·12 days ago·0

CataSync feeds 6/6: niletech go-live + stale Google Listings cleanup (operator OK)

Imported from GitHub issue [El-SaMa/oma#249](https://github.com/El-SaMa/oma/issues/249) by @El-SaMa. --- Part of the Product feeds epic. Last step. **Needs the operator's OK before each live change.** ## What Turn the feeds on for niletech and clear the stale Google Listings & Ads leftovers. ## Steps 1. Create on niletech (site `44fd1064-faae-4fd6-9998-18680bb64601`) via `oma.admin_catasync_feeds`: - `Google Merchant` (preset google_xml, destination google, merchant 684452171) - `hinta.fi`, `Hintaseuranta`, `Hintaopas (Prisjakt)` (their presets, destination url) Show the operator each feed's preview and skipped counts before enabling. 2. Google: dry run first, operator reviews the sample items, then the real run. Verify approved/disapproved counts after 24 h. 3. Comparison sites: give the operator the feed URLs and the merchant sign-up steps for each site (do not sign up or email anyone without the operator approving the exact text; outgoing mail follows the draft-before-send rule). 4. Cleanup (operator OK, read-only check first, backup of the rows to `/root/archive` on nilek): delete the stale Google Listings & Ads post meta on niletech (`_wc_gla_sync_status`, `_wc_gla_errors`, `_wc_gla_synced_at`, `_wc_gla_mc_status`, `_wc_gla_google_ids`, `_wc_gla_notification_status`, `_wc_gla_availabilityDate`) and the `gla_*` options, since the plugin is gone. Flush the object cache afterwards (see handbook niletech notes: Redis runs inside the site container). 5. The 165 published own spare parts with no description stay out of the feeds (requireDescription filter). List them for the operator; no AI text. ## Acceptance - [ ] Four feeds live on niletech, previews approved by the operator. - [ ] Google shows niletech products as approved (count reported), market sync returns benchmarks for many products. - [ ] Stale GLA meta removed with a backup, counts reported. - [ ] Handbook and `ops/sites/niletech/README.md` updated. ## 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.

FeatureOpenGeneral·12 days ago·0

CataSync feeds 2/6: engine shop catalog snapshot (shop_items) + sync

Imported from GitHub issue [El-SaMa/oma#245](https://github.com/El-SaMa/oma/issues/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`: ```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/catalog` with `callSite` (same as `adopt.mjs` uses `/inventory`), `per_page` 100. Incremental by default using `sites.catalog_cursor` as `modified_after`; **full** run every 24 h or when no cursor. Upsert rows (skip when `data_hash` unchanged except `seen_at`). On a full run, delete rows not seen in that run (product unpublished or deleted). Save the response `now` as the new cursor. - Map `catasync_id` -> `product_id` (validate it exists in `products`). - 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 with `push_enabled = 1` (incremental), daily full; route `POST /v1/sites/:id/catalog-sync {full}` (returns counts) and `GET /v1/sites/:id/catalog?q=&limit=&offset=` (rows for the UI preview). - Log events `catalog_sync` with 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.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.

FeatureOpenGeneral·12 days ago·0
HollahoopGet yours