From ec4f82c5af13b63a04c3f6f5e0748a84c74d5a14 Mon Sep 17 00:00:00 2001 From: Justinvolved <64196420+Justinvolved@users.noreply.github.com> Date: Fri, 3 Jul 2026 18:23:07 +0200 Subject: [PATCH 1/3] Fold brand-build field gotchas into the demo corpus (4.1.0) Second batch of live-verified findings from the autonomous partner-simulation build (fresh DW 10.27.4, skills followed verbatim), now through brand re-content, MCP catalog authoring, feature-pack install, and a headless storefront on a second backend. Nine folds across four skills: - Autonomous JSON-RPC MCP transport fallback (dw-demo-base mcp-setup.md) - Root / binding via Area.AreaDomain+AreaFrontpage, no AreaDns on 10.27.x (dw-demo-swift) - Area/style/item-type restart semantics + nav-label-is-data (cache-invalidation.md) - Index-instance Warning benign (dw-demo-pim canonical-setup-order Step 16) - Isolated pack-fragment staging (dw-demo-swift pack-activation.md) - MCP recipe gotchas batch (pim-modelling, search-indexing, extend-mcp-tools, content-modelling) - Product-completeness checklist (dw-demo-pim canonical-setup-order) - Headless drift notes: version-dependent status, dual image keys, env-configurable repo/query names, empty area ecom bindings (dw-demo-headless) Minor bump (substantial new guidance across 4 skills). Stacks on PR #44 (4.0.2). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01BpTYGpMUm8MkKqYSpZVzd5 --- .claude-plugin/marketplace.json | 2 +- CHANGELOG.md | 49 +++++++++++++++++++ .../foundational/cache-invalidation.md | 11 ++++- .../foundational/content-modelling.md | 1 + .../foundational/extend-mcp-tools.md | 1 + .../references/foundational/pim-modelling.md | 3 ++ .../foundational/search-indexing.md | 7 +++ skills/dw-demo-base/references/mcp-setup.md | 17 +++++++ .../references/surface-priority.md | 2 + .../references/headless-backend.md | 22 ++++++++- .../references/headless-frontend.md | 22 ++++++++- .../references/canonical-setup-order.md | 19 ++++++- .../references/deserialize-flow.md | 9 ++++ .../references/pack-activation.md | 19 +++++++ 14 files changed, 176 insertions(+), 8 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 61a6e68..95582b9 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -6,7 +6,7 @@ }, "metadata": { "description": "Claude skills for Dynamicweb 10 — organized by task domain, bundled by role.", - "version": "4.0.2" + "version": "4.1.0" }, "plugins": [ { diff --git a/CHANGELOG.md b/CHANGELOG.md index e3a3482..0633e89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,55 @@ All notable changes to the Dynamicweb Skills plugin are recorded here. The `version` field in `.claude-plugin/marketplace.json` tracks these entries. +## [4.1.0] + +### Added +- **Autonomous/headless MCP transport fallback.** The Claude-client project-server approval is an + interactive-only gate — an unattended agent can wait on "Pending approval" forever. `dw-demo-base` + `mcp-setup.md` now documents the sanctioned fallback: the DW MCP endpoint (`/admin/mcp`) is plain + **JSON-RPC 2.0 over HTTPS**, so with the API-Key bearer the full tool surface (~393 tools on DW + 10.27.x) is directly callable (`initialize` → `tools/list` → `tools/call`) — with the caution that + it bypasses the client's approval layer, so the same guarded-writes discipline still applies. +- **Root `/` binding on DW 10.27.x.** After a baseline deserialize the site root can 404; the binding + is `Area.AreaDomain` + `Area.AreaFrontpage` (there is **no `AreaDns` table** on 10.27.x), host + restart required. Folded into `dw-demo-swift` `deserialize-flow.md` §7, cross-linked from the + `Area`-row cache row. +- **Area/style/item-type restart semantics + nav-label-is-data.** `dw-demo-base` + `foundational/cache-invalidation.md` now carries three restart-only rows (`Area` row / style asset / + item-type XML — all startup-materialised, `CacheInformationRefresh` insufficient), the caveat that a + whole-`Ids` bulk `GetServiceCaches` flush can `500`, and a diagnostic note that nav/menu **labels** + render live from the **group tree** (group rows, sibling item fields like `Subtitle`), so an + un-clearable label is usually data, not a "nav cache". +- **Index-instance Warning is benign.** An index-level `State=Warning` caused solely by an unbuilt + secondary balancer instance is a false alarm — judge by the primary instance's build result + doc + count. Folded into `dw-demo-pim` `canonical-setup-order.md` Step 16 (both variants). +- **Isolated pack-fragment staging.** Staging a pack fragment into a `SerializeRoot` that still holds + the base baseline trees **re-deserializes the base seed** — on a re-contented demo that resurrects + the whole purged sample catalog. `dw-demo-swift` `pack-activation.md` §8 now parks/clears base trees, + stages the fragment isolated, and restores — stated loudly. +- **MCP recipe gotchas batch** (from live brand-build recipes): `create_variant_combinations` leaves + `ProductActive`/`ProductPrice` NULL on combos → variants invisible (`foundational/pim-modelling.md` + §2.5); custom fields index as `CustomField_`, other patterns fail silently + (`foundational/search-indexing.md`); `import_product_images_from_urls` sets no default image and the + Swift card NREs on images-but-no-default, degrading the whole PLP (`foundational/pim-modelling.md` + §2.10); `synchronous: true` on index builds does not actually block — poll + (`foundational/search-indexing.md`); `save_pages` ignores `urlName` (slug derives from `menuText`) — + added to the silent-no-op tables in `foundational/extend-mcp-tools.md` §5 + `foundational/content-modelling.md`. +- **Product-completeness checklist.** `dw-demo-pim` `canonical-setup-order.md` now closes with a + per-product (and per-variant) gate — Active, priced, stocked-or-NeverOutOfStock, a default image, + texts in every language layer — each with its frontend symptom, run as a SQL sweep. +- **`dw-demo-headless` drift notes.** The two-token trap's failure status is **version-dependent** + (404 on 10.26.x, 400 on 10.27.x) — assert "a non-401 error", don't pin a code (`headless-backend.md` + §3); product images live under `assetCategories` **or** `imagePatternImages` — read both + (`headless-frontend.md` §2); repository/query names must be env-configurable (query name **without** + the `.query` extension) so a second-backend swap is pure env (`headless-frontend.md` §3 + + `headless-backend.md` §5); areas can ship with empty ecom bindings (`ecomShopId=""`) so the provider + must pass `LanguageId`/`ShopId` explicitly on every call (`headless-backend.md` §4). + + All nine folds come from the same autonomous partner-simulation build as [4.0.2] (fresh DW 10.27.4, + skills followed verbatim), carried through full brand re-content, catalog authoring via MCP recipes, + feature-pack install, and a headless storefront on a second backend — each verified live. + ## [4.0.2] ### Fixed diff --git a/skills/dw-demo-base/references/foundational/cache-invalidation.md b/skills/dw-demo-base/references/foundational/cache-invalidation.md index 73d1a58..83e2c66 100644 --- a/skills/dw-demo-base/references/foundational/cache-invalidation.md +++ b/skills/dw-demo-base/references/foundational/cache-invalidation.md @@ -42,6 +42,9 @@ If you used MCP for a row whose mutation type appears in the table below, you sh | **Direct SQL UPDATE on layout-composition columns** — `GridRowTopSpacing` / `GridRowBottomSpacing` / `GridRowVerticalAlignment` / `GridRowGapX/Y` / `GridRowColorSchemeId`, and `Page.PageItemType` / `PageItemId` / `PageColorSchemeId` | Page-composition cache (these feed the rendered `data-dw-row-space-*` / colorscheme / header-item attributes) | (none — MCP paragraph/page touches do NOT flush them) | YES — the cached values win until restart, same as the ordering row above. These columns behave like structure, not content. | | **Direct SQL UPDATE on `Page.PageActive` / `PageHidden` (navigation flags)** | Navigation tree cache + friendly-URL provider | (none) | YES — the nav keeps rendering the old page set (and friendly URLs keep/lose their routes) until restart. Flag semantics live in [`swift-building.md` §6](swift-building.md). | | **Group↔shop relation changes** (SQL on `EcomShopGroupRelation`, or an API group save that re-homes the group's shop) | Ecom navigation tree + friendly-URL provider | (none for SQL; API save still leaves the URL provider stale) | YES — group pages/slug resolution reflect the old shop homing until restart. The primary-shop trap itself is in [`commerce-catalog.md` §2.3](commerce-catalog.md). | +| **`Area` row column change** (any surface — `AreaDomain`, `AreaFrontpage`, `AreaEcomShopId/LanguageId/CurrencyId`, area item bindings) | Area-resolution cache built at host startup | `CacheInformationRefresh` is **insufficient** here | **YES — full host restart.** Area rows are materialised once at startup; a targeted service flush does not rebuild the area map, so root/domain/binding changes (see [`../../../dw-demo-swift/references/deserialize-flow.md`](../../../dw-demo-swift/references/deserialize-flow.md) §7) stay stale until the bounce. | +| **Style-asset change** (Color Scheme / Buttons / Typography / Fonts JSON+CSS in `Files/System/Styles/`, or the `Area`'s style wiring) | Style/theme registry loaded at startup | `CacheInformationRefresh` insufficient | **YES — full host restart.** New/edited style assets are enumerated at startup; the admin Design surface and the rendered `data-*` style attributes hold the old set until restart. | +| **Item-type XML change** (drop/edit `Files/System/Items/ItemType_*.xml`) | ItemManager type registry (materialised from XML at startup) | `CacheInformationRefresh` insufficient | **YES — full host restart.** The backing `ItemType_*` table + field registry are (re)built from the XML at startup only; content referencing a not-yet-materialised type fails until the bounce. (Same startup-materialisation fact the headless baseline relies on — [`../../../dw-demo-headless/references/headless-backend.md`](../../../dw-demo-headless/references/headless-backend.md) §6.) | | **Direct SQL UPDATE on `EcomPrices.PriceAmount` / `PriceCurrency` / scope columns** | Resolved-price cache | (none) | YES — old price wins until restart | | **Direct SQL UPDATE on `EcomOrderStates.OrderStateColor` (or other state-row columns read at render time)** | `OrderStates.GetStateById()` in-memory cache | (none) | YES — the badge's inline-style attribute holds the stale color even after a full page reload; storefront-rendered order-state badges and CSS variables fed by `Services.Orders.GetStateById(...).Color` keep the old hex until restart | | MCP `save_paragraphs` / `save_pages` / `save_grid_rows` | Page-composition cache | (auto via MCP) | No — these are the preferred surface for content seeding; the four "Direct SQL INSERT" rows above are the SQL-fallback equivalents | @@ -124,6 +127,10 @@ per-language product-text write; direct SQL was the reliable surface. If a mutation looks like it should have applied but the symptom persists (a stale dashboard count, an empty completeness panel, a missing variant SKU), it's a cache the mutation surface didn't invalidate. Resolve it in order — each rung is cheaper and safer than the one below it: 1. **Find the row above and run its named invalidation surface** — a targeted `CacheInformationRefresh` (enumerate cache ids with `GET /admin/api/GetServiceCaches`). This is the specific fix and the one to reach for first. -2. **Bulk flush** — `GET /admin/api/GetServiceCaches` → `POST /admin/api/CacheInformationsRefresh {"Ids": [...]}` (plural, all service caches at once). This is the mandatory substitute for *every* "YES restart" row on hosted installs — run it on local hosts too, before any restart: it clears every service-backed cache in one call, works when you can't identify which cache holds the stale value, and keeps the warm state a restart throws away. -3. **Restart the host** only when the symptom survives both flushes, or the stale cache is documented as not service-exposed (`Searching:Queries`; the RenderGrid HTML cache survives even a restart — see its row). When a restart is owed, **batch it**: one restart covering all pending restart-owed mutations (the "MCP first, SQL last, one restart" rule above), not one per mutation. Stop the process **port-scoped and ownership-verified** — resolve the PID from this host's own port and confirm the process command line points at this solution folder before killing it; a project-name match kills sibling hosts on a multi-host machine. Then **verify the bounce cold-started** (the `dotnet run` parent/child trap above — killing the parent can leave the real host running with its caches intact). +2. **Bulk flush** — `GET /admin/api/GetServiceCaches` → `POST /admin/api/CacheInformationsRefresh {"Ids": [...]}` (plural, all service caches at once). This is the mandatory substitute for *every* "YES restart" row on hosted installs — run it on local hosts too, before any restart: it clears every service-backed cache in one call, works when you can't identify which cache holds the stale value, and keeps the warm state a restart throws away. **Caveat: a whole-`Ids` bulk flush can return `500`** — some registered service caches throw when flushed out of band. If it 500s, don't treat the flush as done: fall back to the targeted single-service `CacheInformationRefresh` for the cache you actually need (rung 1), or restart. The startup-materialised registries above (`Area`, style, item-type) are **not** service-exposed and a bulk flush does not cover them regardless — those are restart-only. +3. **Restart the host** only when the symptom survives both flushes, or the stale cache is documented as not service-exposed (`Searching:Queries`; the startup-materialised `Area`/style/item-type registries above; the RenderGrid HTML cache survives even a restart — see its row). When a restart is owed, **batch it**: one restart covering all pending restart-owed mutations (the "MCP first, SQL last, one restart" rule above), not one per mutation. Stop the process **port-scoped and ownership-verified** — resolve the PID from this host's own port and confirm the process command line points at this solution folder before killing it; a project-name match kills sibling hosts on a multi-host machine. Then **verify the bounce cold-started** (the `dotnet run` parent/child trap above — killing the parent can leave the real host running with its caches intact). 4. **Re-verify after the fix:** re-run your post-change verification probes — the targeted MCP/SQL checks whose result was stale. + +### Before blaming a "nav cache": nav/menu labels render from the GROUP TREE, not a label cache + +A menu/label that "survives every flush and restart" is almost always **data, not cache**. Storefront navigation and menu labels render live from the **group tree** (`EcomGroups` rows and their translations) and from **sibling item fields** on the page/paragraph (e.g. a `Subtitle`, a menu-text item field), not from a dedicated "nav label" cache. So an old label that won't clear usually means an untouched group row, a stale translation row, or a sibling item field you didn't edit is still supplying it — chase the row, not the cache. (Genuine nav *structure* caching — the tree ordering, friendly-URL provider, `PageActive`/`PageHidden` visibility — is real and restart-owed per the rows above; it is the label *text* that is data-driven and mis-diagnosed as cache.) diff --git a/skills/dw-demo-base/references/foundational/content-modelling.md b/skills/dw-demo-base/references/foundational/content-modelling.md index e746812..1666bcc 100644 --- a/skills/dw-demo-base/references/foundational/content-modelling.md +++ b/skills/dw-demo-base/references/foundational/content-modelling.md @@ -463,6 +463,7 @@ or curl the rendered page) before declaring it done: | Save | Field silently dropped | Verified | Working fallback | |---|---|---|---| | MCP `save_pages` (update path) | `menuText` — the response even echoes the OLD value | DW 10.25.x | SQL `UPDATE Page SET PageMenuText` + host restart (the nav tree caches menu text) | +| MCP `save_pages` (create + update) | `urlName` — ignored; the slug is derived from `menuText` instead | DW 10.27.x | Set `menuText` to drive the slug, or SQL `UPDATE Page SET PageUrlName` + host restart. `urlName` won't pin the slug on its own. | | Management API `ParagraphSave` | `contentItem.groups[].fields[].value` mutations — the `ItemType_*` column never updates | DW 10.25.x | MCP `set_item_field_values` first; SQL UPDATE last resort. `ParagraphSave` is still correct for paragraph-level scalars (Header, Sort, GridRow, Template) | The tool-behaviour root cause (why these MCP / Management API writes drop fields, and the surface model) diff --git a/skills/dw-demo-base/references/foundational/extend-mcp-tools.md b/skills/dw-demo-base/references/foundational/extend-mcp-tools.md index 4364a94..49de714 100644 --- a/skills/dw-demo-base/references/foundational/extend-mcp-tools.md +++ b/skills/dw-demo-base/references/foundational/extend-mcp-tools.md @@ -128,6 +128,7 @@ and silently drops part of the input: | Tool (surface) | What gets silently dropped | Verified | Working fallback | |---|---|---|---| | MCP `save_pages` (update path) | `menuText` — the response even echoes the OLD value | DW 10.25.x | SQL `UPDATE Page SET PageMenuText` + host restart (the nav tree caches menu text) | +| MCP `save_pages` (create + update) | `urlName` — the slug you pass is **ignored**; DW derives the slug from `menuText` instead | DW 10.27.x | Set the intended `menuText` (the slug follows it), or SQL `UPDATE Page SET PageUrlName` + host restart. Don't expect `urlName` to pin the slug independently. | | Management API `ParagraphSave` | `contentItem.groups[].fields[].value` mutations — the `ItemType_*` column never updates | DW 10.25.x | MCP `set_item_field_values` first; SQL UPDATE last resort. `ParagraphSave` IS still correct for paragraph-level scalars (Header, Sort, GridRow, Template) | **Rule:** after any demo-critical update through MCP / Management API, round-trip it — read the value back diff --git a/skills/dw-demo-base/references/foundational/pim-modelling.md b/skills/dw-demo-base/references/foundational/pim-modelling.md index f116059..94b7734 100644 --- a/skills/dw-demo-base/references/foundational/pim-modelling.md +++ b/skills/dw-demo-base/references/foundational/pim-modelling.md @@ -73,6 +73,8 @@ Then trigger a Products index rebuild (`POST /admin/api/BuildIndex {"Repository" Use `INSERT INTO EcomProducts (col1,col2,...) SELECT m.col1, m.col2, ... FROM @combinations v INNER JOIN EcomProducts m ON m.ProductId = v.MasterId AND m.ProductVariantId = ''` — copy master, override 3 fields. Make `ProductNumber` one of the overridden fields, not a copied one. +**MCP `create_variant_combinations` gotcha — it leaves the combo rows NULL where it matters.** The MCP tool creates the per-variant `EcomProducts` rows for you, but it leaves **`ProductActive`** and **`ProductPrice`** **NULL** on the combinations. A NULL-active, NULL-price variant is **invisible storefront-wide** — it does not render in the PDP variant selector and an add-to-cart for it no-ops, exactly like a missing combination row, so it reads as "the tool didn't create them" when the rows are actually there. After `create_variant_combinations`, always set `ProductActive=1` and a real `ProductPrice` (and the §2.5a extras — `ProductDefaultUnitId`, per-variant `ProductStock`) on the new combo rows, then restart/flush. Verify by selecting the combo rows and confirming none have `ProductActive IS NULL` or a NULL price before declaring variants done. + ### 2.5a Single-axis variants — leaner shape + the MCP/SQL surface split (validated DW 10.25.x) When the product has exactly ONE variant axis (a Color selector, a tier ladder), the shape is leaner than §2.5's general case: @@ -132,6 +134,7 @@ When the product has exactly ONE variant axis (a Color selector, a tier ladder), - `DetailProductId`, `DetailVariantId`, `DetailLanguageId`, `DetailType=0` (image), `DetailValue=`, `DetailIsDefault` (primary), `DetailsGroupId` (asset category numeric id — check `EcomDetailsGroup`) - Asset categories = `EcomDetailsGroup` table. Default installs ship with at least `Images` (id=1). New categories (e.g. `Manuals` for PDFs) are SQL-only — no MCP tool exists for `EcomDetailsGroup` mutations. Insert into both `EcomDetailsGroup` (set `DetailsGroupExtensions` to filter file types, e.g. `'pdf'`, and `DetailsGroupDefaultUploadFolder` to the target path) AND `EcomDetailsGroupTranslation` (one row per language). - MCP tools `add_product_image` / `import_product_images_from_urls` / `upload_product_images` handle both download-to-disk + DB row. **Plugin-only — no Management API endpoints back these.** They live entirely in the MCP plugin code path; if the MCP session dies (token expiry, plugin restart, host restart) there is no `POST /admin/api/...` fallback for asset registration. The fallback is direct SQL INSERT on `EcomDetails`. +- **`import_product_images_from_urls` does NOT set a default image** — it registers the `EcomDetails` rows with `DetailIsDefault=0` on all of them. A product then has images-but-no-default, and that is a **frontend-breaking** state, not a cosmetic one: the Swift card template **NREs on a product with images but no default**, and because the PLP renders cards in a loop, one such product **degrades the WHOLE product-list page** (the list throws, not just that one card). After any `import_product_images_from_urls` run, set a default: `UPDATE EcomDetails SET DetailIsDefault=1 WHERE DetailProductId= AND DetailLanguageId='LANG1' AND DetailValue=` (exactly one default per product/variant/language), then flush/restart. The product-completeness checklist in [`../../../dw-demo-pim/references/canonical-setup-order.md`](../../../dw-demo-pim/references/canonical-setup-order.md) makes "a DEFAULT image is set" a per-product gate for exactly this reason. - **Bulk SQL INSERT must set `DetailLanguageId` to a real language code** (e.g. `'LANG1'`), not empty string and not NULL. The admin asset query and the per-product image listings filter strict-equality on this column, so empty-string language renders the row invisible despite being on disk and registered. Symptom: SQL count says 9 details for the product, admin product page shows 0 assets, file is at the path. Recovery: `UPDATE EcomDetails SET DetailLanguageId = 'LANG1' WHERE DetailLanguageId = '' OR DetailLanguageId IS NULL;` then host restart to flush asset caches. The MCP tools always populate this column correctly — this gotcha only fires when bulk SQL inserts skip the field. - After bulk SQL inserts, **restart the host** to flush the EcomDetails cache (same protocol as [`cache-invalidation.md`](cache-invalidation.md) covers for product mutations). diff --git a/skills/dw-demo-base/references/foundational/search-indexing.md b/skills/dw-demo-base/references/foundational/search-indexing.md index 43b15d1..9e0eea9 100644 --- a/skills/dw-demo-base/references/foundational/search-indexing.md +++ b/skills/dw-demo-base/references/foundational/search-indexing.md @@ -70,6 +70,10 @@ Hard constraints: Typical editorial backlog queries: `active_missing_short_description` (`ProductIsActive=True` + `ProductShortDescription IsEmpty`), `active_missing_images` (image field `IsEmpty`), `low_stock_active` (`ProductStock LessThan "5"`), `incomplete_products` (completion rule IDs + languages attached). Remember the saved `.query` leaves `` empty — patch it before the index build (see above). +### Custom product fields index as `CustomField_` + +A **custom** product field (a category field or a custom `EcomProductField`, as opposed to a standard one) lands in the Lucene index under the field name **`CustomField_`** — e.g. a custom field `RoomType` is queryable/facetable as `CustomField_RoomType`, not as `RoomType`, not as `ProductCategory||RoomType` (that pipe form is the *authoring/value* system name from [`pim-modelling.md`](pim-modelling.md) §2.8, not the *index* name). Referencing it by any other plausible pattern **fails silently** — the facet renders empty and the query returns nothing, with **no error** to point at the wrong name. When a facet you added is defined but always empty, check the index field name is `CustomField_` first. Confirm the exact indexed name against the built segment (or the index schema's field list) rather than guessing the casing/prefix. + ## Dashboard query location — Shared ONLY, never duplicate to Repositories For dashboard widget queries, put each `.query` + `.configuration` file in **exactly one** place: @@ -142,6 +146,9 @@ $buildResp = Invoke-RestMethod ` -SkipCertificateCheck # Poll the index status query until Success with a fresh build timestamp (15-min timeout). +# `synchronous: true` in the BuildIndex body does NOT actually block — the POST returns before the +# build finishes regardless, so treating a 2xx as "built" indexes against a stale/empty segment. +# ALWAYS poll the instance/index status below; never rely on the request to be synchronous. # DW 10.26.x contract: no Status/Idle field — State: Success|Warning|Error on the index query, # LifecycleState: NeverBuilt|...|Completed|Failed on the instance query. Live JSON is camelCase # (the api.json catalog declares PascalCase); PowerShell access is case-insensitive. diff --git a/skills/dw-demo-base/references/mcp-setup.md b/skills/dw-demo-base/references/mcp-setup.md index 7ce2992..7f03fb2 100644 --- a/skills/dw-demo-base/references/mcp-setup.md +++ b/skills/dw-demo-base/references/mcp-setup.md @@ -9,6 +9,7 @@ - [Step 3 — Create the MCP configuration in DW10 admin UI (API Key)](#step-3--create-the-mcp-configuration-in-dw10-admin-ui-api-key) - [Step 3b — Paste the bearer into `.mcp.json` and per-demo memory](#step-3b--paste-the-bearer-into-mcpjson-and-per-demo-memory) - [Step 3 (headless alternative) — create the token + MCP config without the admin UI](#step-3-headless-alternative--create-the-token--mcp-config-without-the-admin-ui) +- [Autonomous/headless fallback — call `/admin/mcp` directly as JSON-RPC 2.0](#autonomousheadless-fallback--call-adminmcp-directly-as-json-rpc-20) - [Step 4 — The MCP verification gate](#step-4--the-mcp-verification-gate) - [Step 5 — Install Browser MCP (machine-level, do once per Windows account)](#step-5--install-browser-mcp-machine-level-do-once-per-windows-account) - [Step 6 — Discover bearer tokens (the discover-from-project-files rule)](#step-6--discover-bearer-tokens-the-discover-from-project-files-rule) @@ -141,6 +142,22 @@ Any such bootstrap branch added to `Program.cs` during standup (a password-set, --- +## Autonomous/headless fallback — call `/admin/mcp` directly as JSON-RPC 2.0 + +Steps 1–4 wire the Dynamicweb MCP server into the **Claude Code client**, which gates a newly-configured project server behind a one-time **interactive approval prompt** ("Pending approval"). That prompt is an interactive-only gate: an autonomous or headless agent that never sees a human can wait on it forever — the tools never surface and the run stalls with no error to react to. This is the tool-side twin of the OAuth interactive-click blocker the "Why API Key by default" preamble calls out: the API-Key default clears the *auth* gate, but the client's *approval* gate is separate. + +**Sanctioned fallback: the DW MCP endpoint is a plain JSON-RPC 2.0 service over HTTPS**, not a Claude-proprietary protocol. With the API-Key bearer from Step 3 in an `Authorization: Bearer …` header, the full tool surface is directly callable without the client's tool layer — issue the standard JSON-RPC handshake against `https://localhost:/admin/mcp` yourself: + +1. **`initialize`** — POST `{ "jsonrpc":"2.0","id":1,"method":"initialize","params":{ "protocolVersion":"…","capabilities":{},"clientInfo":{…} } }`. +2. **`tools/list`** — enumerate the catalogue (**~393 tools on DW 10.27.x**; the count grows across DW versions — the >200 gate of Step 4 still holds). +3. **`tools/call`** — `{ "jsonrpc":"2.0","id":N,"method":"tools/call","params":{ "name":"","arguments":{ … } } }`. + +Send `Content-Type: application/json` (add `Accept: application/json, text/event-stream` if the endpoint negotiates SSE) and `-SkipCertificateCheck` for the localhost self-signed cert. The bearer is the same DB-backed API key the client uses, so no separate credential is needed. + +**Caution — this bypasses the client's approval layer.** It is a transport swap, not a licence for unreviewed writes: the guarded-writes discipline is unchanged. `tools/call` reaches the same domain services as the client-driven tools, so [`surface-priority.md`](surface-priority.md)'s build-phase rule and the round-trip verification rule apply exactly as they do through the client. Prefer the client-wired path (Steps 1–4) whenever a human can clear the one-time approval; reach for direct JSON-RPC only when the client gate genuinely can't be cleared in an unattended run. + +--- + ## Step 4 — The MCP verification gate The skill **refuses to declare setup complete** until BOTH conditions pass: diff --git a/skills/dw-demo-base/references/surface-priority.md b/skills/dw-demo-base/references/surface-priority.md index f8b3244..03c3422 100644 --- a/skills/dw-demo-base/references/surface-priority.md +++ b/skills/dw-demo-base/references/surface-priority.md @@ -37,6 +37,8 @@ A demo engagement has two phases with different surface rules, split by the **MC | 2. **Management API** (`/admin/api/...`) | Fallback when MCP doesn't expose the operation. Usually admin-grade actions: `BuildIndex`, `CacheInformationRefresh`, `FeatureManagementToggle`, anything in `/admin/api/docs/`. | Same DW domain services as MCP, just a different transport. | | 3. **Direct SQL** (`sqlcmd ...`, local installs only) | **LAST RESORT** — only for: (a) cleanup/teardown, (b) bulk schema-drift fixes, (c) reading data, (d) cases where you've confirmed both higher surfaces don't support the operation and a vendor patch is the only alternative. | Bypasses every DW service. Misses bookkeeping. Creates orphans. Corrupts caches. **You will not figure out the full bookkeeping for a non-trivial create via SQL — DW does too much per service call.** | +**Clause (a) covers brand re-content removal — do not mis-route it as create-shaped work.** BULK REMOVAL of the generic/sample rows a baseline ships (the stock demo catalog, sample pages, placeholder groups) while re-contenting a host to a brand is exactly case (a) cleanup/teardown — SQL is sanctioned for it on a local install, subject to the cache/restart the removed table owes (see [`foundational/cache-invalidation.md`](foundational/cache-invalidation.md)). Removal is not a create, so it is not gated by the "MCP-first for structural creates" rule and there is no MCP/recipe prerequisite to clear first. The create-shaped discipline (and the SQL-cloning ban below) governs what you *build* to replace the sample data, not the teardown that clears it. Track what you delete so a re-run converges. + Pattern to follow: 1. Try MCP. If the tool name suggests it (e.g. `copy_area`, `copy_page`, `save_pages`), use it. diff --git a/skills/dw-demo-headless/references/headless-backend.md b/skills/dw-demo-headless/references/headless-backend.md index 95dffbe..d91c1ac 100644 --- a/skills/dw-demo-headless/references/headless-backend.md +++ b/skills/dw-demo-headless/references/headless-backend.md @@ -91,10 +91,17 @@ Content-Type: application/json Response: `{ "token": "" }`. Send it as `Authorization: Bearer `. (`POST /dwapi/users/token` is the equivalent path; it also exchanges an existing `Dynamicweb.Extranet` cookie for a JWT.) -**Symptom → cause:** anonymous catalog reads work but every user-scoped call 401s → you are sending +**Symptom → cause:** anonymous catalog reads work but every user-scoped call fails → you are sending the admin token (or no token) instead of a frontend JWT. Mint the JWT via `/dwapi/users/authenticate` first. Never write either token to disk; keep it in conversation/session state only. +> **Don't pin the failure to a code — it is version-dependent.** The exact non-success status a +> wrong/absent token (or an unsupported request shape) returns on `/dwapi` **moves across DW +> versions** — the same trap was observed as **404 on 10.26.x** and **400 on 10.27.x**, not a stable +> 401. Detect the trap by "**a non-401 error**" (equivalently: anything but the anonymous `200`), and +> assert the *shape* — auth works vs it doesn't — rather than matching a fixed HTTP code that a minor +> platform bump will change out from under the check. + For SSR/build-time server fetches where no user is involved, `POST /dwapi/serviceauth/token` `{ "apiKey": "…" }` issues a short-lived service JWT — keep the API key server-side, never expose it to the browser. @@ -116,6 +123,14 @@ Areas carry the shop/language/currency bindings (`AreaEcomShopId` / `AreaEcomLan serialization — set them at provisioning, do not expect them to arrive in a baseline. An EN area and its NL sibling each bind to the same shop and their own language. +**An area can ship with the ecom bindings empty** (`ecomShopId=""`, and likewise language/currency) — +provisioning simply never set them, and nothing forces them non-empty. So the provider **must pass +`LanguageId` and `ShopId` explicitly on every Delivery API call** and must **never** fall back to "the +area's default context", because that default may be blank. Treat the storefront's configured +`ShopId`/`LanguageId` (the `DW_SHOP_ID` / `DW_LANGUAGE_ID` env of +[`headless-frontend.md`](headless-frontend.md) §3) as authoritative and send them on each request; read +the area bindings only as a *seed* for those env values, not as a runtime source of truth. + ## 5. Product search needs a repository + named query The PLP, faceted navigation, and text search all go through the search endpoint, which requires a @@ -139,6 +154,11 @@ Returns `200` with: - A stock/harness `Products` repository typically ships an index with **no resolvable query** (probes → 400/404). A headless demo ships its **own** complete search surface (index + query + facets) — see [`headless-baseline.md`](headless-baseline.md) §"Product search surface". +- **`RepositoryName` and `QueryName` are per-environment — the storefront must read them from env, not + hardcode them.** Pass the `QueryName` **without** the `.query` file extension (the endpoint wants the + bare query name). Wiring them as env (`DW_REPOSITORY_NAME` / `DW_QUERY_NAME`, see + [`headless-frontend.md`](headless-frontend.md) §3) is what makes a swap to a second backend a + pure-env change with no code edit. **Index-free fallback:** `GET /dwapi/ecommerce/products/search?ProductIds=…` (and `GroupId=…`) returns products without any repository query — use it for PDP-by-id and simple collection lists diff --git a/skills/dw-demo-headless/references/headless-frontend.md b/skills/dw-demo-headless/references/headless-frontend.md index bafb4b8..7378c13 100644 --- a/skills/dw-demo-headless/references/headless-frontend.md +++ b/skills/dw-demo-headless/references/headless-frontend.md @@ -56,12 +56,19 @@ Structure of the DW module: | Normalized type | DW source | Key field mapping | |---|---|---| -| `Product` | `GET /ecommerce/products/{id}`, `products[]` of the search result | `handle ← number`; `title ← name`; `descriptionHtml ← longDescription`; `priceRange ← price`/`prices[]`; `variants ← variantInfo` + `/variants/{id}`; `images ← imagePatternImages`; `availableForSale ← active && (neverOutOfstock || stockLevel>0)` | +| `Product` | `GET /ecommerce/products/{id}`, `products[]` of the search result | `handle ← number`; `title ← name`; `descriptionHtml ← longDescription`; `priceRange ← price`/`prices[]`; `variants ← variantInfo` + `/variants/{id}`; `images ← assetCategories` **OR** `imagePatternImages` (**read both** — see note); `availableForSale ← active && (neverOutOfstock || stockLevel>0)` | | `Collection` | `GET /ecommerce/groups`, `/groups/{groupId}` | `handle ← id` (e.g. `GROUP1`); `title ← name`; `path ← '/search/'+id`; `products ← search?GroupId=id` | | `Cart` | `carts/create`, `/carts/{secret}`, `/{secret}/items`, `/{secret}/checkout` | `id ← secret`; `checkoutUrl ← /carts/{secret}/checkout`; `lines ← cart lines`; `cost.* ← cart price fields` | | `Menu` | `GET /frontend/navigations/{areaId}` | `Menu[] ← nodes[]` recursively → `{ title ← node.title, path ← node.link }` | | `Page` | `GET /content/pages/{id}`, `/pages/url`; body via `/content/rows/{pageId}/{device}` | `handle ← path`; `title ← title`; `body ← rows/paragraphs`; `seo ← {title,description}` | +**Images live under one of two keys — read both.** Depending on how the product's assets were +authored, the Delivery API product model exposes images under **`assetCategories`** (grouped asset +records) **or** under **`imagePatternImages`** (the pattern-image set) — a product may populate one and +leave the other empty. The reshaper must read **both** and merge/fall back, not assume a single key, or +products authored the other way render imageless. Coalesce to the normalized `images[]` and pick the +default per [`headless-backend.md`](headless-backend.md)'s product model. + All DW coupling (endpoints, view-model quirks, price/VAT handling, the search-query dependency) lives in this one folder — easy to gate, mock, and evolve as the headless baseline matures. @@ -77,9 +84,20 @@ is hardcoded: | `DW_LANGUAGE_ID` | language context — `ENU` (**not** `LANG1`) | | `DW_CURRENCY_CODE` | currency for price display | | `DW_AREA_ID` | area id for `GET /frontend/navigations/{areaId}` (menu) | +| `DW_REPOSITORY_NAME` | product-search repository name (§5 of [`headless-backend.md`](headless-backend.md)) — **must be env, never hardcoded** | +| `DW_QUERY_NAME` | product-search named query — **without the `.query` extension** (the search endpoint wants the bare query name, not the filename) | + +**The repository and query names MUST be env-configurable — a second-backend swap should be pure +env.** The PLP/search endpoint (`?RepositoryName=…&QueryName=…`) names are environment-specific: a demo +that points the storefront at a *different* DW backend (a second host, a re-provisioned baseline) should +need **only** an env change — set `DW_REPOSITORY_NAME` and `DW_QUERY_NAME` (query name **without** the +`.query` file extension) — with **no code edit**. Hardcoding either name in `lib/dynamicweb/` is the bug +that turns a one-line backend swap into a code change; keep both in env and read them in `dwapi.ts`. Seed the shop/language/currency/area defaults from `GET /dwapi/content/areas` — the area carries the -`ecomShopId` / `ecomLanguageId` / `ecomCurrencyCode` bindings the provider should default to. Remove +`ecomShopId` / `ecomLanguageId` / `ecomCurrencyCode` bindings the provider *may* default to. **But an +area can ship with empty ecom bindings** (`ecomShopId=""`; see [`headless-backend.md`](headless-backend.md) +§4) — do not depend on the area supplying them; keep the explicit env values authoritative. Remove the `SHOPIFY_*` vars and the Shopify webhook revalidation route (`app/api/revalidate` keyed on Shopify topics) when you delete `lib/shopify/`. diff --git a/skills/dw-demo-pim/references/canonical-setup-order.md b/skills/dw-demo-pim/references/canonical-setup-order.md index 3d6cc98..84e11cd 100644 --- a/skills/dw-demo-pim/references/canonical-setup-order.md +++ b/skills/dw-demo-pim/references/canonical-setup-order.md @@ -4,6 +4,7 @@ - [0. Setup-order variants](#0-setup-order-variants) - [1. Canonical setup order (Variant A — Storefront-first)](#1-canonical-setup-order-variant-a--storefront-first) +- [Product-completeness checklist — verify before declaring catalog done](#product-completeness-checklist--verify-before-declaring-catalog-done) - [Appendix: commerce-side order seeding (used by Swift customer-center demos)](#appendix-commerce-side-order-seeding-used-by-swift-customer-center-demos) > The canonical setup order for a Dynamicweb 10 PIM build. Each step depends on earlier ones — skipping or reordering causes rework. Loaded from `~/.claude/skills/dynamicweb-pim-demo/SKILL.md` "Where to find things" table. Cross-references out to `structural-model.md`, `governance.md`, `cache-invalidation.md`, `workflow.md`, `permissions-model.md`. @@ -49,7 +50,7 @@ No `ShopType=1` shop. Products live on `EcomGroupProductRelation` rows under `SH 13. **Dynamic Workspaces** — insert `DynamicStructures` + `DynamicStructureLevels` rows with `UseRelationOnProductCreate=true` so products created from the workspace auto-attach to the source DataModel group. Levels source = `DataModelKey` or `ProductField` (e.g. `ProductWorkflowStateId` for a state-grouped Inbox). License-gated on `LicenseHasFeature("PIM")`. See `structural-model.md` §2.12. 14. **Channels** — one or more `EcomShops(ShopType=3)`, EACH with its OWN group tree + language relation. **Do NOT share catalog groups across Channels** (existing rule, `structural-model.md` §2.3). 15. **Products.index** — hand-write from `ProductIndexBuilder.DefaultSettings`. `Name="Products.index"` (including extension — Lucene resolver gotcha). **Inline `ProductIndexSchemaExtender`** inside `` — load-bearing; without it BuildIndex returns success on an empty segment and every PLP throws `numHits must be > 0`. Full XML + symptom list: `structural-model.md` §2.4. -16. **BuildIndex Full** — `POST /admin/api/BuildIndex {Repository:Products, IndexName:Products.index, BuildName:Full, BuildType:Full}`. Healthy segment is hundreds of KB; 53 bytes = schema accepted zero documents. +16. **BuildIndex Full** — `POST /admin/api/BuildIndex {Repository:Products, IndexName:Products.index, BuildName:Full, BuildType:Full}`. Healthy segment is hundreds of KB; 53 bytes = schema accepted zero documents. **An index-level `State=Warning` caused solely by an unbuilt secondary balancer instance is a false alarm** — the index reports Warning while a second (balancer/replica) instance sits `NeverBuilt`, even though the primary instance built cleanly. Judge success by the **primary instance's build result + doc count** (a fresh `LifecycleState=Completed` with the expected document count), not by the index-level Warning. Don't chase it as a failure; see [`../../dw-demo-base/references/foundational/search-indexing.md`](../../dw-demo-base/references/foundational/search-indexing.md) for the build-status contract. 17. **Permissions** — scope per-role via `UnifiedPermission` entries on the Shop / Group / Product / DynamicStructure / Workflow keys. With `CapabilityControlFeature` ON (DW10.21+), Layer B (capability keys like `/Products/DynamicWorkspaces`) gates UI-section visibility; Layer C (entity keys) gates per-row access. With it OFF, the legacy `PermissionSection("Products")` cascade applies. See `permissions-model.md` for the three-layer model and `permissions-recipes.md` for the role-matrix + grant-seeding SQL. 18. **Smoke-test publish** — pick one product, fire the native "Publish to channel" action from the bulk Product List screen (multi-select target Shop + Channel groups, `PermissionLevel.Edit`, additive `EcomGroupProductRelation` rows). Verify rows created + `ShopUrlDataProvider` lazy cache flushed (the native action triggers `Notifications.Ecommerce.Group.AfterSave`; raw SQL does NOT — restart host if you bypass the action). See `structural-model.md` §2.3a. 19. **Dashboard widgets** — `RepositoryCountWidget` per workflow state (e.g. Draft / Ready / Published / Offline), keyed on `ProductWorkflowStateId`. Prefer `RepositoryCountWidget` / `RepositoryGridWidget` over scalar SQL widgets for drill-through. See `governance.md` "Dashboard query location". @@ -74,7 +75,7 @@ No `ShopType=1` shop. Products live on `EcomGroupProductRelation` rows under `SH 13. **Assets** — `import_product_images_from_urls` with groupId from `get_product_asset_categories`. 14. **Variants** — attach groups + insert combinations + INSERT variant product rows. Each variant `EcomProducts` row MUST have a unique `ProductNumber` (master + dash + variant-option suffix); collisions silently break the PIM-for-BC connector import. Master renames can re-sync variant numbers — see `structural-model.md` §2.5 for the rule, the regression vector, and the idempotent re-suffix UPDATE. 15. **BOM components** — `EcomProductItems` rows per bundle. **RESTART HOST AFTER** to refresh ProductItem cache. -16. **Repository + Index** — create `Files/System/Repositories/Products/Products.index`: hand-write it from `ProductIndexBuilder.DefaultSettings` with the **inline `ProductIndexSchemaExtender`** and `` (extension included), then `POST /admin/api/BuildIndex`. If PLP/PDP throw `numHits must be > 0` after a `state=success` build, the extender is missing (empty Lucene segment). Full XML, the Name-attribute gotcha, the do-NOT-copy-the-Swift-repo-index rule, and the symptom diagnostics: `structural-model.md` §2.4. +16. **Repository + Index** — create `Files/System/Repositories/Products/Products.index`: hand-write it from `ProductIndexBuilder.DefaultSettings` with the **inline `ProductIndexSchemaExtender`** and `` (extension included), then `POST /admin/api/BuildIndex`. If PLP/PDP throw `numHits must be > 0` after a `state=success` build, the extender is missing (empty Lucene segment). Full XML, the Name-attribute gotcha, the do-NOT-copy-the-Swift-repo-index rule, and the symptom diagnostics: `structural-model.md` §2.4. Note: an index-level `State=Warning` whose only cause is an unbuilt secondary balancer instance is benign — judge by the **primary instance's** build result + doc count, not the index-level Warning (see [`../../dw-demo-base/references/foundational/search-indexing.md`](../../dw-demo-base/references/foundational/search-indexing.md)). 17. **Queries** — `create_or_update_product_queries` via MCP. Split by purpose: - **Feed queries** (referenced by `EcomFeed.FeedIndexQueryId`) → move to `Files/System/Repositories/Products/` **root** (NOT a subfolder — feeds don't scan subfolders). - **Dashboard/widget queries** (referenced by widget `QueryId` param) → move to `Files/System/SmartSearches/Ecommerce/Shared/` only — **never GUID-duplicate to Repositories**. Collision mechanism + recovery: `governance.md` "Dashboard query location — Shared ONLY". @@ -87,6 +88,20 @@ No `ShopType=1` shop. Products live on `EcomGroupProductRelation` rows under `SH 23. **Dashboards** — `create_dashboards` (area=`Products`), `add_widgets_to_dashboards`. Prefer `RepositoryCountWidget` / `RepositoryGridWidget` over SQL scalar widgets for drill-through. 24. **FINAL INDEX REBUILD** after all mutations. +## Product-completeness checklist — verify before declaring catalog done + +Before calling catalog authoring finished, verify **every product** (and every variant combination — variants are their own `EcomProducts` rows and fail these independently of the master) against this list. Each gap has a distinct, misleading frontend symptom, so a product that "looks authored" in admin can still be broken on the storefront: + +| Must hold, per product (and per variant combo) | Storefront symptom when it doesn't | +|---|---| +| **`ProductActive=true`** — including variant combos (MCP `create_variant_combinations` leaves them NULL, see [`../../dw-demo-base/references/foundational/pim-modelling.md`](../../dw-demo-base/references/foundational/pim-modelling.md) §2.5) | Product/variant is **invisible** — absent from PLP and the PDP variant selector; add-to-cart no-ops. | +| **A price** — a real `ProductPrice` / `EcomPrices` row, incl. combos | Renders at **€0** (or the variant drops out of the price/qty table). | +| **Stock > 0 OR `NeverOutOfStock=true`** — seed per-variant `ProductStock` for **every** language row (variants default to 0) | Shows **"Out Of Stock"** / not orderable even though the master has stock. | +| **A DEFAULT image is set** — exactly one `EcomDetails.DetailIsDefault=1` per product/variant/language (`import_product_images_from_urls` sets none) | Swift card **NREs on images-but-no-default**, taking down the **whole PLP**, not just that card. | +| **Texts present in all shipped language layers** — name/description on each `ProductLanguageId` row | Blank name/description (or default-language fallback leaking) on the localized storefront. | + +Run this as a SQL sweep, not an eyeball pass: select products/variants where any of `ProductActive IS NULL`, no price row, `ProductStock=0 AND NeverOutOfStock=0`, no `DetailIsDefault=1`, or a missing language-layer text row — every hit is a future storefront defect. Fix, then flush/restart and rebuild the index before the final verification walk. The per-tool root causes live in [`../../dw-demo-base/references/foundational/pim-modelling.md`](../../dw-demo-base/references/foundational/pim-modelling.md) (§2.5 variants, §2.5a per-variant unit/stock, §2.10 assets). + ## Appendix: commerce-side order seeding (used by Swift customer-center demos) The two sections below are commerce / customer-center seeding, not PIM setup order — they apply when a demo seeds order history for Swift's account-side and CSR views. diff --git a/skills/dw-demo-swift/references/deserialize-flow.md b/skills/dw-demo-swift/references/deserialize-flow.md index 8fa7d88..2de26c8 100644 --- a/skills/dw-demo-swift/references/deserialize-flow.md +++ b/skills/dw-demo-swift/references/deserialize-flow.md @@ -200,6 +200,15 @@ Serializer invalidates caches as part of the strict-mode contract — host resta If a host restart turns out to be necessary in practice (for a category not covered by strict mode's cache-invalidation contract), document it in the per-demo `CUSTOMISATIONS.md` so the deviation is visible to the next deserialize on this machine. +### Site root `/` 404s after deserialize — bind `AreaDomain` + `AreaFrontpage` (DW 10.27.x) + +A clean deserialize can still leave the **site root (`/`) returning 404** even though every page exists and resolves under its own path — the area just has no root binding. On **DW 10.27.x the root binding is two `Area` columns**, set on the area the root should serve: + +- **`Area.AreaDomain`** — the host the area answers on (e.g. `localhost`, or `localhost:`). +- **`Area.AreaFrontpage`** — the numeric page id that `/` renders. + +**There is no `AreaDns` table on 10.27.x** — do not look for one; the older DNS-binding table is gone and the binding lives on the `Area` row itself. Set both columns (`UPDATE Area SET AreaDomain = N'localhost', AreaFrontpage = WHERE AreaId = `), then **restart the host** — `Area` rows are materialised at startup, so the new root binding is not live until the bounce (see [`../../dw-demo-base/references/foundational/cache-invalidation.md`](../../dw-demo-base/references/foundational/cache-invalidation.md), the `Area`-row row). These binding columns are per-environment and excluded from serialization, so they arrive unset on a fresh host — set them at provisioning, don't expect them from the baseline. + ## 8. Mandatory next step After this flow returns 2xx, **immediately run [`integrity-sweep.md`](integrity-sweep.md)**. The skill refuses to declare deserialize complete until the sweep passes. diff --git a/skills/dw-demo-swift/references/pack-activation.md b/skills/dw-demo-swift/references/pack-activation.md index 094fb71..d24ca48 100644 --- a/skills/dw-demo-swift/references/pack-activation.md +++ b/skills/dw-demo-swift/references/pack-activation.md @@ -156,6 +156,16 @@ Track exactly what you copy — removing the pack later deletes exactly these fi The fragment is serializer YAML that lands the pack's data. Stage each mode tree named in `fragmentModes` into the host's `SerializeRoot`, then POST the deserialize — one POST per mode. +> **STAGE THE FRAGMENT ISOLATED — say it loudly.** A `POST /Admin/Api/SerializerDeserialize?mode=` +> deserializes **everything in `SerializeRoot//`**, not just the files you copied in. If the base +> baseline's trees are still sitting in `SerializeRoot/deploy/` and `SerializeRoot/seed/` from the +> §"deserialize-flow.md" run, dropping the fragment alongside them **re-deserializes the base seed too** — +> and on a host you have since **re-contented** (purged the sample catalog, authored brand data), the seed +> pass **resurrects the entire purged sample catalog** on top of your brand content. The fragment install +> silently turns into a base-baseline re-import. **Park or clear the base trees first, stage the fragment +> alone, deserialize, then restore the base trees** (or just delete the staged fragment). Never POST a +> deserialize against a `SerializeRoot` whose contents you have not just verified are fragment-only. + ```powershell # Bind the running host's HTTPS port and a Management API token BEFORE the loop. # $token is the CLAUDE.hex captured in the current conversation (see §2); $port is @@ -167,6 +177,12 @@ if (-not $token -or $token -like '<*') { throw 'Capture a Management API token ( if (-not $port) { throw 'Set $port to the running host HTTPS port first' } $serializeRoot = "$hostRoot\wwwroot\Files\System\Serializer\SerializeRoot" +# ISOLATE: park any base-baseline trees out of SerializeRoot so the deserialize sees ONLY the fragment. +$parked = "$hostRoot\wwwroot\Files\System\Serializer\_parked-base" +if (Test-Path $serializeRoot) { + New-Item -ItemType Directory -Path $parked -Force | Out-Null + Get-ChildItem $serializeRoot -Directory | Move-Item -Destination $parked -Force +} foreach ($mode in $pack.fragmentModes) { # e.g. 'seed', or 'deploy','seed' $modeSrc = "$packDir\baseline-fragment\$mode" if (Test-Path $modeSrc) { @@ -175,8 +191,11 @@ foreach ($mode in $pack.fragmentModes) { # e.g. 'seed', or 'deploy','se $resp = Invoke-RestMethod ` -Uri "https://localhost:$port/Admin/Api/SerializerDeserialize?mode=$mode" ` -Method POST -Headers @{ Authorization = "Bearer $token" } -SkipCertificateCheck + Remove-Item -Recurse -Force "$serializeRoot\$mode" # clear the staged fragment before the next mode } } +# RESTORE the parked base trees so a later base re-serialize/deserialize still has them. +if (Test-Path $parked) { Get-ChildItem $parked -Directory | Move-Item -Destination $serializeRoot -Force; Remove-Item $parked -Force } ``` Keep strict mode on (the default) — the fragment must land cleanly against the base graph. `seed` From e7b30e586a4fe0722a026364180142c4a1341426 Mon Sep 17 00:00:00 2001 From: Justinvolved <64196420+Justinvolved@users.noreply.github.com> Date: Fri, 3 Jul 2026 18:28:42 +0200 Subject: [PATCH 2/3] Add next/image local-backend SSRF-guard gotcha to the headless frontend reference --- skills/dw-demo-headless/references/headless-frontend.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/skills/dw-demo-headless/references/headless-frontend.md b/skills/dw-demo-headless/references/headless-frontend.md index 7378c13..30f568d 100644 --- a/skills/dw-demo-headless/references/headless-frontend.md +++ b/skills/dw-demo-headless/references/headless-frontend.md @@ -115,6 +115,15 @@ This is the same class of bypass `dynamicweb-demo-base` wires for the MCP/browse the process. For a hosted demo, terminate TLS with a real certificate and drop this var. Confine it to the storefront's local `.env`/shell; do not commit it into any deploy config. +**Sibling gotcha — `next/image` refuses local backends (SSRF guard).** On Next 15.6+, the image +optimizer rejects any upstream whose host resolves to a loopback/private IP with +`400 "url" parameter is not allowed` — **even when the host matches `images.remotePatterns`**. So a +storefront pointed at a DW backend on `localhost`/a LAN IP renders every `/Files/**` product image +broken while the same URL serves 200 directly. Fix in `next.config.ts`: set +`images.dangerouslyAllowLocalIP: true`, but gate it on the backend host actually being +local/private (derive from the API-base env var) so a public backend keeps its SSRF protection. +The flag is baked into the build — rebuild after changing it; a restart alone flips nothing. + ## 5. Build-time RSC fetch caveat The starter's pages are React Server Components that fetch during rendering — and the App Router From b632f2ccdf6e8ecbb41dfd3ebbad898bdabeaff2 Mon Sep 17 00:00:00 2001 From: Justinvolved <64196420+Justinvolved@users.noreply.github.com> Date: Fri, 3 Jul 2026 18:29:21 +0200 Subject: [PATCH 3/3] Record the next/image SSRF-guard gotcha in the 4.1.0 changelog entry --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0633e89..c12b722 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ All notable changes to the Dynamicweb Skills plugin are recorded here. The ## [4.1.0] ### Added +- **next/image SSRF guard vs local DW backends** (`dw-demo-headless` headless-frontend.md): Next + 15.6+ rejects loopback/private upstream hosts with a 400 even when `remotePatterns` match; gate + `images.dangerouslyAllowLocalIP` on the backend host being local, and rebuild — the flag is baked + into the build. - **Autonomous/headless MCP transport fallback.** The Claude-client project-server approval is an interactive-only gate — an unattended agent can wait on "Pending approval" forever. `dw-demo-base` `mcp-setup.md` now documents the sanctioned fallback: the DW MCP endpoint (`/admin/mcp`) is plain