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

CataSync feeds 3/6: engine named feeds, field repeater, presets, XML/CSV builder

Imported from GitHub issue [El-SaMa/oma#246](https://github.com/El-SaMa/oma/issues/246) by @El-SaMa. --- Part of the Product feeds epic. Needs "Shop catalog snapshot" live. ## What Any number of **named feeds per shop**, each fully customizable (fields repeater, filters, format). Example set on niletech: `hinta.fi`, `Hintaseuranta`, `Hintaopas (Prisjakt)`, `Google Merchant`. Feeds can be created from a preset, duplicated, edited, paused and deleted. ## Data model Migration `ops/catasync/engine/migrations/016_feeds.sql`: ```sql CREATE TABLE IF NOT EXISTS feeds ( id CHAR(36) PRIMARY KEY, site_id CHAR(36) NOT NULL, name VARCHAR(80) NOT NULL, destination ENUM('url','google') NOT NULL DEFAULT 'url', format ENUM('xml','csv') NOT NULL DEFAULT 'xml', preset VARCHAR(40) NULL, token_hash CHAR(64) NOT NULL, -- sha256 of the public token; the token itself is shown once enabled TINYINT(1) NOT NULL DEFAULT 1, settings JSON NOT NULL, fields JSON NOT NULL, -- ordered repeater rows filters JSON NOT NULL, built_at DATETIME NULL, item_count INT NULL, build_ms INT NULL, last_error VARCHAR(300) NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY (site_id, name) ); ``` - `settings` XML: `{ rootTag, itemTag, namespaces: {prefix: uri}, wrapper: "rss" | "plain" }`. CSV: `{ delimiter: "," | ";" | "\t", header: true }`. Both: `backorderDays` (default **24**), `idPrefix` (default empty), `includeVariations` (default true), `shipping: { "wave-products": 9.90, "default": 6.21, "freeFrom": 100, "freeExcludeClasses": ["wave-products"] }` (editable per feed). - `fields` row: `{ name, kind: "source" | "text" | "template", source?, value?, transform?: "none" | "strip_html" | "truncate" | "upper" | "lower" | "price_number" | "price_with_currency" | "yes_no" | "date_iso", maxLength?, required: boolean, cdata?: boolean }`. Max 60 rows. `name` may carry a namespace prefix (`g:price`) for XML; for CSV it is the column header. When `source` is `gallery` the row repeats once per image (for `g:additional_image_link`). - `filters`: `{ stock: ["instock","onbackorder"], requireImage: true, requireDescription: true, minPrice?, maxPrice?, categoriesInclude?: string[], categoriesExclude?: string[], shippingClassesExclude?: string[], catasyncOnly: false, readyOnly: false }`. ## Sources available to rows - From `shop_items.data`: every connector field (`id, sku, ean, mpn, name, short_description, description, permalink, image, gallery, regular_price, sale_price, price, currency, stock_status, stock_quantity, weight, shipping_class, categories, brand, energy_class, lead_text, item_group_id`) plus `category_path_last`. - Computed: - `availability` (Google words `in_stock` / `out_of_stock` / `backorder`), `availability_fi` (`Varastossa` / `Tilaustuote` / `Loppu`) - `availability_date`: today + `backorderDays` for backorder (ISO 8601), else empty. Computed at build time, never stored. - `delivery_days`: instock `1-3` (no class, f9-products, also-products), `5-10` (wave-products); backorder `14-24` - `shipping_price`: from `settings.shipping` by shipping class; 0 when price >= `freeFrom` and the class is not in `freeExcludeClasses` - `condition` (`new`), `supplier` (CataSync offer supplier or `own`), `id_with_prefix` - Templates use `{source}` placeholders, e.g. `{brand} {name}`. ## Builder - New module `ops/catasync/engine/src/feedsout.mjs` (not the supplier `feeds.mjs`): `buildFeed(feedId) -> { contentType, body, count, skipped: { reason: n } }`. Streams rows from `shop_items` for the site, applies filters, rows and transforms. XML: proper escaping (or CDATA when the row says so), UTF-8, invalid control chars removed. Items missing a `required` field are skipped and counted. - Cache the built body on disk at `/var/lib/catasync/feeds/<feedId>.xml|csv` (create the dir in `install.sh`, owned by the service user). Rebuild after each catalog sync that changed the site's data, and on feed edit. Serve the cached file. - Presets in `ops/catasync/engine/src/feedpresets.mjs`, all editable after creation: - `google_xml`: RSS 2.0 with `xmlns:g="http://base.google.com/ns/1.0"`, rows `g:id, title, description, link, g:image_link, g:additional_image_link (gallery), g:availability, g:availability_date, g:price, g:sale_price, g:brand, g:gtin, g:mpn, g:condition, g:item_group_id, g:product_type, g:shipping_weight, g:shipping (country FI + price)`; filters require image and description. - `hinta_fi`, `hintaseuranta`, `hintaopas_prisjakt`: start from the Google XML layout plus `delivery_days` and `shipping_price` rows. These sites generally accept Google-format feeds, but **check each site's own merchant feed spec before go-live** (merchant pages / ask their support) and adjust the preset; note the result in the handbook. - `csv_basic`: `id;name;price;url;image;ean;brand;availability;delivery_days;shipping_price`, delimiter `;`. - `empty`: no rows. - Routes in `server.mjs`: - `GET /v1/sites/:id/feeds` - `POST /v1/sites/:id/feeds` (create from preset or a body; returns the **token once**) - `POST /v1/feeds/:id` (update), `POST /v1/feeds/:id/duplicate` (new name, new token once), `POST /v1/feeds/:id/rotate-token` (token once), `DELETE /v1/feeds/:id`, `POST /v1/feeds/:id/rebuild` - `GET /v1/feeds/:id/preview?limit=5` (rendered sample + skipped counts) - `GET /v1/feeds/by-token/:token` (sha256 lookup, enabled feeds only, returns the cached file and content type; 404 otherwise) - Error codes: `feed_name_taken`, `feed_fields_invalid`, `feed_too_many_fields`, `feed_not_found`, `feed_disabled`, `feed_preset_unknown`. Add them to `KNOWN_ERRORS` in `services/core-api/src/catasync/engine.ts` in issue 4. - Tests (pure functions): XML escaping, CDATA, gallery repeat, templates, transforms, filters, required skip, CSV quoting, availability date. ## Acceptance - [ ] Engine tests pass (`node --test test/*.test.mjs`); engine rolled. - [ ] On maxfort: one feed from each preset; preview rows correct; XML passes `xmllint --noout`; CSV opens in a spreadsheet. - [ ] Handbook section "Product feeds" (model, presets, sources list, filters, tokens). ## 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 4/6: OMA public feed URL, staff MCP tool, Feeds UI with field repeater

Imported from GitHub issue [El-SaMa/oma#247](https://github.com/El-SaMa/oma/issues/247) by @El-SaMa. --- Part of the Product feeds epic. Needs "Feeds model and builder" live in the engine. ## What Staff manage feeds in OMA; comparison sites (hinta.fi, Hintaseuranta, Hintaopas/Prisjakt, ...) fetch them from a public OMA URL. ## Public URL - Next route `apps/web/app/feeds/[token]/route.ts`, served as `https://oma.lanetti.fi/feeds/<token>.xml` (or `.csv`). It calls core-api, which calls engine `GET /v1/feeds/by-token/:token`. Stream the body with the engine's content type, `Cache-Control: public, max-age=900`. - Unknown or disabled token: plain 404 with no detail. Rate limit per IP with the limiter already used for public routes. Never redirect; never expose internal hosts. Run `scripts/public-smoke.mjs` after deploy. - Tokens: 32 random bytes, base64url. Only the sha256 is stored. Shown once on create, duplicate and rotate, with a copy button. ## Staff MCP tool - `oma.admin_catasync_feeds` with `action`: `list` (siteId), `get` (feedId), `preview` (feedId, limit), `create` (siteId, name, preset or full definition), `update`, `duplicate`, `rotate_token`, `delete`, `rebuild`. - Writes return a proposal + confirmToken and apply via `oma.confirm_tool` (handler in `services/core-api/src/platform/confirm.ts`). Engine client methods + types in `services/core-api/src/catasync/engine.ts`. Zod schema in `services/mcp-server/src/tools.ts`. Register in the tool registry like the other `oma.admin_catasync_*` tools. - Tests in `services/core-api/src/agent-tools/tools-catasync.test.ts`: validation, proposal shape, token only returned by create/duplicate/rotate confirms. ## UI (`apps/web/components/catasync/`) - Shops > (shop) > new side-nav item **Feeds** (vertical side nav, never a second tab row). - Feed list as shadow cards: name, destination (URL / Google), format, item count, last built, skipped count, enabled toggle; buttons Edit, Duplicate, Copy URL, Rotate link, Delete (confirm modal). - **New feed** modal: name, preset (Google XML, hinta.fi, Hintaseuranta, Hintaopas (Prisjakt), CSV basic, Empty), destination, format. - **Edit feed** modal (wide): - settings and filters sections - the **fields repeater**: one row per field with drag handle (reorder), field name, kind (Source / Fixed text / Template), source select or value input, transform, max length, required, CDATA (XML only), remove; an "Add field" button - a live **preview** panel: first 5 items + skipped counts by reason, refreshed on save - Error codes mapped in `catasync-shared.ts`; every string en/fi/sv native. ## Acceptance - [ ] Every `oma.admin_catasync_feeds` action called live through the MCP after deploy, on maxfort. - [ ] Public URL fetched from outside the LAN (a non-VPN host) returns the feed; a wrong token returns 404. - [ ] Public smoke passes after deploy. - [ ] Handbook: making a feed, presets, giving the URL to a comparison site, rotating a link. ## 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 5/6: Google Merchant Center destination (Merchant API v1)

Imported from GitHub issue [El-SaMa/oma#248](https://github.com/El-SaMa/oma/issues/248) by @El-SaMa. --- Part of the Product feeds epic. Needs the Feeds UI/MCP issue live. ## What A feed with `destination = google` is sent straight to Google Merchant Center over **Merchant API v1** (no plugin on the shop). This replaces the removed Google Listings & Ads plugin on niletech and gives CataSync many more market prices. ## Where the code goes - core-api, because the Google token lives there: new `services/core-api/src/catasync/merchant-feed.ts`. Reuse `gfetch`, `InsightsGoogleError`, `merchantNeedsRegistration` / `merchantRegisterGcp` from `services/core-api/src/insights/google.ts` and the account lookup in `services/core-api/src/catasync/market.ts` (connected account with merchant coverage). - Items come from the engine: new route `GET /v1/feeds/:id/items?cursor=&limit=500` returning JSON items rendered with the feed's rows **as Google ProductInput attributes** (not XML) plus a per-item `hash`. Engine table `feed_sent (feed_id, offer_id, hash, sent_at, PRIMARY KEY (feed_id, offer_id))` for change detection (migration `017_feed_sent.sql`); route `POST /v1/feeds/:id/sent` to record results. ## Exact behaviour 1. **Data source**: once per feed, create an API primary data source (`POST https://merchantapi.googleapis.com/datasources/v1/accounts/{merchantId}/dataSources`, `primaryProductDataSource` with `contentLanguage: "fi"`, `feedLabel: "FI"`, `countries: ["FI"]`, displayName `CataSync <feed name>`). Store its name in the feed `settings.googleDataSource`. Never touch other data sources. 2. **Insert/update**: `POST https://merchantapi.googleapis.com/products/v1/accounts/{merchantId}/productInputs:insert?dataSource=<name>` with `offerId` (feed `g:id`, default `gla_<shop id>` so old Google history and the market sync's `gla_<id>` mapping keep working), `contentLanguage: "fi"`, `feedLabel: "FI"`, `productAttributes` (title, description, link, imageLink, additionalImageLinks, availability, availabilityDate for backorder = today + 24 days, price {amountMicros, currencyCode}, salePrice, brand, gtins, mpn, condition, itemGroupId, productTypes, shippingWeight, shipping [{country: "FI", price}]). - Send only items whose hash changed **or** that were last sent more than **20 days** ago (Google expires products after 30 days without an update). - Pace: max 5 requests/second, retry 429/5xx with backoff, stop the run on 401/403 and record `last_error`. 3. **Delete**: items that left the feed (unpublished, filtered out, deleted on the shop): `DELETE .../productInputs/{contentLanguage}~{feedLabel}~{offerId}?dataSource=<name>`; remove from `feed_sent`. 4. **Schedule**: an OMA worker job (in `services/workers`, same pattern as other periodic jobs) every 3 hours per enabled Google feed; plus `rebuild` from the MCP tool triggers a run. 5. **Statuses**: daily, read item issues via the Reports API (`product_view` with `id, offer_id, aggregated_reporting_context_status, item_issues`) and store counts per feed (approved, pending, disapproved, top issues) for the UI card and MCP `get`. 6. **Shipping and returns**: do **not** change Merchant Center account shipping settings automatically. Shipping goes per item (`shipping` attribute from `shipping_price`). Note in the handbook how to set account-level shipping by hand if wanted. 7. After a run, call the existing market price sync (`syncCatasyncMarketPrices`) for that site. ## MCP / UI - `oma.admin_catasync_feeds` gains `action: "google_sync"` (confirm) and Google status fields in `get`. - Feed card shows: sent, pending, approved, disapproved, top 5 issues with counts, last run, last error. - Error codes: `google_api_error` (already exists, keep status/reason/message), `merchant_not_set`, `google_not_connected`. ## Acceptance - [ ] Unit tests for attribute mapping (micros, availability date, gallery, shipping) and change detection. - [ ] Live on **maxfort first** if maxfort has a Merchant Center account; otherwise a dry-run mode (`dryRun: true` in the MCP action: builds and validates items, sends nothing) used on niletech first, output reviewed by the operator. - [ ] Then niletech real run (operator OK in issue 6), counts verified with the Reports API, and `oma.admin_catasync_market sync` returns many more benchmarks than 2 within a few days. - [ ] Handbook section "Google Merchant feed". ## 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