Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
{
Expand Down
53 changes: 53 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,59 @@
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
- **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
**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_<SystemName>`, 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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.)
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions skills/dw-demo-base/references/foundational/pim-modelling.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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=<file path>`, `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=<id> AND DetailLanguageId='LANG1' AND DetailValue=<chosen path>` (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).

Expand Down
Loading
Loading