diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index fb10e4f..61a6e68 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.1" + "version": "4.0.2" }, "plugins": [ { diff --git a/CHANGELOG.md b/CHANGELOG.md index ae15d53..e3a3482 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,35 @@ All notable changes to the Dynamicweb Skills plugin are recorded here. The `version` field in `.claude-plugin/marketplace.json` tracks these entries. +## [4.0.2] + +### Fixed +- **License step folded into the canonical first-run flow.** On a fresh DW 10.27.x install the Setup + Guide forces `/admin/license` immediately after the database step, before any admin-user setup. + `dw-demo-base` `scaffold.md` §3 now walks the license step (Suite Trial for demos; ~30-day expiry + recorded in `CUSTOMISATIONS.md`), with the platform-level detail + headless trial path in + `foundational/setup-install.md` §7. +- **Headless admin-password recovery documented.** The license gate can skip the set-admin-password + step, leaving every seeded user inactive with an empty password and no usable admin login. + `foundational/setup-install.md` §7 documents the one-shot `Program.cs` recovery via + `Dynamicweb.Security.UserManagement.UserService` (`ChangePassword` + `user.Active = true` + `Save`). +- **Serializer config path corrected (version-sensitive).** On DW 10.27.4 + engine 0.6.8-beta the + engine reads `Files/System/Serializer/Serializer.config.json`, not the `Files/` root. + `serializer-reference.md` Step 3 (and the config-path mentions in `deserialize-flow.md`) now stage + and cite the `Files/System/Serializer/` location; the engine's actual read location wins, confirmed + by where `SerializeRoot/` is created. +- **Deserialize is a two-POST sequence.** A bare `POST /Admin/Api/SerializerDeserialize` runs the + Deploy pass only; the Seed pass must be requested explicitly with `?mode=Seed`. `deserialize-flow.md` + §4 now documents both passes (deploy then seed) for the swift/2.3 deploy+seed baseline. +- **`excludeAreaColumns` semantics clarified.** The setting governs serialize-OUT (which Area columns + are written to YAML), not deserialize-IN — it does not suppress "source column not present on target + schema" drift for a baseline captured on an older platform. Recovery (strip the column from the + STAGED `area.yml`, never the downloaded original) is documented in `deserialize-flow.md` §3 and the + matching failure pattern in `serializer-reference.md`. + + All five folds come from a fresh-DW-10.27.4 autonomous demo build (Serializer engine 0.6.8-beta) + following the skills verbatim — each was a real first-run failure. + ## [4.0.1] ### Fixed diff --git a/skills/dw-demo-base/references/foundational/setup-install.md b/skills/dw-demo-base/references/foundational/setup-install.md index a2d100f..92cf4f1 100644 --- a/skills/dw-demo-base/references/foundational/setup-install.md +++ b/skills/dw-demo-base/references/foundational/setup-install.md @@ -236,3 +236,47 @@ debugger, `dotnet watch`, an IDE-managed reload) is respawning it — stop the u Commerce + Users + Files + Settings + Headless. - **Do not use the `dotnet new dw10-cms` template** (CMS-only) for a solution that needs Commerce + PIM. Use `dw10-suite` (the full Suite template). + +## 7. First-run license gate + headless admin-password recovery + +On a fresh DW **10.27.x** install the Setup Guide forces `/admin/license` immediately after the +database step — **before** any admin-user setup. Complete the license step; a **Suite Trial** is fine +for demos (the trial expiry lands ~30 days out — record it in the demo's `CUSTOMISATIONS.md` so the +next run knows when the demo goes dark). The `dw-setup-install` skill ships a headless +trial-activation path (`scripts/activate-free-trial.ps1`, driving `/admin/license/TrialInstallStep`) +for boxes where the browser flow is not an option. + +### 7.1 — The license gate can skip the set-admin-password step + +Verified on a fresh 10.27.4 host: when the license step runs first, the Setup Guide can jump straight +past the "set admin password" prompt. The result is a host whose seeded users are all **inactive with +empty passwords** — there is NO usable admin login, and the standard first-run flow dead-ends (every +credential is rejected at `/admin`, with no UI path back to the password prompt). + +### 7.2 — Canonical fix: headless password reset via `UserService` + +Recover by resetting the admin user in code — a **one-shot** temporary branch in `Program.cs`, removed +after it runs once. Use `Dynamicweb.Security.UserManagement` (`UserManagementServices.Users`, i.e. +`Dynamicweb.Security.UserManagement.UserService`): call `ChangePassword(user, newPassword)`, flip +`user.Active = true`, then `Save(user)`. This API is not on the docs site — it was resolved from the +platform's bin XML docs. + +```csharp +// TEMPORARY one-shot — place after the host is built, run once, then DELETE this block. +using Dynamicweb.Security.UserManagement; + +var users = UserManagementServices.Users; +// Resolve the seeded admin (by username/id/email — see dw-users-permissions "Reading Users"). +var admin = users.GetUserByUserName("Administrator"); +if (admin is not null) +{ + users.ChangePassword(admin, ""); + admin.Active = true; + users.Save(admin); +} +``` + +Restart the host, log in at `/admin` with the new password, then remove the block and restart again so +the reset cannot re-run. Never leave the reset branch in a committed `Program.cs` — it is a +plaintext-password write path. The `ChangePassword` / `Save` surface is the same one documented in +[`../../../dw-users-permissions/SKILL.md`](../../../dw-users-permissions/SKILL.md) ("Writing Users"). diff --git a/skills/dw-demo-base/references/scaffold.md b/skills/dw-demo-base/references/scaffold.md index 5a659c4..270b25e 100644 --- a/skills/dw-demo-base/references/scaffold.md +++ b/skills/dw-demo-base/references/scaffold.md @@ -56,7 +56,8 @@ The Setup Guide on first run will: 1. Auto-create the demo database under `MSSQL$SQLEXPRESS` if the connecting SQL user has `dbcreator`. The DB name defaults to the solution folder name unless overridden. 2. Initialise the `Files/` folder under `Dynamicweb.Host.Suite/wwwroot/` with the empty default structure. -3. Print a one-time admin user prompt (capture the credentials — they're needed for the admin UI walkthrough in `references/mcp-setup.md` Step 3). +3. **Force the license step.** On DW 10.27.x the Setup Guide redirects to `/admin/license` immediately after the database step, BEFORE any admin-user setup. Complete it — a **Suite Trial** is fine for demos. The trial expiry lands ~30 days out; record it in the demo's `CUSTOMISATIONS.md` so the next run on this box knows when the demo goes dark. Platform-level detail + the headless trial-activation path: [`foundational/setup-install.md`](foundational/setup-install.md) §7. +4. Print a one-time admin user prompt (capture the credentials — they're needed for the admin UI walkthrough in `references/mcp-setup.md` Step 3). **Gotcha:** the license gate can skip this step entirely, leaving every seeded user inactive with an empty password — no usable admin login, and the standard flow dead-ends. If `/admin` rejects every credential after setup, apply the headless admin-password recovery in [`foundational/setup-install.md`](foundational/setup-install.md) §7 before going further. Once the Setup Guide completes, the `Properties/launchSettings.json` file has its final `applicationUrl` and the `GlobalSettings.Database.config` has the actual DB name. **These two files are the source of truth for port and DB name from now on** (the discover-from-project-files rule). diff --git a/skills/dw-demo-base/references/serializer-reference.md b/skills/dw-demo-base/references/serializer-reference.md index 7a56d41..62f3895 100644 --- a/skills/dw-demo-base/references/serializer-reference.md +++ b/skills/dw-demo-base/references/serializer-reference.md @@ -48,15 +48,19 @@ Copy-Item $dll.FullName "Dynamicweb.Host.Suite\bin\Debug\net10.0\" -Force The DLL is built net8.0 (Serializer ships single-target net8.0 per its csproj). .NET 10's runtime back-loads net8.0 assemblies fine. Restart the host after the copy so the new DLL is picked up. Note: the README and `docs/getting-started.md` still say "copy to `/path/to/your-dw-host/bin/`" — that's the published-deployment shape and does NOT work for local `dotnet run` hosts; always use the TFM subfolder. -### Step 3 — Stage `Files/Serializer.config.json` +### Step 3 — Stage `Files/System/Serializer/Serializer.config.json` -The Serializer requires a config at `/wwwroot/Files/Serializer.config.json`. Without one, `/Admin/Api/SerializerDeserialize` returns `Serializer.config.json not found (also checked ContentSync.config.json)`. The Serializer repo ships a canonical Swift 2.2 baseline config in the project's `Configuration\` folder — copy that as the starting point: +The Serializer requires a config at `/wwwroot/Files/System/Serializer/Serializer.config.json` (version-sensitive — see the path note below). Without one, `/Admin/Api/SerializerDeserialize` returns `Serializer.config.json not found (also checked ContentSync.config.json)`. The Serializer repo ships a canonical Swift 2.2 baseline config in the project's `Configuration\` folder — copy that as the starting point: ```powershell +$cfgDir = "Dynamicweb.Host.Suite\wwwroot\Files\System\Serializer" +New-Item -ItemType Directory -Path $cfgDir -Force | Out-Null Copy-Item "$($serializerProj.FullName)\Configuration\swift2.2-combined.json" ` - "Dynamicweb.Host.Suite\wwwroot\Files\Serializer.config.json" -Force + "$cfgDir\Serializer.config.json" -Force ``` +**Path note (version-sensitive).** On DW **10.27.4** + Serializer engine **0.6.8-beta** the engine reads the config from `Files/System/Serializer/Serializer.config.json`. Older installs staged it at the `Files/` root (`Files/Serializer.config.json`); the engine's actual read location is what wins, so stage it where the running engine looks. Confirm the location on a given host by where the engine creates `SerializeRoot/` — it lands under `Files/System/Serializer/`, alongside the config (the deserialize flow reads `Files/System/Serializer/SerializeRoot//`). + The shipped config uses the current schema: a single flat `predicates: [...]` list with a per-entry `"mode": "Deploy"|"Seed"` field — see "Deploy vs Seed" below for the schema break vs the legacy `deploy: { predicates: [...] }` shape. ### Verification @@ -153,6 +157,8 @@ WHERE Link LIKE '%Default.aspx?%=3421%'; 2. Drop the column on source: align downward instead of upward. 3. Accept the drift: the column is silently dropped from MERGE, the rest of the row writes correctly. Lenient mode only. +**Area-column drift specifically (older baseline → newer host).** When the offending column is on `[Area]` (e.g. an `area.yml` captured on an older platform), the predicate's `excludeAreaColumns` setting does NOT help — it governs serialize-OUT (which Area columns get *written*), not deserialize-IN. Strip the offending column from the **staged** `Files/System/Serializer/SerializeRoot/deploy/_content//area.yml` (never the downloaded original under `baselines\`) and re-POST. See the deserialize flow's §3 note: [`../../dw-demo-swift/references/deserialize-flow.md`](../../dw-demo-swift/references/deserialize-flow.md). + ### "template 'T' not found at Files/Templates/T" **Symptom:** `WARNING: template 'eCom_Catalog' not found at Files/Templates/eCom_Catalog.cshtml`. diff --git a/skills/dw-demo-swift/references/deserialize-flow.md b/skills/dw-demo-swift/references/deserialize-flow.md index 5eb0973..8fa7d88 100644 --- a/skills/dw-demo-swift/references/deserialize-flow.md +++ b/skills/dw-demo-swift/references/deserialize-flow.md @@ -25,7 +25,7 @@ - The baseline release has been downloaded from the baseline distribution repo (`justdynamics/Truvio.Commerce.Serializer.Baselines` by default; overridable via `$env:DW_BASELINE_REPO`) into the demo's own `baselines\` folder (see §3 — the staging snippet downloads it on first run). - [`../../dw-demo-base/references/scaffold.md`](../../dw-demo-base/references/scaffold.md) produced a running `Dynamicweb.Host.Suite` (port reachable, host responds at `/admin`). - [`../../dw-demo-base/references/mcp-setup.md`](../../dw-demo-base/references/mcp-setup.md) verification gate passed (`claude mcp list` shows `dynamicweb-commerce-mcp ✓ Connected` AND in-conversation `ToolSearch +dynamicweb` returns >200 tools). -- **The DW Serializer is installed in the host** per [`../../dw-demo-base/references/serializer-reference.md`](../../dw-demo-base/references/serializer-reference.md) "Installation" section (DLL built + copied to `bin/Debug/net10.0/`, `Files/Serializer.config.json` staged, host restarted). This is a one-time-per-host step. +- **The DW Serializer is installed in the host** per [`../../dw-demo-base/references/serializer-reference.md`](../../dw-demo-base/references/serializer-reference.md) "Installation" section (DLL built + copied to `bin/Debug/net10.0/`, `Files/System/Serializer/Serializer.config.json` staged, host restarted). This is a one-time-per-host step. - A Management API bearer token has been captured via `AskUserQuestion` in the current conversation. Format: `CLAUDE.`. Token lives in conversation state, never persisted to disk. Do not write the token to any file. If any of those are unmet, return to the relevant reference before attempting a deserialize. A deserialize against a half-wired host is the fastest way to corrupt a demo's state silently. @@ -91,7 +91,7 @@ Captured via `AskUserQuestion` in the current conversation. Format: `CLAUDE.\baselines\` folder — the per-demo copy downloaded from the baseline distribution repo (`justdynamics/Truvio.Commerce.Serializer.Baselines` by default; overridable via `$env:DW_BASELINE_REPO`). No hardcoded machine-wide literals. -**The Serializer reads from `Dynamicweb.Host.Suite/wwwroot/Files/System/Serializer/SerializeRoot//`** (joined from `outputDirectory: "Serializer"` in `Files/Serializer.config.json` + `outputSubfolder` per mode). The demo's `baselines/` folder is the **download/staging copy only** — the deserialize endpoint never reads it. The snippet below copies `baselines\\` INTO `SerializeRoot/`; that copy step is what makes the content visible. Skipping the copy (or pointing the flow at `baselines/` directly) silently no-ops — any "121 updated" you then see comes from whatever else is already in `SerializeRoot/deploy/` (typically a previous serialize roundtripping itself). Verified during a Swift2 baseline import — the original recipe pointed at `baselines/` and silently no-op'd. +**The Serializer reads from `Dynamicweb.Host.Suite/wwwroot/Files/System/Serializer/SerializeRoot//`** (joined from `outputDirectory: "Serializer"` in `Files/System/Serializer/Serializer.config.json` + `outputSubfolder` per mode). The demo's `baselines/` folder is the **download/staging copy only** — the deserialize endpoint never reads it. The snippet below copies `baselines\\` INTO `SerializeRoot/`; that copy step is what makes the content visible. Skipping the copy (or pointing the flow at `baselines/` directly) silently no-ops — any "121 updated" you then see comes from whatever else is already in `SerializeRoot/deploy/` (typically a previous serialize roundtripping itself). Verified during a Swift2 baseline import — the original recipe pointed at `baselines/` and silently no-op'd. **Baseline shape — full deploy + seed (swift/2.3).** The canonical `swift-2.3` baseline release is a full `config/deploy/seed` tree, NOT the content-only shape the older `Swift2.2` baselines used. `deploy/` ships framework `_sql/` (EcomCountries, EcomCurrencies, EcomLanguages, EcomShops, EcomPayments, EcomShippings, EcomVatGroups, AccessUser, UrlPath, order flow/states) alongside `_content/` (the `Swift 2` + `Swift 2 Nederlands` areas); `seed/` ships catalog `_sql/` (EcomGroups, EcomProducts, EcomPrices, EcomGroupProductRelation, variant + discount tables) alongside its own `_content/`; `config/swift-2.3.json` carries the predicate config. Because the framework rows now ship WITH the baseline (deploy `_sql/`), a `swift-2.3` deserialize no longer requires the target DB to be pre-seeded with shops/currencies/countries the way the content-only `Swift2.2` baselines did. The area YAML still hardcodes `"AreaEcomShopId": "SHOP1"` and `"AreaEcomCountryCode": "DE"` as **string FKs**; with swift/2.3 those FKs resolve against the framework rows the deploy pass lands. **Mode-semantics warning for PIM-curated hosts:** framework rows travel in `deploy`, and `deploy` is **source-wins** — the baseline's SHOP1/DE/EUR/LANG1 rows UPDATE matching rows already in the target. Only the `seed` pass (catalog) is destination-wins. A host with hand-curated framework rows is therefore NOT automatically preserved: review (and if needed trim) `deploy/_sql/` against the target's curated framework data before deserializing. **Verify the two-pass (deploy then seed) deserialize against a running host before relying on it — this repoint was prepared without a host run; confirm counts against the baseline's `deploy-manifest.json` / `seed-manifest.json`.** @@ -122,7 +122,7 @@ foreach ($mode in 'deploy','seed') { **Pre-import: re-serialize before merging baseline YAML.** If the target host has any pre-existing predicates (e.g. `"Content - "`), POST `/Admin/Api/SerializerSerialize` FIRST so the deploy folder reflects current DB state. Otherwise the deserialize will revert any in-DB changes you made since the last serialize (we hit this in practice: a recent area-rename via API was reverted by re-applying stale YAML for the old area name). After serializing, also delete any folders in `_content/` whose name matches a stale area name — `Serialize` writes the current name's folder but does NOT clean the old one (e.g. `_content//` survives a rename to `_content//`). -**Predicate config: each `_content//` folder needs a matching predicate.** Add a content predicate to `Files/Serializer.config.json` per area you want imported: +**Predicate config: each `_content//` folder needs a matching predicate.** Add a content predicate to `Files/System/Serializer/Serializer.config.json` per area you want imported: ```json { "name": "Content - ", @@ -136,27 +136,41 @@ foreach ($mode in 'deploy','seed') { ``` **`areaId` must be > 0** — the validator rejects `0` with `"deploy.predicates[N] is missing required field 'areaId' (must be > 0)"`. For NEW areas not yet in target (e.g. importing Swift2.2's "Swift 2" area onto a host that doesn't have it), pre-create a stub area first via `POST /admin/api/AreaSave` with `Id: 0, Name: "", ItemType: "Swift-v2_Master", LayoutTemplate: "Designs/Swift-v2/Swift-v2_Page.cshtml"` (and other Swift defaults), capture the assigned numeric id from the response, set the predicate's `areaId` to it, then deserialize. The deserialize will populate the area's pages + paragraphs from the YAML; GUID identity ensures the YAML's `areaId: ` survives the local numeric-id assignment. +**`excludeAreaColumns` governs serialize-OUT, not deserialize-IN.** The `excludeAreaColumns` field in the predicate above controls which `Area` columns get *written to* `area.yml` when you serialize; it does NOT suppress `source column [Area].[] not present on target schema` drift when *applying* an `area.yml` captured on an older platform to a newer host. Setting it has no effect on an inbound deserialize. Working recovery when an older baseline's `area.yml` carries a column the newer host's schema no longer has: strip the offending column lines from the **staged** copy (`Files/System/Serializer/SerializeRoot/deploy/_content//area.yml`), never from the downloaded original under `baselines\`, then re-POST. Baselines captured on older DW versions can legitimately carry a few such Area columns on a newer host — see the failure-pattern entry in [`../../dw-demo-base/references/serializer-reference.md`](../../dw-demo-base/references/serializer-reference.md) ("source column ... not present on target schema"). + **Restart the host after editing `Serializer.config.json`** — config is loaded at startup, not on each request. **Strategy note (verified during a Swift2 baseline import):** Two strategies were considered — - **(a)** Copy YAML directly into `Dynamicweb.Host.Suite/wwwroot/Files/System/Serializer/SerializeRoot/deploy/` (this snippet — verified working). -- **(b)** Configure `Files/Serializer.config.json` `outputDirectory` to point at `\baselines\\` directly. Faster (no copy), but the running host's serialize would also write back into the downloaded baseline copy — contaminating your pristine reference of what the release shipped. Not recommended; (a) is the canonical approach. +- **(b)** Configure `Files/System/Serializer/Serializer.config.json` `outputDirectory` to point at `\baselines\\` directly. Faster (no copy), but the running host's serialize would also write back into the downloaded baseline copy — contaminating your pristine reference of what the release shipped. Not recommended; (a) is the canonical approach. **Single canonical swift/2.3 path:** `$baseline = "swift-2.3"` resolves to `\baselines\swift-2.3\` — the single canonical generic baseline (a `config/deploy/seed` tree). Per-demo customer-flavoured baselines (named `-base/` by convention) live alongside `swift-2.3/` under the same `baselines\` folder once they have been derived; both share this flow. Legacy content-only `Swift2.2` baselines predate this model and are no longer the default. ## 4. Step 2 — POST against running host +**Two POSTs — deploy first, then seed.** A bare `POST /Admin/Api/SerializerDeserialize` executes the **Deploy** pass ONLY. The Seed pass is not implicit — it must be requested explicitly with `?mode=Seed` in a **second** POST. For the swift/2.3 baseline (which stages both a `deploy/` and a `seed/` tree, per §3) you need both calls, in order: deploy lands framework + content (source-wins), then seed lands catalog (destination-wins). Running only the bare POST leaves the `seed/` tree (EcomGroups/EcomProducts/EcomPrices, catalog `_content/`) unapplied — a storefront with structure but no products. + ```powershell -$resp = Invoke-RestMethod ` +# Pass 1 — Deploy (the default mode; framework + content). +$deploy = Invoke-RestMethod ` -Uri "https://localhost:$port/Admin/Api/SerializerDeserialize" ` -Method POST ` -Headers @{ Authorization = "Bearer $token" } ` -SkipCertificateCheck + +# Pass 2 — Seed (catalog). The ?mode=Seed is REQUIRED — omitting it re-runs Deploy, not Seed. +$seed = Invoke-RestMethod ` + -Uri "https://localhost:$port/Admin/Api/SerializerDeserialize?mode=Seed" ` + -Method POST ` + -Headers @{ Authorization = "Bearer $token" } ` + -SkipCertificateCheck # Strict mode is on by default for API callers (per Serializer README). -# On failure: HTTP 4xx with CumulativeStrictModeException details. +# On failure: HTTP 4xx with CumulativeStrictModeException details (read the body — it's the diagnostic). ``` +(A content-only legacy `Swift2.2` baseline ships no `seed/` tree, so the second POST is a no-op there — only swift/2.3-shape deploy+seed baselines need both passes.) + **Keep strict mode on; never disable it** by passing a `strictMode` query parameter or body field set to a falsy value. Strict mode is the first line of defence (FK orphans, missing templates, cache failures, schema drift). Disabling it produces a deserialized DB that *looks* succeeded but is silently inconsistent — the deserialize-blind failure mode in its purest form. If the POST returns 4xx with a `CumulativeStrictModeException` body, the body itself is the diagnostic. Read the listed FK orphans / missing templates / schema drift entries; the fix is almost always upstream (the baseline YAML), not on the host. Cross-check the downloaded baseline's release tag/version against the host's DW10 version (the baseline-drift self-diagnosis rule).