domma-cms 0.43.1 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/CLAUDE.md +53 -53
  2. package/README.md +27 -27
  3. package/admin/css/admin.css +1 -1
  4. package/admin/dist/domma/domma-tools.css +3 -3
  5. package/admin/dist/domma/domma-tools.min.js +3 -3
  6. package/admin/index.html +2 -2
  7. package/admin/js/app.js +1 -1
  8. package/admin/js/lib/card-builder.js +2 -2
  9. package/admin/js/lib/crud-tutorial.js +1 -1
  10. package/admin/js/lib/effect-defs.js +1 -1
  11. package/admin/js/lib/effects-builder.js +1 -1
  12. package/admin/js/lib/image-editor.js +1 -1
  13. package/admin/js/lib/markdown-toolbar.js +2 -2
  14. package/admin/js/lib/project-context.js +1 -1
  15. package/admin/js/lib/project-quick-create.js +1 -1
  16. package/admin/js/lib/scribe-composer.js +1 -1
  17. package/admin/js/lib/server-restart.js +1 -1
  18. package/admin/js/lib/shortcode-context-menu.js +2 -2
  19. package/admin/js/lib/simple-editor.js +1 -1
  20. package/admin/js/lib/themes.js +1 -1
  21. package/admin/js/lib/timeline-builder.js +1 -1
  22. package/admin/js/templates/action-editor.html +8 -8
  23. package/admin/js/templates/api-endpoint-editor.html +7 -7
  24. package/admin/js/templates/api-endpoints.html +1 -1
  25. package/admin/js/templates/api-reference.html +1 -1
  26. package/admin/js/templates/block-editor.html +6 -6
  27. package/admin/js/templates/collection-editor.html +1 -1
  28. package/admin/js/templates/component-editor.html +2 -2
  29. package/admin/js/templates/dashboard/cache.html +3 -3
  30. package/admin/js/templates/dashboard/journeys.html +3 -3
  31. package/admin/js/templates/dashboard/kpi-strip.html +3 -3
  32. package/admin/js/templates/dashboard/top-pages.html +1 -1
  33. package/admin/js/templates/dashboard/traffic-chart.html +1 -1
  34. package/admin/js/templates/docs/api-actions.html +1 -1
  35. package/admin/js/templates/docs/api-collections.html +4 -4
  36. package/admin/js/templates/docs/api-pages.html +2 -2
  37. package/admin/js/templates/docs/api-plugins.html +1 -1
  38. package/admin/js/templates/docs/api-settings.html +2 -2
  39. package/admin/js/templates/docs/api-users.html +1 -1
  40. package/admin/js/templates/docs/components-howto.html +4 -4
  41. package/admin/js/templates/docs/components-reference.html +21 -21
  42. package/admin/js/templates/docs/components-rules.html +14 -14
  43. package/admin/js/templates/docs/components-walkthrough.html +13 -13
  44. package/admin/js/templates/docs/tutorial-crud.html +49 -49
  45. package/admin/js/templates/docs/tutorial-forms.html +4 -4
  46. package/admin/js/templates/docs/tutorial-plugin.html +10 -10
  47. package/admin/js/templates/docs/usage-actions.html +6 -6
  48. package/admin/js/templates/docs/usage-cta-shortcode.html +9 -9
  49. package/admin/js/templates/docs/usage-dconfig.html +4 -4
  50. package/admin/js/templates/docs/usage-navigation.html +1 -1
  51. package/admin/js/templates/docs/usage-pages.html +1 -1
  52. package/admin/js/templates/docs/usage-shortcodes.html +24 -24
  53. package/admin/js/templates/docs/usage-users-roles.html +3 -3
  54. package/admin/js/templates/docs/usage-views.html +4 -4
  55. package/admin/js/templates/documentation.html +1 -1
  56. package/admin/js/templates/effects.html +25 -25
  57. package/admin/js/templates/form-editor.html +5 -5
  58. package/admin/js/templates/login.html +5 -5
  59. package/admin/js/templates/menu-editor.html +10 -10
  60. package/admin/js/templates/navigation.html +11 -11
  61. package/admin/js/templates/notifications.html +1 -1
  62. package/admin/js/templates/page-editor.html +4 -4
  63. package/admin/js/templates/pro-docs.html +14 -14
  64. package/admin/js/templates/role-editor.html +1 -1
  65. package/admin/js/templates/settings.html +10 -10
  66. package/admin/js/templates/tutorials.html +3 -3
  67. package/admin/js/templates/view-editor.html +15 -15
  68. package/admin/js/views/action-editor.js +1 -1
  69. package/admin/js/views/actions-list.js +1 -1
  70. package/admin/js/views/api-endpoint-editor.js +2 -2
  71. package/admin/js/views/api-endpoints.js +2 -2
  72. package/admin/js/views/api-tokens.js +3 -3
  73. package/admin/js/views/blocks.js +2 -2
  74. package/admin/js/views/collection-editor.js +1 -1
  75. package/admin/js/views/collection-entries.js +1 -1
  76. package/admin/js/views/collections.js +1 -1
  77. package/admin/js/views/component-editor.js +1 -1
  78. package/admin/js/views/components.js +4 -4
  79. package/admin/js/views/dashboard/widgets/cache.js +1 -1
  80. package/admin/js/views/dashboard/widgets/journeys.js +1 -1
  81. package/admin/js/views/dashboard/widgets/kpi-strip.js +1 -1
  82. package/admin/js/views/form-editor.js +7 -7
  83. package/admin/js/views/form-submissions.js +1 -1
  84. package/admin/js/views/forms.js +1 -1
  85. package/admin/js/views/layouts.js +1 -1
  86. package/admin/js/views/login.js +2 -2
  87. package/admin/js/views/menu-editor.js +3 -3
  88. package/admin/js/views/menu-locations.js +1 -1
  89. package/admin/js/views/menus.js +3 -3
  90. package/admin/js/views/navigation.js +8 -8
  91. package/admin/js/views/page-editor.js +40 -40
  92. package/admin/js/views/pages.js +3 -3
  93. package/admin/js/views/plugin-code.js +2 -2
  94. package/admin/js/views/plugins.js +1 -1
  95. package/admin/js/views/project-settings.js +1 -1
  96. package/admin/js/views/projects.js +1 -1
  97. package/admin/js/views/roles.js +1 -1
  98. package/admin/js/views/user-editor.js +1 -1
  99. package/admin/js/views/users.js +3 -3
  100. package/admin/js/views/view-editor.js +1 -1
  101. package/admin/js/views/view-preview.js +1 -1
  102. package/admin/js/views/views-list.js +1 -1
  103. package/bin/cli.js +8 -8
  104. package/bin/lib/config-merge.js +5 -5
  105. package/bin/update.js +16 -16
  106. package/config/site.json +86 -86
  107. package/package.json +2 -2
  108. package/plugins/_template/admin/views/index.js +1 -1
  109. package/plugins/_template/config.js +1 -1
  110. package/plugins/_template/plugin.js +1 -1
  111. package/plugins/analytics/admin/templates/analytics.html +3 -3
  112. package/plugins/analytics/admin/views/analytics.js +1 -1
  113. package/plugins/analytics/config.js +1 -1
  114. package/plugins/analytics/plugin.js +4 -4
  115. package/plugins/analytics/plugin.json +1 -1
  116. package/plugins/analytics/public/inject-body.html +2 -2
  117. package/plugins/analytics/public/inject-head.html +1 -1
  118. package/plugins/blog/admin/templates/blog.html +4 -4
  119. package/plugins/blog/admin/views/blog.js +3 -3
  120. package/plugins/blog/admin/views/categories.js +2 -2
  121. package/plugins/blog/admin/views/comments.js +4 -4
  122. package/plugins/blog/admin/views/post-editor.js +1 -1
  123. package/plugins/blog/plugin.js +2 -2
  124. package/plugins/blog/plugin.json +4 -4
  125. package/plugins/contacts/admin/views/contacts.js +8 -8
  126. package/plugins/contacts/plugin.js +4 -4
  127. package/plugins/contacts/plugin.json +1 -1
  128. package/plugins/invoice/admin/templates/editor.html +6 -6
  129. package/plugins/invoice/admin/templates/index.html +4 -4
  130. package/plugins/invoice/admin/views/editor.js +3 -3
  131. package/plugins/invoice/admin/views/index.js +4 -4
  132. package/plugins/invoice/admin/views/issuers.js +1 -1
  133. package/plugins/invoice/admin/views/party-view.js +3 -3
  134. package/plugins/invoice/admin/views/receivers.js +1 -1
  135. package/plugins/invoice/config.js +1 -1
  136. package/plugins/invoice/plugin.js +1 -1
  137. package/plugins/invoice/plugin.json +4 -4
  138. package/plugins/notes/admin/views/notes.js +4 -4
  139. package/plugins/notes/plugin.js +1 -1
  140. package/plugins/notes/plugin.json +1 -1
  141. package/plugins/site-search/admin/templates/site-search.html +3 -3
  142. package/plugins/site-search/admin/views/site-search.js +1 -1
  143. package/plugins/site-search/config.js +1 -1
  144. package/plugins/site-search/plugin.js +4 -4
  145. package/plugins/site-search/plugin.json +1 -1
  146. package/plugins/surveys/admin/templates/results.html +1 -1
  147. package/plugins/surveys/admin/templates/survey-editor.html +4 -4
  148. package/plugins/surveys/admin/views/audience.js +6 -6
  149. package/plugins/surveys/admin/views/results.js +3 -3
  150. package/plugins/surveys/admin/views/survey-editor.js +1 -1
  151. package/plugins/surveys/admin/views/surveys.js +2 -2
  152. package/plugins/surveys/plugin.js +1 -1
  153. package/plugins/surveys/plugin.json +4 -4
  154. package/plugins/theme-switcher/admin/views/theme-switcher.js +1 -1
  155. package/plugins/theme-switcher/plugin.json +3 -3
  156. package/plugins/theme-switcher/public/inject-body.html +28 -28
  157. package/plugins/theme-switcher/public/inject-head.html +1 -1
  158. package/plugins/todo/plugin.js +1 -1
  159. package/public/css/forms.css +1 -1
  160. package/public/js/collection-browser.js +1 -1
  161. package/public/js/collection-context.js +1 -1
  162. package/public/js/form-logic-engine.js +1 -1
  163. package/public/js/forms.js +2 -1
  164. package/public/js/site.js +1 -1
  165. package/scripts/build.js +100 -6
  166. package/scripts/create-plugin.js +2 -2
  167. package/scripts/fresh.js +2 -2
  168. package/scripts/gen-instance-secret.js +1 -1
  169. package/scripts/pro.js +12 -12
  170. package/scripts/reset.js +2 -2
  171. package/scripts/setup.js +15 -15
  172. package/scripts/users.js +5 -5
  173. package/scripts/verify-assets.mjs +6 -6
  174. package/server/config.js +1 -1
  175. package/server/middleware/auth.js +11 -11
  176. package/server/middleware/managerAuth.js +3 -3
  177. package/server/routes/api/actions.js +4 -4
  178. package/server/routes/api/api-endpoints.js +7 -7
  179. package/server/routes/api/api-tokens.js +5 -5
  180. package/server/routes/api/auth.js +10 -10
  181. package/server/routes/api/blocks.js +3 -3
  182. package/server/routes/api/collections.js +18 -18
  183. package/server/routes/api/dashboard.js +5 -5
  184. package/server/routes/api/effects.js +2 -2
  185. package/server/routes/api/endpoints-public.js +5 -5
  186. package/server/routes/api/forms.js +47 -47
  187. package/server/routes/api/menu-locations.js +3 -3
  188. package/server/routes/api/menus.js +7 -7
  189. package/server/routes/api/notifications.js +2 -2
  190. package/server/routes/api/pages.js +14 -2
  191. package/server/routes/api/plugin-marketplace.js +3 -3
  192. package/server/routes/api/plugins.js +6 -6
  193. package/server/routes/api/projects.js +9 -9
  194. package/server/routes/api/scaffold.js +7 -7
  195. package/server/routes/api/settings.js +6 -6
  196. package/server/routes/api/sidebar.js +1 -1
  197. package/server/routes/api/users.js +5 -5
  198. package/server/routes/api/versions.js +1 -1
  199. package/server/routes/api/views.js +1 -1
  200. package/server/routes/docs-public.js +3 -3
  201. package/server/routes/public.js +14 -14
  202. package/server/server.js +34 -16
  203. package/server/services/actions.js +19 -19
  204. package/server/services/adapters/FileAdapter.js +2 -2
  205. package/server/services/adapters/MongoAdapter.js +5 -5
  206. package/server/services/apiEndpoints.js +15 -15
  207. package/server/services/apiTokens.js +7 -7
  208. package/server/services/blocks.js +13 -13
  209. package/server/services/cache/drivers/MemoryDriver.js +3 -3
  210. package/server/services/cache/drivers/NoneDriver.js +1 -1
  211. package/server/services/cache/index.js +5 -5
  212. package/server/services/collections.js +14 -14
  213. package/server/services/components.js +9 -9
  214. package/server/services/connectionManager.js +4 -4
  215. package/server/services/content.js +25 -2
  216. package/server/services/docs.js +14 -14
  217. package/server/services/email.js +1 -1
  218. package/server/services/filterEngine.js +6 -6
  219. package/server/services/forms.js +18 -10
  220. package/server/services/health.js +1 -1
  221. package/server/services/hooks.js +7 -7
  222. package/server/services/images.js +4 -4
  223. package/server/services/managerClient.js +1 -1
  224. package/server/services/markdown.js +79 -79
  225. package/server/services/menuRender.js +5 -5
  226. package/server/services/menus-migration.js +3 -3
  227. package/server/services/menus.js +31 -31
  228. package/server/services/permissionRegistry.js +1 -1
  229. package/server/services/pluginFiles.js +5 -5
  230. package/server/services/pluginInstaller.js +17 -17
  231. package/server/services/pluginScaffold.js +1 -1
  232. package/server/services/plugins.js +17 -17
  233. package/server/services/presetCollections.js +2 -2
  234. package/server/services/projects.js +35 -26
  235. package/server/services/recipes/contact-list.json +1 -1
  236. package/server/services/recipes/onboarding.json +8 -8
  237. package/server/services/references.js +8 -8
  238. package/server/services/renderer.js +9 -9
  239. package/server/services/roles.js +9 -9
  240. package/server/services/rowAccess.js +10 -10
  241. package/server/services/scaffolder.js +30 -30
  242. package/server/services/sidebar-migration.js +10 -10
  243. package/server/services/sitemap.js +1 -1
  244. package/server/services/userProfiles.js +4 -4
  245. package/server/services/userRoles.js +7 -7
  246. package/server/services/users.js +5 -5
  247. package/server/services/versions.js +3 -3
  248. package/server/services/viewPipeline.js +6 -6
  249. package/server/services/views.js +11 -11
  250. package/server/templates/page.html +5 -5
  251. package/config/navigation.json.bak +0 -31
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Scaffolder — one-shot creator for a working CRUD system from a recipe.
2
+ * Scaffolder - one-shot creator for a working CRUD system from a recipe.
3
3
  *
4
4
  * A "recipe" is a JSON file under `server/services/recipes/` that describes:
5
5
  * - one Collection (schema + initial api access rules)
@@ -14,7 +14,7 @@
14
14
  * and template variable.
15
15
  *
16
16
  * Atomicity: pre-flight check refuses to start if ANY target slug already
17
- * exists — keeps the user out of a partial-create state. If pre-flight passes
17
+ * exists - keeps the user out of a partial-create state. If pre-flight passes
18
18
  * we apply collection → form → actions in order. Action creation requires
19
19
  * MongoDB (Pro feature); when not configured we still complete the
20
20
  * collection + form and report actions as skipped with a warning.
@@ -64,7 +64,7 @@ export async function listRecipes() {
64
64
  options: r.options || []
65
65
  });
66
66
  } catch {
67
- /* skip malformed recipe — don't break the listing */
67
+ /* skip malformed recipe - don't break the listing */
68
68
  }
69
69
  }
70
70
  return recipes;
@@ -101,7 +101,7 @@ function substituteTokens(value, tokens) {
101
101
  if (typeof value === 'string') {
102
102
  return value.replace(/\{\{\s*([A-Za-z0-9_]+)\s*\}\}/g, (_m, key) => {
103
103
  // Leave action template variables (entry.*, user.*, now, env.*) untouched
104
- // — those are resolved at action execution time, not at scaffold time.
104
+ // - those are resolved at action execution time, not at scaffold time.
105
105
  if (key.includes('.') || key === 'now') return _m;
106
106
  if (key === 'entry' || key === 'user' || key === 'env') return _m;
107
107
  return tokens[key] != null ? String(tokens[key]) : _m;
@@ -117,18 +117,18 @@ function substituteTokens(value, tokens) {
117
117
  }
118
118
 
119
119
  /**
120
- * Resolve effective options for a recipe — recipe defaults merged with caller
120
+ * Resolve effective options for a recipe - recipe defaults merged with caller
121
121
  * overrides, with non-empty user values winning. Returns a plain `{ name: value }`
122
122
  * map suitable to pass to `substituteTokens`.
123
123
  *
124
124
  * **Default-chaining**: an option's `default` may template-reference earlier
125
- * options via `{{otherOption}}`. This is the "namespacing" affordance — set
125
+ * options via `{{otherOption}}`. This is the "namespacing" affordance - set
126
126
  * one `namespace` option and let `collectionSlug`, `formSlug`, `actionPrefix`
127
127
  * default to `{{namespace}}`, `{{namespace}}-form`, `{{namespace}}` etc. The
128
128
  * recipe author chooses which options participate; users can still override
129
129
  * any individual option to break the chain.
130
130
  *
131
- * Resolution is left-to-right over the options array — so the option being
131
+ * Resolution is left-to-right over the options array - so the option being
132
132
  * referenced MUST appear earlier in the array than the option that references
133
133
  * it. (If forward references become a recurring need we can topologically
134
134
  * sort, but the explicit-order rule keeps recipe files readable.)
@@ -160,7 +160,7 @@ function resolveOptions(recipe, userOverrides = {}) {
160
160
  * Check whether anything the recipe wants to create already exists. Returns a
161
161
  * list of human-readable conflict descriptions; empty array means clear.
162
162
  *
163
- * Action conflict checks are skipped silently when MongoDB isn't configured —
163
+ * Action conflict checks are skipped silently when MongoDB isn't configured -
164
164
  * the apply phase will degrade those into warnings rather than blocking.
165
165
  *
166
166
  * @param {object} resolvedRecipe
@@ -188,12 +188,12 @@ async function preflight(resolvedRecipe) {
188
188
  const existing = await getAction(action.slug);
189
189
  if (existing) conflicts.push(`Action "${action.slug}" already exists`);
190
190
  } catch {
191
- // Mongo unavailable — treat as not-conflicting; the apply step
191
+ // Mongo unavailable - treat as not-conflicting; the apply step
192
192
  // will report actions as skipped instead.
193
193
  }
194
194
  }
195
195
 
196
- // Roles + users are NOT treated as pre-flight conflicts — they're a
196
+ // Roles + users are NOT treated as pre-flight conflicts - they're a
197
197
  // looser kind of resource that frequently already exists (re-running a
198
198
  // recipe, admin-created role, etc.) and where overwriting would lose
199
199
  // data the user cares about (existing role's permissions tweaks, user's
@@ -247,7 +247,7 @@ export async function applyRecipe(recipeSlug, opts = {}) {
247
247
  }
248
248
  }
249
249
 
250
- // Project — auto-create if the recipe declares one and it doesn't exist.
250
+ // Project - auto-create if the recipe declares one and it doesn't exist.
251
251
  // Uses the recipe's `namespace` token as the project slug, so a single user
252
252
  // choice ties together every artefact's `meta.project` tag below.
253
253
  let projectSlug = null;
@@ -273,7 +273,7 @@ export async function applyRecipe(recipeSlug, opts = {}) {
273
273
 
274
274
  const conflicts = await preflight(resolved);
275
275
  if (conflicts.length) {
276
- const err = new Error(`Cannot apply recipe — conflicts:\n - ${conflicts.join('\n - ')}`);
276
+ const err = new Error(`Cannot apply recipe - conflicts:\n - ${conflicts.join('\n - ')}`);
277
277
  err.statusCode = 409;
278
278
  err.conflicts = conflicts;
279
279
  throw err;
@@ -283,16 +283,16 @@ export async function applyRecipe(recipeSlug, opts = {}) {
283
283
  const skipped = [];
284
284
  const warnings = [];
285
285
 
286
- // 0. Roles — create FIRST so subsequent actions can reference them by name.
286
+ // 0. Roles - create FIRST so subsequent actions can reference them by name.
287
287
  // If a role with the same name already exists (re-run, admin-created),
288
- // we skip silently rather than failing — the role's existence is what
288
+ // we skip silently rather than failing - the role's existence is what
289
289
  // matters for the actions that reference it, not who created it.
290
290
  const existingRoles = getRoleMap();
291
291
  for (const role of (resolved.roles || [])) {
292
292
  if (!role.name) continue;
293
293
  if (existingRoles.has(role.name)) {
294
294
  skipped.push(`role:${role.name}`);
295
- warnings.push(`Role "${role.name}" already exists — using existing definition (recipe-defined permissions not applied)`);
295
+ warnings.push(`Role "${role.name}" already exists - using existing definition (recipe-defined permissions not applied)`);
296
296
  continue;
297
297
  }
298
298
  try {
@@ -310,7 +310,7 @@ export async function applyRecipe(recipeSlug, opts = {}) {
310
310
  }
311
311
  }
312
312
 
313
- // 0b. Menus — write each declared menu and, if requested, map it to a slot.
313
+ // 0b. Menus - write each declared menu and, if requested, map it to a slot.
314
314
  for (const menu of (resolved.menus || [])) {
315
315
  if (!menu.slug) continue;
316
316
  try {
@@ -331,7 +331,7 @@ export async function applyRecipe(recipeSlug, opts = {}) {
331
331
  let touched = false;
332
332
  for (const slot of menu.locations) {
333
333
  if (map[slot] && map[slot] !== menu.slug && !menu.force) {
334
- warnings.push(`Slot "${slot}" already mapped to "${map[slot]}" — left unchanged (set force:true to overwrite)`);
334
+ warnings.push(`Slot "${slot}" already mapped to "${map[slot]}" - left unchanged (set force:true to overwrite)`);
335
335
  continue;
336
336
  }
337
337
  map[slot] = menu.slug;
@@ -358,11 +358,11 @@ export async function applyRecipe(recipeSlug, opts = {}) {
358
358
  api: resolved.collection.api,
359
359
  // rowAccess is a top-level schema field; createCollection accepts arbitrary keys via spread,
360
360
  // but to be explicit we set it on the schema after create. Simpler: write it via the schema
361
- // file directly — createCollection already writes the schema, so we re-read + patch + write.
361
+ // file directly - createCollection already writes the schema, so we re-read + patch + write.
362
362
  ...(projectSlug && {meta: {project: projectSlug}})
363
363
  });
364
364
 
365
- // Patch in rowAccess (if any) — collections.createCollection doesn't take it directly
365
+ // Patch in rowAccess (if any) - collections.createCollection doesn't take it directly
366
366
  if (resolved.collection.rowAccess) {
367
367
  const {getCollection: get, updateCollection} = await import('./collections.js');
368
368
  const cur = await get(tokens.collectionSlug);
@@ -386,7 +386,7 @@ export async function applyRecipe(recipeSlug, opts = {}) {
386
386
  created.form = tokens.formSlug;
387
387
  }
388
388
 
389
- // 2b. Users — recipe-defined seed accounts. Password is mandatory (the
389
+ // 2b. Users - recipe-defined seed accounts. Password is mandatory (the
390
390
  // scaffolder won't invent one); if omitted, the user is reported as
391
391
  // skipped so the admin knows to create them manually. Auto-creating
392
392
  // accounts with a default password would be a meaningful security trap.
@@ -398,14 +398,14 @@ export async function applyRecipe(recipeSlug, opts = {}) {
398
398
  const existing = await getUserByEmail(u.email);
399
399
  if (existing) {
400
400
  skipped.push(`user:${u.email}`);
401
- warnings.push(`User "${u.email}" already exists — left unchanged`);
401
+ warnings.push(`User "${u.email}" already exists - left unchanged`);
402
402
  continue;
403
403
  }
404
404
  } catch { /* no-op */ }
405
405
 
406
406
  if (!u.password) {
407
407
  skipped.push(`user:${u.email}`);
408
- warnings.push(`User "${u.email}" skipped — no password set in recipe. Create the account manually under Users → New.`);
408
+ warnings.push(`User "${u.email}" skipped - no password set in recipe. Create the account manually under Users → New.`);
409
409
  continue;
410
410
  }
411
411
  try {
@@ -423,7 +423,7 @@ export async function applyRecipe(recipeSlug, opts = {}) {
423
423
  }
424
424
  }
425
425
 
426
- // 3. Actions — best-effort; skip with warning when Mongo isn't available
426
+ // 3. Actions - best-effort; skip with warning when Mongo isn't available
427
427
  for (const action of (resolved.actions || [])) {
428
428
  if (!action.slug) continue;
429
429
  try {
@@ -441,7 +441,7 @@ export async function applyRecipe(recipeSlug, opts = {}) {
441
441
  created.actions.push(a.slug);
442
442
  } catch (err) {
443
443
  const reason = err.message.includes('MongoDB') || err.message.includes('Mongo')
444
- ? `MongoDB not configured — action "${action.slug}" skipped (Pro feature)`
444
+ ? `MongoDB not configured - action "${action.slug}" skipped (Pro feature)`
445
445
  : `Action "${action.slug}" failed: ${err.message}`;
446
446
  skipped.push(action.slug);
447
447
  warnings.push(reason);
@@ -449,17 +449,17 @@ export async function applyRecipe(recipeSlug, opts = {}) {
449
449
  }
450
450
 
451
451
  // Wire the form's actionSlug to the first created action if the recipe
452
- // wanted it — convenient for "submit this form, fire that action" patterns.
452
+ // wanted it - convenient for "submit this form, fire that action" patterns.
453
453
  if (created.form && created.actions.length && resolved.form?.settings?.actionSlug === '') {
454
454
  try {
455
455
  const formDef = await readForm(created.form);
456
456
  formDef.settings = formDef.settings || {};
457
457
  formDef.settings.actionSlug = created.actions[0];
458
458
  await writeForm(created.form, formDef);
459
- } catch { /* non-fatal — admin can wire it manually */ }
459
+ } catch { /* non-fatal - admin can wire it manually */ }
460
460
  }
461
461
 
462
- // API tokens — generated at apply time; the plaintext rides ONCE in
462
+ // API tokens - generated at apply time; the plaintext rides ONCE in
463
463
  // `created.apiTokens[].token` (only a hash is stored). Idempotent:
464
464
  // re-applying never re-issues an existing name+project token, so the
465
465
  // credential the user already deployed survives recipe re-runs.
@@ -471,7 +471,7 @@ export async function applyRecipe(recipeSlug, opts = {}) {
471
471
  if (!t.name) continue;
472
472
  if (await findTokenByName(t.name, tokenProject)) {
473
473
  skipped.push(`apiToken:${t.name}`);
474
- warnings.push(`API token "${t.name}" already exists for project "${tokenProject}" — left unchanged (token value NOT re-issued)`);
474
+ warnings.push(`API token "${t.name}" already exists for project "${tokenProject}" - left unchanged (token value NOT re-issued)`);
475
475
  continue;
476
476
  }
477
477
  try {
@@ -489,7 +489,7 @@ export async function applyRecipe(recipeSlug, opts = {}) {
489
489
  }
490
490
  }
491
491
 
492
- // Custom API endpoints — declared as definition objects (path, collection,
492
+ // Custom API endpoints - declared as definition objects (path, collection,
493
493
  // filter, ...). Idempotent: an existing (project, path-shape) match is
494
494
  // skipped so re-applying a recipe never clobbers a tuned definition.
495
495
  const endpointDecls = resolved.apiEndpoints ? [].concat(resolved.apiEndpoints) : [];
@@ -500,7 +500,7 @@ export async function applyRecipe(recipeSlug, opts = {}) {
500
500
  if (!ep.path) continue;
501
501
  if (await findEndpointByPath(epProject, ep.path)) {
502
502
  skipped.push(`apiEndpoint:${ep.path}`);
503
- warnings.push(`API endpoint "${ep.path}" already exists for project "${epProject}" — left unchanged`);
503
+ warnings.push(`API endpoint "${ep.path}" already exists for project "${epProject}" - left unchanged`);
504
504
  continue;
505
505
  }
506
506
  try {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Admin sidebar auto-migration.
3
3
  *
4
- * Runs once on server boot. Idempotent — guarded by the existence of
4
+ * Runs once on server boot. Idempotent - guarded by the existence of
5
5
  * config/menus/admin-sidebar.json. Seeds the admin sidebar menu with the
6
6
  * standard tree (Overview, Projects, Content, Data, System, Documentation)
7
7
  * and patches config/menu-locations.json to map the `admin-sidebar` slot.
@@ -148,7 +148,7 @@ async function readJson(p) {
148
148
 
149
149
  async function writeJson(p, data) {
150
150
  await fs.writeFile(p, JSON.stringify(data, null, 2) + '\n', 'utf8');
151
- // Menu files written outside the menus service — drop its cached index so
151
+ // Menu files written outside the menus service - drop its cached index so
152
152
  // binding resolution sees the seeded sidebar menu.
153
153
  invalidateMenuIndex();
154
154
  }
@@ -180,7 +180,7 @@ export async function runMigration(opts = {}) {
180
180
 
181
181
  let locations = {};
182
182
  if (await exists(locPath)) {
183
- try { locations = await readJson(locPath); } catch { /* malformed — start fresh */ }
183
+ try { locations = await readJson(locPath); } catch { /* malformed - start fresh */ }
184
184
  }
185
185
  locations['admin-sidebar'] = 'admin-sidebar';
186
186
  await writeJson(locPath, locations);
@@ -193,8 +193,8 @@ export async function runMigration(opts = {}) {
193
193
  * Append-if-absent: ensure a single item exists in the persisted admin
194
194
  * sidebar menu. Lets updates surface new admin pages on EXISTING installs
195
195
  * (the seed above only reaches fresh ones) without clobbering admin edits:
196
- * if any node in the tree already has the item's url — even moved, renamed,
197
- * or hidden — this is a no-op.
196
+ * if any node in the tree already has the item's url - even moved, renamed,
197
+ * or hidden - this is a no-op.
198
198
  *
199
199
  * Returns `{ensured: boolean, reason?: string}`.
200
200
  *
@@ -237,13 +237,13 @@ export async function ensureSidebarItem({groupText, item}, opts = {}) {
237
237
  * per-topic submenus. Idempotent and non-clobbering:
238
238
  *
239
239
  * - If the persisted menu has no "Documentation" group, do nothing (the admin
240
- * may have removed it deliberately — don't resurrect it).
241
- * - Convert each flat leaf — "Usage" (`#/documentation`), "Tutorials"
242
- * (`#/tutorials`) and "API Reference" (`#/api-reference`) — into its
240
+ * may have removed it deliberately - don't resurrect it).
241
+ * - Convert each flat leaf - "Usage" (`#/documentation`), "Tutorials"
242
+ * (`#/tutorials`) and "API Reference" (`#/api-reference`) - into its
243
243
  * submenu, replacing the node in place so its position is preserved. A leaf
244
244
  * that the admin already turned into a parent, or whose new pages already
245
245
  * appear anywhere in the group, is left untouched.
246
- * - Add the Components submenu only if `#/docs/components` isn't present yet —
246
+ * - Add the Components submenu only if `#/docs/components` isn't present yet -
247
247
  * inserted before "API Reference" when that node exists.
248
248
  *
249
249
  * Returns `{updated: boolean, reason?: string}`.
@@ -290,7 +290,7 @@ export async function ensureDocumentationSubmenus(opts = {}) {
290
290
  changed = true;
291
291
  }
292
292
 
293
- // Components submenu — add if absent, before API Reference.
293
+ // Components submenu - add if absent, before API Reference.
294
294
  if (!hasUrl(docGroup.items, '#/docs/components')) {
295
295
  const apiIdx = findByText('API Reference');
296
296
  const node = DOC_COMPONENTS_SUBMENU();
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Sitemap Service
3
3
  * Builds sitemap.xml from the page list. Filter logic lives in
4
- * shouldIncludeInSitemap() — see the TODO below.
4
+ * shouldIncludeInSitemap() - see the TODO below.
5
5
  *
6
6
  * Cache: callers should wrap generate() with the 'sitemap' tag so it
7
7
  * invalidates whenever a page is created, updated, renamed, or deleted.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * User Profiles Service
3
- * Preset Collection — stores extended profile data for each user,
3
+ * Preset Collection - stores extended profile data for each user,
4
4
  * keyed by user UUID. Seeded on startup; entries auto-managed on user lifecycle.
5
5
  */
6
6
  import fs from 'fs/promises';
@@ -70,7 +70,7 @@ async function writeData(entries) {
70
70
  export async function seed() {
71
71
  await fs.mkdir(DIR, {recursive: true});
72
72
 
73
- // Only write schema if absent — admin edits to fields must be preserved across restarts
73
+ // Only write schema if absent - admin edits to fields must be preserved across restarts
74
74
  try {
75
75
  await fs.access(SCHEMA_PATH);
76
76
  } catch {
@@ -164,7 +164,7 @@ export async function deleteProfile(userId) {
164
164
 
165
165
  /**
166
166
  * Return a Map of userId → profile data object.
167
- * Reads data.json once — use this for list endpoints to avoid N+1 reads.
167
+ * Reads data.json once - use this for list endpoints to avoid N+1 reads.
168
168
  *
169
169
  * @returns {Promise<Map<string, object>>}
170
170
  */
@@ -178,7 +178,7 @@ export async function listProfiles() {
178
178
  }
179
179
 
180
180
  /**
181
- * Migration helper — ensure every existing user has a profile entry.
181
+ * Migration helper - ensure every existing user has a profile entry.
182
182
  * Called once on startup after seed().
183
183
  *
184
184
  * @returns {Promise<void>}
@@ -1,21 +1,21 @@
1
1
  /**
2
- * User Roles — multi-role support helpers.
2
+ * User Roles - multi-role support helpers.
3
3
  *
4
4
  * A user has ONE primary role (`user.role`) and an optional array of
5
5
  * additional roles (`user.additionalRoles`). Permission, visibility, and
6
6
  * row-access checks consult ALL of them and grant access if any one role
7
- * satisfies — "any path wins" semantics that matches how most apps actually
7
+ * satisfies - "any path wins" semantics that matches how most apps actually
8
8
  * model multi-membership (you can be both a candidate AND a recruiter).
9
9
  *
10
10
  * Hierarchical-level comparisons (`canManageUser`, `requireAdmin`, the
11
11
  * level-clamp in `checkSingleVisibility`) use `getEffectiveLevel` which
12
- * returns the LOWEST level number across all the user's roles — i.e. the
12
+ * returns the LOWEST level number across all the user's roles - i.e. the
13
13
  * highest privilege the user has access to. This is what makes "additional
14
14
  * admin" meaningfully grant admin-tier access without rewriting the level
15
15
  * arithmetic everywhere.
16
16
  *
17
17
  * Backward compatibility: users with no `additionalRoles` field behave
18
- * exactly as before — `getEffectiveRoles` returns `[user.role]`, `getEffectiveLevel`
18
+ * exactly as before - `getEffectiveRoles` returns `[user.role]`, `getEffectiveLevel`
19
19
  * returns `getRoleLevel(user.role)`. Existing callers that read `user.role`
20
20
  * directly continue to work; the new helpers are opt-in for callers that
21
21
  * want the union semantics.
@@ -40,7 +40,7 @@ export function getEffectiveRoles(user) {
40
40
  }
41
41
 
42
42
  /**
43
- * Return the effective role level for a user — the LOWEST level number
43
+ * Return the effective role level for a user - the LOWEST level number
44
44
  * across all roles. Lower number = higher privilege, so the user's most
45
45
  * privileged role wins all hierarchical checks.
46
46
  *
@@ -57,7 +57,7 @@ export function getEffectiveLevel(user) {
57
57
 
58
58
  /**
59
59
  * Check whether a user holds a specific role (primary or additional).
60
- * Exact-name match — does NOT walk the hierarchy. For hierarchical "user
60
+ * Exact-name match - does NOT walk the hierarchy. For hierarchical "user
61
61
  * is at least this level" checks use `getEffectiveLevel` against the target
62
62
  * role's level instead.
63
63
  *
@@ -72,7 +72,7 @@ export function userHasRole(user, role) {
72
72
 
73
73
  /**
74
74
  * Check whether a user holds ANY of the given roles. Convenience helper
75
- * for "anyone in this set can do X" patterns — equivalent to OR-ing
75
+ * for "anyone in this set can do X" patterns - equivalent to OR-ing
76
76
  * `userHasRole` over the list.
77
77
  *
78
78
  * @param {object|null} user
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * User Storage Service
3
- * File-based CRUD — one JSON file per user in content/users/{id}.json.
3
+ * File-based CRUD - one JSON file per user in content/users/{id}.json.
4
4
  * Mirrors the page-per-file pattern from the content service.
5
5
  */
6
6
  import fs from 'fs/promises';
@@ -96,7 +96,7 @@ export async function getUserById(id) {
96
96
  }
97
97
 
98
98
  /**
99
- * Get a user by email address — includes password hash (needed for login).
99
+ * Get a user by email address - includes password hash (needed for login).
100
100
  *
101
101
  * @param {string} email
102
102
  * @returns {Promise<object|null>}
@@ -135,7 +135,7 @@ export async function createUser({ name, email, password, role = 'user', additio
135
135
  name: name.trim(),
136
136
  password: hash,
137
137
  role,
138
- // Additional roles beyond the primary — permission/visibility checks
138
+ // Additional roles beyond the primary - permission/visibility checks
139
139
  // grant access if ANY role satisfies. The primary `role` is what
140
140
  // appears in UI badges and is used for hierarchical level comparisons
141
141
  // when a single value is needed.
@@ -180,7 +180,7 @@ export async function updateUser(id, updates) {
180
180
 
181
181
  await writeUserFile(updated);
182
182
 
183
- // Invalidate per-user cache when the project access-scope changes — downstream
183
+ // Invalidate per-user cache when the project access-scope changes - downstream
184
184
  // request handlers cache scope-filtered responses keyed by `user:<id>`.
185
185
  if (updates.projects !== undefined) {
186
186
  const before = JSON.stringify(user.projects || []);
@@ -208,7 +208,7 @@ export async function deleteUser(id) {
208
208
  try {
209
209
  await deleteProfile(id);
210
210
  } catch {
211
- // Profile deletion is best-effort — never block user deletion
211
+ // Profile deletion is best-effort - never block user deletion
212
212
  }
213
213
  }
214
214
 
@@ -33,7 +33,7 @@ function versionsDirForPath(urlPath) {
33
33
  }
34
34
 
35
35
  /**
36
- * Validate a version filename — must not contain path separators and must end in .md.
36
+ * Validate a version filename - must not contain path separators and must end in .md.
37
37
  *
38
38
  * @param {string} filename
39
39
  * @throws {Error}
@@ -120,7 +120,7 @@ export async function createVersion(urlPath, {author = 'system', type = 'auto',
120
120
  try {
121
121
  content = await fs.readFile(livePath, 'utf8');
122
122
  } catch {
123
- return null; // Page doesn't exist yet — nothing to snapshot
123
+ return null; // Page doesn't exist yet - nothing to snapshot
124
124
  }
125
125
 
126
126
  const dir = versionsDirForPath(urlPath);
@@ -182,7 +182,7 @@ export async function pruneVersions(urlPath, {keep = 5} = {}) {
182
182
  const dir = versionsDirForPath(urlPath);
183
183
  const meta = await readMeta(dir);
184
184
 
185
- // Newest first — index 0 is the most recent snapshot
185
+ // Newest first - index 0 is the most recent snapshot
186
186
  const sorted = [...meta].sort((a, b) => b.createdAt.localeCompare(a.createdAt));
187
187
 
188
188
  // Manually-named snapshots are always preserved (deliberate restore points).
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Executes the Views feature's allowed aggregation-stage subset in plain
5
5
  * JavaScript so views work on file-backed collections (no MongoDB needed).
6
- * MongoDB-backed sources keep using native aggregation in views.js — this
6
+ * MongoDB-backed sources keep using native aggregation in views.js - this
7
7
  * module exists so the SAME view definition produces the same results on
8
8
  * either storage adapter. Documents are the raw entry shape ({id, data,
9
9
  * meta}) and field paths are dot-paths ('data.price', 'meta.createdAt').
@@ -14,7 +14,7 @@
14
14
  * that visibly "should" match. (Same trade filterEngine makes.)
15
15
  * - $sort places missing/null values LAST regardless of direction (Mongo
16
16
  * treats missing as lowest).
17
- * - $addFields supports literals and '$path' references only — operator
17
+ * - $addFields supports literals and '$path' references only - operator
18
18
  * expressions throw a clear error rather than mis-evaluating.
19
19
  *
20
20
  * Pure module: no imports from other services. $lookup resolves foreign
@@ -103,7 +103,7 @@ function deletePath(target, path) {
103
103
  if (typeof node === 'object' && node !== null) delete node[parts[parts.length - 1]];
104
104
  }
105
105
 
106
- /** Number('') is 0 — treat empty strings as non-numeric like filterEngine. */
106
+ /** Number('') is 0 - treat empty strings as non-numeric like filterEngine. */
107
107
  function maybeNumber(value) {
108
108
  if (typeof value === 'number') return value;
109
109
  if (typeof value === 'string' && value.trim() !== '' && Number.isFinite(Number(value))) {
@@ -342,7 +342,7 @@ function evalProject(docs, config) {
342
342
 
343
343
  return docs.map((doc) => {
344
344
  const out = {};
345
- // id carried unless explicitly excluded ({..., id: 0} — mongo's _id exception)
345
+ // id carried unless explicitly excluded ({..., id: 0} - mongo's _id exception)
346
346
  const idExcluded = entries.some(([k, v]) => (k === 'id' || k === '_id') && (v === 0 || v === false));
347
347
  if (!idExcluded && doc.id !== undefined) out.id = doc.id;
348
348
  for (const [path, spec] of inclusions) {
@@ -522,7 +522,7 @@ async function evalLookup(docs, config, resolveCollection, lookupCache) {
522
522
  throw new ViewPipelineError('$lookup requires a collection resolver', '$lookup');
523
523
  }
524
524
 
525
- // Mongo-authored views reference `cms_<slug>` collection names — accept both.
525
+ // Mongo-authored views reference `cms_<slug>` collection names - accept both.
526
526
  const slug = String(from).replace(/^cms_/, '');
527
527
  if (!lookupCache.has(slug)) {
528
528
  lookupCache.set(slug, await resolveCollection(slug));
@@ -548,7 +548,7 @@ async function evalLookup(docs, config, resolveCollection, lookupCache) {
548
548
 
549
549
  /**
550
550
  * Execute a pipeline (mongo-form stage objects) over an array of documents.
551
- * Returns the FULL result set — the caller paginates.
551
+ * Returns the FULL result set - the caller paginates.
552
552
  *
553
553
  * @param {object[]} docs
554
554
  * @param {object[]} stages - [{$match: {...}}, {$sort: {...}}, ...]
@@ -10,12 +10,12 @@
10
10
  * - everything else → the built-in JS pipeline evaluator (viewPipeline.js)
11
11
  * over the collection adapter's documents
12
12
  *
13
- * The same view definition therefore works on both storage adapters —
13
+ * The same view definition therefore works on both storage adapters -
14
14
  * field paths are raw-document dot-paths ('data.price', 'meta.createdAt')
15
15
  * in both cases.
16
16
  *
17
17
  * Older installs that stored view configs in the MongoDB `cms__views`
18
- * collection are migrated to files automatically (one-time, read-only —
18
+ * collection are migrated to files automatically (one-time, read-only -
19
19
  * the Mongo collection is never modified).
20
20
  *
21
21
  * Allowed aggregation stage types:
@@ -40,10 +40,10 @@ export const VIEWS_DIR = path.join(ROOT, 'content', 'views');
40
40
  /** Legacy MongoDB collection where pre-0.34 view configs were stored. */
41
41
  const LEGACY_VIEWS_COLLECTION = 'cms__views';
42
42
 
43
- /** Prefix applied to CMS data collections — mirrors MongoAdapter.PREFIX. */
43
+ /** Prefix applied to CMS data collections - mirrors MongoAdapter.PREFIX. */
44
44
  const COLLECTION_PREFIX = 'cms_';
45
45
 
46
- /** Filename/lookup guard — view slugs are slugified, so keep this strict. */
46
+ /** Filename/lookup guard - view slugs are slugified, so keep this strict. */
47
47
  const SLUG_RE = /^[a-z0-9-]+$/;
48
48
 
49
49
  const ALLOWED_STAGES = new Set([
@@ -103,7 +103,7 @@ function buildPipelineStages(stages) {
103
103
  // ---------------------------------------------------------------------------
104
104
 
105
105
  /**
106
- * Ensure the views directory exists. Lazy — engine updates never touch
106
+ * Ensure the views directory exists. Lazy - engine updates never touch
107
107
  * content/, so the dir is created on first write.
108
108
  *
109
109
  * @returns {Promise<void>}
@@ -113,7 +113,7 @@ export async function ensureViewsDir() {
113
113
  }
114
114
 
115
115
  /**
116
- * Read a single view config by slug (no migration hook — internal/raw).
116
+ * Read a single view config by slug (no migration hook - internal/raw).
117
117
  *
118
118
  * @param {string} slug
119
119
  * @returns {Promise<object>}
@@ -164,14 +164,14 @@ export async function migrateFromMongo({ getDbImpl } = {}) {
164
164
  try {
165
165
  const entries = await fs.readdir(VIEWS_DIR);
166
166
  if (entries.some(e => e.endsWith('.json'))) return;
167
- } catch { /* dir absent — proceed */ }
167
+ } catch { /* dir absent - proceed */ }
168
168
 
169
169
  let db;
170
170
  try {
171
171
  const getDb = getDbImpl ?? (await import('./connectionManager.js')).getDb;
172
172
  db = getDb('default');
173
173
  } catch {
174
- return; // file-mode install — nothing to migrate
174
+ return; // file-mode install - nothing to migrate
175
175
  }
176
176
 
177
177
  let docs;
@@ -238,7 +238,7 @@ export async function listViews() {
238
238
  export async function getView(slug) {
239
239
  await ensureMigrated();
240
240
 
241
- // Strict slug guard — this is hit with raw URL params (public route) and
241
+ // Strict slug guard - this is hit with raw URL params (public route) and
242
242
  // the slug becomes a filename.
243
243
  if (typeof slug !== 'string' || !SLUG_RE.test(slug)) return null;
244
244
 
@@ -414,7 +414,7 @@ async function executeMongoView(view, { page, limit, user }) {
414
414
 
415
415
  /**
416
416
  * Execute a view against any other source via the JS pipeline evaluator.
417
- * The full collection and full pipeline output are materialised in memory —
417
+ * The full collection and full pipeline output are materialised in memory -
418
418
  * same trade the FileAdapter already makes for whole collections.
419
419
  */
420
420
  async function executeFileView(view, { page, limit, user }) {
@@ -456,7 +456,7 @@ export async function executeView(slug, {page = 1, limit = 25, user = null} = {}
456
456
  validateStages(view.pipeline?.stages || []);
457
457
 
458
458
  // Dispatch on the SOURCE collection's storage adapter. A mongodb-declared
459
- // schema whose connection is unavailable falls back to the JS evaluator —
459
+ // schema whose connection is unavailable falls back to the JS evaluator -
460
460
  // matching adapterRegistry's own file fallback.
461
461
  let useMongo = false;
462
462
  try {
@@ -18,7 +18,7 @@
18
18
 
19
19
  <!-- Site CSS -->
20
20
  <link rel="stylesheet" href="/public/css/site.css">
21
- <link rel="stylesheet" href="/public/css/forms.css?v=20260817-multi-chooser">
21
+ <link rel="stylesheet" href="/public/css/forms.css?v=20260821-wizard-steps">
22
22
 
23
23
  <!-- Font overrides -->
24
24
  {{fontStyleTag}}
@@ -26,7 +26,7 @@
26
26
  <!-- Plugin head injection -->
27
27
  {{headInject}}
28
28
 
29
- <!-- Late head injection — custom CSS always loads last so it can override everything -->
29
+ <!-- Late head injection - custom CSS always loads last so it can override everything -->
30
30
  {{headInjectLate}}
31
31
  </head>
32
32
  <body class="dm-cloaked dm-theme-{{theme}} {{layoutBodyClass}}" data-layout="{{layout}}">
@@ -116,7 +116,7 @@
116
116
  </script>
117
117
 
118
118
  <!-- Site initialisation -->
119
- <script src="/public/js/site.js?v=20260817-menu-bindings" type="module"></script>
119
+ <script src="/public/js/site.js?v=20260821-wizard-steps" type="module"></script>
120
120
  <script src="/public/js/collection-context.js?v=20260819-export" type="module"></script>
121
121
 
122
122
  <!-- Interactive Collection Browser (used by [collection] shortcodes with
@@ -124,8 +124,8 @@
124
124
  <script src="/public/js/collection-browser.js?v=20260523-browser"></script>
125
125
 
126
126
  <!-- Core Forms (logic engine must load before renderer) -->
127
- <script src="/public/js/form-logic-engine.js?v=20260817-transition-speed"></script>
128
- <script src="/public/js/forms.js?v=20260817-multi-chooser" type="module"></script>
127
+ <script src="/public/js/form-logic-engine.js?v=20260821-wizard-steps"></script>
128
+ <script src="/public/js/forms.js?v=20260821-wizard-steps" type="module"></script>
129
129
 
130
130
  <!-- Core effects runtime -->
131
131
  <script src="/public/js/effects.js"></script>