domma-cms 1.6.59 → 1.7.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 (143) hide show
  1. package/CHANGELOG.md +4344 -0
  2. package/README.md +1 -1
  3. package/admin/dist/domma/domma-tools.css +3 -3
  4. package/admin/dist/domma/domma-tools.min.js +3 -3
  5. package/admin/js/api.js +1 -1
  6. package/admin/js/app.js +2 -2
  7. package/admin/js/lib/draft-owner.js +1 -0
  8. package/admin/js/lib/effect-defs.js +1 -1
  9. package/admin/js/lib/effects-builder.js +2 -2
  10. package/admin/js/lib/page-editor-reference.js +5 -5
  11. package/admin/js/lib/shortcode-tags.js +1 -1
  12. package/admin/js/templates/docs/usage-shortcodes.html +39 -0
  13. package/admin/js/views/user-editor.js +1 -1
  14. package/docs/DommaFeatures.md +3005 -0
  15. package/docs/MultiTenant.md +176 -0
  16. package/docs/api-reference.md +1667 -0
  17. package/docs/cache.md +96 -0
  18. package/docs/configuration.md +1811 -0
  19. package/docs/editions-and-licences.md +190 -0
  20. package/docs/getting-started.md +201 -0
  21. package/docs/guides/form-triggers.md +260 -0
  22. package/docs/guides/mongodb-storage-adapter.md +282 -0
  23. package/docs/guides/multi-page-forms.md +121 -0
  24. package/docs/images/scribe-composer.png +0 -0
  25. package/docs/markdown-shortcodes.md +1738 -0
  26. package/docs/pentest-report-2026-04-03.md +181 -0
  27. package/docs/plans/2026-02-28-admin-topbar-login-forms.md +349 -0
  28. package/docs/plans/2026-02-28-domma-ui-polish.md +286 -0
  29. package/docs/plans/2026-02-28-reset-seed-fresh-commands.md +520 -0
  30. package/docs/plans/2026-03-02-cookie-consent-design.md +86 -0
  31. package/docs/plans/2026-03-02-cookie-consent-plugin.md +453 -0
  32. package/docs/plans/2026-03-02-tooltip-sweep-and-back-to-top-plugin-design.md +126 -0
  33. package/docs/plans/2026-03-02-tooltip-sweep-back-to-top.md +427 -0
  34. package/docs/plans/2026-03-05-cookie-consent-btt-core.md +115 -0
  35. package/docs/plans/2026-03-05-custom-css-migrate-to-core-design.md +35 -0
  36. package/docs/plans/2026-03-05-custom-css-migrate-to-core.md +352 -0
  37. package/docs/plans/2026-03-05-scribe-script-shortcode.md +373 -0
  38. package/docs/plans/2026-03-05-user-types-design.md +139 -0
  39. package/docs/plans/2026-03-05-user-types-implementation.md +1035 -0
  40. package/docs/plans/2026-03-09-page-editor-view-cta-inserts-design.md +78 -0
  41. package/docs/plans/2026-03-09-page-editor-view-cta-inserts.md +388 -0
  42. package/docs/plans/2026-03-09-pro-documentation-design.md +80 -0
  43. package/docs/plans/2026-03-09-pro-documentation.md +1017 -0
  44. package/docs/plans/2026-06-10-core-project-design.md +240 -0
  45. package/docs/plugin-development.md +827 -0
  46. package/docs/recruitment-recipe.md +501 -0
  47. package/docs/scaffolding.md +312 -0
  48. package/docs/screenshots/calculator-floating.png +0 -0
  49. package/docs/screenshots/calculator-standard.png +0 -0
  50. package/docs/screenshots/email-settings.png +0 -0
  51. package/docs/screenshots/messages-count.png +0 -0
  52. package/docs/structure/user-types.md +107 -0
  53. package/docs/superpowers/plans/2026-05-25-admin-sidebar-via-menus.md +1112 -0
  54. package/docs/superpowers/plans/2026-06-08-navbar-mobile-drawer-and-menu-enrichment.md +1161 -0
  55. package/docs/superpowers/plans/2026-06-25-domma-docs-project.md +695 -0
  56. package/docs/superpowers/plans/2026-06-26-surveys-HANDOFF.md +68 -0
  57. package/docs/superpowers/plans/2026-06-26-surveys-pro-plugin.md +1902 -0
  58. package/docs/superpowers/plans/2026-08-19-collection-context-sort-export.md +1577 -0
  59. package/docs/superpowers/plans/2026-10-09-content-api.md +1192 -0
  60. package/docs/superpowers/specs/2026-03-12-form-followup-actions-design.md +152 -0
  61. package/docs/superpowers/specs/2026-03-12-page-editor-form-insert-and-per-page-theme-design.md +115 -0
  62. package/docs/superpowers/specs/2026-03-17-nav-hidden-items-design.md +85 -0
  63. package/docs/superpowers/specs/2026-03-19-accordion-insert-design.md +175 -0
  64. package/docs/superpowers/specs/2026-03-25-static-block-shortcode-design.md +153 -0
  65. package/docs/superpowers/specs/2026-03-25-text-shortcode-design.md +168 -0
  66. package/docs/superpowers/specs/2026-04-05-card-builder-design.md +315 -0
  67. package/docs/superpowers/specs/2026-04-06-insert-builders-design.md +278 -0
  68. package/docs/superpowers/specs/2026-04-06-layouts-design.md +226 -0
  69. package/docs/superpowers/specs/2026-04-07-blog-plugin-design.md +300 -0
  70. package/docs/superpowers/specs/2026-04-07-calendar-plugin-design.md +538 -0
  71. package/docs/superpowers/specs/2026-04-17-components-area-design.md +368 -0
  72. package/docs/superpowers/specs/2026-05-02-effects-slideover-overhaul-design.md +193 -0
  73. package/docs/superpowers/specs/2026-05-06-dashboard-rebuild-design.md +257 -0
  74. package/docs/superpowers/specs/2026-05-24-menus-design.md +466 -0
  75. package/docs/superpowers/specs/2026-05-25-admin-sidebar-via-menus-design.md +358 -0
  76. package/docs/superpowers/specs/2026-05-25-projects-design.md +380 -0
  77. package/docs/superpowers/specs/2026-06-08-navbar-mobile-drawer-and-menu-enrichment-design.md +292 -0
  78. package/docs/superpowers/specs/2026-06-08-sidebar-tools-plugins-folders-design.md +59 -0
  79. package/docs/superpowers/specs/2026-06-25-domma-docs-project-design.md +192 -0
  80. package/docs/superpowers/specs/2026-06-26-domma-charts-upstream-proposal.md +44 -0
  81. package/docs/superpowers/specs/2026-06-26-manager-scheduler-upstream-proposal.md +46 -0
  82. package/docs/superpowers/specs/2026-06-26-surveys-pro-plugin-design.md +206 -0
  83. package/docs/superpowers/specs/2026-07-02-shortcode-context-menu-design.md +96 -0
  84. package/docs/superpowers/specs/2026-08-19-collection-context-sort-export-design.md +330 -0
  85. package/docs/superpowers/specs/2026-09-22-dcms-marketplace-design.md +1150 -0
  86. package/docs/superpowers/specs/2026-10-09-content-api-design.md +325 -0
  87. package/docs/superpowers/specs/plans/2026-03-12-form-followup-actions.md +624 -0
  88. package/docs/superpowers/specs/plans/2026-03-12-page-editor-form-insert-and-per-page-theme.md +125 -0
  89. package/docs/superpowers/specs/plans/2026-03-17-nav-hidden-items.md +578 -0
  90. package/docs/superpowers/specs/plans/2026-03-19-accordion-insert.md +635 -0
  91. package/docs/superpowers/specs/plans/2026-03-25-text-shortcode.md +545 -0
  92. package/docs/superpowers/specs/plans/2026-04-05-card-builder.md +1970 -0
  93. package/docs/superpowers/specs/plans/2026-04-06-insert-builders.md +1049 -0
  94. package/docs/superpowers/specs/plans/2026-04-06-layouts.md +1015 -0
  95. package/docs/superpowers/specs/plans/2026-04-07-calendar-plugin.md +1526 -0
  96. package/docs/superpowers/specs/plans/2026-04-08-plugin-monetisation-split.md +242 -0
  97. package/docs/superpowers/specs/plans/2026-04-17-components-area.md +2038 -0
  98. package/docs/superpowers/specs/plans/2026-05-02-effects-slideover-overhaul.md +2335 -0
  99. package/docs/superpowers/specs/plans/2026-05-06-dashboard-rebuild.md +1999 -0
  100. package/docs/superpowers/specs/plans/2026-05-17-cache-system.md +1436 -0
  101. package/docs/superpowers/specs/plans/2026-05-24-menus.md +3096 -0
  102. package/docs/superpowers/specs/plans/2026-05-25-projects.md +2869 -0
  103. package/docs/superpowers/specs/plans/2026-09-22-marketplace-step2-consolidation.md +505 -0
  104. package/docs/superpowers/specs/plans/2026-09-22-marketplace-step3-catalogue-and-key.md +154 -0
  105. package/docs/superpowers/specs/plans/2026-09-22-tools-promotion.md +260 -0
  106. package/docs/theming.md +320 -0
  107. package/package.json +2 -2
  108. package/plugins/_lib/admin/mail/keyboard.js +49 -0
  109. package/plugins/_lib/admin/mail/reader-view.js +4 -2
  110. package/public/js/celebrations/core/canvas.js +1 -1
  111. package/public/js/celebrations/index.js +1 -1
  112. package/public/js/celebrations/licenses/ABeeZee-Regular-OFL.txt +93 -0
  113. package/public/js/celebrations/licenses/Allura-Regular-OFL.txt +93 -0
  114. package/public/js/celebrations/message-attrs.mjs +1 -0
  115. package/public/js/celebrations/themes/anniversary.js +1 -0
  116. package/public/js/celebrations/themes/autumn.js +1 -0
  117. package/public/js/celebrations/themes/birthday.js +1 -0
  118. package/public/js/celebrations/themes/butterfly-flight.js +1 -0
  119. package/public/js/celebrations/themes/calendar.js +1 -0
  120. package/public/js/celebrations/themes/chinese-new-year-dates.js +1 -0
  121. package/public/js/celebrations/themes/chinese-new-year.js +1 -0
  122. package/public/js/celebrations/themes/christmas.js +1 -1
  123. package/public/js/celebrations/themes/easter.js +1 -0
  124. package/public/js/celebrations/themes/garden-drawing.js +1 -0
  125. package/public/js/celebrations/themes/garden.js +1 -0
  126. package/public/js/celebrations/themes/graduation.js +1 -0
  127. package/public/js/celebrations/themes/greeting.js +1 -0
  128. package/public/js/celebrations/themes/make-it-rain.js +1 -0
  129. package/public/js/celebrations/themes/money-rain.js +1 -0
  130. package/public/js/celebrations/themes/new-year.js +1 -0
  131. package/public/js/celebrations/themes/occasion-drawing.js +1 -0
  132. package/public/js/celebrations/themes/occasions.js +1 -0
  133. package/public/js/celebrations/themes/rain-control.js +1 -0
  134. package/public/js/celebrations/themes/sparkler-font-data.js +1 -0
  135. package/public/js/celebrations/themes/sparkler-font.js +1 -0
  136. package/public/js/celebrations/themes/sparkler-writing.js +1 -0
  137. package/public/js/celebrations/themes/summer-garden.js +1 -0
  138. package/public/js/celebrations/themes/winter.js +1 -0
  139. package/public/js/celebrations/trait-data.mjs +1 -1
  140. package/public/js/effects.js +1 -1
  141. package/scripts/build.js +4 -0
  142. package/server/services/markdown.js +5 -2
  143. package/server/templates/page.html +1 -1
package/CHANGELOG.md ADDED
@@ -0,0 +1,4344 @@
1
+ # Changelog
2
+
3
+ ## [1.7.0] - 2026-10-15
4
+
5
+ - **20 celebration scenes in every site.** Add New Year, Chinese New Year, Easter, Summer Garden, Winter, Autumn, Graduation, Birthday, Anniversary, standalone sparkler greetings, weather rain and Money Rain. Choose them in the Effects builder or right-click Celebrations menu, with controls for each scene's decorations.
6
+ - **Personal sparkler messages.** Set `message` to your own wording, including names, Latin accents and `{year}` substitution. Choose stroke or cursive lettering and gold or silver sparks, or the Best wishes preset with a year. Chinese New Year writes 新年快乐 by default, with a dragon, lanterns and fireworks.
7
+ - **Garden movement.** Bees visit flowers, Easter bunnies hop and butterflies use the existing illustrated meadow flight. Automatic dates handle moving Easter and Chinese New Year and give holidays priority over Autumn and Winter.
8
+ - **Make it rain means weather.** Actual water streaks and splashes, with the banknote effect separately named Money Rain. Both accept a duration and remove their canvas and listeners when finished.
9
+ - **Editor and documentation.** Update the User Manual, editor reference, shortcode guide and feature documentation. Existing self-closing celebrations reopen for editing without losing their personalised settings. Quotes and brackets survive message editing safely. Published packages include the docs, scene preview and bundled font licence notices.
10
+ - Built against Domma JS 0.52.4 and domma-celebrate 1.4.0.
11
+
12
+ ## [1.6.60] - 2026-10-15
13
+
14
+ - **Dialogs answer only the keys meant for them** (domma-js 0.52.3). A confirm opened by pressing Enter - on a list row or a menu item - no longer answers itself with that same Enter, so a "Throw away…?" or "Delete…?" question waits for a real answer. Escape closes only the top-most dialog or panel, not the panel underneath too. Enter on a focused dialog button does what that button says, and a `danger: true` confirm starts on Cancel. See [its release notes](https://github.com/pinpointzero73/domma/releases/tag/v0.52.3).
15
+ - **Unsaved drafts never pass between people on a shared browser.** When a different user signs in, every kept draft that does not belong to them or to the person who used the browser last is cleared before any screen can offer it - for every Tool and plugin (`admin/js/lib/draft-owner.js`). Signing out and back in as yourself keeps your work. A half-finished edit of someone's account in Users is kept for the admin who made it (`user-editor.draft.<editorId>.for-<editedId>`), never offered to that account's owner.
16
+ - **Mail: Enter and Space work again on buttons** in the compose window and other panels (Send, Attach files, More options…). The reader's single-key shortcuts now apply only to the reader itself (`keyBelongsElsewhere` in `plugins/_lib/admin/mail/keyboard.js`). Enter on a focused message opens it.
17
+
18
+ ## [1.6.59] - 2026-10-15
19
+
20
+ - **Mail editor: Font and Size menus**, usable by mouse or keyboard, in compose, signatures, templates and the away message. 49 email-safe fonts in groups (sans-serif, serif, monospace, display and handwriting, web fonts, generic), each shown in its own face; every choice is written inline as a span carrying the full fallback stack, so mail looks right in Outlook and Gmail. Web fonts are marked as needing a fallback and are never loaded into the sent mail. Sizes 8-72px. Domma's rich editor has none of its own: `plugins/_lib/admin/mail/font-controls.js` adds them, the lists are `fonts.js` (pure).
21
+
22
+ ## [1.6.58] - 2026-10-15
23
+
24
+ - **Mail reader: editions can add their own actions.** `createReaderView({extensions})` takes `paneActions` (buttons in the Folders pane head, beside New folder), `folderItems` and `mailboxItems` (menu items on those rows), each handed a `view` with the current account, folders, API and lifetime. Actions run a task later, so the Enter that chose one cannot answer a question it opens. Email Pro 0.8.2 uses it to put **Import mail…** beside New folder and on every folder's and mailbox's menu.
25
+ - Escape in the mail reader no longer clears the selection or search while a panel is open.
26
+
27
+ ## [1.6.57] - 2026-10-15
28
+
29
+ The Tool Upgrade's second pass for the built-in Tools: menus on every list, counts on filter buttons, skeletons while lists first load, theme tokens only, and unsaved work kept as a per-user draft with one question - **"Throw away…?"**, where OK throws the changes away and Cancel or Escape keeps them. The question says when the record changed or was deleted since, leaving while it is open keeps the draft, and the Enter that opened a row no longer answers it.
30
+
31
+ - **Analytics and Calculator.** Pages in every Analytics list open on the site by click or Enter; their menu adds "Edit the page" for the site's own pages. Tabs show counts, the first load shows a skeleton, and the chart no longer holds up the figures.
32
+ - **Notes and Todo.** The Notes editor opens again on current Domma; the category filter shows counts; skeletons while loading.
33
+ - **Forms.** The form editor no longer lets you save a form that failed to load, which used to replace it with an empty one.
34
+ - **Contacts.** Sidebar count, skeleton, menus on groups (right-click or Shift+F10). Changes left on a deleted contact can be saved as a new one; a contact in Trash keeps its draft until it is restored; a contact saved and then edited again keeps its draft as an edit of that contact. Bank details are never kept in browser drafts (`protectToolEditor`'s new `persist` option). Leaving Notes with a dirty editor no longer leaves a dialog over the next screen (`viewLifetime().leaving`).
35
+ - **SEO and System › Search.** SEO's page, redirect, import and settings panels keep unsaved work as a draft; filter buttons show counts; the search-result preview follows the theme. An SEO Pro tab is disposed when you switch to any other tab, and one still loading can no longer land in the wrong tab. Search shows a skeleton, keeps typing done during a save, and keeps unsaved settings per signed-in user.
36
+ - **Mail (Mail Reader and Email Pro).** Menus on folders and mailboxes in both editions; the mailbox form keeps unsaved changes per user (never the password); skeletons; theme tokens only. The unread badge turns red when a mailbox cannot be checked and updates after saves and refreshes. User text in dialogs is escaped; the section context gains `guard()` and `onClose()`; dialogs close properly on Escape; no more "Reply was already sent" in the log when the mail server fails.
37
+ - **Free Tools, synced from the marketplace:** Blog 1.10.0, Booking 1.7.0, Security 1.2.0, Shopping Cart 1.7.0, Feedback 1.2.0 and Support 1.2.0 - the same upgrade, with drafts kept per person.
38
+
39
+ ## [1.6.56] - 2026-10-15
40
+
41
+ - Vertical navigation keeps the current page's parent menus open after following a submenu link or reloading, including nested submenus. Opening the mobile drawer reveals the current branch. Menu highlighting and decorations now resolve the correct item at every nesting level, including menus with separators and spacers.
42
+
43
+ ## [1.6.55] - 2026-10-15
44
+
45
+ - **Built against domma-js 0.52.2**, whose router no longer drops an address change made while a screen is changing - the root cause of the fix in 1.6.54. Route changes now run one at a time and catch up with the address; `admin/js/lib/router-catch-up.js` stays for a site still on an older Domma. See [its release notes](https://github.com/pinpointzero73/domma/releases/tag/v0.52.2).
46
+
47
+ ## [1.6.54] - 2026-10-15
48
+
49
+ - **The screen follows the address, however fast it changes.** Changing the address while a screen was still changing - a link, Back or Forward, or a typed address within about 300 ms of the last change - moved the address but left the old screen showing (`#/seo/sitemap` with Redirects on screen, or any two admin screens). Domma's router ignores an address change during its fade between screens. The admin now notes a change it dropped and, once the screen in hand is drawn, shows the one the address names (`admin/js/lib/router-catch-up.js`, installed before `R.init()`).
50
+
51
+ ## [1.6.53] - 2026-10-14
52
+
53
+ - **Names with a double quote survive a rename.** The rename and copy boxes - a list tag in Pages and Blocks, a Contacts group, a dashboard, a view's copy - opened a name like `Q3 "draft"` cut short at the quote, and OK saved it that way. Domma's prompt sanitises its starting value into an HTML attribute and turns `&quot;` back into a bare quote, so escaping did not help. `promptWith(message, options, value)` in `admin/js/lib/tool-kit.js` sets the value on the input instead; a test keeps `inputValue` out of the admin.
54
+
55
+ ## [1.6.52] - 2026-10-14
56
+
57
+ - **Screens that read the keyboard themselves keep their keys.** An element marked `data-own-keys` counts as typing for the admin's hotkeys, as fields do. Ctrl+D and single-key shortcuts reach it, and Ctrl+K still opens the palette. The Sheets grid uses it: Ctrl+D (fill down) opened the Tool switcher instead.
58
+ - **The page-editor browser test no longer fails a release at random.** It waits for the editor to load and the save to land, rather than for fixed times. Each Chrome launch gets its own profile. A Chrome that hangs before rendering is retried once; a failed check is never retried. The cut-release run for 1.6.51 failed once on such a hang.
59
+
60
+ ## [1.6.51] - 2026-10-14
61
+
62
+ - **Admin tags in the page editor.** Page Details has an **Admin tags** field under Tags, with the Pages list's tags as suggestions. A save sends admin tags only when you changed them in the editor, so a tag added from the Pages list while the page was open is kept. An empty list leaves no `adminTags` key in the file.
63
+ - **The page editor's Tags field shows all of a page's tags.** It showed only the first tag, as "undefined". The pillbox runs its change handler while it is being built, which failed and stopped the build. The editor also gave it plain-string suggestions, which Domma turns into blank pills.
64
+
65
+ ## [1.6.50] - 2026-10-14
66
+
67
+ - **Tags for pages and blocks.** Cluttered lists can now be sorted with tags that only the admin sees. A page or block can have several. Right-click one in the Pages or Blocks list and choose **Add tag ▸** (an existing tag, or New tag…) or **Remove tag ▸**. The new cog in each list's banner picks which tag the list shows (All, No tag, or one tag, remembered in your browser) and adds, renames or deletes tags. Deleting a tag only takes it off its pages or blocks; nothing else changes. Tags are stored as `adminTags` in a page's frontmatter and in a block's `.meta.json`, and the tag names in `config/list-tags.json`. A page's public `tags` are untouched. Tagging is not an edit, so it adds no revision and does not change "updated". API: `/api/list-tags/:kind` (`server/services/listTags.js`).
68
+
69
+ ## [1.6.49] - 2026-10-14
70
+
71
+ - **The admin wears the site's name.** The top bar said "Domma CMS" on every site, whatever it was called. It now shows the admin brand - Site Settings › Branding › **Admin name**, logo and icon, or the site title when the name is empty - and so do the sidebar header and the browser tab ("Pages - ACME Ltd"). The brand comes with `GET /api/auth/permissions` (`adminBrandFor()` in `server/services/adminBrand.js`), so every signed-in role sees it; the sidebar header used to read Site Settings and showed "Admin" to anyone without the settings permission. Tab titles are rebranded by one observer on `<title>` (`admin/js/lib/admin-brand.js`), so views and plugins that set "… - Domma CMS" keep working, and renaming the admin updates an open tab.
72
+
73
+ ## [1.6.48] - 2026-10-14
74
+
75
+ - **Security: `scope="mine"` collections are sanitised.** A per-user collection is drawn in the browser from `POST /api/collections/render-scope`, after the page was cleaned, and that fragment was never cleaned itself - so an `on*` handler or `javascript:` link in a block, or in a field shown with `{{{raw}}}`, reached the page there. The fragment now goes through the same sanitiser as the page (`sanitizePageHtml` in `markdown.js`).
76
+ - **A Blocks chapter in the documentation.** Documentation > Usage > Blocks (`#/docs/usage/blocks`, `/domma-docs/usage/blocks`) covers what a block is, the three ways to place one (`[collection]`/`[view]` with `display="block"`, `[block]` and the short tag), placeholders and date formats, the doc comment, scoped CSS, the Blocks screen and editor, export/import, a worked team-page example and permissions. The Blocks list and the block editor have a **Blocks guide** button in the banner.
77
+ - **"Writing a Plugin" is now "Writing a Tool".** The tutorial says what a Tool is and adds a section on making one feel like the built-in Tools: sidebar folder, group and hotkey, a `countUrl` badge with its popover, banner actions, help popovers and the tool kit loaded at run time, `requires`/`uses`, shipping collections and a block (created once in `onEnable`, never overwritten), and sharing a `.dcmsplugin`. The Marketplace banner links to it. Existing admin sidebars rename the item once, only where it still says Writing a Plugin (`meta.writingToolName`).
78
+
79
+ ## [1.6.47] - 2026-10-14
80
+
81
+ - **Built against domma-js 0.52.1** - see [its release notes](https://github.com/pinpointzero73/domma/releases/tag/v0.52.1).
82
+ - **Required dropdowns start empty.** A required select in a generated form (Surveys, Onboard Pro form steps and others) now opens on "-- Select --" instead of showing the first option as chosen while sending nothing, which made the form refuse a person who wanted that first option.
83
+
84
+ ## [1.6.46] - 2026-10-14
85
+
86
+ - **The admin asks for a reload after an update.** An open admin tab keeps the screen code it loaded but fetches forms fresh, so after an update it could show new settings and save without them (the Menus editor's Staggered entrance did). Signed-in `/api` answers now carry `X-Domma-CMS-Version` (the version the server booted with), and when it differs from the one the tab started on, a bar asks to save and reload; Later hides it until the next version (`admin/js/lib/update-notice.js`).
87
+
88
+ ## [1.6.45] - 2026-10-14
89
+
90
+ - Custom collection and View block cards now match heights in each row, including Regimen’s About team cards. Authored widths, wrapping, full-width blocks, filters and grouping are preserved.
91
+ - Refreshed the public stylesheet cache version for returning visitors.
92
+
93
+ ## [1.6.44] - 2026-10-14
94
+
95
+ - **Menus can stagger their items in.** A Staggered entrance switch on a menu's Behaviour tab brings the items in one after another when the menu is drawn - fade, slide (up, down, left, right), zoom, pop, flip or blur, with the delay between items and each item's duration. It applies on the navbar, footer, `[menu]` and floating panels; dropdowns and footer columns stagger on their own as they open. Stored as `entrance: {enabled, animation, stagger, duration}` (`entranceAttrs` / `applyEntrance` in `menu-decor.mjs`, animated in `menu-highlight.css`). Visitors who asked for less motion see the items at once.
96
+ - **A menu's CSS classes are saved.** `createMenu` / `updateMenu` dropped the `class` field, so the Behaviour tab's CSS classes never reached the file.
97
+
98
+ ## [1.6.43] - 2026-10-14
99
+
100
+ - Image carousels align and crop images from the top with a shared height. Choose the shortest scaled image or the midpoint of the shortest and tallest with `height-mode`, or set a fixed pixel `height`; the page editor exposes both controls. Collection and View carousels accept the same options.
101
+ - Cards stretch to match the tallest card in each grid or flex row, including cards inside columns and collection cards. Side-by-side card layouts retain their responsive stacking.
102
+ - Admin theme application always targets the admin body and leaves the public site's saved theme preference alone. Forest Dark and Lemon Light changes keep the saved admin colours.
103
+ - Updated public asset cache versions so returning visitors receive the layout fixes.
104
+ - **Built against domma-js 0.52.0** - see [its release notes](https://github.com/pinpointzero73/domma/releases/tag/v0.52.0).
105
+
106
+ ## [1.6.42] - 2026-10-13
107
+
108
+ - **A page can read another site's public API.** `csp.connectHosts` in `config/server.json` adds hosts to the Content-Security-Policy's `connect-src` (`connectSources()` in `server/services/embedCsp.js`, beside `frameSources()`; bare hostnames, https only). The policy was fixed to the site and cdn.jsdelivr.net, so a `fetch` to anywhere else was blocked.
109
+
110
+ ## [1.6.41] - 2026-10-13
111
+
112
+ - **A plugin's dependencies on its details.** The Marketplace's plugin slideover shows **Needs** (its `requires`, by display name, each On, Off or Not installed) and **Works better with** (its `uses`, with what each adds), with help popovers saying that switching the plugin on switches what it needs on too. Bound with domma-reactive (`data-each`, text by binding); the rows come from the pure `dependencyRows()` in `admin/js/lib/plugins-arrange.js`. The site Manager now also switches on what a plugin needs when it grants it or turns Pro on - reactive's Cards Pro and Shopping Cart Pro were licensed and enabled but never loaded because Shopping Cart was off.
113
+
114
+ ## [1.6.40] - 2026-10-13
115
+
116
+ - **Built against domma-js 0.51.14** - the CSS fireworks animate again (they named keyframes that had been renamed).
117
+
118
+ ## [1.6.39] - 2026-10-13
119
+
120
+ - **Mailboxes can sign in with Microsoft or Google (OAuth2 / XOAUTH2).** The shared mail core (`plugins/_lib/mail`) gains `oauth.js` (providers, token exchange and renewal, revoke) and `oauthRoutes.js` (Connect, the provider callback, Disconnect, Test), and the mailbox form a "Sign in with Microsoft / Google" choice with a setup guide (`_lib/admin/mail/oauth-panel.js`). The customer registers their own app; the client secret and tokens are sealed like passwords. The access token is renewed before expiry and once after a refused IMAP or SMTP login (the pool, and nodemailer's `provisionCallback`); Microsoft's rotated refresh tokens are saved; a renewal the provider refuses marks the mailbox "Needs reconnecting" and fails fast from then on, never a retry loop. Only Email Pro 0.6 registers the routes; Mail Reader still reads a signed-in mailbox. Password mailboxes are unchanged. A sign-in must be finished in the browser that pressed Connect (an HttpOnly cookie bound to the one-time ticket); a renewal can never undo a Disconnect or Reconnect (a generation counter); a queued send or snooze whose mailbox needs reconnecting fails at once with the reason; the mailbox store's writes are serialised. The request log now redacts `code` and `state` query values.
121
+ - **Built against domma-js 0.51.13** - dialog footers follow the theme (dark themes showed a white bar).
122
+ - **The unsubscribe page names onboarding emails too**, now that Onboard Pro invites and reminders carry the unsubscribe footer.
123
+ - **Bundled Tools:** Shopping Cart 1.6.3 (the refund dialog is a real form - part refunds, a reason, restock), Feedback 1.1.8 and Support 1.1.6 (a site forgets reports and requests dcm deleted, and clears their files).
124
+
125
+ ## [1.6.38] - 2026-10-13
126
+
127
+ - **Built against domma-js 0.51.12** (domma-reactive 1.3.1) - names with `&`, `<` or quotes now read as typed in the tooltips, screen-reader labels and row menus of every reactive list in the admin, instead of showing `&amp;`.
128
+ - **Bundled Tools upgraded.** Blog 1.9.7, Booking 1.6.4, Feedback 1.1.7, Support 1.1.5, Shopping Cart 1.6.2 and Mail Reader 1.2.8: cached badge counts and public reads; the Booking diary rebuilt; part refunds in the Shop; file attachments and settings cogs in Feedback and Support.
129
+
130
+ ## [1.6.37] - 2026-10-13
131
+
132
+ - **Outreach unsubscribe needs a click.** `GET /campaign-unsubscribe` now shows a confirm button and changes nothing; only the `POST` (the button, or a mail client's RFC 8058 one-click `List-Unsubscribe=One-Click`) puts the address on the shared suppression list. Mail security scanners that prefetch every link used to unsubscribe people who never clicked - and, the list being shared, silence Surveys and Onboard for them too. The outreach library gains `unsubscribeHeaders()` and `sendBatch` takes `headersFor`, so Campaigns Pro, Surveys and Onboard Pro can send the `List-Unsubscribe` headers Gmail and Yahoo require of bulk senders.
133
+ - **Outreach library reads one row, not the collection.** `_lib/outreach` ledger lookups (`invitesFor`, `findByCode`) and the suppression check (`isSuppressed`, `add`, `remove`) pass a structured `filter` to `listEntries`, so a campaign claim, a tracking hit or a send no longer loads every invite or opt-out on the site (Mongo answers from an index; the file adapter matches in memory). Plugins on an older engine keep working - the in-memory match is unchanged.
134
+ - **Email reply and forward markers.** Successful sends mark the original message in the list. The markers survive saved drafts, delayed sends and mailbox refreshes; failed sends leave messages unmarked.
135
+
136
+ ## [1.6.34] - 2026-10-12
137
+
138
+ - Added date/time, per-IP or per-session duration and view limits, IP allowlists, and once-per-session expiry controls to every shortcode. Expired content is excluded from public render caching; editor previews do not consume visitor limits.
139
+ - Centred images vertically within carousels by default.
140
+
141
+ All notable changes to this project will be documented in this file.
142
+
143
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
144
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
145
+
146
+ ---
147
+
148
+ ## [1.6.36] - 2026-10-13
149
+
150
+ - **Security sweep (2026-10-13).** A sign-in is refused when an after-login check (a second factor) throws instead of being skipped - one malformed cookie used to sign a Security Pro user in without TOTP. The request log redacts tokens, keys and secrets from query strings. Mail account and SMTP test routes refuse private and loopback hosts (`allowPrivateMailHosts` to opt out) and are rate-limited; attachment downloads honour `maxMessageBytes`. The bundled Blog, Booking, Feedback, Security, Shopping Cart, Support and Mail Reader ship their sweep fixes (slug collisions, SSRF-safe fetches, rate limits, no email links from a Host header, crypto codes, no account enumeration) - see each Tool's guide.
151
+
152
+ - **Calls: TURN credentials only on admission.** The ICE servers (with the coturn time-limited credential, when TURN is configured) now arrive in a participant's `admitted` event, made as they are let in, instead of in the join reply that a guest still waiting - or about to be turned away - also received. Anyone holding a call link used to get a relay credential for the full TTL; the default TTL is now 4 hours (was 12). The shared call page reads them from the event.
153
+ - **Collections API keeps to its owners.** The generic `/api/collections/:slug/...` routes (and the public `/public` and `/v1` aliases) show an admin only their own notes, tasks and contacts, as the Tools do; contact groups are readable by all and changed by their creator or root. A collection a plugin created needs that plugin's own permission when it declares one, so holding `collections` alone no longer opens every Tool's data. Root (level 0) sees everything as before.
154
+ - **Analytics beacon hardened.** `POST /api/analytics/hit` now accepts only a site path (`/...`, no whitespace, 2 KB at most) and refuses prototype names as counter keys - a `constructor` url used to turn every total into a string, and an oversized url became a permanent key in lifetime.json. Journey days older than 90 days are pruned on write; nothing read them and the file was rewritten on every hit.
155
+
156
+ ## [1.6.35] - 2026-10-13
157
+
158
+ - **Request log redaction.** Query-string credentials (`?token=`, `?key=`, `hostKey`, `inviteKey`, `t`, anything named *secret* or *password*) are replaced with `[redacted]` in the request log. The mail reader's body grant and e-card links no longer sit in server logs.
159
+ - **Mail: no connections to the server's own network.** Testing or saving a mailbox or outgoing server refuses a private, loopback or link-local host (the site's own configured SMTP host excepted) unless `allowPrivateMailHosts` is set; the test routes are limited to 10 a minute. Attachment downloads now honour `maxMessageBytes` like message bodies.
160
+ - **Pages list transitions.** The page context menu offers Transition → Published, Draft and Review. Review pages show their status in the list, remain hidden from visitors, and keep Review selected in the editor.
161
+ - **Carousel appearance controls.** Choose the background and arrow colours and place the arrows at the top, middle or bottom in the editor and shortcode context menu.
162
+ - **Scribe web fonts.** Choose from the Domma Theme web font list, with the selected font loaded in previews and on the site.
163
+ - **Admin theme picks apply instantly.** Content > Theme: choosing an admin theme shows it at once so you can try them on. Save makes it everyone's; discarding or leaving without saving puts the saved theme back. Custom (Theme Roller) themes preview with their full CSS via `GET /api/theme/admin?id=`.
164
+ - **Shopping Cart 1.6.0: platform commission.** When the platform sets a commission for a site (on the manager), each sale records its commission when it is paid, at the rate of that day; a refund returns it. The shop sees it in Sales, on the order and in Shop settings; customers never do.
165
+ - **Shopping Cart 1.5.0: Sales.** A Sales tab: takings for any range by day, week or month, where the money came from (Shop, downloads and keys, Takeaway, other add-ons), how it was paid, discounts, delivery, tax, refunds and net, and a CSV for the accountant.
166
+ - **Shopping Cart 1.4.0: customer accounts.** Optional, from Shop settings: customers create an account confirmed by email, sign in and reset their password on pages under the shop's address, keep their details and up to 8 addresses, see every order placed with their email, and delete their account. Checkout fills in a signed-in customer, and the receipt offers to create an account. Needs the site's email set up.
167
+ - **Shopping Cart 1.3.0: checkout for add-ons such as Takeaway.** An add-on can sell lines it prices itself (a dish with its chosen protein and extras, shown under the line on the receipt, the order and the packing slip), add a delivery charge, take collection or delivery details, refund an order, and word the receipt's payment note ("Pay when you collect"). A shop without add-ons works as before.
168
+
169
+ ## [1.6.33] - 2026-10-12
170
+
171
+ - **Ctrl+/ toggles page comments.** Comment or uncomment selected text; with no selection, toggle the current paragraph. Cmd+/ works on Mac, and edits support native undo.
172
+ - **Page comments.** HTML comments and lines starting with `//` are hidden before shortcode rendering; code examples and URLs are preserved.
173
+ - **Page history names its author.** Saves, named versions and pre-restore snapshots record the signed-in user's name instead of falling back to System.
174
+ - **Scribe typography.** Font, size, weight, colour, italic, alignment, line height and letter spacing are supported by `[scribe]`, the composer, edit form and context menu, with matching previews.
175
+ - **Favicon generator in Site Settings.** A Favicon tab generates a downloadable ICO from a Media image, with 16, 32, 48 and 256 pixel sizes, and selects it for the next settings save.
176
+ - **Timeline event icons and statuses.** Preserve event icons and show Completed, Planned, In progress and Blocked badges after the timeline is built.
177
+
178
+ ## [1.6.32] - 2026-10-12
179
+
180
+ - **Shopping Cart 1.2.0: room for add-ons in the Shop.** An add-on such as Shopping Cart Pro can now add its own section to the product form and to an order, keep its own details on a product, show a section on the shopper's receipt page, and move an order on. Product saves and sales no longer overwrite each other. Copying a product gives its options new ids. A Cart without add-ons works as before.
181
+
182
+
183
+ ## [1.6.31] - 2026-10-12
184
+
185
+ - **Insert Image at the top of the editor Insert menu.** Open Media to select or upload an image and insert it at the cursor.
186
+ - **Page editor title follows the page.** Show the page name in the editor banner and browser tab, updating as its title is edited.
187
+ - **Clone pages from the editor.** The small Save icon is now Clone. Choose a title and URL to create a draft copy with the current edits; the main Save button and Ctrl+S remain available.
188
+
189
+ ## [1.6.30] - 2026-10-12
190
+
191
+ - **Pick carousel and hero images from Media.** Slide images and the hero background image have a button that opens Media to choose or upload a picture, in the insert form, each carousel slide row and the right-click menu. Typing an address still works.
192
+
193
+ ## [1.6.29] - 2026-10-12
194
+
195
+ - **Page editor image upload shortcut.** The Custom CSS button says CSS, and an image button immediately before Help opens Media to upload or select a picture and insert it at the saved cursor position. Insert > Image uses the same picker.
196
+
197
+ ## [1.6.28] - 2026-10-12
198
+
199
+ - **Card icon size and alignment.** `[card]` takes `icon-size` (xs, sm, md, lg, xl, 2xl, or an exact size such as `48px` or `3rem`) and `icon-align` (left, center or right, lining up the icon and title together). Both work with inline and stacked icons and the icon-top / icon-inline layouts, and are in the shortcode panel and card builder. The builder's inline/stacked toggle now highlights the right button.
200
+
201
+ ## [1.6.27] - 2026-10-12
202
+
203
+ - **Extra large keeps the full-width image fill.** Restore cover sizing as the default; Show whole image remains an explicit Background fit choice. Full width is visible beside Size in the hero form and still works independently of height and image fit.
204
+
205
+ ## [1.6.26] - 2026-10-12
206
+
207
+ - **Extra large heroes show the whole image.** The new Extra large size uses at least 75% of the viewport height and contains the background image without cropping. Background fit offers Show whole image or Fill area (crop), independently of image position and content alignment.
208
+
209
+ ## [1.6.25] - 2026-10-12
210
+
211
+ - **Spaced notifications label and count.** The admin top bar separates the bell from “Notifications” and puts the unread-count pill to the right of the label.
212
+
213
+ ## [1.6.24] - 2026-10-12
214
+
215
+ - **Named notifications link.** The admin top bar shows “Notifications” beside the bell, with the unread badge kept on the icon.
216
+
217
+ ## [1.6.23] - 2026-10-12
218
+
219
+ - **Branding in Site Settings.** Choose the public brand name, logo, icon, link and favicon, plus the admin sidebar name, logo and icon. Choose or upload images through Media; branding uses the screen's Save, drafts and change indicators.
220
+ - **Hero background positioning.** The editor form and right-click menu offer horizontal Left/Centre/Right and vertical Top/Centre/Bottom background positions, independently of content alignment.
221
+ - **Dismissible banners work without forms.** The close handler now runs on every page. Both `dismissible` and `dismissable` are accepted, and an explicit false leaves the close button off.
222
+
223
+ ## [1.6.22] - 2026-10-12
224
+
225
+ - **New Tools start right.**
226
+ - The scaffold template's example route used the full `/api/plugins/<slug>/…` path. The CMS already mounts a Tool's routes under that prefix, so a route written that way answered at a doubled address. The template now shows a relative path and explains why.
227
+ - Tools scaffolded on an installed site now get the template's `CLAUDE.md` and `docs/guide.md`, which the build had left out of the package. The New plugin panel promised the `CLAUDE.md`. The plugin-authoring guide `plugins/CLAUDE.md` ships too.
228
+ - **Your own Tool says so.** A Tool you made yourself was labelled *Included with Domma CMS* on its banner. Only the plugins the engine ships say that now. Bought ones say *Commercial licence*, and your own say *Your own Tool*.
229
+
230
+ ## [1.6.21] - 2026-10-12
231
+
232
+ - **Built against domma-js 0.51.11** - see [its release notes](https://github.com/pinpointzero73/domma/releases/tag/v0.51.11).
233
+
234
+ ## [1.6.20] - 2026-10-12
235
+
236
+ - **Scribe keeps words together.** Animated text wraps between whole words, including character entrance effects. The composer preview also stops forcing breaks inside words.
237
+
238
+ - **Actions keep to their own rules everywhere.** Three fixes:
239
+ - A run that stops part-way (a step fails) is reported as a failure on the site. The page's button used to say *Action completed.* even when a step had failed. The visitor gets a plain message, and the step's error goes to the log.
240
+ - The admin's ⚡ menu offers only the actions you may run, and running one applies the action's **Who can run it**. Before, anyone who could open Actions could run every one.
241
+ - **Run an Action** in authored right-click menus works. It posted to a route that does not exist. It now runs on the entry you right-clicked, signed in, by the same rules as an Action button on a page.
242
+
243
+ ## [1.6.19] - 2026-10-11
244
+
245
+ - **Built against domma-js 0.51.10** - see [its release notes](https://github.com/pinpointzero73/domma/releases/tag/v0.51.10).
246
+
247
+ ## [1.6.18] - 2026-10-11
248
+
249
+ - **A footer laid out in columns.** A footer menu with groups (top-level items with sub-items) is now drawn as columns - your brand, a short blurb and the social links on the left, a column per group beside them - with copyright, the legal menu and the Reduce motion control in a bar beneath. Before, the groups ran together on one line, headings and all. Footers without groups keep the single row. **Site Settings > Footer** gains **Layout** (Automatic, Columns, One row) and **Blurb**.
250
+
251
+ ## [1.6.17] - 2026-10-11
252
+
253
+ - **Complete Scribe shortcode controls.** Both editor forms expose characters, words or sentences, entrance effects, typing/deletion speeds, cursor style/character/blinking, counted or continuous loops, loop delay, hover pause, manual start, reduced motion and playback controls. Script render/redraw steps inherit the default effect and can override it. Callback integrations use bubbling Scribe DOM events. Fixes the composer preview and releases old preview players.
254
+ - **Scribe animation improvements.** Built against the updated Domma JS release with accurate pacing, reliable pause/resume, whole emoji, working bounce/glow entrances and readable reduced-motion text.
255
+ - **Built against domma-js 0.51.9** - see [its release notes](https://github.com/pinpointzero73/domma/releases/tag/v0.51.9).
256
+
257
+ ## [1.6.16] - 2026-10-11
258
+
259
+ - **Two kinds of menu, named apart.** The menus built in the admin are now **Site menus** (the screen, its banner, the permission and the sidebar item - an existing sidebar's "Menus" item is renamed once, unless you named it yourself), and the list a layout builds from a page's own headings is **Page contents** throughout Layouts ("Sidebar" there is now "Page contents"). Docs and help say which is which.
260
+
261
+ ## [1.6.15] - 2026-10-11
262
+
263
+ - **Page contents settings, and `[contents]`.** The list a layout with a sidebar builds from the page's headings has settings - on each layout in **Layouts** (Page contents settings), and per page with the `[contents ... /]` tag:
264
+ - *Layout*: side, width, stays in view or scrolls, floating panel (anchor, movable by visitors), or none on this page.
265
+ - *Behaviour*: collapsible to a button (remembered per visitor, applied before the first paint), start collapsed, autohide off its edge, scroll-spy on or off.
266
+ - *Content*: which headings (sections, H2, H3, or H2 and H3 nested), numbered (1, 2.1...), a title, headings to leave out.
267
+ - *Look*: the menus' hover and selected effects, an accent, font family, size and weight.
268
+ Visitors can right-click the list to collapse or show it, copy a link to the section being read, or go back to the top. The list is now the CMS's own markup (no Domma sidebar), so the menus' highlight styles apply to it. In the page editor it is under Insert → Layout, with one-click actions on right-click.
269
+
270
+ ## [1.6.14] - 2026-10-11
271
+
272
+ - **Reduced motion: a site default, and a checkbox.** **Site Settings → Footer** gains *Reduced motion for new visitors* - follow their device (as before), off (full motion until they ask for less) or on - and *The control*: the switch, or a plain checkbox. A visitor's own choice always wins, and it is applied before the first paint as before.
273
+
274
+ ## [1.6.13] - 2026-10-11
275
+
276
+ - **Docked menus: width, collapse, autohide, hide on scroll.** A vertical navbar rail takes a width (160-480px), can collapse to its icons with a toggle (labels become tooltips, sub-menus fly out beside it; each visitor's choice is remembered and applied before the first paint), can start collapsed, or can autohide off its edge and come back on hover, tap or focus. A horizontal bar can hide while scrolling down and return on the way up. Set them in **Menus → Behaviour**. Visitors can right-click the rail to collapse it, hide it when not in use, or go back to the site's setting.
277
+ - **`[navbar]` sets a page's own navbar.** Any Behaviour setting of the navbar menu - position, orientation, side, highlight, fonts, the floating panel, and the new docked options - for one page, e.g. `[navbar collapsed="true" /]`. `true` / `false` switch settings on or off; an override that would not validate is ignored. In the page editor under Insert → Layout, with one-click actions on right-click.
278
+ - Page editor: a tag can now offer one-click actions and a shortlist of settings in its right-click menu (the registry's `actions` and `quickAttrs`).
279
+
280
+ ## [1.6.12] - 2026-10-10
281
+
282
+ - **Built against domma-js 0.51.8: factory modals work again.** Modals made with `E.createModal()` / `E.showModal()` - in a site's own scripts or plugins - showed their HTML content as text and drew no buttons, so a `showModal()` promise never settled from a button. They now show their markup and buttons. See [the Domma release notes](https://github.com/pinpointzero73/domma/releases/tag/v0.51.8).
283
+
284
+ ## [1.6.11] - 2026-10-10
285
+
286
+ - **The theme switcher can sit in a menu.** Menus gain a **Theme switcher** item (Menus editor: **Add theme switcher**, or right-click an item) that draws the site's theme dots among the links - in the navbar, a vertical rail, the footer or a `[menu]`. It shows the themes chosen under **Theme > Theme switcher** and works with the floating switcher turned off.
287
+ - **`[theme-switcher]` puts one on a page.** Inline where the tag stands, or pinned with `position` (`top-left`, `top-right`, `left-center`, `right-center`, `bottom-left`, `bottom-right`) and `offset-x` / `offset-y`; `themes` narrows the list. On its page it takes the floating switcher's place, so there are never two. It is in the page editor's Insert menu under Components, and right-clicking one edits it.
288
+
289
+ ## [1.6.10] - 2026-10-10
290
+
291
+ - **A layout's sidebar can be on the right.** Layouts gain `sidebarSide` (`left`, the default, or `right`), set in **System > Layouts** under the Sidebar switch; a page can choose its own with `sidebarSide` in its front matter. The page contents then sit beside the article on the right, the usual place for a documentation page's contents. The small layout drawings in the list now show the sidebar on the side it is really on - they had always drawn it on the right.
292
+
293
+ ## [1.6.9] - 2026-10-10
294
+
295
+ - **The With Sidebar layout works.** It had never shown its contents: Domma's sidebar has no heading auto-generation, so the list the layout asked for came out empty - and the sidebar was started in Domma's fixed, push-the-content mode inside a layout that already gave it a grid column, so it left the grid and the push margin was taken from the wrong column, squeezing the page to 10px wide. The sidebar now lists the page's sections (its H2s, or its H3s when it has none; headings inside heroes, cards, accordions, tabs and figures are left out), sits in its column, sticks below the navbar while the page scrolls, and marks the section being read. A page with nothing to list gets one column instead of an empty rail.
296
+ - Docs: the `step-numbered` card's `step` attribute (the number on its badge) is now listed.
297
+
298
+ ## [1.6.8] - 2026-10-10
299
+
300
+ - **The sitemap leaves out pages of a switched-off project.** They 404 (the public renderer already refused them, and site search left them out), but `/sitemap.xml` still listed them, so search engines were sent to dead pages and a backup restore test - which checks every sitemap page - failed on them. The rule now lives once in `offlinePageUrls()` (`server/services/projects.js`), which search and the sitemap both use.
301
+
302
+ ## [1.6.7] - 2026-10-10
303
+
304
+ - **Notes, Todo and Contacts have a Trash.** Deleting a note, task or contact - including completed tasks cleared together and contacts deleted in bulk - moves it to that Tool's Trash instead of removing it. From Trash an item can be restored, keeping its id and every field, or deleted permanently. Trashed items stay out of normal lists, badges, notifications and contact sharing (`server/services/tool-trash.js`; `GET /trash`, `POST <items>/:id/restore`, `DELETE /trash/:id` on each Tool's API). `contact:deleted` and `note:deleted` now fire when an item is purged, not when it is trashed, so its Calendar links survive a restore.
305
+ - **Unsaved changes in the Notes, Todo and Contacts editors are kept.** Closing an editor with changes offers to keep them as a browser draft, per user and item; opening the item again offers to restore or discard it. Todo also keeps an unfinished task in its quick-add bar. A list that fails to load shows Retry instead of looking empty.
306
+ - **Analytics says when its figures are stale.** The figures show when they last refreshed; a failed refresh keeps the previous ones, marks them stale and offers Retry, and the live visitor count says when it is unavailable or stale. A slow live count can no longer overwrite a newer one.
307
+ - **Non-English text in context menus and collection views.** Their base64 payloads were decoded as Latin-1, so accented letters and symbols such as £ came out garbled. They are decoded as UTF-8 now (`public/js/utf8-base64.mjs`).
308
+ - **Effects:** hover fireworks have a hit target and fire again; a non-numeric effect setting falls back to its default instead of `NaN`; with Reduce motion on, typing effects show their text instead of nothing, and the effects showcase explains why demos are static and offers to enable them. `firework` shortcode classes are escaped.
309
+
310
+ ## [1.6.6] - 2026-10-10
311
+
312
+ - **The sidebar's first folder is Dashboards, not Overview.** New installs are seeded with it, and an existing sidebar is renamed once on its next boot - only while the folder still says Overview, so a name an admin gave it stays. Its key is still `overview`, so Tools placed in it and its open or closed state are unchanged.
313
+
314
+ ## [1.6.5] - 2026-10-10
315
+
316
+ - **Frequent follows you, not the browser.** The sidebar's Frequent folder was kept in the browser's storage, so each browser or device had its own list and clearing site data lost it. It is now kept on the server per user (`content/sidebar/frequent/<user id>.json`, `GET`/`PUT /api/sidebar/frequent`) and copied into the browser to draw from. A list already in a browser is sent up the first time it signs in, if the server has none yet.
317
+
318
+ ## [1.6.4] - 2026-10-10
319
+
320
+ - **Security: a user limited to some projects was not limited at all.** Project access (the user editor's **Project access**, `user.projects`) was never applied: the access token does not carry it and `authenticate()` did not read it, so every `canSeeArtefact()` check saw an empty scope - unrestricted - and such a user could list, open and change every project's collections, forms, blocks, components, views, actions, menus, API endpoints and tokens. `authenticate()` now reads the scope from the user record on each request, so a change to someone's access applies at once. The route tests had stubbed `authenticate()` with a user that already carried `projects`, which is how it went unnoticed; `tests/routes/project-access-scope.test.js` uses real tokens and a real user.
321
+ - **Pages follow project access too.** The page routes had no project check of their own: the list, its badge and tags leave out pages in projects outside the user's scope, and opening, saving, deleting, versioning or sharing such a page answers 403. A page can no longer be created in, renamed into or re-filed under a project outside the scope. A page's project is the usual one - its front matter, else its address (`canSeePage()` in `server/services/projects.js`). Core pages stay visible to everyone.
322
+ - **A project outside your access is not there.** Opening, editing, deleting, untagging or listing the contents of such a project answers 404, as Delete with contents already did; the projects list already left it out once the scope was read.
323
+
324
+ ## [1.6.3] - 2026-10-10
325
+
326
+ - **Parallel page loads no longer compete with their assets.** Uploaded media is excluded from the request budget, and public pages have a separate budget from background API calls. Login and other explicit route limits stay enforced.
327
+ - **Analytics keeps concurrent page hits.** Page-hit recording and counter resets are serialised, preventing lost counts and temporary-file rename errors during concurrent requests.
328
+ - **Inline shortcode examples stay literal.** Code spans already rendered inside collection answers are protected from shortcode processing, fixing the FAQ answer that expanded its sitemap example into the whole site map.
329
+
330
+ ## [1.6.2] - 2026-10-09
331
+
332
+ - **A hint after G.** Press G and pause, and a small chip at the foot of the screen shows what comes next: your Frequent numbers, then the letters ("G then 1 Pages · D Dashboards · E Email…", and ? for the rest). It goes as soon as you press the letter, or when G lapses - a quick G E never shows it.
333
+
334
+ ## [1.6.1] - 2026-10-09
335
+
336
+ - **The palette leads with what you came for.** With nothing typed, Tools with a hotkey come first; a project's own copy of a screen (Projects › Core › Pages) now sits after the main one instead of before it, and its name no longer carries the count ("Pages", not "Pages (8)").
337
+
338
+ ## [1.6.0] - 2026-10-09
339
+
340
+ - **Hotkeys.** Press **G**, let go, then a Tool's letter to open it from anywhere in the admin: G E Email, G D Dashboards, G P Pages, G , Site Settings and so on. **G then 1-9** opens the Tools in your Frequent folder, in order. **Ctrl+K or Ctrl+D** (Cmd on a Mac) opens a palette: type part of any Tool's name, press Enter. **?** lists every hotkey you can use. Hover over a Tool in the sidebar to see its key; it is beside Open in the right-click menu too. Tools declare their own letter (`hotkey` on their sidebar entry); the built-in ones come with theirs. Keys wait while you type, and only ever cover Tools you can open.
341
+
342
+ ## [1.5.0] - 2026-10-09
343
+
344
+ - **Dashboards: one entry, as tabs.** With Dashboard Pro, the sidebar's Dashboard and My Dashboard become one entry, **Dashboards**. Its first tab, **Overview**, is the Dashboard everyone knows - always there, fixed - and your own boards follow it as tabs, each at its own address you can bookmark. **+** on any tab makes a board; right-click a board's tab to rename, duplicate, move or delete it. The banner reads "Dashboards › Sales". "Open the admin on this board" is now **Open on my boards** (the board you used last), with the Overview one click away. Old My Dashboard links land on your boards. Without Dashboard Pro nothing changes.
345
+ - **Email: Ctrl+Enter sends** (Cmd+Enter on a Mac) from anywhere in the compose window, the editor included - after an "Are you sure?" that says who it goes to, the subject, and when (now, after the undo delay, or at the time you scheduled). **Keep writing** goes back to the message. It is in the shortcut list (press ?) and the Send help.
346
+
347
+ ## [1.4.0] - 2026-10-09
348
+
349
+ - **Frequent is easier to find.** The sidebar now shows the **Frequent** folder from the start, with a hint - "Right-click any Tool and choose Add to Frequent" - until you add one. Right-click the empty folder for **Hide Frequent until I add a Tool**; adding a Tool brings it back. The User Manual (Built-in Tools) now covers Frequent.
350
+ - **Buy and stop paid Tools yourself.** On a site we host, a paid Tool you do not have shows **Buy…** in Marketplace > Browse (**Renew…** once its licence has expired). It opens **Manage my site** in a new tab with the Tool ticked, so you see the new monthly price and confirm there; then press Install. **Uninstalling a paid Tool now tells your site's manager**: one you pay for on its own comes off your monthly bill, with the rest of the month credited.
351
+
352
+ ## [1.3.0] - 2026-10-09
353
+
354
+ - **Dashboard Pro: several boards.** Keep as many as twelve boards - Sales, Content, Support - as tabs above the Dashboard: **+** makes one, and right-clicking a tab renames, duplicates, moves or deletes it. Each has its own widgets and layout, and the board you were on is the one that opens. Right-clicking anywhere on the board now offers **Add widget…**, a new board, fold or open everything, refresh and the settings; a widget's own menu starts with Add widget too.
355
+ - **Dashboard Pro: a settings cog.** The board's banner has a cog with your own settings - a **name** for your board (shown in its banner and the sidebar), a **greeting** with today's date, **3, 4 or 6 columns**, **compact cards**, how often widgets **update** (live, slower, or only when you press Refresh), and whether the admin **opens on your board**. Administrators also set the board's name for everyone and can **lock the layout**, so everyone else sees the site's default and can fold and refresh widgets but not change the board.
356
+
357
+ ## [1.2.0] - 2026-10-09
358
+
359
+ - **Dashboard Pro: build your own Dashboard from widgets.** With the new Dashboard Pro Tool (paid), the Dashboard becomes a board you arrange yourself. **Add widget** lists everything the Tools you use can show - traffic, visitors now, what needs attention, recent activity, site health, top pages, and a widget for every Tool with a count in the sidebar, so unread email, bookings, support tickets and form submissions are there from the start. Drag cards by their grip, size them Small to Full width, fold them down to their title (one at a time or all at once - a folded card stops refreshing until it is opened), and remove them from their menu. Your layout is yours; **Reset** goes back to the site's default, and an administrator can make their layout that default. Where another Tool owns the Dashboard (the fleet manager's), the board is at **My Dashboard** in the sidebar. Without Dashboard Pro the Dashboard is exactly as it was.
360
+ - **Tools can offer Dashboard widgets** with `admin.widgets` in `plugin.json` - a kind (`count`, `stat`, `list` or `chart`) and a data route, no code. See "Dashboard widgets" in `docs/plugin-development.md`.
361
+ - **The admin sidebar has a right-click menu, and a Frequent folder.** Right-click any Tool for Open, Open in a new tab, Add to Frequent and Copy link; right-click a folder to open or close it, or all of them. **Frequent** is your own folder at the top of the sidebar - up to twelve Tools, in the order you choose (Move up / Move down), cleared from the folder's own menu. It is yours alone: kept in your browser, never in the shared sidebar menu, and it only ever shows Tools you are allowed to open. Count badges show there too.
362
+
363
+ ## [1.1.0] - 2026-10-09
364
+
365
+ - **Your own CSS classes in more places.** The same class boxes menus and forms have, and the same rules for names:
366
+ - **Collections and Views:** `item-class` on `[collection]` and `[view]` puts your classes on every entry - each card, list item, accordion panel, timeline event, carousel slide, list-group item or block. (Table rows are drawn in the browser and do not take them.)
367
+ - **Footer:** Site Settings > Footer has CSS classes for the footer itself and for every social media icon.
368
+ - **Every page:** Site Settings > General > Body CSS classes puts your classes on the `<body>` of every public page - one hook for site-wide styling.
369
+ - **Context menus:** a menu's Appearance takes CSS classes for its panel, and each item takes its own in the item's panel (item classes need Domma 0.51.7).
370
+ - **Built against domma-js 0.51.7** - see [its release notes](https://github.com/pinpointzero73/domma/releases/tag/v0.51.7).
371
+
372
+ ## [1.0.0] - 2026-10-09
373
+
374
+ **Domma CMS 1.0.** From here the version number is a promise: anything that breaks a site or a plugin - the plugin hooks and manifest, the config file shapes, the shortcodes, the REST and Content APIs - waits for a new major version. Features arrive in minor releases and fixes in patches, as they have been. Nothing changes on upgrade; 1.0.0 is 0.120.0 plus the change below.
375
+
376
+ - **Menus: Darkened hover is stronger.** The hover wash goes from 12% to 16% black, so it reads clearly on a light bar; the selected item stays at 22%, still the darker of the two.
377
+
378
+ ## [0.120.0] - 2026-10-09
379
+
380
+ - **New sites start lighter.** `npx domma-cms <name>` now creates a site on the **Dreamy Light** theme with a **Corporate Light** admin and **Open Sans** as the font (the setup wizard's theme prompt defaults to Dreamy Light too), and its main menu is a light bar that darkens the item under the pointer and, more strongly, the page you are on. Existing sites are not touched - the updater never rewrites `site.json`, `theme.json` or a menu.
381
+ - **Menus: a Darkened hover effect.** `highlight.hover: "darken"` puts a flat dark wash behind the item under the pointer, on light and dark bars alike - the hover twin of the selected item's Darkened, and a little lighter so the current page still stands out. Offered in the menu's Behaviour tab and in each item's own Highlighting.
382
+
383
+ ## [0.119.0] - 2026-10-09
384
+
385
+ - **Forms: your own CSS classes.** The form builder has a **CSS classes** box in Settings (added to the `<form>`), under Submit Button (the submit button, and Finish on a multi-step form) and on every field (its wrapper, `[data-field="name"]`), so a designer can style a form from Custom CSS. They apply on every render path - the `[form]` shortcode, `data-form` embeds and the builder's preview - and on each step of a multi-step form. Names follow the same rules as menu classes; a bad one is refused on save. The `[form class=…]` attribute still sets the placement's wrapper.
386
+
387
+ ## [0.118.0] - 2026-10-09
388
+
389
+ - **Menus: your own CSS classes.** A menu item's editor has a **Styling > CSS classes** box, and a menu has one on its **Behaviour** tab, so a designer can style a menu or a single item from Custom CSS. An item's classes go on its `<li>` - in the navbar (dropdown children included), the footer and the `[menu]` shortcode - so they can lay the item out as well as style it; target the link with `.your-class > a`. A menu's classes go on its outer element: the navbar, the footer column's `<nav>`, or the `[menu]` wrapper (before the shortcode's own `class=`). Names are letters, digits, hyphens and underscores; anything else is refused on save. See **Designer classes** in `docs/configuration.md`.
390
+
391
+ ## [0.117.0] - 2026-10-09
392
+
393
+ - **Content API: headless, read-only access to your pages.** `/api/content/v1` serves the page list, one page by its path (as HTML, Markdown or both), the menu at a location, the site settings public pages already show, and search, as JSON. It is off until you switch it on at **System > Content API**, which also sets who may call it - anyone, a project API token, or a role - what it serves and in which formats, and has a Try it console that fetches a page as an anonymous caller would get it, before the API is on. A page it serves is exactly a page the site would serve the same caller: both now make one decision (`resolvePublishedPage()` in `server/services/pagePublishing.js`), and a test holds them to it. Front matter `api: false` keeps a page off the API. Payloads are allowlists - no custom front matter, no mail settings - and each page names the Domma behaviour its HTML needs. Settings: `config/content-api.json`; reference: **API Reference > Content API** and `docs/api-reference.md`.
394
+ - **API tokens: content scopes.** A scope line of `@pages`, `@menus`, `@site` or `@search` limits a token to those Content API resources. A token reads only its own project's pages; a token scoped only to collections gets no content.
395
+ - **Search no longer finds unpublished pages other than drafts.** It hid only `status: draft`, so a scheduled or status-less page came back in results that led to a 404. Search, its sidebar count and the Content API now share one rule: published, in an enabled project, and visible to the visitor.
396
+
397
+ ## [0.116.8] - 2026-10-08
398
+
399
+ - **Email: choose how many messages a page lists.** A picker under the message list (between Newer and Older) offers 25, 50, 100 or 200 per page, remembered in this browser. Changing it keeps the first message of the current page in view. It applies to folders, conversations and search; the routes already honoured `limit` up to `maxListLimit` (200), the reader just never sent it. Mail Reader gets it too.
400
+
401
+ ## [0.116.7] - 2026-10-08
402
+
403
+ - **Email: double-click a folder to fold it.** Double-clicking a folder that has folders inside it opens or closes it, as the chevron does; a single click still just opens the folder.
404
+ - **Email: a default mailbox.** Settings > Mailboxes > Make default marks one mailbox as yours to start with: it is listed first, in the settings and in the folder pane, and the screen opens on it rather than on the last one looked at. One per person; Unset default clears it (`PUT /accounts/:id/default`, `setDefaultAccount()` in `_lib/mail/accounts.js`). Mail Reader gets both too.
405
+
406
+ ## [0.116.6] - 2026-10-07
407
+
408
+ - **No more lost entries when two changes land at once.** A file collection's entries are kept in one `data.json` that every change reads, alters and writes back whole; two changes overlapping (two form submissions, two imports, a save during a migration) each started from the same file and the later write silently dropped the earlier one's entry. Changes to one collection now run one at a time (`server/services/adapters/fileLock.js`, also used by `userProfiles.js`, which writes its file directly), and every write goes to a temporary file that is renamed over the old one, so a crash mid-write can no longer leave a truncated file that reads as empty. Found when two Contacts Pro imports at once stored 149 of 150 contacts.
409
+
410
+ ## [0.116.5] - 2026-10-07
411
+
412
+ - **Right-click the email itself.** In Email (and Mail Reader), right-clicking the open message - or the menu key / Shift+F10 inside it - opens a menu: Copy and Quote in reply for selected text, then the same actions as the message list (Reply, Reply all, Forward, read/unread, star, spam, snooze, Move to, Save to the site, Delete, and the actions other Tools add, such as Call or Book). Links and images in the message keep the browser's own menu. The message frame now runs exactly one script, ours, allowed by its hash in the body's CSP (`MENU_BRIDGE` in `_lib/mail/render.js`); it still has no same-origin access, and the mail's own scripts are stripped and refused.
413
+ - **Mail Reader 1.2.4** (free-tier copy): its guide and notes describe the right-click menu on the open message.
414
+
415
+ ## [0.116.4] - 2026-10-07
416
+
417
+ - **Site search is a disc on the right by default.** A new site (or one with no `config/search.json`) gets the floating search button, a 52px circle 32px from the right edge and halfway down the page, instead of the navbar icon. Sites that chose their own placement keep it.
418
+
419
+ ## [0.116.3] - 2026-10-07
420
+
421
+ - **The handbook catches up with 0.103-0.116.** Built-in Tools: the purpose groups (Productivity, Communication, Marketing, Publishing, Business, Maintenance) replace the Core group in the text, a Support section, Booking among the Marketplace Tools, and what SEO Pro adds now (Search Console with sending the sitemap to Google, verification, IndexNow, speed). Navigation: sidebar counts that show when nothing needs attention, and resizing or folding the sidebar. Shortcodes: `[celebrate character="…"]`, the optional Christmas and St Patrick's scenes, and the page editor's Celebrations submenu. Pages: deleting a project with its contents, and Remove sample content. Site Settings: where the browser-tab icon comes from.
422
+
423
+ ## [0.116.2] - 2026-10-05
424
+
425
+ - **Mail "Test connection" errors in plain English** (Email Pro and the mail reader). A wrong port/TLS pairing used to show OpenSSL's `wrong version number` and a self-signed server Node's `--use-system-ca` hint; they now say what to change - untick Implicit TLS on 587/143, or put a trusted certificate covering the hostname on the mail server. Hostname mismatches, expired certificates, a refused SMTP login and unreachable servers read plainly too.
426
+
427
+ ## [0.116.1] - 2026-10-04
428
+
429
+ - **Support 1.1.0** (bundled): right-click a support request (or use its ⋮ button, or Shift+F10) to manage it. On a site: mark as read, mark resolved, **archive** it or **remove** it from the list - Domma keeps the conversation, and a new reply from Domma brings it straight back; removing a request that never reached Domma cancels it. In the Domma inbox: resolve or reopen, mark urgent, **archive** (out of the badge until the customer writes again), copy the customer's email, open their site's admin, delete; and right-click a message to copy it or quote it in a reply. Both screens are fully reactive, the request panel included.
430
+
431
+ ## [0.116.0] - 2026-10-04
432
+
433
+ - **Celebration characters on their own.** `[celebrate character="…"]` puts one character on a page at any time of year, independent of the theme, the date and the seasonal particles: `elf`, `peeing-elf`, `thieving-elf`, `mooning-elf`, `leprechaun`, `mooning-leprechaun`, `bagpiper`, `knight`, `dragon`, `welsh-dragon`, `cupid`, `robin` and `snowman`. `size` is `small` (96px), `medium` (160px, the default), `large`, `xlarge` or a pixel height; `animate="false"` shows a still pose, and reduced motion stops it too. See `docs/markdown-shortcodes.md`.
434
+ - **Two optional scenes:** `[christmas mooning-elf="true" /]` and `[st-patricks mooning-leprechaun="true" /]` add a walk-in, wiggle and walk-out. Both are off unless asked for.
435
+
436
+ ## [0.115.5] - 2026-10-04
437
+
438
+ - **Footer menus look like footer menus.** Nothing styled them, so every site with a footer menu showed a bulleted list; they are one wrapped row of links now, in the theme's quieter text colour. A site's own `custom.css` still wins.
439
+ - **Dreamy heroes centre their tagline and buttons.** The 48-character measure on `gradient-dreamy-light` hero text had no centring, so in a full-width (or any wide) centred hero they sat off to the left.
440
+
441
+ ## [0.115.4] - 2026-10-04
442
+
443
+ - **Support 1.0.2** (bundled): Domma's replies to a request are emailed to the address the site was **bought with** (its owner on the Domma server), not to whatever email the requester's profile on the site holds - a site's admin can set that to anyone. A site made by hand still uses the profile email. The Support inbox says which address a reply will go to, and why.
444
+
445
+ ## [0.115.3] - 2026-10-04
446
+
447
+ - **Support 1.0.1** (bundled): the address is **support@domma-cms.com** - on the contact card and for new-request alerts - and a reply emailed to a customer comes from that mailbox with Reply-To it, so a customer who answers the email reaches Domma Support.
448
+
449
+ ## [0.115.2] - 2026-10-04
450
+
451
+ - **Support is on from the start on every install**, not only on sites a Domma server runs: a new project (`npx domma-cms`) has it switched on, and an update that brings it in switches it on (as it does Security). A standalone site shows the contact card and an email link; nothing can be sent from it.
452
+
453
+ ## [0.115.1] - 2026-10-04
454
+
455
+ - **The pricing card's button really is readable now.** 0.115.0 set its colour, but the content-link rules (which skip only classes containing "btn", "nav" or "footer") still coloured and underlined it as a link. The button carries `card-pricing-btn` too, so they leave it alone - white (the theme's text-on-primary) and not underlined, checked in a browser.
456
+
457
+ ## [0.115.0] - 2026-10-04
458
+
459
+ - **Remove sample content in one step.** A project can now be deleted together with everything filed under it - its pages (with their versions and share links), collections and their entries, forms, blocks, views, components, actions, menus, API endpoints (offline at once) and API tokens. Links to its pages leave every other menu (however they are written), and menus and right-click menus stop being bound to it - one bound only to it, or only to its pages, goes. It shows exactly what will go, then asks for the project's slug to be typed back. In **Projects > (project) > Settings**, *Delete with contents*; a project marked as sample content (a starter's example pages) shows **Remove sample content** on its overview.
460
+ - **Refused with nothing touched** while a user or role is filed under the project (people are never deleted with content), while one of its menus fills a site slot, or while a built-in collection is filed under it.
461
+ - **If something fails part way**, the project is kept so what is left can still be found under it, and the failures are listed.
462
+ - It needs the right to delete projects, pages and collections, and access to that project.
463
+ - **Every site has a browser-tab icon.** The Settings favicon or the brand logo, or otherwise the first letter of the site's name - there was none, and every page load asked for `/favicon.ico` and got a 404.
464
+ - **The pricing card's button is readable and goes somewhere.** It took the page's link colour (primary text on a primary button); it now uses the theme's text-on-primary colour. `[card layout="pricing" href="…"]` sets where it goes - it always went to `#`.
465
+
466
+ ## [0.114.1] - 2026-10-04
467
+
468
+ - **Embedded videos play.** The Content-Security-Policy had no `frame-src`, so browsers blocked every `[embed]` player - YouTube and Vimeo included. It now allows the site itself, YouTube's privacy-friendly player, Vimeo's player and the hosts in `config/embeds.json`.
469
+ - **Timelines with icons show their events.** `[event icon="…"]` (and `[collection display="timeline" icon-field="…"]`) emptied every item: the icon was written where the icon scanner turns the whole element into an icon. It is kept as `data-event-icon` now; the markers show the status icon.
470
+ - **Countdowns count down to their date.** `[countdown to="…"]` ran a one-minute timer whatever the date, `duration` was read as milliseconds rather than seconds, and `DD:HH:mm:ss` showed minutes and seconds. All three work as documented.
471
+
472
+ ## [0.114.0] - 2026-10-04
473
+
474
+ - **Support** (bundled, free on every site): a customer can reach Domma from their own admin.
475
+ - **The contact card** (email, phone, hours) is always on screen, set once on the Domma server and passed to every site on its next check. A site the Domma server did not start shows the card and an email link.
476
+ - **Ask for help** opens a conversation with the Domma team: normal or urgent, replies in the admin and by email, a badge for replies not yet read, and *Mark resolved*. Anything written while the Domma server is restarting waits on the site and is sent when it answers.
477
+ - **On the Domma server** it is the inbox of every site's requests: urgent first, a red badge while one is waiting, reply (the customer is emailed), resolve, priority and internal notes.
478
+
479
+ ## [0.113.0] - 2026-10-03
480
+
481
+ - **Signing in from a link to a plugin's screen works.**
482
+ - **Before:** a `#/login?next=` link (an expired session, a link in an email) to any plugin screen came back as "not found" after signing in, with no sidebar or top bar until a reload. The login screen went there before the plugins' routes were loaded, and the router's "not found" never runs the post-sign-in step.
483
+ - **Now:** an address that matches nothing while you are signed in loads the plugins and is tried again, once. A link to nothing, straight after signing in, lands on your home page with a note rather than a bare "not found". Plugin routes are no longer added twice when you sign in again.
484
+ - **Shop 1.1.0** (bundled): other plugins can sell their own products through the Shop's checkout. Cards Pro sells card-credit packs this way. Such products never appear in the shop itself.
485
+
486
+ ## [0.112.1] - 2026-10-03
487
+
488
+ - **Email stays in step with its unread badge.** An open inbox checks for mirrored arrivals every 30 seconds without interrupting search, selection or older pages. Refresh checks the mail server, returns to the first page and bypasses the cached message list; conversations and search use the newly synchronised mirror.
489
+
490
+ ## [0.112.0] - 2026-10-03
491
+
492
+ - **Every Tool's sidebar badge shows a count when nothing needs attention.** Before, a Tool with nothing pending showed a blank:
493
+ - **Todo** with nothing open shows how many tasks are done ("All done - 7 tasks completed").
494
+ - **Analytics** with nobody on the site shows today's page views.
495
+ - **Booking, Feedback and Shop** (bundled): the bookings in the next seven days, the reports sent or received, and the products.
496
+
497
+ These counts are inventories, so a closed folder's total still only adds up what needs attention.
498
+ - **Bundled plugins:** Booking's badge popover had arrived as a single block of text and now shows its rows. Feedback no longer answers with an empty label.
499
+ - `calculator` is now on the retired-plugins list, because the marketplace reserves the built-in Tool's name.
500
+
501
+ ## [0.111.0] - 2026-10-03
502
+
503
+ - **Emails can carry pictures, a Reply-To and list headers.** `sendEmail()` now passes `replyTo`, `headers` and `attachments` on to the mail transport, and omits each one when it is empty. An attachment with a `cid` is an inline image that the HTML can show with `<img src="cid:…">`. The Cards plugin uses this for the picture of a card's front, for replies going to the card's sender, and for one-click unsubscribe. Existing callers are unchanged.
504
+ - **A sender's name can no longer change the sender's address.** The From header is now passed to the mail transport as a separate name and address, so the name is encoded as text. Before, a display name containing quotes, `<…>` or a comma could put a different address in From. This applies to every email the CMS sends, including form notifications.
505
+
506
+ ## [0.110.0] - 2026-10-02
507
+
508
+ - **Calculator Tool.** Basic and Scientific layouts can open on their own page or in a floating panel that stays open during admin navigation. Choose a saved floating position from the settings cog or drag the title bar. Uses domma-reactive for shared state, with a coloured live display, calculation history, MC/MR/MS/M+/M− memory controls and selectable accents. The settings cog lives in the standard Tool card header. Includes keyboard input, degree/radian trigonometry, powers, factorials and safe expression parsing. Calculator participates in Tool switches and role permissions.
509
+
510
+ ## [0.109.5] - 2026-10-01
511
+
512
+ - **Messages keeps its unread count visible.** Sidebar count responses may request `showZero`, preserving an explicit zero badge on initial rendering and live updates when all messages are read. Other Tools keep their existing badge behavior.
513
+
514
+ ## [0.109.4] - 2026-10-01
515
+
516
+ - **A second licence-signing key, for licences only.** Each trusted key now says what it may sign: plugin code, licences, or both. The marketplace key keeps both. The new `svr-2-1` key belongs to the second manager server and is trusted for licences only, so a plugin signed with it is refused like any unknown key. Licences it issues load on sites running this release or later.
517
+
518
+ ## [0.109.3] - 2026-10-01
519
+
520
+ - **Explicit rate limits retain their IP-based buckets.** Login, password-reset and other routes with their own limit keep the same shared IP budget even when requests carry valid access tokens. The larger per-user allowance applies to the default API limit.
521
+
522
+ ## [0.109.2] - 2026-10-01
523
+
524
+ - **Email reading preferences.** Mail Settings has a Settings tab with a configurable delay before an open message is marked as read. Enter seconds; zero marks it read immediately. The default remains one second. Preferences are saved in the browser across mailboxes, and the read-only edition continues to leave messages unchanged.
525
+ - **Admin reloads no longer exhaust the API rate limit.** Browser assets do not consume the global request budget. Verified access tokens receive a separate allowance of 3,000 requests per minute per user; public traffic remains limited to 500 per minute per IP. Explicit login, password-reset and other route limits remain enforced.
526
+
527
+ ## [0.109.1] - 2026-10-01
528
+
529
+ - **Curled ticker tape and the remaining seasonal character redraws.** Ticker paper tumbles with shaded folds, consistent timing and retina rendering in Domma JS 0.51.6. Bonfire Night decorations, leprechauns, bagpiper, knight, English and Welsh dragons, and Cupid use the illustrated artwork from domma-celebrate 1.3.0 in the CMS renderer.
530
+ - **Built against domma-js 0.51.6** - see [its release notes](https://github.com/pinpointzero73/domma/releases/tag/v0.51.6).
531
+
532
+ ## [0.109.0] - 2026-10-01
533
+
534
+ - **The admin sidebar can be resized and collapsed.** Drag its right edge (mouse, touch or pen) - the handle now sits on the sidebar itself with a visible grip; it used to land on the far right of the window, where nobody could find it. Focus the handle for the keyboard: arrows step the width (Shift for bigger steps), Home/End go to the narrowest and widest, Enter collapses. Double-click resets to the natural width. The chevron in the sidebar header (or dragging the edge in past the minimum) collapses it to a rail of icons: hover or focus one for its name and what its count means, click a folder to open its contents in a flyout beside the rail. Width and collapsed state are remembered per browser. A floating sidebar's gutter now follows its width, so page content no longer slides under it after a resize.
535
+
536
+ ## [0.108.0] - 2026-10-01
537
+
538
+ - **Blur your background in a call.** A **Blur** button on the join screen and in every call's control bar (instant calls, online bookings, online meetings, Messages calls - for everyone in the call). The person stays sharp and the room behind them is blurred, worked out inside the caller's own browser: no picture leaves it to do this, and nothing is fetched from anyone but the site. Loads on first use (a few seconds), is remembered by the browser, and turns itself off on a machine that cannot keep up. Built on MediaPipe's selfie segmenter (Apache 2.0), hosted by the site with its usage logging removed. The site's security policy now allows WebAssembly (`'wasm-unsafe-eval'`; JavaScript `eval` stays blocked).
539
+
540
+ ## [0.107.1] - 2026-10-01
541
+
542
+ - **Celebration controls in the Page Editor right-click menu.** Insert any seasonal theme, butterflies, ticker tape or fireworks from a dedicated submenu. Existing seasonal shortcodes expose their theme’s own switches, including falling particles only. Off switches filter initial and newly spawned artwork on the published site.
543
+
544
+ - **Illustrated Halloween characters and Valentine’s butterflies.** Candlelit carved pumpkins, drifting cloth ghosts, articulated bats, a stitched scarecrow, a crooked haunted house and butterflies with shaped wings, veins and markings.
545
+ - **Email: hidden preview text stays hidden.** Marketing mail (LinkedIn job alerts, for one) hides its inbox preview line with `display:none`; the reader stripped that but kept its `width:0`, so the text showed one letter per line under a tall gap. The reader now keeps the styles that hide things (`display`, `visibility`, `height`, `max-height`, `line-height`, `overflow: hidden`, `opacity`, `vertical-align`), still none that can position or load anything.
546
+ - **Built against domma-js 0.51.5** - see [its release notes](https://github.com/pinpointzero73/domma/releases/tag/v0.51.5).
547
+
548
+ ## [0.107.0] - 2026-09-30
549
+
550
+ - **Add people to a call that is under way.** The host clicks **Add people** in the call, searches their contacts and the team (or types a name and email), and sends each person their own Join link - by email, and for team members also as a notification (bell and desktop) and a direct message in Messages. **Copy link** makes a link to pass on. Everyone added knocks and the host lets them in; nobody is ever given the host's link or the guest's own, and someone rung into a call does not see what was said before they came. Contacts' addresses are shown masked and only a whole address finds one; Join emails are limited per person and to 30 an hour for the whole site; links use the site's address. Instant calls ("Call Sam now…") have it, and so have Booking and Calendar calls. For Tools: `mountCallRoutes({invite: {search, send}})` adds `GET :room/people` and `POST :room/invite` (the host only, 10 people a request, 20 a minute), `_lib/call/invite.js` (signed per-person invite passes, matching, limits), `server/services/callInvites.js` (finding people and sending) and a new hook, `registerDirectMessenger` / `sendDirectMessage`, which Messages provides.
551
+ - **Booking 1.6.0 / Calendar 1.14.0 / Messages 1.7.0.** Add people in an online booking's call (each person their own link for the session; when the person booked has no account on the site, their call offers Copy link only) and in an online meeting's call (each person their own link for that date only). In Messages, anyone in a conversation's call can ring in another team member they may message: they get a notification and a message with a link of their own that works for that call only, they are not added to the conversation and do not see the call's earlier chat.
552
+ - **The call screen, corporate.** Full-bleed under the site header with no page margins; a charcoal stage with one accent colour; video tiles fill the height for any number of people; a docked control bar with labelled buttons; the chat in quiet grey blocks with your own messages in the accent; the join screen a clean card.
553
+
554
+ ## [0.106.0] - 2026-09-30
555
+
556
+ - **The call screen, redesigned.** The call fills the window on a dark stage: the other person large, your own camera in a small corner window, a top bar with a timer, and round controls at the bottom. The chat reads like WhatsApp - grouped bubbles, times, day dividers - and people who join late see what was already said.
557
+ - **A call's chat is saved against the call.** What people type in a call's chat is kept, and a call opened again - after everyone has left, or after a restart - shows the earlier messages. Instant calls ("Call Sam now…") keep it on the call itself (the last 500 lines): a contact's Calls list says how many messages a call had, and clicking that opens the chat, read-only, for the person who made the call or an admin (`GET /api/calls/:id/chat`). Tools get the same: `_lib/call/chatStore.js` (a small JSON store for the engine's `chat: {load, save}`), `save(room, item, {identity})` now tells the Tool who the line is from (server side only, never sent to a browser), `whoOf` is exported from rooms.js, and `plugins/_lib/admin/ui/transcript.js` shows a chat read-only in a slideover.
558
+ - **Booking 1.5.0.** An online booking's panel has "Call chat (N messages)": the chat from its call, read-only, for staff who can see bookings. A class's shared call has one chat.
559
+ - **Calendar 1.13.0 / Messages 1.6.0** (Pro). Calendar keeps each date's call chat and shows the latest in the event editor for its organiser and calendar admins. In Messages, what you type in a conversation's call is posted into the conversation as your own messages ("in the call"), once - read already by everyone who was in the call.
560
+
561
+ ## [0.105.2] - 2026-09-30
562
+
563
+ - **The snowman joins the Christmas redraw.** Softly shaded snow, twig arms, coal features, a carved carrot, a striped knitted scarf and a holly-trimmed felt top hat, matching domma-celebrate 1.1.1.
564
+ - **Built against domma-js 0.51.4** - see [its release notes](https://github.com/pinpointzero73/domma/releases/tag/v0.51.4).
565
+
566
+ ## [0.105.1] - 2026-09-30
567
+
568
+ - **Christmas decorations have been redrawn with domma-celebrate 1.1.0's illustrated style.** Layered fir trees with lights and presents, evergreen wreaths, feathered robins, dressed elves, a lacquered red sleigh and the vintage green Christmas express.
569
+ - **Two naughty elves, both off by default.** Enable `peeing-elf="true"` or `thieving-elf="true"` on `[christmas /]` or `[celebrate theme="christmas" /]`, or use their checkboxes in the effect builder. The thief takes a real present from a visible tree, then runs away carrying it. Snow only keeps both off, regardless of attribute order.
570
+ - **Built against domma-js 0.51.3** - see [its release notes](https://github.com/pinpointzero73/domma/releases/tag/v0.51.3).
571
+
572
+ ## [0.105.0] - 2026-09-30
573
+
574
+ - **Call someone now, from any Pro Tool with calls.** "Call Sam now…" on a contact's menu and an email's menu starts a video call on your own site with that one person, straight away - not a booking or an event. A Call now screen (`#/calls/new`) picks who, emails them a Join the call link if you like, and opens the call in a new tab with you as host; they knock and you let them in. The link lasts 3 hours, and a contact shows your recent calls with them. It is part of the CMS now (it was Booking Pro's), switched on by any of Booking Pro 0.4, Contacts Pro 0.8 or Calendar 1.12 through the new `hooks.enableInstantCalls()`; a site without one of them has no instant calls, and every route answers 404. A new **Calls** permission decides who may call (Admin is given it once). You can call a contact shared with you (Contacts Pro). The call page is `/_call/<id>`; it carries none of the site's added scripts, and the link's key leaves the address bar once the page has it (on every Tool's call page). At most 10 calls a minute per person and 3 call emails an hour to any one address.
575
+ - **Booking 1.4.1.** Its call page sends a reloaded address back to the link.
576
+ - **Reloading a call page keeps you in the call** - the link leaves the address bar once the page has it (Booking, Calendar and Messages call pages too), and a reload of this tab returns to it.
577
+
578
+ ## [0.104.2] - 2026-09-30
579
+
580
+ - **The page editor's right-click menu and effect builder offer Christmas's options** - Snow only, and each decoration (trees, wreaths, North Star, snowmen, sleigh, elf, train, fireworks, robin) Shown or Off - on `[celebrate]` and on `[christmas /]`, which it now recognises.
581
+
582
+ ## [0.104.1] - 2026-09-30
583
+
584
+ - **`[christmas snow-only="true" /]`** - just the falling snow, every decoration off. Switching one decoration off by name now works whatever its case - `northstars="off"` never did.
585
+
586
+ ## [0.104.0] - 2026-09-30
587
+
588
+ - **The call screen has icon buttons** - mic, camera, share screen, chat (with an unread count), record and leave; on a phone the bar shows icons only.
589
+ - **Booking 1.4.0.** Call now: an online booking's call can be opened at once, even when it is booked for later - the customer's existing link works straight away. Extensions can add buttons to Booking's banner (Booking Pro's Call now uses it).
590
+
591
+ ## [0.103.0] - 2026-09-30
592
+
593
+ - **Online meetings: video calls on your own site.** A new shared call engine (`plugins/_lib/call`) holds meetings in the browser - camera, microphone, screen sharing, chat and a waiting room the host lets people in from - with nothing to install and no outside service. Up to 6 people a call. The host can record from their own browser (the file is saved on their computer; everyone is told). Booking Pro's online appointments and the Calendar's online meetings use it; a site gets calls through those Tools. An optional `config/calls.json` points calls at a relay (TURN) server for strict office and mobile networks.
594
+ - **Booking 1.3.1.** Serves the call for online bookings (made by Booking Pro), and keeps it working for bookings already made if Pro lapses. The booking page is rebuilt on domma-reactive. Book from an email: a sender already in your Contacts is booked as that contact.
595
+ - **Tools start in groups in the sidebar** by what they are for - Productivity, Communication, Marketing, Publishing, Business, Maintenance - built-in and marketplace Tools alike. Anything you have already arranged stays where you put it.
596
+ - **New free Tool: Booking.** People book appointments with you or your team, and rooms or equipment, from a booking page (`/booking`, or the `[booking /]` shortcode): opening hours, days off, buffers and notice; confirmation and reminder emails with a link to move or cancel; a Diary in the admin. Book straight from a contact or an email; bookings show in the Calendar and customers become contacts. Arrives switched off - turn it on in Tools.
597
+ - **Tools can add actions to an email's menu.** `registerRelatedItems('message', {id, label, actions})` puts entries such as Booking's "Book Sam..." in a message's menu in Email and Mail Reader; `{email}`, `{name}` and `{subject}` in an action's url become the sender's. Any related-items action can now carry `permission: {resource, action}`, and is shown only to roles that hold it.
598
+
599
+ ## [0.102.1] - 2026-09-29
600
+
601
+ - **Placeholders in block templates are filled in one pass.** A raw value (`{{{field}}}` - a post body, pre-rendered HTML) is no longer read again for `{{placeholders}}`, so text that shows `{{date}}` or `{{title}}` - a code sample, a merge-field example - keeps it instead of getting the date or title. The same fix is in Blog 1.9.4 and Blog Pro 0.5.5, whose post layouts had the same two-pass fill.
602
+
603
+ ## [0.102.0] - 2026-09-29
604
+
605
+ - **Cards and boxes can span columns in a grid.** `[card span="2"]` or `[box span="2"]` (2 to 6, or `full` for the whole row) takes that many columns of a `[grid]`, whether it sits straight in the grid or inside a `[col]`. The span never takes more columns than the grid has at that screen width, so on phones it is full width and a wide card never adds a phantom column. Set it in the Card Builder, the Edit Card and Edit Box slideovers, or the right-click menu (Span (in a grid)).
606
+
607
+ ## [0.101.11] - 2026-09-29
608
+
609
+ - **Cards and boxes now transition their border colour on hover.** The border moves smoothly from its existing colour to the theme colour, without an outline or lift; tinted boxes use their own theme tone.
610
+ - **Built against domma-js 0.51.2** - see [its release notes](https://github.com/pinpointzero73/domma/releases/tag/v0.51.2).
611
+
612
+ ## [0.101.10] - 2026-09-29
613
+
614
+ - **Cards and boxes highlight instead of lifting on hover.** The default hover uses a subtle outline in the theme's primary colour; tinted boxes use their own theme colour. `hover="off"` still disables the highlight.
615
+ - **Built against domma-js 0.51.1** - see [its release notes](https://github.com/pinpointzero73/domma/releases/tag/v0.51.1).
616
+
617
+ ## [0.101.9] - 2026-09-29
618
+
619
+ - **Fixed: stray characters in place of middle dots on some browsers.** HTML pages, public docs pages, shared-link pages and error pages are now sent as `text/html; charset=utf-8`, and the middle dots in the admin Documentation, blog blocks and cart are written as `&middot;`, so they no longer show as garbled characters when a browser guesses the wrong encoding.
620
+ - **Built-in array helpers in bindings.** Tool and plugin views can use `len`, `includes`, `count`, `where`, `sortBy` and more straight from `data-bind-*`, `data-if` and `data-each` expressions (domma-reactive 1.3.0), with no computed needed.
621
+ - **Built against domma-js 0.51.0** - see [its release notes](https://github.com/pinpointzero73/domma/releases/tag/v0.51.0).
622
+
623
+ ## [0.101.8] - 2026-09-28
624
+
625
+ ### Security
626
+
627
+ - **Database connection passwords no longer reach the browser.** Data > Collections > Database connections
628
+ showed each MongoDB URI with its password in plain text. `GET /api/collections/connections` now masks the
629
+ password in every URI, secret query parameters and secret-looking driver options. Saving puts the stored value
630
+ back for a mask left untouched, but only while the connection still points at the same scheme, user, hosts,
631
+ query and driver options (renaming it or changing the database is fine) - point it anywhere else and the password must be typed
632
+ again, so the stored one can never be sent to another server. A mask it cannot match is refused, never saved.
633
+
634
+ ### Fixed
635
+
636
+ - Saving connections from the rows view no longer wipes driver `options` set in the raw JSON.
637
+
638
+ ## [0.101.7] - 2026-09-28
639
+
640
+ - **Built against domma-js 0.50.3** - see [its release notes](https://github.com/pinpointzero73/domma/releases/tag/v0.50.3).
641
+
642
+ ## [0.101.6] - 2026-09-28
643
+
644
+ - **Clearer warning for bindings inside an unkeyed list.** Domma Reactive 1.2.1 gives one warning per unkeyed `{{#each}}` naming every binding it dropped and pointing at `key=`, instead of a second message advising to move them out of the block.
645
+ - **Built against domma-js 0.50.2** - see [its release notes](https://github.com/pinpointzero73/domma/releases/tag/v0.50.2).
646
+
647
+ ## [0.101.5] - 2026-09-28
648
+
649
+ - **Fixed: accordions open to their answers again.** Every `[accordion]` and collection accordion showed an empty panel (a Domma 0.48 change paired each header with the wrong panel); fixed in domma-js 0.50.1. Reactive bindings handed an observable instead of its value now warn and stay hidden rather than showing their content (domma-reactive 1.2.0).
650
+ - **Fixed: the component preview on sites installed or updated from npm.** `admin/preview/` was missing from the package, so the component editor's Live preview and the page editor's component inserter showed "Route GET:/admin/preview/component-preview.html not found". A test now checks every admin file loaded by URL ships in the package.
651
+ - **Built against domma-js 0.50.1** - see [its release notes](https://github.com/pinpointzero73/domma/releases/tag/v0.50.1).
652
+
653
+ ## [0.101.4] - 2026-09-28
654
+
655
+ - **New: `hover="off"` on `[box]` and `[card]`.** Boxes and cards lift a little when the pointer is over them; `hover="off"` (also `false`, `none`, `no`, `0`) keeps one still - for a box that is not clickable. It is in the Box insert form, the editor reference and the shortcode docs. A bare `hover` still asks for the opt-in lift.
656
+
657
+ ## [0.101.3] - 2026-09-28
658
+
659
+ - **Fixed: the block editor's preview shows your site's styles.** Blocks were drawn inside the admin, so the site's theme, fonts, site styles and Custom CSS never reached them and the preview looked unstyled. The preview now draws the block inside your site's own page and redraws it as you type. It can show one copy or three in a collection grid, at full, tablet or mobile width, and in another site theme to try. `{{{field}}}` placeholders now render as HTML (they showed stray braces), template comments are left out as on the site, and image and link fields get a picture and `#` as samples.
660
+ - **Security: the block editor's preview cleans a block's markup as the site does.** A saved block could carry script (an `onerror` handler, a `javascript:` link, a nested frame) that the public site strips but the editor's preview ran - with the signed-in admin's access - when someone opened the block. The preview now removes it before drawing, and talks to the preview by message only.
661
+ - **Block editor: smaller fixes.** Ctrl+S saves while the preview has focus; a sample you have not changed follows its placeholder when you switch between `{{field}}` and `{{{field}}}` (raw rows are labelled `{{{field}}}`, and Insert/Copy use that form); placeholders inside template comments no longer get sample rows; only fields whose names end in image, photo, logo and the like get a picture (not imageCaption or photoCredit); a remembered preview theme that no longer exists falls back to the site's; a preview page that cannot hold the block says so instead of "Loading" for ever.
662
+
663
+ ## [0.101.2] - 2026-09-28
664
+
665
+ - **Users list: avatars.** Each user shows their profile picture, or their initials on a colour of their own, the same colour wherever they appear. Inactive users' avatars are faded.
666
+ ## [0.101.1] - 2026-09-28
667
+
668
+ - **Built against domma-js 0.50.0** - avatars and avatar groups (`E.avatar`, `E.avatarGroup`) and accessibility/text utilities (`.sr-only`, `.skip-link`, `.line-clamp-*`, `.stretched-link`, `.divider-text`); see [its release notes](https://github.com/pinpointzero73/domma/releases/tag/v0.50.0).
669
+
670
+ ## [0.101.0] - 2026-09-28
671
+
672
+ - **Changed: admin help popovers use Domma's popover.** The (?) help icons open the same text as before, but the panel now follows the page as you scroll instead of closing, flips to the other side when there is no room, and Esc inside a slideover closes only the help. Screen readers hear the help's title as the panel's name. Sites on a Domma core older than 0.49 keep the previous panel.
673
+ - **Added: loading placeholders in the admin.** While a list or the dashboard loads, grey shapes of what is coming - rows, a table, media tiles, the dashboard's figures, lists and chart - stand in for the old "Loading..." text or empty space. A quick load shows nothing extra, and an error still shows the screen's error message. On Dashboard, Pages, Collections and their entries, Forms and submissions, Media, Users, Menus, Marketplace and Notifications. Needs domma-js 0.48 or later; an older one keeps the "Loading..." text.
674
+
675
+ ## [0.100.2] - 2026-09-27
676
+
677
+ - **Built against domma-js 0.49.0** - popovers (`E.popover`): rich panels anchored to a trigger, click or hover, keyboard and screen-reader friendly; see [its release notes](https://github.com/pinpointzero73/domma/releases/tag/v0.49.0).
678
+
679
+ ## [0.100.1] - 2026-09-27
680
+
681
+ - **Built against domma-js 0.48.0** - skeleton loaders (`E.skeleton`, `.skeleton-*`), and an accessibility sweep: Modal, Tabs, Accordion, Toast and Table add their own ARIA roles and keyboard support, and 31 theme contrast failures are fixed; see [its release notes](https://github.com/pinpointzero73/domma/releases/tag/v0.48.0).
682
+
683
+ ## [0.100.0] - 2026-09-27
684
+
685
+ - **Built against domma-js 0.47.0** - input extras (show / hide, clear, stepper, counter) and readable link colours on 25 themes; see [its release notes](https://github.com/pinpointzero73/domma/releases/tag/v0.47.0).
686
+ - **Added: helpers on form and collection fields.** A password field can have a show / hide button, text fields and textareas a clear button (x) and a character counter, and number fields - and + buttons that respect the field's min, max and step. Tick them on the field in the form editor or the collection editor's field panel. Needs domma-js 0.47.
687
+ - **Added: style the submit button, give it icons, and add a second button.** The form editor's Settings tab has a **Submit Button** card: the text and position, a style (Primary, Secondary, Success, Danger, Warning, Info, the Outline styles or Ghost), a size (Small, Normal, Large) and an icon before and/or after the text, picked with the icon picker. A **second button** can sit beside it: **Clear form** empties the answers and puts the form back as it started (asking first if the visitor has typed anything), or a **Cancel link** takes the visitor to a page. It has its own text, style and icons; choose which button comes first and whether the two sit side by side or stacked, and Position lines the pair up. Works on pages and in embeds, and the live preview shows it. On a multi-step form, Next and Finish take the style and size and Finish the icons; there is no second button there. When a trigger ends a form early, its Finish keeps the icons and style. A Cancel address must be a page on this site or an http(s), mailto or tel address - anything else is refused when you save. Forms you do not change look exactly as before.
688
+ - **Fixed: links were hard to read on 25 themes.** Where a theme's main colour is too pale to read as text - lemon-light, silver-light, sunset-light, wedding-light, royal and others - page links now use a darker (or, on dark themes, lighter) shade of it that reads at 5:1 or better. Themes whose colour already reads keep it unchanged. Needs domma-js 0.47.
689
+ - **Added: choose where a form's submit button sits.** The form editor's Settings tab has a **Submit Button Position**: Default (as before, straight after the last field), Left, Centre, Right or Full width. Left, Centre and Right give the button a row of its own at that side of the form, and Full width stretches it across. It works in grid, stacked and inline forms, on pages and in embeds, and the live preview shows it. It takes over from the grid-only Submit Button Span - a form set to a full-width span keeps its look and shows as Left. Multi-step forms keep Previous and Next / Finish in their footer as before. Forms nobody positions do not move.
690
+ - **Fixed: the form editor's preview put the submit button on the right when visitors saw it on the left.** The preview now draws the button row the way the public page does.
691
+ - **Boxes follow the theme, and can be coloured.** `[box color="success"]` (or `primary`, `secondary`, `info`, `warning`, `danger`) tints a box with that colour from the site's theme - pale on a light theme, deep on a dark one - and it changes with the theme switcher. `[box color="#0f766e"]` keeps your own colour on every theme and picks black or white text for it automatically. `variant="primary"` now works on a box too.
692
+ - **Fixed: dark, glass and glow cards and boxes were hard to read on some themes.** `variant="dark"` had a footer at 1.9:1 contrast, links in the theme's colour on the dark panel (1.8:1) and pale code; on light themes a dark card's header strip stayed light. Everything on the dark panel is now set for it. `glass` was a white film that greyed out dark themes and is now a see-through pane of the theme's own surface, and the footers of the clean, glass, accent, gradient and glow variants use the theme's secondary text colour (the glow footer was 1.7:1 on Lemon). Applies to `[card]` and `[box]` alike.
693
+
694
+ ## [0.99.0] - 2026-09-27
695
+
696
+ - **Built against domma-js 0.46.1** - joined input groups (`E.inputGroup`, addons on selects and textareas) and a Forms icon set; see [its release notes](https://github.com/pinpointzero73/domma/releases/tag/v0.46.1).
697
+ - **Added: icons and text joined to form inputs.** A form field or a collection field can now have an icon or a short text joined to the start or end of its input, Bootstrap-style - a magnifier before a search box, £ before a price, kg after a weight. Set it under Before / After in the form editor's field card, or Joined to the input in the collection editor's field panel, with the icon picker. Works on text, number, email, phone, URL, date, time, dropdown and textarea fields.
698
+ - **Added: form trigger messages can show the visitor's answers.** Write `{{name}}` (any field's name) in a banner, toast, block reason or end-form message, and it shows what the visitor answered - "Hello {{name}}" updates as they type. Choices show their labels, a tickbox shows Yes, and passwords, files, images and anything a trigger has hidden are never repeated back. Answers always show as plain text. An **Insert field** list beside the message box picks the field for you, and saving warns about any token that will come out blank. Redirects and admin notifications don't fill tokens yet.
699
+
700
+ ## [0.98.0] - 2026-09-27
701
+
702
+ - **Context Menus editor: drag items into order.** In a context menu's Items tab you can now drag a row to move it, with the other rows sliding aside; the arrow buttons, the row menu and Alt+Up/Down still work.
703
+ - **Collection editor: smoother field dragging.** Field rows drag the same way as everywhere else now - the others slide out of the way, touch works and Esc cancels.
704
+ - **Form editor: smoother field dragging.** Drag a field card by the six-dot grip in its header and the other cards slide out of the way; it works on touch screens, Esc cancels, and Alt+Up/Down moves the focused card (the Move up/down menu items now show the shortcut).
705
+
706
+ ## [0.97.0] - 2026-09-27
707
+
708
+ - **Added: Arrange mode for the admin sidebar.** A six-dot button at the top of the sidebar (for users who may edit menus) lets you rearrange the sidebar where it is: drag items to move them, drop one on the middle of another to put it in that folder, hide or show items with the eye, or use Alt+arrow keys. Done saves it for everyone; Cancel or Esc puts it back. It changes the same menu as Content > Menus > Admin sidebar, which is still where labels, icons and who-sees-what are edited.
709
+
710
+ ## [0.96.0] - 2026-09-27
711
+
712
+ - **Added: Tool groups in the admin sidebar.** A Tool can name a `group` (`registerSidebarItem({group})`, or `group` on a plugin.json `admin.sidebar` entry) and then sits in a sub-folder of its folder. The built-in Tools - Notes, Todo, Analytics, Contacts and SEO - now sit in **Tools > Core**; Marketplace plugin Tools stay directly in Tools unless they name a group. The group is made when the sidebar draws and only stored once you save something inside it; moving a Tool (or the whole group) in the Menus editor still wins, **Back to its default place** returns a Tool to its group, and a user who may use none of a group's Tools does not see an empty folder.
713
+ - **Fixed: two sidebar folders with the same name opened and closed together.** A folder's open/closed state (and its closed-folder total) was keyed by its name alone, so Tools > Core and a project called Core, or two "Components" folders, shared one. Nested folders are now keyed by their key or their place in the tree; top-level folders keep the state you left them in.
714
+ - **Menus editor: drag rows the smooth way.** Dragging a row by its grip now uses Domma's new `E.sortable`: the row follows the pointer, a line shows where it will land above or below a row and an outline shows it going inside one, and the rows glide to their new places when you let go. Esc cancels, it works on touch screens, and a drop that would put a row inside its own branch or inside a separator is refused before you let go. Dropping on the line just under a folder now lands the row there, at the top of that folder, rather than below the whole folder. The grip is a six-dot handle with room before the label.
715
+ - **A site repo no longer picks up the CMS's own bookkeeping.** The generated `.gitignore` now leaves out `content/.notification-sources.json` (when each notification source last ran) and `content/.migrations.json` (which one-off migrations have run) alongside the SEO audit, so they stop showing as changes after every start.
716
+ - **Built against domma-js 0.45.0** - see [its release notes](https://github.com/pinpointzero73/domma/releases/tag/v0.45.0).
717
+
718
+ ## [0.95.0] - 2026-09-27
719
+
720
+ - **Changed: the seeded Admin role can use the built-in Tools.** Admin is given Contacts, Notes, Todo, Analytics and SEO once, the first time the site starts on this release. Untick one under System > Roles and it stays unticked - it is not put back on the next start. (Until now only the level-0 role had Notes, Todo, Contacts and Analytics, so on sites whose owners are Admins those Tools were out of reach.) SEO Pro uses the same SEO permission, so it follows.
721
+ - **Security: applying a scaffold recipe needs permission for everything the recipe creates.** It used to need only `collections.create`, although a recipe can also create roles, user accounts, menus, a project, Actions, API tokens and API endpoints. Apply now checks each (`users.create` for roles and users, `menus.create`, `projects.create`, `actions.create`, `api-tokens.create`, `api-endpoints.create`) before it writes anything, and refuses with 403 naming every missing one. A role the recipe creates must sit below your own level and grant only permissions you hold, and a user it creates must get a role below your level. The level-0 role passes.
722
+ - **Fixed: scaffold recipes mangled emails and passwords.** Every option was turned into a slug, so the onboarding recipe's HR email `hr@acme.com` became `hr-acme-com` and the password was changed too. Options now have a `type`: only `slug` options (the default) are slugified; `email` must look like an address, `password` is kept exactly as typed (at least 8 characters, never logged or echoed) and `text` is trimmed. The Apply template form shows email and password fields for them.
723
+ - **Fixed: a scaffold apply refused for a conflict left its new project behind.** The project is now created after the conflict check.
724
+ - **Fixed: Marketplace > Browse said only "Installation failed." for a plugin that needs a newer Domma CMS.** It now shows the reason, as Install from file does ("SEO Pro needs Domma CMS 0.90.0 or later; this site runs 0.89.2. Update the CMS first."), and the card says **Needs newer CMS** before you click, instead of Install or Update. `GET /api/plugins/marketplace` adds `cmsVersion` and a `needsCms` sentence per plugin.
725
+
726
+ ## [0.94.0] - 2026-09-27
727
+
728
+ - **Changed: `npx domma-cms` stops on a Node older than 24 again.** In 0.93.0 it only warned below the `engines` minimum, because the Domma fleet still ran on Node 22; the fleet now runs Node 24, so the scaffolder and `update` refuse to run on older Node, naming the version they found.
729
+ - **Docs: `docs/DommaFeatures.md` brought up to 0.93.0** - the 0.92 and 0.93 changes in the feature text (dd/mm/yyyy dates and their helpers, the Contacts group rule, action roles, `scope="mine"` with transitions, id-keeping storage moves, Notes and Todo on MongoDB, the access fixes, `minCmsVersion`, Node 24), plugin guides and the new Documentation pages, Feedback and Waypoint Pro sections, Social Media Pro's LinkedIn posting, a catalogue table rebuilt from the Marketplace manifests, and a feature history mapping reference and file fields, row-level access, storage migration and the timeline, carousel and listgroup displays to the versions they arrived in.
730
+ - **Added: plugin guides in the Documentation folder.** A plugin's `docs/*.md` files show under **Documentation > Plugins** (`#/docs/plugins`, one page per guide, tabs for extra files) while the plugin is enabled, and only to users who can use it - the manifest's `docs.permission`, else its sidebar permission. Markdown is sanitised and shortcodes are shown as written. Every Marketplace plugin now ships a guide; `plugins/_template` has a starter. `GET /api/documentation/plugins[/:name[/:page]]`.
731
+ - **Docs: the admin Documentation folder brought up to 0.93.0.** New pages: Editions & Licences, Collections & Forms, Built-in Tools, and API reference for Forms, Scaffold, the External API and tokens, and API Builder (also `docs/editions-and-licences.md`). Every existing Usage, API, Tutorial and Components page was checked against the code and corrected (roles and the visibility ladder, Actions roles, Views access, the Navigation page that still described `navigation.json`, Site Settings fields, `[view]` needing no MongoDB, and more). Template braces and blank lines inside code blocks no longer vanish or break the public handbook.
732
+
733
+ ## [0.93.0] - 2026-09-27
734
+
735
+ - **Changed: dates read dd/mm/yyyy everywhere a person sees one** (dd/mm/yyyy HH:mm where a time is shown). Admin lists that fell back to ISO (`2026-09-01`) after a fortnight, draft-restore notes ("27 Sep 14:30"), entry, version, media, user and project details, collection date fields and their filter chips, the SEO screen, related items, the analytics chart axis, sidebar badge popovers, notifications (licence, API token, crash and sample notices), the password-changed email, the share-link expiry note, and the shared mail reader, compose window, send-later text and the `{date}` / `{time}` template tokens (which followed the browser's locale, so could read 9/20/2026 or 2:30 PM). "Relative" wording ("3 days ago", "tomorrow at 09:00") is kept. Stored values, API payloads, date inputs, filenames, feeds and CSV exports are unchanged. One helper per side does the formatting: `admin/js/lib/dates.js`, `server/services/dates.js`, and for plugins `plugins/_lib/admin/ui/dates.js` (browser and server) - Marketplace plugins updated to use it need this release.
736
+ - **Fixed: Notes and Todo declared MongoDB storage but were always kept in files**, so on a site with MongoDB any notes or todos stored there were invisible. Notes, Todo and Contacts (the Tools that hold people's own data) now use MongoDB when their collection declares it (Notes and Todo do; Contacts declare files) and the site has a working MongoDB connection - MongoDB is a Pro feature, so a site without one keeps using files, as before. A site is never switched to an empty store: while the files hold any notes or todos the collection stays on files, and where both stores hold rows it stays on files and the server log names the collection with both row counts. A collection's **Storage** move (which keeps ids) now works for these four collections and is how such a site settles it; a restart no longer resets their storage choice. Roles, user profiles, projects, notifications and API tokens/endpoints stay on files and cannot be moved.
737
+ - **Changed: any role an action lists is enough to run it.** An action allowed to `admin` and `hr` used to admit only admins and above - the check took the most senior role listed and compared levels, so `hr` was ignored. Each listed role now lets its holders in, and everyone more senior, across all the roles a user holds - the rule page and view visibility use (`canReadView`). It decides `POST /api/actions/:slug/public` and which transition buttons a row offers. A role the site does not have still admits only the level-0 role; an action naming no roles is for admins (levels 0 and 1), as the Actions list has always said. The action editor's help says so.
738
+ - **New: `transitions` on a `[collection scope="mine"]`.** An interactive "my entries" block (`searchable`, `sortable`, `filterable` or `paginate`) now renders as the Collection Browser with the viewer's own rows, so each row gets its state-machine buttons - the recruitment recipe's candidate dashboard. It used to fall back to a static list and drop the buttons. The server keeps the viewer to their rows: paging, the transition list and running a transition or `cta` from that block all say `scope: "mine"`, and a row the viewer did not create is refused with 403 (as a row-level rule refuses it) before the state check, so an illegal move on their own row is still 409. Transition buttons also honour the action's row-level rule now, so an owner-only action is no longer offered on someone else's row.
739
+ - **Fixed: the interactive Collection Browser never started.** `collection-browser.js` built its browsers before the class it builds them from was defined, so every `searchable` / `sortable` / `filterable` / `paginate` block stayed on its no-JS fallback. Its `cta` and transition buttons were also never wired, because `site.js` is a module and its button wiring was not visible to the browser script.
740
+ - **Changed: only a contact group's creator, or someone allowed to manage every group, can rename or delete it.** Groups are shared by everyone who uses Contacts, so a rename or delete changes every user's contacts - and until now anyone with the Contacts edit or delete permission could do it. New groups record who created them; the new **Contacts > Manage groups** permission (`contacts.manageGroups`) lets its holders rename or delete any group. The level-0 role holds it, and `admin` is granted it once (removed by hand, it stays removed). Groups created before this release have no recorded creator, so only holders of the permission can rename or delete them. Anyone can still file their own contacts in any group. In **Manage groups** the Rename and Delete buttons are greyed out where the server would refuse, with a help popover saying why; `GET /api/contacts/groups/access` answers who may do what, and `GET /api/contacts/groups` is still a plain list of names.
741
+ - **Fixed: moving a collection between file and MongoDB storage gave every entry a new id and date**, so references, links and row ownership pointing at them broke, and an entry that no longer passed validation was dropped. The move now copies each entry as it is - same id, data and `meta` (`createdAt`, `updatedAt`, `createdBy`, `source`, ...) - in both directions. It refuses, changing nothing, when the target already holds an entry with one of the ids (answering 409 with the ids) or the source holds an id twice, and 503 when the MongoDB connection is not available. The old copy is set aside, never deleted: `data.json` becomes `data.json.bak` (timestamped if one exists), and a MongoDB collection is renamed `cms_<slug>__moved_<time>` so a later move back does not collide with it. System collections (roles, user profiles, projects, ...) can no longer be moved at all - they always use file storage.
742
+ - **Security: an action allowed only to a role the site does not have could be run by any signed-in user** through `POST /api/actions/:slug/public`, and its transition buttons were offered to everyone - the same unknown-role gap 0.92.1 closed for collections. Unknown role names now count for nothing, and an action naming no role that exists is for the level-0 role only.
743
+ - **Fixed: in the role editor, the built-in Tools' permissions (Contacts, Notes, Todo, Analytics, SEO) were listed after the plugins' ones**; "Tools" now has its place before Plugins.
744
+ - **New in the action editor: "Create an entry in another collection".** The `createInCollection` step has run on the server for a long time but could only be added by hand-editing an action; it is now in the step picker, with a check that its data is a JSON object, and in Documentation › Actions.
745
+ - **Fixed: importing entries skipped the reference check** that every other save makes, so an import could store a reference to an entry that does not exist. Such entries are now skipped and reported with the rest. The import help no longer says that fields the collection lacks are ignored (they are stored as given) and names Data Transfer as the Marketplace plugin it is.
746
+ - **Changed: the page editor no longer offers "Show in Navigation"**. Nothing on the site has read it since menus replaced the old navigation; the site's navigation is edited in Menus. A value already stored in a page is kept.
747
+ - **Fixed: block templates named after a real shortcode could shadow it.** The reserved-name list had `list-group` (the tag is `listgroup`) and `effect` (no such shortcode), and lacked `embed`, `component`, `listgroup` and the named celebrations (`[christmas /]` and the rest), so a block called `embed` would have been read as a block short tag.
748
+ - **Fixed: installing a plugin ran its `onEnable` straight away**, with nothing passed in, although the plugin is installed switched off; a plugin that read its argument failed at install, and every plugin's `onEnable` ran twice (install, then enable). It now runs once, when the plugin is enabled, with `{fastify, services, hooks}`.
749
+ - **New: a plugin's `minCmsVersion` is enforced.** Installing or updating a plugin that needs a newer Domma CMS is refused with both version numbers ("… needs Domma CMS 0.93.0 or later; this site runs 0.92.2"), and an update leaves the installed version in place. A site whose CMS version cannot be read is not locked out.
750
+ - **New for plugin authors: `options.hooks.registerComponent({name, source})`** adds a Domma component from a `.dmc` source, used on pages as `<dm-name>` or `[component]` and listed read-only in Components. It existed in core but was never handed to plugins.
751
+ - **Changelog correction:** the 0.68.0 entry announced `POST /api/plugins/entitlement` for renewals; that route was never shipped. The entry now says how a renewal is applied.
752
+ - **Fixed: `npx domma-cms` checked for Node 18**, although Domma CMS needs Node 24 (`engines` in package.json). The check now takes the minimum from `engines` and warns, naming the version it found; it does not stop, because fleet updates still run on older Node. Its usage comment names the real `--no-seed` flag, and the list of plugins "enabled by default" - all three long since core or gone - is removed; bundled plugins start switched off as before.
753
+ - **Fixed: running an action whose state-machine transition no longer applies answered 400**; it now answers 409, as documented, on both `POST /api/actions/:slug/execute` and `/public`, so a client can tell "this entry has moved on" from a bad request. The action editor's help no longer mentions a `[transitions]` shortcode, which never existed - transition buttons come from an interactive `[collection … transitions]`.
754
+
755
+ ## [0.92.1] - 2026-09-27
756
+
757
+ - **Security: a collection's API access set to a role the site does not have let any signed-in user through.** The collection editor used to offer `subscriber`, `editor` and `manager`, which a default install has never had; an unknown role came out as the lowest level, which every signed-in user meets. It now admits only the level-0 role, the same rule page visibility uses, on `/api/v1`, the public collection route and API Builder endpoints alike. The editor lists the site's real roles ("User and above") and marks a stale one "manager (role not found)" with a note, instead of quietly swapping it.
758
+ - **Fixed: `[view]` showed a view's rows to anyone who could open the page**, whatever the view's own access said. The static display, the interactive one and `/api/views/:slug/public` now make one decision (`canReadView`): public views for everyone, otherwise the view's roles on the usual ladder, and row-level rules applied as the viewer. A page with a per-user view is rendered fresh for signed-in visitors rather than served from the role-shared cache, and an anonymous visitor to a row-level view gets no rows (it used to match every row with no owner).
759
+ - **Fixed: views with row-level `reference` mode did no row filtering at all**, on file and MongoDB storage. Rows are now checked per viewer, and totals and page counts include only the rows that viewer may see. A `field` rule with no field set shows nothing instead of everything.
760
+ - **Fixed: site search listed pages the visitor cannot open.** Role-gated pages came up for anonymous visitors, title, description and snippet included. Results now follow the searcher's roles (signed-in visitors are recognised by their session, as on pages) and leave out `noindex` pages - in the page's own SEO or set in SEO - and pages in a disabled project. System › Search's "Left out" count says which.
761
+ - **Fixed: collection export set to a role the site does not have was hidden from everyone**, super-admins included; it is now offered to the level-0 role only.
762
+ - **Changed: a new view with no roles chosen is for the admin tier by level**, not by the names `admin` and `super-admin`, so renaming a role no longer changes who can read it.
763
+ - **Docs brought in line with the code**: getting started (the scaffolder installs for you, the real setup steps, what `reset` and `fresh` do), the full shortcode list and processing order, `SECURITY.md` (sign-outs have survived restarts since 0.83.0), `TRUST_PROXY=loopback` rather than `true` (which lets visitors fake their address), the seeded roles in place of Manager/Editor/Subscriber, the plugin hook reference, and cache keys. `docs/DommaFeatures.md` is new: the full feature reference for the CMS and the Marketplace plugins.
764
+
765
+ ## [0.92.0] - 2026-09-27
766
+
767
+ - **Feedback (new, free plugin): tell Domma what is wrong or missing.** Admins of a managed site send a bug, a request, a question or praise from the admin and see what became of it - its status and any reply - in the same place. On the manager the plugin is the inbox for every site's reports, able to file one as a Waypoint Pro issue that the report then follows. A report is queued before it is sent, so a manager restart loses nothing; the site's fleet token alone says which site sent it.
768
+
769
+ ## [0.91.1] - 2026-09-27
770
+
771
+ - **Fixed: changes in SEO settings (who runs the site, blog posts as articles, robots rules) could take a while to show on pages**, which are cached; saving now refreshes them at once. The same for a blog post's SEO title and description set in SEO.
772
+ - **Fixed: four front-end files changed without their cache-buster moving** (`site.css`, `collection-context.js`, `menu-float.js`, `collection-browser.js`). Their `?v=` tokens are bumped, and the test that was meant to catch this now compares each file's content, not just its token, and covers every versioned asset the page links.
773
+
774
+ ## [0.91.0] - 2026-09-27
775
+
776
+ - **New: `[box]`** - a card with no header, for wrapping content in a plain container. Without attributes it has no shadow and a 2px corner radius; `shadow` and `rounded` change either, and the other card attributes (padding, borderless, variant, footer, class, effects) work as on `[card]`. A card can go inside a box and a box inside a card. It is in the editor's Insert menu, the Editor Reference and Documentation › Shortcodes; `rounded="xs"` (2px) is also new on `[card]`.
777
+
778
+ ## [0.90.5] - 2026-09-27
779
+
780
+ - **Fixed: SEO settings would not save "who runs the site" without a name.** The name is optional now - search engines are given the site's name when it is empty.
781
+ - **The Media Picker for pictures in SEO**: the logo in SEO settings and a page's share image are chosen from Media - or uploaded there and then - with a Choose… button, instead of typing a path (typing one still works).
782
+ - **Fixed: after saving a settings slideover, "Discard the changes you have not saved?" came up anyway.** Closing a slideover tidied it away through the same check that guards unsaved changes, which still compared against the values from before the save. Every Tool's settings had it (SEO, Security and others); tidying up no longer asks.
783
+
784
+ ## [0.90.4] - 2026-09-27
785
+
786
+ - **Built against domma-js 0.44.9** - a fresh install of the CMS (a new site, or `npm install` in one) no longer pulls in domma-js's development web server, `live-server`, and the ~180 packages under it; see [its release notes](https://github.com/pinpointzero73/domma/releases/tag/v0.44.9).
787
+
788
+ ## [0.90.3] - 2026-09-27
789
+
790
+ - **The page editor has settings** (the cog in its banner): choose the editor's font - Fira Code, JetBrains Mono, Source Code Pro, IBM Plex Mono and other monospace faces, or a plain sans-serif or serif - its size, line spacing, and whether long lines wrap. Changes show as you choose and are kept in your browser for every page; the line numbers stay level with the text whatever you pick.
791
+
792
+ ## [0.90.2] - 2026-09-27
793
+
794
+ - **Fixed: the page editor did not fill the window, and its editor and preview would not scroll.** The Tool banner put the editor inside two more boxes that did not pass the window's height down, so the panes grew to the page's full length: on a long page there were no scrollbars, on a short one the editor stopped part way down. It fills the window again, and each pane scrolls on its own. Live Preview no longer cuts off the bottom of the page.
795
+
796
+ ## [0.90.1] - 2026-09-27
797
+
798
+ - **Built against domma-js 0.44.8** - admin and site sidebars follow the active theme; see [its release notes](https://github.com/pinpointzero73/domma/releases/tag/v0.44.8).
799
+
800
+ ## [0.90.0] - 2026-09-27
801
+
802
+ **SEO is built in.** What was the free SEO plugin in 0.89.0 is now part of Domma CMS - a built-in Tool like Analytics, in the sidebar and on by default (it can be switched off in System › Plugins like the other Tools). SEO Pro, in the Marketplace, still adds the not-found log, the link monitor and SEO history on top.
803
+
804
+ - **SEO in the sidebar on every site** (`#/seo`): the audit of every page, the search result and share card as they will look, redirects, the sitemap and robots.txt - as the SEO plugin had them. Its API is `/api/seo` (was `/api/plugins/seo`); a new `seo` permission (View / Manage) is given to Admins once.
805
+ - **A site that used the SEO plugin keeps everything.** Its redirects, page overrides and settings move across on the first start (to `content/seo/` and `config/seo.json`); the old plugin folder is never loaded again.
806
+ - **Redirects and overrides are part of the site's content** (`content/seo/redirects.json`, `overrides.json`), tracked with the rest; how often each redirect is used, and the last audit, are kept out of the repo.
807
+ - For plugin authors: SEO's registry (`globalThis[Symbol.for('domma.seo')]`) is unchanged, so SEO Pro and anything else built on it works as before; `RETIRED_PLUGINS` in services/plugins.js lists plugins that became part of the CMS, whose folders are skipped.
808
+
809
+ ## [0.89.1] - 2026-09-27
810
+
811
+ - **Fixed: renaming a page failed with an error, and menu links kept pointing at the old address.** Changing a page's URL Path moved the page, then failed while updating the old navigation file the menus replaced - so the editor said the save failed and stayed on the old address. Menu links to the page (every menu, sub-items included) now follow it to the new address, as the field's help always said.
812
+ - **The page editor is built on Domma's reactive bindings.** Every field, the Unsaved and problem notes, Discard, the view modes, the CSS pane, Live Preview, History, share links and named versions read one set of state instead of being shown, hidden and filled in by hand. Nothing looks different; History's select-all box now shows when only some versions are ticked. The Editor Reference is data drawn by one template, so adding a shortcode's entry is adding an object.
813
+ - **Removed: the old Navigation screen's code.** `/navigation` has sent you to Menus since menus replaced it; the unused screen behind it is gone.
814
+ - **Built against domma-js 0.44.7** - domma-reactive 1.1.1; see [its release notes](https://github.com/pinpointzero73/domma/releases/tag/v0.44.7).
815
+
816
+ ## [0.89.0] - 2026-09-27
817
+
818
+ **SEO.** A new free plugin, SEO, ships with the engine (switched off until you turn it on in System › Plugins): an audit of every page, the search result and share card as they will look, redirects, and control of the sitemap and robots.txt. SEO Pro, a paid add-on in the Marketplace, adds a not-found log, a link monitor and SEO history.
819
+
820
+ - **SEO 1.0.0 (new, free).** Checks every page in the sitemap - title, description, headings, image descriptions, links, a focus keyphrase, canonical link, share image, structured data - and titles or descriptions used on more than one page. Open a page to see its Google result and share card as they will look, and change its title, description, share image and focus keyphrase, or hide it from search engines. Redirects (301, 302, 307, 308, 410 gone; `/old/*` moves everything under it) are added for you when a page is renamed, and re-aimed so they never chain. The sitemap's sources can be switched off and addresses left out; robots.txt can take extra rules, or ask every search engine to stay away while a site is not ready; the home page can tell search engines who runs the site (organisation, local business or person), and blog posts are marked as articles.
821
+ - **Blog posts are in the sitemap.** Until now `/sitemap.xml` listed only pages; plugins can now add what they serve (hooks.registerSitemapSource). Blog 1.9.0 lists its posts; Blog Pro 0.5.0 every blog's.
822
+ - **Fixed: blog pages had no canonical link** (and no `og:url`) - the Blog never told the page its address.
823
+ - **Fixed: saving a page in the editor wiped the SEO fields it does not show** (share image, hidden from search engines). The editor sends only the SEO title and description; the rest of a page's `seo` is now kept. The Blog's post editor had the same fault, fixed in Blog 1.9.0 / Blog Pro 0.5.0.
824
+ - **Share images and structured-data images are full addresses** (`https://…/media/photo.jpg`, not `/media/photo.jpg`) - Facebook, LinkedIn and others ignore a relative one.
825
+ - **Fixed: a page saved without a visibility was left out of the sitemap**, although it is served to everyone.
826
+ - For plugin authors: the `seo:meta` transform (one model behind `<title>`, the description and every SEO tag, on pages and plugin pages - `renderBlogPage` passes `seoMeta.item`), `sitemap:entries` and `robots:lines` transforms, `hooks.sitemapChanged()`, `sitemap.listSitemapEntries()`, and the `content:pageRenamed` event. JSON-LD now escapes every `<`.
827
+
828
+ ## [0.88.1] - 2026-09-27
829
+
830
+ - **Fixed: the first-run wizard's "Name your site" step failed on every new site.** Continue asked for the old navigation file, which the menus migration retired, so it stopped with an error. The site's name now goes on the navbar brand in the same save.
831
+ - **Fixed: the theme picked in the first-run wizard was never used.** It was saved where nothing reads it any more; it now goes to the site's theme settings (Content > Theme).
832
+ - **The sign-in screen is built on Domma's reactive bindings.** Setup, onboarding, sign in, the two-factor step, forgot and reset password are one set of state the markup reads, rather than panels shown and hidden by hand. Nothing looks different.
833
+ - **Fixed: what visitors sent through a form was committed to the site's repo.** A form storing its submissions in its own collection (`find-a-tutor`, `feedback`, anything but the built-in `enquiries`) had every entry - names, emails, phone numbers - tracked by the site's content-only `.gitignore`. The file now keeps out the entries of every collection a visitor can write to: a form's collection when the form is placed on a page or block, a collection a form created, and - the moment a submission is stored - whatever collection it went into. A content collection with a form of its own (every collection made in the admin gets one) stays tracked. Cart Pro's orders and discount codes, Calendar RSVPs and the job board's applications, candidates and signups joined the private list.
834
+
835
+ ## [0.88.0] - 2026-09-27
836
+
837
+ **Password resets that work on every site, and Tools you can switch off.** Reset links have not opened from their email on any site until now; this release fixes that and rebuilds the whole reset flow. Contacts, Notes, Todo and Analytics can now be switched off, plugins say what they depend on, and a site's manager can lock a switch.
838
+
839
+ - **Fixed: a password reset link opened from its email showed "404 Page not found".** A page loaded on an admin address carrying a query (`#/reset-password?token=...`) matched no screen, so the link only ever worked in a tab that already had the admin open. It opens the reset screen now.
840
+ - **Fixed: password reset links did not open on sites behind a proxy.** The link carried the CMS's internal port (`https://example.com:4096/...`); it now uses the site's real address.
841
+ - **Fixed: a reset request could make the site email a genuine reset link to another website.** The link was built from whatever host the request claimed, so someone could ask for a reset of your account "from" their own domain and collect the token if you clicked. Links now go only to the Site URL, or to an address someone has signed in at.
842
+ - **Fixed: how long "Forgot your password?" took to answer showed whether an account existed.** It now answers at once, the same way for every address, and sends the email afterwards.
843
+ - **Fixed: without a mail server, "Forgot your password?" pretended to send a link.** The login screen now says the site cannot email one and to ask an administrator; the admins are notified when someone asks (new notification: Password reset not sent).
844
+ - **Fixed: signing in straight after the page loaded - a password manager that fills and submits - could leave you on the sign-in screen**, signed in but going nowhere.
845
+ - **Fixed: the reset email and login screen always said the link expires in 1 hour**, whatever `resetTokenExpiry` was set to; the account name is now escaped in the email; a reset link for an account made inactive since no longer works; malformed reset requests get a 400.
846
+ - **The reset screen checks the link as it opens.** An expired, used or withdrawn link says so straight away, with Send me a new link - not after you have typed a new password twice. A good one names the account and how long the link has left, checks the new password against the site's password rules as you type, and ends on a Password changed screen with your email ready to sign in. Password managers file the new password under the right account.
847
+ - **Back to where you were.** Opening an admin page while signed out - or having your session run out - takes you to sign in and then straight back to that page; a session that ran out says so.
848
+ - **Password boxes have show / hide and warn when Caps Lock is on**, on sign in, setup and reset. The email you typed at sign in comes with you to Forgot your password; the card shows the site's name.
849
+ - **Administrators can reset a password from Users.** Right-click a user (or ⋮, or Shift+F10): Send password reset email, Copy a reset link - for a site without email or a mailbox that is not receiving, shown once - and Withdraw the reset link. A Reset link open chip marks who has one, and the Users badge counts them. The user editor has the same under the password (Or let them choose it). As everywhere in Users: not your own account, only someone your role is senior to, and only an active account - greyed with the reason otherwise.
850
+ - **"Your password was changed" email** to the account holder after any change - a reset, My Profile or an administrator - saying how and when, with what to do if it was not them.
851
+ - **Reset emails are limited to 3 an hour per account**, whatever address they are asked for from, so nobody can fill someone's inbox (an administrator's resets are not counted).
852
+ - **Site URL in Settings › General** (`site.baseUrl`) - the site's own address, which reset links, canonical links and the sitemap are built on; checked as you type. A client sending it back through `PUT /api/settings` no longer gets a 400.
853
+ - New: `POST /api/auth/reset-password/check`, `POST`/`DELETE /api/users/:id/password-reset`, events `auth:resetRequested` and `auth:resetIssued` (the auth events are now documented in docs/plugin-development.md). A user update can no longer write reset tokens.
854
+
855
+ - **The built-in Tools can be switched off.** Contacts, Notes, Todo and Analytics are still on unless you say otherwise - they now have their own switches at the top of System › Plugins. A Tool that is off disappears from the sidebar, its screens and API stop answering, its notices and calendar entries go, and Analytics stops counting. Nothing is deleted: switch it back on and everything is where it was.
856
+ - **Plugins say what they depend on, and a switch tells you before it breaks anything.** A plugin that needs a Tool (Contacts Pro needs Contacts) goes off with it; one that only uses it (Calendar invites contact groups) stays on and you are told which feature it loses. Switching a plugin on brings back what it needs. You confirm first. A plugin whose requirement is off is not started, whatever else says so.
857
+ - **Your site's manager can lock a switch.** A Tool or plugin marked Locked can only be changed by whoever manages the site; everything else is yours to switch as before.
858
+ - **Mail Reader 1.2.1** says it uses Contacts (Add sender to contacts). Marketplace plugins updated alongside: Calendar 1.8.0, Surveys 1.4.0, Invoices 1.6.1 (its contact picker works again - it had called an address gone since 0.67.0), Contacts Pro 0.7.1, Shopping Cart Pro 0.1.1, Email 0.5.1.
859
+
860
+ ## [0.87.0] - 2026-09-26
861
+
862
+ - **To-dos now notify their owner.** Each morning, anyone with overdue to-dos gets one notice listing them, and one for what is due today (gone at midnight) - never one per task. Completing, deleting or re-dating a to-do updates it straight away. Switch it off under Notifications › Sources (*To-dos overdue or due today*).
863
+
864
+ ## [0.86.0] - 2026-09-26
865
+
866
+ - **Blog 1.8.0: who may do what on the blog is now a permission, checked on every route.** Until now any signed-in account could read every draft and commenters' email addresses, write drafts, and approve or delete comments. Posts, categories and comments each have their own permission (under Roles), given to admins; Blog Author writes and edits their own posts, Blog Editor runs everything. An editor held as an additional role now counts.
867
+ - **Mail Reader 1.2.0: an Email permission**, for admins by default. A mailbox here opens connections from your server, so it is no longer open to every signed-in account; give it to other roles under Roles.
868
+ - **Blog 1.7.0: a comment waiting for approval now shows in Notifications** (source *Comments awaiting moderation*, for Super Admins by default), naming the post and quoting the comment. It goes once the comment is approved, rejected or deleted.
869
+ - **Fixed: an account confined to a few admin screens could still use every plugin's API.** A role can confine its holders to some screens (Job Board's companies, agents and job seekers - people who may have signed themselves up on the public site). The admin kept them on those screens, but the server did not: a plugin route that only checked for a signed-in user answered them too, so a job seeker could, for one, publish or delete blog posts through the API. Now a confined account can call a plugin's API only if one of that plugin's screens is open to it. Staff and every unconfined role are unaffected, and so is the public side. Their sidebar also stops asking other Tools for their badge counts, which had shown a confined account figures such as outstanding invoices and their clients.
870
+ - **Notifications with no audience now go to admins and up, not every role.** A manager push, or anything raised before notifications had audiences, was shown to every role that can read notifications. From this release, roles a plugin opens the bell to (Job Board's companies, agents and job seekers, who may have signed themselves up on the public site) see only what is meant for them.
871
+ - **Sample notifications are only for whoever added them.** *Add samples* in the Notifications settings no longer shows the four samples to everyone.
872
+
873
+ ## [0.85.0] - 2026-09-26
874
+
875
+ - **Built against domma-js 0.44.6** - see [its release notes](https://github.com/pinpointzero73/domma/releases/tag/v0.44.6).
876
+ - **Added Corporate theme suite:** `corporate-light` and `corporate-dark` are now available in the admin theme picker, theme settings, and setup wizard.
877
+ - **Shopping Cart, a new built-in plugin - switched off until you turn it on.** Products with options, stock and pictures; a shop at `/shop` (the address is a setting) with categories, search and sort; a basket that follows shoppers round the site; one-page checkout paid through Stripe, PayPal or offline; orders to fulfil, track and refund; `[shop-products]`, `[shop-product]` and `[buy-button]` for any page; sample products to try it with. Turn it on under System › Plugins. Shopping Cart Pro (discount codes, delivery zones, tax rules, order emails, insights) is in the marketplace.
878
+ - **Added Admin Slate finish:** `admin-slate-steel`, `admin-slate-indigo`, and `admin-slate-teal` complete the 3×3 Admin theme matrix with a balanced mid-tone chrome for long administrative sessions.
879
+
880
+ ## [0.84.1] - 2026-09-26
881
+
882
+ - **Fixed: Security's health check warned about HTTPS on every site** when it ran without a public address to check (a site's own report). Headers and HTTPS are now judged only through the site's address.
883
+ - **Fixed: Security could block the server's own address.** Requests the server makes to itself through a site's public name (health checks, certificate checks) count as local now, and are never blocked.
884
+
885
+ ## [0.84.0] - 2026-09-26
886
+
887
+ - **Fixed: Security said every site was out of date.** Its health check read the site's own version (from the site's `package.json`) as the CMS's; it now reads the CMS version the updater records.
888
+ - **Setting up two-factor at sign-in.** A sign-in check can now show a QR code and a key to type into an authenticator app on the *One more step* screen - so a role that requires two-factor sign-in (Security Pro) can have it set up right there. Only an image the server made itself is shown.
889
+
890
+ ## [0.83.0] - 2026-09-26
891
+
892
+ - **Security, a new built-in Tool - switched on.** Under Tools › Security:
893
+ - **A health check** of what could let someone in, each with why it matters and how to fix it: unused admin accounts, the CMS version, the HTTPS certificate, security headers, private files being served, visitor addresses behind a proxy, email for resets, plugin licences. A score, and a sidebar count of what needs attention.
894
+ - **Sign-in lockout.** Repeated wrong passwords lock the account and the address for a while - longer each time - and while locked, even the right password is refused.
895
+ - **Password rules** for every new password: at least 10 characters, not one of the 10,000 most common, not containing your name or email.
896
+ - **A log of every sign-in**, with CSV export, and notifications when an account is locked or someone signs in after many wrong passwords.
897
+ - **Scanners turned away.** The requests they send every site - WordPress logins, `.env` and `.git` files, database dumps - get an empty *not found*, and an address that keeps asking is blocked for a while. Addresses can be blocked by hand.
898
+ - **Locked out yourself?** `node plugins/security/bin/unlock.js --all` in the site's folder.
899
+ An update switches Security on the first time it arrives; a site that switches it off keeps it off.
900
+ - **Fixed: behind a proxy that was not trusted, nobody was rate limited - the sign-in included.** Loopback was exempt from rate limiting, and a CMS behind Apache with no `TRUST_PROXY` saw every visitor as loopback. Only a genuinely local call (a loopback connection with no `X-Forwarded-For`) is exempt now.
901
+ - **`TRUST_PROXY=loopback`.** `TRUST_PROXY` also takes Fastify's own values: `loopback` (recommended behind Apache or the manager - the visitor is the address the local proxy saw, which the visitor cannot forge), a list of addresses or ranges, or a hop count. `1` keeps its old meaning (trust every hop). New sites made by the manager get `loopback`.
902
+ - **Signing out now lasts.** Each sign-in is a session, kept on disk (`content/sessions/`, never committed). Signing out, signing out everywhere, a new password or a password reset end sessions for good - until now a signed-out session came back after a restart. A new password ends your other sessions; an admin changing someone's password, or switching the account off, ends theirs. `GET /api/auth/sessions`, `DELETE /api/auth/sessions/:sid` and `POST /api/auth/sessions/revoke-others` for your own.
903
+ - **Sign-in and password checks for plugins.** `registerLoginCheck` (before the password - a lockout - or after it - two-factor, which can ask one more question on a new *One more step* sign-in screen) and `registerPasswordCheck` (wherever a password is set). Events: `auth:loginFailed` (with the reason), `auth:loginSucceeded`, `auth:loggedOut`, `auth:passwordChanged`, `auth:rateLimited`.
904
+ - **Audit events.** Every successful change a signed-in person makes through the API raises an `audit` event - who, what, when, from where, and the names of the fields sent (never their values). Nothing is stored by the CMS; Security Pro keeps the trail.
905
+
906
+ ## [0.82.0] - 2026-09-26
907
+
908
+ - **A picture on My Profile.** Click the circle (or drop an image on it) to add your own avatar - any common photo up to 8 MB, iPhone HEIC included. It is cropped square around the subject, made small and saved at once, and shows on your profile and in the top bar; *Remove* brings your initials back. The uploaded file itself is never kept or served: it is re-encoded to a 256 px WebP. Deleting a user removes their picture.
909
+ - **Fixed: a doubled full stop in form errors.** A refused submission ended "…no more than 5.." - the route added a full stop after messages that already had one.
910
+
911
+ ## [0.81.1] - 2026-09-26
912
+
913
+ - **Fixed: number limits were ignored.** A number field's min and max (and a text field's min and max length) were stored by the form builder but dropped by the `[form]` embed, which is how most pages show a form, and never checked by the server. A "lessons per week, 1-5" field took any number. The embed now applies them, and the server refuses a submission outside them, naming the limit ("Lessons must be no more than 5."). A number field also refuses something that is not a number.
914
+ - **Fixed: field tooltips did not show on `[form]` embeds.** A tooltip set in the builder appeared only on the older `data-form` embed.
915
+
916
+ ## [0.81.0] - 2026-09-26
917
+
918
+ - **Forms can end early, and tell the admins.** Two new trigger actions:
919
+ - **End the form here.** When an answer means there is nothing more to ask (under age, a region you do not serve), everything after the field is masked, your message takes its place, and the submit button - or a wizard's *Next* - becomes *Finish*. Finishing saves the answers given so far and shows your closing message. The masked fields are neither required nor stored, even where the form marks them required, and the entry is tagged `meta.outcome = {type: 'ended', trigger, field}`. Change the answer and the rest of the form comes back.
920
+ - **Save the answers given so far** (on by default): untick it to keep nothing. No entry is stored and the form's email, webhook and action do not run.
921
+ - **Let the visitor finish** (on by default): untick it to stop the form dead. The rest is still masked, but the submit is disabled and the server refuses the post, so nothing is saved and nothing is raised.
922
+ - **Notify the admins.** Raised by the server when the form is submitted with the condition holding, so it cannot be skipped. It names the form, the question and the answer, and links to the submissions. It goes to every admin by default - Notifications' settings, source **Form triggers**.
923
+ - **Actions can notify too.** A new **Notify the admins** step (title, body, severity, link, all with template variables), source **Actions**.
924
+ - **The trigger editor is readable.** Every control has a label (a filled box used to have only a vanished placeholder). When / Then / Otherwise are distinct sections. Each action is a collapsible card whose head names it, summarises it in one line and says when it happens. Options that do not apply are hidden, and a narrow panel stacks instead of overflowing.
925
+ - **Fixed: a trigger could not test its own field.** The editor left the field a trigger belongs to out of its condition list, so a saved "when this answer is No" came back testing the next field, and the next save rewrote it. Worth checking any trigger saved since triggers arrived in 0.57.0. The field is now listed first, marked *(this field)*.
926
+ - **Fixed: a hidden required field refused the submission.** A field hidden by a trigger was excused by the form but still demanded by its submissions collection, which copies the form's required flags. The form's own decision now holds in both.
927
+
928
+ ## [0.80.0] - 2026-09-26
929
+
930
+ - **The site tells its Super Admins what needs them.** Notifications used to come only from the manager. Now core and plugins raise their own, and they go to Super Admins unless the source is set otherwise:
931
+ - **System sources:**
932
+ - Critical: a plugin failed to start, a licensed plugin stopped (unlicensed or expired), a MongoDB connection failed (collections fall back to files), the site restarted after a crash.
933
+ - Warning: a licence in its grace period or ending within 14 days, an API token expiring, repeated failed sign-ins or the sign-in limit hit, email not delivered or not set up, a form's email, webhook or action failing, someone given Admin or Super Admin, low disk space, a start-up repair left to retry.
934
+ - Off by default: every form submission, users added or removed.
935
+ - **Where to control them:** Notifications' settings cog lists every source under **What this site tells you**. Super Admins see this section only; each source can be switched off or sent to all admins or to everyone, and timed checks have **Run now**.
936
+ - **Repeats and resolving:** a repeat updates the one notification rather than adding another, and is unread again only if it got worse. A notification ends by itself when its cause goes away (the plugin loads again, the licence is renewed).
937
+ - **Links:** a notification can link to an admin screen (**Go there**) as well as a web address.
938
+ - **For plugins:** `options.hooks` now carries `notify({title, body, severity, link, source, dedupeKey, audience, expiresAt})`, `resolveNotification(dedupeKey)` and `registerNotificationSource({id, label, description, severity, defaultOn, every, atBoot, check})`, stamped with the plugin's name. `plugin.public.js` (survey responses, RSVPs) and `onEnable`/`onDisable` get them too.
939
+ - Scheduled checks run `boot`, every `15m`, `hourly` or `daily` (08:00 site time).
940
+ - A source may send at most 30 new notifications an hour, and a failure while raising one never breaks the caller.
941
+ - A notification's `audience` (`{roles}` or `{userIds}`) decides who sees it. Manager pushes and older entries have none, so everyone who can read notifications still sees them.
942
+ - **More sidebar badges.**
943
+ - **Effects:** how many effects the editor offers.
944
+ - **My Profile:** your details not yet filled in.
945
+ - **Layouts and API Tokens:** a count while all is well, and the problem count, as before, when pages use a missing layout or tokens are expiring. A count route may now answer `inventory: true` to keep its number out of a closed folder's total.
946
+ - **Desktop notifications.** Notifications has a settings cog. Its **Desktop notifications** switch asks the browser for permission and then pops up each new notification on the desktop, even when the admin is in a background tab. Clicking one brings you back to Notifications. You choose which ones pop up: all, warnings and critical, or critical only. Critical ones stay until you close them, and **Send a test** checks the setup. The setting is per browser, like the permission it needs. Turning it on never brings up what was already there, and a notification pops up once in that browser, however many admin tabs are open. The admin's once-a-minute bell check does the delivery, so no new polling is added.
947
+ - **Sample notifications.** The same panel's **Add samples** adds four made-up notifications, one of each severity, shaped like what the site's own sources will send. Use them to see the screen, the bell, the sidebar badge and desktop notifications working on a quiet site. They stay unread and go by themselves after a week; adding them again replaces the earlier ones. This needs the Notifications delete permission (`POST /api/system/notifications/samples`).
948
+ - **The sign-in pop-ups can be turned off.** In the same panel, **Pop-ups when you sign in** controls the sticky toasts for unread warnings and critical notifications (on by default, as before).
949
+
950
+ ## [0.79.1] - 2026-09-26
951
+
952
+ - **Right-click menus in the admin now highlight the item under the pointer.** The pages, layouts, forms, roles, settings, analytics and API-token views, and the shortcode editor menu, showed no hover at all in dark themes and a barely visible one in light themes. Built against domma-js 0.44.5 - see [its release notes](https://github.com/pinpointzero73/domma/releases/tag/v0.44.5).
953
+
954
+ ## [0.79.0] - 2026-09-25
955
+
956
+ - **Todo, Contacts and Notes meet the Calendar.** Two small registries let a plugin work with the built-in Tools without core ever importing it:
957
+ - **Calendar sources** (`registerCalendarSource` / `getCalendarSources`, handed to plugins in `options.hooks`): dated items a Tool shows on the Calendar plugin, asked for per range and per viewer - nothing is copied, so nothing falls out of step. Todo registers **To-dos**: your open to-dos, on their due day.
958
+ - **Related items** (`registerRelatedItems`, `GET /api/related`): what a plugin shows beside a contact or a note. The Calendar adds **Events** to the contact and note editors, and **Schedule with Sam...** / **Add to calendar...** to their menus. Core checks the record is the viewer's own before any plugin is asked; a failing or slow one is named and left out. Without such a plugin nothing appears.
959
+ - **Contacts and Notes say when something is deleted** (`contact:deleted`, `note:deleted` on the hooks bus), so a plugin that points at one - the Calendar's event links - can let go.
960
+
961
+ ## [0.78.1] - 2026-09-25
962
+
963
+ - **Built against domma-js 0.44.4** - no change in behaviour; see [its release notes](https://github.com/pinpointzero73/domma/releases/tag/v0.44.4).
964
+
965
+ ## [0.78.0] - 2026-09-25
966
+
967
+ - **The server stores number fields as numbers**, whoever writes them - the admin, the external API (`/api/v1`), form submissions, import and Actions. A `number` field sent as text ("42", as HTML forms and many integrations send it) is saved as 42, so it sorts, filters and totals properly. **API clients:** such a field now comes back as a number where it used to come back exactly as sent. Text that is not a number is still stored as sent rather than refused, so no request that worked before now fails. Plugins writing through a storage adapter directly are not covered.
968
+
969
+ ## [0.77.2] - 2026-09-25
970
+
971
+ - **Number fields stored as text are converted, once.** On its first start, a site turns every number field that the entry form saved as text before 0.77.1 ("12") into a number (12), in every collection - MongoDB-backed ones included - so they sort, filter and total properly again. Only plain decimal text in `number` fields changes; empty values and anything that is not a number are left as they are, and entries keep their "last changed" dates. It runs once (recorded in `content/.migrations.json`); a MongoDB collection whose connection is down is retried on the next start.
972
+
973
+ ## [0.77.1] - 2026-09-25
974
+
975
+ - **Fixed: a number field saved from a collection's entry form was stored as text** ("42", not 42), so it sorted, filtered and totalled as text in the API, `[collection]` displays and views. The form now saves numbers as numbers (0, negatives and decimals included); an empty box is still saved empty. Entries saved wrongly before are corrected the next time they are saved from the form.
976
+
977
+ ## [0.77.0] - 2026-09-25
978
+
979
+ The editors get the Tool Upgrade - every page, menu, form, collection, view, action, API, block, component,
980
+ role, user, project and plugin-code editor, the entries and submissions screens, Menu locations and the core
981
+ Dashboard. Each is in the Tool banner ("Pages › About"), with one Save (banner, an unsaved bar, Ctrl+S), a
982
+ leave guard that also covers moving from one item to another, a draft per item offered back on your next
983
+ visit (never a password, token or secret), context menus on its inner lists, help on the non-obvious
984
+ controls and theme tokens only. The three largest editors (form, page, menu) were upgraded in place rather
985
+ than rewritten.
986
+
987
+ - **The Dashboard is a Tool** (sites without the site manager). Page views today, visitors now, pages and drafts, form submissions this week and site health at a glance; "Needs attention" gathers what the rest of the admin is flagging - the red and amber sidebar badges and any health check that is not OK - each a click from its screen. Recent activity, traffic, top pages, spikes and visits are rows you can click, right-click (or ⋮, or Shift+F10) to open on the site, copy or edit the page. Refresh and New page are in the Tool banner; the chart follows the theme. Fixed: the "last 30 days" chart only ever showed the last week, and every new entry in any collection - roles and API tokens included - was listed and counted as a form submission.
988
+ - **The page editor is a Tool.** One Save (banner, the bar, Ctrl+S) that checks the address and DConfig first and shows the tab a problem is on; an Unsaved marker that clears again if you undo your changes; Discard. Leaving with unsaved changes asks - going to another page included - and keeps a draft of that page, offered back on your next visit (it says so if the saved page changed meanwhile). The banner names the page, with Back, Editor reference and View on the site. Version history rows have a right-click / ⋮ / Shift+F10 menu (compare, restore, delete). Help on every non-obvious field.
989
+ - **The menu editor and Menu locations are Tools.** Banner Save and Ctrl+S, a leave guard with a per-menu draft (moving to another menu included), context menus on every menu item (edit, add below, move, indent, outdent, duplicate, hide, remove - locked admin-sidebar rows and Tools keep their rules) and on every location slot (edit the menu, clear, undo, copy the shortcode), a list of what would stop a save before it is sent, warnings for slots nested too deep or pointing at a missing menu, and help on the non-obvious controls.
990
+ - **The context menu editor is a Tool.** One Save (banner, the unsaved bar, Ctrl+S); leaving with unsaved changes - for another screen or another menu - asks and keeps a draft for that menu; the banner names the menu. Items are rows: click to edit one in a slideover (checked as you type, asks before discarding), right-click (or ⋮, or Shift+F10) to move, duplicate, add beside it or remove it; Alt+↑/↓ move. Checks mirror the server's, so problems show - on the tab that has them - before Save. Fixed: clearing a menu's Pages / Projects or setting its look back to theme defaults never stuck (an update merged, and empty scope and theme were not sent).
991
+ - **The collection editor is a Tool.** One form and one Save (banner, the unsaved bar, Ctrl+S) for settings, fields, API access, export and storage, with every problem marked on its tab; leaving with unsaved changes asks - moving to another collection too - and keeps a draft per collection. Fields are rows: click to edit in a slideover that checks the name as you type, drag or right-click (or ⋮, Shift+F10, Alt+↑/↓) to move, duplicate or remove them; removing or renaming a field that has entries says what happens to their values. Help on every setting. Fixed: saving a storage change and choosing to move the entries skipped the rest of the edit, and declining the move switched silently - it now asks.
992
+ - **A collection's entries are a Tool.** A table whose rows you can reach by keyboard - click to edit, right-click (or ⋮, or Shift+F10) to see all of an entry, duplicate it, copy its id, run one of the collection's actions, filter by the value you clicked, or delete it; tick rows to delete several; sort by any column. The entry form is a slideover that asks before throwing changes away and keeps a draft (never a password) when you leave. Add, Import (now from a file too), Export, Edit fields and Form are in the banner, which names the collection. Big collections load 500 at a time with "Load more" (it stopped at 500 before). Fixed: an entry's 0 was saved back as empty, and > / < filters treated every date in a year as equal. Sites without MongoDB no longer get a 503 for actions on this screen.
993
+ - **The form editor is a Tool.** "Forms › <title>" in the banner with Save (Ctrl+S), All forms and Submissions; one unsaved-changes bar with Discard. Leaving with unsaved changes asks - moving to another form's editor included - and keeps a draft of that form, offered back on your next visit to it (never the webhook URL). Every field row has a menu - ⋮, right-click on its header, or Shift+F10 - to edit it, open its logic and triggers, duplicate it (a unique name), move it up or down, copy its name or delete it; Enter opens it. Help on the non-obvious settings; theme tokens only.
994
+ - **Form submissions is a Tool.** One row per submission - named after the sender, the other answers under it - with Inbox / New this week / Spam, a search over every answer and a date range. Click one for everything that was sent; right-click it (or ⋮, or Shift+F10) to copy it, copy or email the sender, mark it as spam (kept out of the inbox, not deleted) or delete it. All forms, Export (CSV or JSON) and Clear all are in the Tool banner, which reads "Forms › <form> submissions". Fixed: export, clear all and delete used the form's own collection, not the one its collection action writes to - so for such a form export was empty and delete failed. New: POST /api/forms/:slug/submissions/:id/spam. Where several forms write to one collection, each form's submissions screen now lists, counts, exports, clears and deletes only its own entries (those it stamped `form:<slug>`), and every submissions route answers 404 for a form outside the user's projects.
995
+ - **The view editor is a Tool.** One Save (banner, the unsaved bar, Ctrl+S), with what is wrong dotted on its tab; leaving - for another screen or another view - asks and keeps a draft for that view. Stages are cards with a summary line; right-click one (or ⋮, or Shift+F10) to move, duplicate or delete it, or to see the results up to it. A Results tab runs the pipeline being edited, saved or not (`POST /api/views/_preview`), and follows your edits. All ten stages the server allows are offered ($limit, $skip and $count were missing), roles come from the site's roles (the list was fixed at admin/manager/editor/subscriber), and the slug is read-only once the view exists (the server always ignored a change). The preview page runs on opening, pages through results, and copies a row as JSON.
996
+ - **The action editor is a Tool.** One Save (banner, the unsaved bar, Ctrl+S); leaving with unsaved changes asks - another action's editor included - and keeps a draft for that action's next visit. Steps are numbered cards with a summary; right-click one (or ⋮, or Shift+F10) to move, duplicate or remove it. New: the state transition (field, from, to) - the service always had it, the editor did not; allowed roles come from System › Roles (they were a hard-coded four, one of which, "manager", did not exist) and a role that is gone is kept and flagged; unticking Bundled now saves. Without MongoDB it explains that actions need it instead of failing. Help on every control.
997
+ - **The API Builder editor is a Tool.** One Save (banner, the unsaved bar, Ctrl+S), checked as you type against the server's own rules; leaving with unsaved changes asks - moving to another endpoint included - and keeps a draft for that endpoint. Filters, the response-field list and the test history have right-click / ⋮ / Shift+F10 menus (move, duplicate, use a path param, tick all, run again, copy as curl). Try it gains Save and send; the token field is a password field and is never kept. Help on every control. The missing "history" icon is fixed.
998
+ - **The block editor is a Tool.** "Blocks › <name>" in the banner, with Save (and Ctrl+S), All blocks and Copy shortcode; one Unsaved bar with Discard; leaving with unsaved changes asks - switching to another block included - and keeps a draft for that block that the next visit offers back. The HTML and CSS panes are syntax-highlighted editors; right-click a sample value to insert its placeholder, copy it or reset it; help on every field. A new block can no longer take an existing block's name - saving it used to overwrite that block.
999
+ - **The component editor is a Tool.** One Save (banner, the Unsaved bar, Ctrl+S) over the name, project, Bundled and the source; leaving with unsaved changes asks - opening another component included - and keeps a draft per component, offered back on your next visit. Preview props are rows with a right-click / ⋮ / Shift+F10 menu: reset, copy as an attribute or a whole tag, jump to it in <props>. Help on every part. "View source" on a plugin's component opens it read-only (it 404'd and bounced back to the list), with Copy to a new component.
1000
+ - **The role editor is a Tool.** One Save (banner, the unsaved bar, Ctrl+S); leaving with unsaved changes asks - moving to another role's editor too - and keeps a draft for that role. The permission matrix gains search, Granted / Not granted, one-click actions and Full, and right-click (or ⋮, or Shift+F10) on an area for view only / none, or on a group heading to grant or clear the whole group. "What it can do" reads the ticks back in words. Editing a role you hold warns before you make yourself less senior or remove what you need to come back and fix it; renaming a role people hold says how many.
1001
+ - **The user editor is a Tool.** One form and one Save (banner, Ctrl+S) for account, roles, projects and profile fields - it was two forms, and a new user's profile could not be saved at all. What you may do mirrors the server: roles you cannot give are greyed with the reason (judged, like the server, on everyone's most senior role, primary or additional), a user you may not edit opens read-only, and your own account cannot be switched off here. A taken email shows on the field. Leaving asks (moving to another user too) and keeps a draft per user - never the password. Fixed: a new user's project access, project ownership, active switch and profile were sent in the create request, which ignored them, and were lost; `POST /api/users` now takes them all, so the account is made whole in one request.
1002
+ - **A project's overview and settings are Tools.** The overview's tiles open their list (or start the first) on click, and have a right-click / ⋮ / Shift+F10 menu (open, new, new in its editor); a disabled project says so, with Enable. Settings is one form checked as you type with the same rules as Manage projects, one Save (banner, Ctrl+S), a leave guard and a per-project draft; the danger zone explains what untagging cannot move (pages under the root URL). The banner reads "Projects › <name>".
1003
+ - **Signing out asks first when an editor has unsaved changes** - and now while you are still signed in: Cancel leaves you on the editor with a working session (sign-out used to happen before the question, so a Save after Cancel failed), OK keeps the draft and signs out.
1004
+ - **The view editor's Results preview checks joined collections too**: a `$lookup` into a collection outside your projects is refused, like the source already was.
1005
+ - **The plugin code editor is a Tool** (Marketplace › <plugin>). Files grouped by folder with a filter and a ● on anything unsaved; right-click a file (or ⋮, or Shift+F10) to save, revert, copy its path, add a file beside it, duplicate, rename or delete it (plugin.json, plugin.js and config.js cannot be). Ctrl+S saves the open file, Ctrl+Shift+S all of them; a Server file / Admin file chip says whether saving needs a restart, and a bar offers the restart once one is pending. Leaving with unsaved files asks, and keeps them as a draft the next visit offers back - naming any file changed on the server meanwhile. New file, Save all, Restart server and Back are in the Tool banner; new, rename and duplicate paths are checked as you type.
1006
+
1007
+ ## [0.76.0] - 2026-09-25
1008
+
1009
+ Every screen in System gets the Tool Upgrade - Notifications, Roles, Users, Site Settings, Effects, Layouts,
1010
+ the Marketplace, My Profile, API Tokens and Search - and so does the mail client shared by Email and Mail
1011
+ Reader. Settings screens get one Save (banner, Ctrl+S), a leave guard and a draft; lists get rows with
1012
+ right-click / ⋮ / Shift+F10 menus; badges say what needs doing.
1013
+
1014
+ ### System
1015
+
1016
+ - **Notifications is a Tool.** One row per notification - severity, when, from where, a link - with New / severity filters and search; click one to read it all, right-click (or ⋮, or Shift+F10) to open its link, copy it, mark it unread, dismiss it or delete it for everyone. Dismiss all and Refresh are in the Tool banner; the sidebar badge keeps the unread count and turns red while anything unread is critical (GET /api/system/notifications/_count). Fixed: notifications pushed by the manager were stored without an id or data wrapper, and marking one read wrote it back the same way, so the list skipped them and none ever showed; they are now stored whole. Opening the screen marks them read in one request (POST /read-all) - the per-notification requests it made at once lost updates on file storage.
1017
+ - **Roles is a Tool.** One row per role, most senior first - its badge, name, permissions, level, how many users hold it, plugin and admin scope - with All / Built in / Yours / Plugins and search. Click to set its permissions; right-click (or ⋮, or Shift+F10) to duplicate it, copy its name or delete it (built-in and plugin roles cannot be - the old list offered Delete on admin and user). New role is a slideover that checks the name and level as you type and keeps a draft; one made from a project's list is filed under it. A sidebar badge counts roles and turns red while a user holds a role that no longer exists (GET /api/roles/_count, /_holders).
1018
+ - **Users is a Tool.** One row per user - email, when they last signed in, role and additional roles, project access, inactive - with All / Active / Inactive, a role filter, search and sort (name or last signed in). Click to edit; right-click (or ⋮, or Shift+F10) to copy their email, make the account active or inactive, or delete it - actions your role cannot take are greyed with the reason. New user is in the Tool banner; the sidebar badge keeps its count and its popover shows who is inactive, who never signed in and the latest sign-ins (GET /api/users/_count).
1019
+ - **Site Settings is a Tool.** One Save for the whole screen - general, SEO, footer, email, back to top,
1020
+ cookie consent, breadcrumbs and custom CSS (the last two had their own buttons) - in the banner, the
1021
+ unsaved bar and Ctrl+S; a dot on each tab with unsaved changes; leaving with unsaved changes asks, and
1022
+ keeps a draft the next visit offers back (never the SMTP password). "Needs attention" says what is
1023
+ quietly wrong - no SMTP host above all, where mail goes to a test inbox and still reports success - and a
1024
+ sidebar badge counts the same (`GET /api/settings/_count`). The cache tab lists cached pages as rows
1025
+ (open the page, copy its key) and can turn the cache on or off. Help on every non-obvious setting.
1026
+ - **Effects is a Tool.** Live now shows what the site uses; the reveal defaults are one form with a single Save (banner, Ctrl+S) - leaving with unsaved changes asks and keeps a draft. A new Effects tab lists every effect the page editor inserts, from the same catalogue: filter by category, search by name or setting, click for its settings and an example, right-click to copy its shortcode. The long-form reference is kept as the Guide tab. PUT /api/effects/settings now stores only the four settings it knows, checked (it wrote the request body to config/effects.json verbatim, and GET serves that to every public page).
1027
+ - **Layouts is a Tool.** One row per layout with a little drawing of its shape (navbar, width, sidebar, footer, background) and how many pages use it; Built in / Yours and search. Click to edit in a slideover with a live drawing, checked as you type; right-click (or ⋮, or Shift+F10) to see the pages that use it, duplicate it, copy its `layout:` line or delete it (the confirm says how many pages fall back to Default). Pages that name a layout which no longer exists - they quietly show as Default - are called out on the screen and in a sidebar badge (`GET /api/layouts/_count`, `GET /api/layouts/_usage`). The spacer options have Ctrl+S, a leave guard and a draft.
1028
+ - **The Marketplace is a Tool.** Installed plugins are rows - version, author, release date, On / Off / Built in / Not running / Replaced, licence state and "Update to x" - with On / Off / Needs attention and search; what needs doing comes first. Click a plugin for its details and settings; right-click it (or ⋮, or Shift+F10) to switch it on or off (it asks - the server restarts), configure, update, view its source, export or uninstall it. Configure works at last: the form comes from a plugin's settingsSchema or, failing that, its config.js settings (the old Settings button needed a schema no plugin had, so it never appeared), and saves only what changed. Browse is part of the same screen (Available / Installed / Updates, search). New plugin checks as you type and keeps a draft. A sidebar badge counts plugins, turns red when a licensed plugin that is on is not running, and lists updates (GET /api/plugins/_count). GET /api/plugins carries settingsSchema and effectiveSettings.
1029
+ - **My Profile is a Tool.** One form and one Save (banner, the unsaved-changes bar, Ctrl+S) for your name, email, password and profile fields - it used to be two forms, and saving one threw away changes to the other. Save sends only what changed. Leaving with unsaved changes asks and keeps them for your next visit - never a new password. A card shows your role, project access and account dates; help on each part. A required profile field no longer stops you changing your password.
1030
+ - **API Tokens is a Tool.** One row per token - its ending, project, scopes, expiry, who made it and when it was last used - with tokens that need attention first, All / Active / Expiring soon / Expired / Off, a project filter and search. Click a token to edit its name, scopes or expiry, or switch it off (it could not be edited before); right-click (or ⋮, or Shift+F10) to switch it on or off, copy its ending, show only its project or revoke it. New token is a slideover that checks as you type and keeps a draft - never the token, which is shown once and asks before being closed uncopied. A sidebar badge counts tokens that have expired or expire within two weeks, red once one has expired (`GET /api/api-tokens/_count`).
1031
+ - **Search is a Tool.** Live now shows what visitors get (on/off, trigger, shortcut, results, how many pages are searchable). One Save for the screen (banner, Ctrl+S); leaving with unsaved changes asks and keeps a draft; a number outside its range says what it will be saved as. The floating button is set on a preview at its real size and a map of the screen. New: Try it - the site's own search run from the admin with the settings being edited, results right-clickable to open or edit the page (GET /api/search/preview). Help on every control; a sidebar badge counts the searchable pages (GET /api/search/_count).
1032
+
1033
+ ### Mail
1034
+
1035
+ - **Mail (Email and Mail Reader).** The screen now tears itself down when you leave it - its keyboard shortcuts
1036
+ stayed live on every other admin screen, and piled up per visit. Compose asks before losing text by any way
1037
+ out, and leaving the screen saves a changed message to Drafts. Context menus on messages and folders by
1038
+ right-click, ⋮ or Shift+F10 (Reply / Reply all / Forward added; the free reader gets one too), and their
1039
+ dividers draw as rules instead of blank rows. Compose, Refresh and Settings are in the Tool banner; every
1040
+ settings section explains itself; dialogs closed by their X no longer hang. Mail Reader gains an unread
1041
+ badge (INBOX, by STATUS, never marking anything read).
1042
+
1043
+ ### Fixed (security and integrity)
1044
+
1045
+ - **An account could take another account's email.** Creating a user refused a taken address, but updating
1046
+ one (Users, and My Profile's own `PUT /api/auth/me`) did not check - two accounts could share a sign-in
1047
+ email. Both now answer 409.
1048
+ - **The built-in roles could be deleted.** `DELETE /api/collections/roles/entries/:id` refused only the root
1049
+ role, so `admin` and `user` could go (the old Roles screen offered it); and clearing the roles collection
1050
+ was allowed outright. Both are refused now.
1051
+ - **Plugin secrets reached the browser.** `GET /api/plugins` sent every plugin setting as stored - a DVLA key,
1052
+ an SMTP password, a connection URI. Secret-looking values (and `settingsSchema` fields marked `secret`) are
1053
+ now sent masked, and saving an untouched one keeps what is stored. `settingsSchema` is documented in
1054
+ docs/plugin-development.md.
1055
+ - **Screen-confined roles got two 403s on every page** (a job-board candidate, say): the sidebar read Site
1056
+ Settings for its title and the top bar polled notifications. Neither is asked without the permission now.
1057
+
1058
+ ## [0.75.0] - 2026-09-25
1059
+
1060
+ Every screen in Content and Data gets the Tool Upgrade: Pages, Media, Menus, Context Menus and Theme;
1061
+ Collections, Forms, Views, Actions, API Builder, Blocks and Components. Each is on domma-reactive, in the
1062
+ Tool banner with its actions, with right-click / ⋮ / Shift+F10 menus, help popovers, a sidebar badge with a
1063
+ popover, and - where a screen has a form - a guard and a draft for unsaved work.
1064
+
1065
+ ### Content
1066
+
1067
+ - **Pages is a Tool.** One row per page - address, layout, revisions, when it changed, tags - with Published/Draft, visibility, project, Bundled and plugin chips; All / Published / Drafts, search, sort (navigation order, recently changed, title, URL) and the Tree view, remembered per viewer. Click a page to edit it; right-click (or ⋮, or Shift+F10) to view it, copy its address, trim its revisions or delete it. Trimming revisions is a slideover (it was a browser prompt), and the Pages badge names the drafts in its label and popover (GET /api/pages/_count). GET /api/pages now carries each page's plugin and bundled flags, so the plugin chip and the "managed by a plugin" delete warning show at last.
1068
+ - **Media is a Tool.** A grid of tiles - thumbnail, name, size, age, type - with filters by kind, search and sort. Click a file for its details (preview, dimensions, its address and a Markdown snippet to copy, rename, edit image, delete); right-click it (or ⋮, or Shift+F10) for the same actions. Upload is in the Tool banner, and files dropped anywhere on the screen upload too - one at a time, each reporting its own error, and asking before replacing a file of the same name (the server overwrites silently). A sidebar badge shows the file count and space used (`GET /api/media/_count`). Deleting a file that is already gone answers 404 instead of a 500.
1069
+ - **Menus is a Tool.** One row per menu with the location(s) it fills, its binding (flagged when it can never
1070
+ match) and whether it is bundled; All / In a location / Not in a location, and search. Click to edit;
1071
+ right-click (or ⋮, or Shift+F10) to duplicate, copy its `[menu]` shortcode, open Locations or delete (still
1072
+ refused while a location uses it, and the menu says so). New menu and Locations are in the Tool banner; a
1073
+ sidebar badge counts menus and lists empty locations (`GET /api/menus/_count`).
1074
+ - **Context Menus is a Tool.** One row per menu - its target, item count, where it applies (site-wide, its binding rules, or never) and whether it is on - with All / On / Off and a search. Click a menu to edit it; right-click it (or ⋮, or Shift+F10) to switch it on or off, duplicate it, copy its slug or delete it. The Tool banner, with New context menu; help popovers; a sidebar badge that calls out menus switched off and ones whose binding matches nothing (`GET /api/context-menus/_count`).
1075
+ - **Theme is a Tool.** Live now shows what the site and admin are using (day/night included).
1076
+ One Save for the screen (banner, Ctrl+S): leaving with unsaved changes asks, and keeps them as a draft
1077
+ the next visit offers back. Element overrides are a list - click to edit in a slideover (token names
1078
+ checked against the stylesheet; the "what adopting this theme changes" panel kept), right-click to
1079
+ switch off, duplicate, move or remove; the generated CSS follows your edits. Help on every
1080
+ non-obvious control. The sidebar badge keeps the override count and turns red when a theme in use no
1081
+ longer exists (`GET /api/theme/count`).
1082
+
1083
+ ### Data
1084
+
1085
+ - **Collections is a Tool.** One row per collection - its fields, project and entry count, with MongoDB / Built in / Bundled / plugin chips - with All / Yours / Built in / Plugins filters and a search box. Click one for its entries; right-click it (or ⋮, or Shift+F10) to edit it, open its form, copy its slug or delete it (built-in ones cannot be). New collection is a slideover that checks the slug as you type and keeps a draft; one made from a project's list is filed under that project. Database connections has a dirty marker, Ctrl+S, and keeps unsaved edits when you leave (in memory only - a URI can carry a password). A sidebar badge counts collections and entries (GET /api/collections/_count), and GET /api/collections marks the built-in ones (`builtIn`).
1086
+ - **Forms is a Tool.** One row per form - questions, wizard steps, conditional logic, when it last had a
1087
+ submission and how many - with All / New this week / No submissions and a search box. Click a form to
1088
+ edit it; right-click it (or ⋮, or Shift+F10) for its submissions, its shortcode, Duplicate or Delete.
1089
+ New form and Test email are slideovers in the Tool banner; Duplicate asks for the copy's title (a
1090
+ second "(Copy)" used to fail). The sidebar badge adds submissions this week to its label and popover
1091
+ (`GET /api/forms/_count`). The list now counts submissions where the submissions screen reads them -
1092
+ a form whose collection action writes elsewhere showed the wrong number.
1093
+ - **Views is a Tool.** One row per view - what it reads from (and whether that collection still exists), its stages, display mode, engine (JS pipeline or MongoDB aggregation), project and who can see it - with Public / Restricted / Missing source filters and search. Click to edit; right-click (or ⋮, or Shift+F10) to preview, copy its `[view]` shortcode, duplicate or delete. A view whose collection was deleted is listed first, in red, and turns the sidebar badge red (`GET /api/views/_count`). `GET /api/views?sources=1` adds each view's `sourceInfo`.
1094
+ - **Actions is a Tool.** One row per action - its collection, button, state transition, steps and roles - with a collection filter and search; click to edit, right-click (or ⋮, or Shift+F10) to duplicate, open the collection's entries, copy the slug or delete. Duplicate is a slideover that keeps the whole config. Without MongoDB the screen explains that actions are a Pro feature instead of raising an error. Sidebar badge: `GET /api/actions/_count` (count 0, `available: false`, in file mode).
1095
+ - **Blocks is a Tool.** One row per block with its description (from the block's own comment), size, when it changed and where it is used; All / In use / Unused and search. Right-click (or ⋮, Shift+F10) to copy its shortcode, see where it is used, duplicate, export or delete it; Import and New block are in the Tool banner. A sidebar badge counts blocks, with the unused ones in its popover (GET /api/blocks/_count). GET /api/blocks carries hasCss, description and example, and ?usage=1 where each is used.
1096
+ - **Components is a Tool.** One row per component - its tag, props, size and last change - with Yours / Built in / Plugins, search by name or prop, and sort by name or last change. Click to edit; right-click (or ⋮, or Shift+F10) to copy its tag, duplicate it (a new slideover that checks the name as you type and keeps a draft), export it or delete it. New component and Import are in the Tool banner, and a sidebar badge counts them (`GET /api/components/_count`).
1097
+ - **API Builder is a Tool.** One row per endpoint - its GET URL, collection, params and who may call it - with All / Enabled / Disabled / Public, a project filter and search. Click opens it in the builder; right-click (or ⋮, or Shift+F10) to copy its URL, open it, enable or disable it, or delete it. New endpoint is in the Tool banner, and a sidebar badge counts endpoints with the public ones called out (`GET /api/api-endpoints/_count`). A project's own API list (`#/projects/<slug>/apis`) keeps its filter and pre-fills that project on New endpoint.
1098
+
1099
+ ### Sidebar
1100
+
1101
+ - Each of these Tools brings its own badge route, which keeps the number the sidebar always showed there and
1102
+ adds a popover. The sidebar no longer downloads those lists just to count them - which also ends the
1103
+ `/api/actions` 503 logged on every render of a site without MongoDB.
1104
+ - A closed folder's total adds up signals (open tasks, new messages), not inventories: registered items
1105
+ marked `inventory: true` (all of these, and Projects) are left out, so a closed Content folder does not
1106
+ show pages + files + menus as one meaningless number. The Forms and Blocks badges keep their costly parts
1107
+ (submission stats, where blocks are used) for a minute between the sidebar's polls.
1108
+
1109
+ ## [0.74.0] - 2026-09-25
1110
+
1111
+ Manage projects gets the Tool Upgrade.
1112
+
1113
+ - **Manage projects is a Tool.** One row per project - its icon, root URL and what is filed under it
1114
+ ("12 artefacts - 5 pages, 3 forms…") - with All / Enabled / Disabled and a search box. Click a project
1115
+ for its overview; right-click it (or ⋮, or Shift+F10) to edit it, open its settings page, enable or
1116
+ disable it, untag everything under it or delete it. It wears the Tool banner, with New project in it.
1117
+ - **New and Edit are a slideover** on the list instead of a page, checked as you type against the
1118
+ same rules as the server, asking before throwing typed changes away and keeping a draft if you leave.
1119
+ A new project's slug follows its name. `#/projects/new` and `#/projects/edit/<slug>` still work -
1120
+ they open it. An edit can now clear a root URL (the old form could not).
1121
+ - **A sidebar badge on Manage projects**: how many projects, with the disabled ones called out
1122
+ (`GET /api/projects/_count`). `GET /api/projects?counts=1` answers the list in one request instead of
1123
+ one per project.
1124
+ - Creating, deleting, enabling or disabling a project re-renders the sidebar in place (the new
1125
+ `dm:sidebar-rebuild` event) instead of reloading the admin.
1126
+
1127
+ ## [0.73.0] - 2026-09-25
1128
+
1129
+ Custom themes are themes: a plugin's themes (the Theme Roller's) join the built-in ones everywhere a theme
1130
+ is chosen.
1131
+
1132
+ - **Custom themes are themes.** A plugin can offer themes with `hooks.registerThemes()` - a built-in
1133
+ base plus the tokens it changes - and they join the built-in ones everywhere a theme is chosen: the
1134
+ site, the admin, day and night, and element overrides (Content › Theme lists them under Custom
1135
+ themes). Applied as `dm-theme-<base> dm-custom-<id>` with one generated rule, on both render paths,
1136
+ in the day/night switch and in the admin (`GET /api/theme/admin`, any signed-in user). The Theme
1137
+ Roller 1.1 is the first provider. Before this, a Theme Roller theme could not actually be used: the
1138
+ page template never applied the custom class, and saving one as the site theme was refused.
1139
+ - config/theme.json remembers each custom theme's base (`customBases`), so a page keeps it if the
1140
+ theme's plugin goes away. The public theme switcher refuses custom themes (Domma cannot switch to
1141
+ them).
1142
+
1143
+ ## [0.72.0] - 2026-09-25
1144
+
1145
+ Every admin screen is a Tool: placeable anywhere in the sidebar, some locked, and the admin's home is a
1146
+ setting.
1147
+
1148
+ - **Choose the admin's home screen.** System > Site Settings > Admin home picks the screen the admin
1149
+ opens on, and where Dashboard goes - any Tool or page in the sidebar, or the built-in dashboard.
1150
+ Someone without the permission for it, or a home whose plugin has since been disabled, gets the
1151
+ built-in dashboard; a confined role keeps its own home. `/api/auth/permissions` answers `adminHome`.
1152
+
1153
+ - **Some Tools cannot be hidden or lost from the sidebar.** Dashboard, Menus, Users and My Profile can
1154
+ be moved, but not hidden or removed - the Menus editor greys those buttons out, and the sidebar puts
1155
+ one back in its folder if a menu has lost it or hidden it, including inside a hidden folder. They
1156
+ are how an admin finds their way back from a sidebar edit gone wrong. Role gates (`visibility`,
1157
+ `permission`) still apply.
1158
+
1159
+ - **Admins can put any Tool anywhere in the sidebar.** Content > Menus > Admin sidebar now lists every
1160
+ enabled Tool where it appears, badged with where it comes from (a plugin's name, or Built in). Move,
1161
+ hide or relabel one and Save: only the Tools you placed are saved, as `{url, ref}` placeholders,
1162
+ so the rest keep following their default folder. A Tool's address and
1163
+ permission are fixed; its bin puts it back in its default place. A placeholder for a Tool that is
1164
+ not enabled is kept (shown as "Not enabled") and takes effect again when it is.
1165
+
1166
+ - **Every admin screen is a Tool with a default folder, and the sidebar places it.** A plugin's
1167
+ `admin.sidebar` entries (and `registerSidebarItem`) may name a `folder` - `tools` unless they say
1168
+ otherwise - and the renderer places each Tool by `url`: where the admin-sidebar menu puts it if it
1169
+ does, else in its default folder. Nothing looks different yet; this is the groundwork for moving
1170
+ Tools anywhere from the Menus editor.
1171
+ - **Sidebar folders have stable keys.** Existing installs get `key`s on the seeded folders and a
1172
+ persisted Tools folder (once - deleting it is respected), so a Tool still finds its folder after
1173
+ an admin renames it. The Menus editor now keeps `key` and `ref` when it saves, and an empty keyed
1174
+ folder is a valid menu item.
1175
+ - **Hiding a plugin's sidebar item works.** The documented way - a hidden item in the admin-sidebar
1176
+ menu - never did: `/api/sidebar/menu` dropped hidden items before the renderer could see which
1177
+ Tools they hid. Hide by `url` now; the old same-text form is honoured too - so a hidden, url-less
1178
+ item that shares a Tool's name now hides that Tool, as documented.
1179
+ - **A hand-added sidebar link to a Tool's url is now that Tool's place.** Before, the Tool also
1180
+ appeared in Tools; now it renders once, where the link is, with the Tool's badge, permission and
1181
+ sub-items. A Tool restricted with `visibility` stays out of Tools for the users it is hidden from.
1182
+
1183
+ - **A plugin can take over a built-in Tool.** Notes, Todo, Analytics and Contacts are core since
1184
+ 0.67.0, so `supersedes` could not displace them. A loaded plugin that names one now takes it over:
1185
+ the core sidebar entry is hidden and `#/<tool>` goes to the plugin's first admin route. Nothing is
1186
+ stored - a lapsed licence brings the core Tool straight back. First user: Contacts Pro.
1187
+ - **The plugin banner follows the screen.** A view with no template was mounted over the previous
1188
+ route's content, so the banner kept the old plugin's name and gained a second settings cog. The
1189
+ banner also names the screen when a plugin has several sidebar entries (Site Manager > Sites).
1190
+ - **Every wait shows a throbber.** One loader appears whenever a same-origin fetch runs past 200ms,
1191
+ beside the banner name or in the top bar.
1192
+ - **domma-js 0.44.3** (with domma-reactive 1.0.2): a computed refreshed early by another computed
1193
+ mid-flush no longer swallows the change, so every binding reading it updates. Includes 0.44.2's
1194
+ context-menu submenus closing when a sibling is hovered.
1195
+ - **Tests:** the page-editor smoke test clears a leftover fixture user instead of failing every run
1196
+ after an interrupted one, and the sidebar grouping tests - which sat outside `tests/**` - now run.
1197
+
1198
+ ## [0.71.0] - 2026-09-24
1199
+
1200
+ - **Roles can have a site of their own.** A signed-in visitor now sees the navbar, footer, overlay
1201
+ and `[menu]` items meant for their role (the cache is keyed by role set). Visibility gains an
1202
+ exact gate, `=role` ("X only"), beside "X and above"; the page editor lists the live roles. The
1203
+ admin sidebar no longer needs `menus.read`, so any role gets its own sidebar.
1204
+ - **Plugins bring their own roles and permissions.** `plugin.json` `permissions[]` and
1205
+ `roles/*.json` are registered on every boot, and `hooks.registerRole()` /
1206
+ `hooks.unregisterRole()` create and remove a plugin's roles from code - never the site's or
1207
+ another plugin's. Uninstall removes the roles a plugin still owns. Plugins receive
1208
+ `requirePermission` in `options.auth`.
1209
+ - **Role-driven admin scope.** A role can carry `admin: {home, allow}`, confining its holders to
1210
+ those admin routes (edited under Roles > Admin area); this replaces the job-board roles that were
1211
+ hardcoded into core. Saving a role no longer erases its `plugin` and `grantedBy`, and the topbar's
1212
+ bell, badge and Settings now hide for users who cannot use them.
1213
+ - **Blog 1.6.1-1.6.3.** `/blog/` redirects to the index instead of "Not found"; the base path in
1214
+ the Blog settings applies as soon as it is saved (pages, shortcodes, preview and the navbar link
1215
+ follow it); and posts Blog Pro files under its other blogs stay out of `/blog`, so a lapsed Pro
1216
+ licence falls back to the free Blog cleanly.
1217
+
1218
+ ## [0.70.1] - 2026-09-23
1219
+
1220
+ - **The mail reader's right-click menus show the glyph cursor too.** The message list and folder
1221
+ menus, shared by Mail Reader and Email Pro, were the last CMS-side menus still on the plain
1222
+ `context-menu` keyword.
1223
+
1224
+ ## [0.70.0] - 2026-09-23
1225
+
1226
+ - **Right-click menus show that they are there.** Every context menu in the CMS now uses Domma's
1227
+ glyph cursor on hover - an arrow with a small list beside it - instead of the CSS `context-menu`
1228
+ keyword, which Windows and most Linux themes draw as the plain arrow. That covers authored public
1229
+ menus, collection displays, the Context Menu editor's preview, and the Todo, Notes, Contacts and
1230
+ Analytics menus. Collection displays drop their own funnel cursor for it. The page editor's
1231
+ shortcode menu covers the whole text area, so it keeps the text I-beam.
1232
+ - **Blog 1.6.0.**
1233
+ - The post editor's Status card shows when the post was published (or goes live), last saved and
1234
+ created.
1235
+ - Categories and tags are typeable pick-lists with removable pills; tags suggest the ones already
1236
+ in use, take Enter, commas and pastes, and reuse an existing spelling whatever its case.
1237
+ - Two new post layouts: **Split** (picture and headline side by side) and **Essay** (a serif long
1238
+ read with a drop cap and pull quotes).
1239
+ - Enabling the blog adds a Blog link to the navbar; disabling it removes that link, never one the
1240
+ site made itself.
1241
+ - Ctrl+S in the post editor saved twice, so a new post could be created twice.
1242
+ - **Domma 0.44.1**: Pillbox pills are styled. The stylesheet had been targeting class names the
1243
+ component never used, so every pill arrived as bare text beside a grey button.
1244
+
1245
+ ## [0.69.4] - 2026-09-23
1246
+
1247
+ - **A withdrawn plugin is never committed as a site's own code.** A site's repo keeps a plugin only
1248
+ when the site wrote it, judged by "no upstream ships this name" - and nothing ships a withdrawn
1249
+ plugin, so a leftover `shopping-cart` folder was committed as site code. Every slug the
1250
+ marketplace reserves (promoted, retired or withdrawn) now counts as upstream.
1251
+ - **The old `contacts` collection stays out of site repos**, like the core Contacts collections:
1252
+ names, emails and phone numbers are data, not content.
1253
+
1254
+ ## [0.69.3] - 2026-09-23
1255
+
1256
+ - **Published to npm again.** No change to what a site runs - the code is 0.69.2's. Three signing
1257
+ tests needed a private key that only the release machine holds, so every publish since 0.67.0
1258
+ failed in CI and npm stayed at 0.66.0. They now skip, saying why, where the key is absent; the
1259
+ rest of the suite runs everywhere.
1260
+
1261
+ ## [0.69.2] - 2026-09-23
1262
+
1263
+ - **A site's repo holds its content and never the engine.** A new site's `.gitignore` is now an
1264
+ allowlist: everything is ignored, then `content/`, `config/` and the site's own plugins are let
1265
+ back in. Pages, media, blocks, views, forms, components, collections, API endpoints, menus and
1266
+ theme are all committed; `server/`, `admin/`, `public/`, `scripts/`, `node_modules/` (domma-js and
1267
+ domma-reactive) and every distributed plugin never are. `npx domma-cms <name>` writes it - the old
1268
+ file ignored `content/media/`, so uploaded images never reached the repo.
1269
+ - **A plugin created in the admin is committed with the site.** Scaffolding one adds its own allow
1270
+ rule, since it is the site's code rather than the updater's.
1271
+ - **Plugins can declare where users' work lives inside them.** `contentPaths` in `plugin.json` lists
1272
+ folders (e.g. `["data"]`) that hold what users author; installing the plugin tracks just those and
1273
+ leaves its code out. Docs and Theme Roller are covered for installs that predate the field.
1274
+ - Secrets, visitor-submitted and per-user data (users, api-tokens, contacts, invoices, enquiries,
1275
+ notes, todos), version history, analytics and share-link tokens stay out.
1276
+
1277
+ ## [0.69.1] - 2026-09-23
1278
+
1279
+ - **The Blog and Mail Reader ship exactly as the marketplace publishes them.** They are written in
1280
+ dcms-marketplace and copied here by its `make sync-free-tier`, which will not copy over a change
1281
+ made here or left uncommitted. The engine no longer minifies or re-stamps them, so a site updated
1282
+ by the engine runs the same bytes as one installed from the catalogue - and Marketplace Sync no
1283
+ longer reports the engine's Blog as "modified".
1284
+ - **An engine update never takes a plugin back a version.** A site whose Blog came newer from the
1285
+ catalogue keeps it; before, the updater replaced every bundled plugin with the engine's copy.
1286
+
1287
+ ## [0.69.0] - 2026-09-23
1288
+
1289
+ - **Mail: the sidebar hears when you read or unread a message**, so an unread badge moves straight
1290
+ away, and the mail mirror can list the newest unread messages (`listUnread`) for a badge's popover.
1291
+
1292
+ - **A closed sidebar folder shows the total of its Tools' counts.** Collapse Tools and it still says
1293
+ 3 - red if anything inside is overdue - and hovering lists what makes it up ("Todo: 3 open tasks",
1294
+ "Invoices: 1 outstanding, 1 overdue"). Only the Tools' own counts add up, not totals like Pages, and
1295
+ the badge exists only when there is something to count.
1296
+
1297
+ - **Contacts, rebuilt on the Tool upgrade path, with a menu on every contact.** Right-click a contact
1298
+ (or its ⋮) to edit, email or call it, copy its email or phone, star it, add it to or take it out of
1299
+ a group, duplicate, select or delete it. Editing opens in a resizable side panel with every field
1300
+ the old form had - company, VAT, bank details, addresses, extra emails and phones. One row of chips
1301
+ filters by starred or group and shows the counts; search covers names, emails, phones, company,
1302
+ notes and groups. The toolbar ⋮ holds export, import and group management; selecting several
1303
+ contacts offers add-to-group and delete. The cog sets sort, starred first, density and columns.
1304
+
1305
+ - **Fixed: editing a contact stored some values in the wrong shape.** Payment terms were saved as text
1306
+ ("30") and a favourite sent as the text "false" counted as starred. Editing now shapes values the
1307
+ same way creating does, and leaves fields it was not sent alone.
1308
+
1309
+ - **Blog 1.4.4: the post editor saves and stays.** Save (or Ctrl+S) stores the post without changing
1310
+ its status - a draft stays a draft, a live post updates - and keeps you on the page; the header
1311
+ says "Unsaved changes" or when it was last saved. A Posts button goes back to the list, asking first
1312
+ if anything is unsaved, and closing the tab with unsaved work asks too. Publish and Schedule also
1313
+ stay on the page. "Save Draft", which quietly took a live post off the site, is now an explicit
1314
+ "Move to drafts".
1315
+
1316
+ - **One typeface and one type scale across the Tools.** Every button, input, select and text box in
1317
+ a Tool was drawn in the browser's Arial beside Roboto text; they now use the page font, and
1318
+ monospace is kept for slugs, Markdown and addresses. Font sizes in Notes, Todo, Analytics, Blog and
1319
+ Marketplace Sync are on Domma's scale (xs / sm / base / lg / xl) instead of a dozen one-off sizes.
1320
+
1321
+ - **Tools show their numbers in the sidebar.** Todo shows open tasks and turns red when any are
1322
+ overdue; Blog shows comments waiting for approval; Analytics the visitors on the site right now
1323
+ (with a pulsing dot); Notes how many notes you have. Hover a badge - or focus its item - for a
1324
+ popover with the detail behind it: Todo's overdue, due-today and in-progress figures and its most
1325
+ urgent tasks; Blog's pending comments (who, on which post), drafts and next scheduled post;
1326
+ Analytics' views and sessions today and which pages people are on now; Notes' categories and most
1327
+ recently edited notes. Badges move the moment you act - completing a task or approving a comment
1328
+ updates the count with no reload - and refresh every minute. A plugin gets the same with `countUrl`
1329
+ on its sidebar entry, answering `{count, label, tone, details}`, and a `dm:sidebar-count` event
1330
+ when its number changes.
1331
+
1332
+ - **Blog 1.4.1: the blog's own link colours win again.** The rule that keeps the theme's link styling
1333
+ off the blog's buttons and chips outranked its own exceptions, so on some themes the active
1334
+ display button read dark-on-dark and category chips lost their colour.
1335
+
1336
+ - **Blog 1.4.0: layouts built from blocks, and listings you can search and right-click.** Every post
1337
+ picks a layout in the editor - Classic, Feature (a full-width hero), Minimal or Sidebar (a contents
1338
+ list that follows the reader) - or follows the site default in Blog settings. Listings show as
1339
+ Cards, List, Magazine or Compact, and readers can switch between them. Both are ordinary blocks
1340
+ (`blog-post-*`, `blog-card-*`): restyle one in the Blocks tool and every post using it changes;
1341
+ copy one under a new name and it becomes a new choice. The listing now searches, filters by
1342
+ category pill, sorts and shows more in the page with domma-reactive, keeps its filters in the
1343
+ address, and every post has a right-click menu - open, copy link, More in this category, More
1344
+ tagged, More by this author. Posts gain reading progress, related posts, older/newer links,
1345
+ sharing, and a comment form that posts in place. The editor previews the unsaved post in any
1346
+ layout at desktop, tablet or phone width. Posts > ⋮ adds a set of sample posts, and removes them.
1347
+
1348
+ - **Fixed: the blog was a blank page in every browser.** Its pages were sent without returning the
1349
+ reply, and under response compression - which every browser asks for - the body was lost: a 200
1350
+ with nothing in it. Tools like curl, which do not ask for compression, still saw the page.
1351
+
1352
+ - **Fixed: a blog author could publish without an editor.** Saving a post took `status` from the
1353
+ request as-is, so `{"status": "published"}` skipped the publish check. Status now changes only
1354
+ through Publish and Schedule, which check who is asking; an author can still move a post to draft.
1355
+
1356
+ - **Fixed: exported plugins downloaded corrupt.** The `.dcmsplugin` file is already gzip, and the
1357
+ server's compression labelled it as such, so browsers unzipped it on arrival (saving a bare tar) or
1358
+ failed with "Decoding failed". Plugin exports, and Site Manager's licensed downloads, are now sent
1359
+ as they are.
1360
+
1361
+ - **Todo, rebuilt on the Tool upgrade path.** Add a task by typing and pressing Enter, with an
1362
+ optional priority and due date. Due dates read "Today", "Tomorrow" or "3 days ago", and an overdue
1363
+ task is marked. One filter menu (Open, All, each status, Overdue, with counts) and a search replace
1364
+ the row of filter buttons, and completed tasks always sink to the bottom. Every change to a task -
1365
+ complete, status, priority, due today/tomorrow/next week, edit, delete - is on its right-click menu,
1366
+ also reached from its ⋮ or by clicking its status or priority pill. Editing opens a side panel.
1367
+ The cog holds what the list opens on, its sort, the default priority, relative dates and delete
1368
+ confirmation.
1369
+
1370
+ - **Analytics, rebuilt on the Tool upgrade path, and it shows the journeys it was already recording.**
1371
+ Sessions and bounce rate join the figures. A Journeys tab lists entry pages, exit pages and the
1372
+ most common page-to-page paths, and a Spikes tab lists pages running well above their usual rate
1373
+ today - both were computed by the API and never displayed. "Active now" refreshes itself. A range
1374
+ menu replaces five buttons, pages carry a share bar and a menu (open on the site, copy address,
1375
+ filter), and export and reset move behind the toolbar's ⋮. The chart takes the theme's colours.
1376
+ The cog sets the opening range, rows shown, line or bar chart, live refresh and reset confirmation.
1377
+
1378
+ - **Fixed: a page address could run script in the Analytics screen.** The public hit counter accepts
1379
+ any URL string and the old table wrote it into the page unescaped, so anyone could plant markup that
1380
+ ran when an administrator opened Analytics. Addresses are now always text.
1381
+
1382
+ - **Blog 1.3.0, on the Tool upgrade path.** Settings open from the cog in a side panel instead of a
1383
+ separate page (the old address still works). Posts: status chips with counts filter the list,
1384
+ with search, a category filter and sort. Every row has a menu - edit, view on the site, copy link,
1385
+ publish or unpublish, duplicate as a draft, delete. Categories show how many posts use them, open
1386
+ in a side panel, and "Show its posts" jumps to Posts already filtered. Comments load once, with a
1387
+ count on every status. Rejected comments can finally be listed. Each row names its post, opens to
1388
+ the full text, and carries approve, pending, reject, spam, reply-by-email and delete. "Approve all
1389
+ pending" is one click. Help (?) icons explain each part.
1390
+
1391
+ - **Fixed: a blog with comments switched off still accepted them.** The public comment route never
1392
+ checked the setting, and it read moderation from the settings the plugin started with, so changing
1393
+ either did nothing until a restart. Both are now read live. Saving one blog setting also no longer
1394
+ resets the others to their defaults.
1395
+
1396
+ - `admin/js/lib/tool-kit.js` gathers what every upgraded Tool shares - slideover clean-up, the ⋮
1397
+ button that opens the row's menu, per-browser settings, and tear-down when the view is left.
1398
+
1399
+ - **Notes, rebuilt on the Tool upgrade path.** Notes show as cards or a dense list, found by a
1400
+ search that also matches categories and a category filter that fits in the toolbar. Everything you
1401
+ can do to a note - edit, duplicate, copy, filter by its category, delete - is on its right-click
1402
+ menu, or its ⋮ button. Notes open in a resizable side panel with categories as pills, a word count
1403
+ and Ctrl+Enter to save. The banner's cog opens settings: layout, sort, preview length, category
1404
+ chips, delete confirmation and default categories for new notes, remembered per browser. Help (?)
1405
+ icons explain each part in place; press / to search and N for a new note. The help popovers are a
1406
+ shared admin helper (`admin/js/lib/help-popover.js`) for the other Tools to adopt.
1407
+
1408
+ - **Every plugin's banner shows when its licence runs out, and holds its settings cog.** A paid
1409
+ plugin's banner gains an expiry chip after the licence: "Expires" for a rental, "Support to" for a
1410
+ perpetual licence, whose plugin keeps running when support lapses. It turns amber inside 30 days and
1411
+ red once a rental has lapsed. Settings sits as a cog to the left of the chips on every plugin -
1412
+ Email and Mail Reader move theirs there from the Folders pane, and Blog's Settings leaves the
1413
+ sidebar for it (the page is still at its old address).
1414
+
1415
+ - **A managed site can browse and install from its manager.** The Browse tab lists what the fleet
1416
+ sells, with the monthly price, whether this site is licensed, and Update when the catalogue is ahead
1417
+ of what is installed. A paid plugin arrives with an entitlement minted for that site, so it runs as
1418
+ soon as it is enabled. Sites that manage themselves are unaffected and keep installing from a file.
1419
+
1420
+ - **Fixed: the Browse tab could never load its own template**, so it had never mounted once - it asked
1421
+ Domma's HTTP helper for HTML and that helper always parses JSON.
1422
+
1423
+ ## [0.68.0] - 2026-09-22
1424
+
1425
+ - **Install a plugin from a file, and update one.** The Marketplace screen has an **Install from
1426
+ File** button and a per-plugin export, so the envelope routes are reachable without curl. A second
1427
+ upload of a plugin already installed offers to **update** it rather than refusing: the new version
1428
+ is extracted and validated before the working one is disturbed, `data/` and the entitlement carry
1429
+ across, and the previous version goes back if anything fails. A downgrade is refused unless asked
1430
+ for, and version comparison is numeric - 1.10.0 is newer than 1.2.0. Installing an unsigned plugin
1431
+ still takes an explicit confirmation; so does replacing one that is running.
1432
+
1433
+ - **A plugin card stops claiming to be enabled when it is not running.** A lapsed or unlicensed
1434
+ entitlement reads "Not running" with the reason on the badge, and a plugin displaced by one that
1435
+ replaces it reads "Replaced by <name>". Both facts were already in the API and were being dropped
1436
+ by the card.
1437
+
1438
+ - **A licensed plugin must be entitled to run.** Plugins declaring `closedSource` are checked against
1439
+ a signed entitlement at load; everything else - including every plugin you wrote yourself - is never
1440
+ asked. A **rental** stops when its term and grace period end. A **perpetual licence never stops**:
1441
+ what lapses is support and updates, so a plugin bought outright keeps running indefinitely, offline.
1442
+ An entitlement lives beside its plugin, or at `config/entitlements/` for one covering the whole
1443
+ instance. *(Correction, 0.92.2: this entry announced a `POST /api/plugins/entitlement` route for
1444
+ renewals; it was never shipped. A renewal is the new entitlement file placed at
1445
+ `plugins/<slug>/.entitlement.json` or in `config/entitlements/` followed by a restart, or the
1446
+ plugin re-installed from a `.dcmsplugin` carrying it, as an update.)*
1447
+
1448
+ ## [0.67.0] - 2026-09-22
1449
+
1450
+ - **The Plugins screen is now the Marketplace.** You install, enable and configure there; an enabled
1451
+ plugin appears under Tools. The dead "Bundled" badge is gone - nothing read the value it wrote.
1452
+
1453
+ - **A plugin travels as one file.** `GET /api/plugins/:name/export` produces a `.dcmsplugin`
1454
+ envelope, and `POST /api/plugins/install-upload` installs one - the same format a marketplace
1455
+ purchase arrives in, so both enter through the same installer with the same traversal guard and
1456
+ manifest validation. Runtime `data/` and internal notes never travel; `?includeData=1` opts the
1457
+ first back in. A **licensed plugin cannot be exported**, on the same flag that refuses its source.
1458
+ An envelope exported from a running CMS is unsigned, because the marketplace key is offline:
1459
+ installing one takes an explicit confirmation, and a site can refuse them outright with
1460
+ `requireSignedPlugins` in `config/server.json`. A bundle whose signature is present but broken is
1461
+ always refused - that is tampering, never "unsigned".
1462
+
1463
+ - **Contacts is a built-in Tool.** Enabled on every site already, so it is part of the CMS rather
1464
+ than something to install. Your contacts and groups are untouched - they live in
1465
+ `contacts-contacts` and `contacts-groups`, and the slugs are unchanged. It gains `contacts.*`
1466
+ permissions, and the sample contacts the plugin shipped are no longer seeded: a fresh install
1467
+ starts empty rather than with two fictional people in it.
1468
+
1469
+ - **Analytics is a built-in Tool.** It was already unswitchable-off, so the code now matches what the
1470
+ behaviour has been for a while. **Your traffic history is preserved**: the counters are copied from
1471
+ `plugins/analytics/data/` to `content/analytics/` on first start, verified byte for byte before the
1472
+ old directory is removed, and a failed copy abandons the whole retirement rather than proceeding.
1473
+ Do-Not-Track, per-session dedup and the `/admin` and `/api` exclusions are unchanged. `analytics-pro`
1474
+ is unaffected - it counts server-side and remains a separate paid plugin.
1475
+
1476
+ - **Notes and Todo are built-in Tools.** Both plugins are retired into the CMS and removed on first
1477
+ start. **Your notes and tasks are untouched** - they already lived in the `notes-notes` and
1478
+ `todo-items` collections rather than inside the plugin, and the slugs are unchanged. They gain
1479
+ proper permissions (`notes.*`, `todo.*`), so a role can be given or denied either, and their admin
1480
+ screens now refresh an expired session instead of failing the save.
1481
+
1482
+ - **The theme switcher is a theme setting, not a plugin.** Enable it at **Content → Theme**; the
1483
+ `theme-switcher` plugin is retired into the CMS and removed on first start, with its placement
1484
+ lifted into `config/theme.json`. It now offers **every** theme the stylesheet ships - the plugin
1485
+ hardcoded 26 dots against 32 real themes - and each dot takes its colours from that theme's own
1486
+ tokens, so a new theme appears without touching anything.
1487
+
1488
+ - **`[demo]` is a built-in shortcode.** The `demo-viewer` plugin is retired into the CMS and removed
1489
+ on first start. **It also fixes a bug that made it useless:** the sanitiser drops a relative
1490
+ `iframe` src whenever `allowedIframeHostnames` is set, so every `[demo]` rendered an iframe
1491
+ pointing at nothing. Relative sources are same-origin by definition and are now allowed; the host
1492
+ gate on `[embed]` is unchanged.
1493
+
1494
+ - **`invoice` and `surveys` are no longer bundled.** Both move to the plugin marketplace as paid
1495
+ plugins (`dcms-marketplace`). `surveys`' 11 test files moved with it and run there against a
1496
+ sibling CMS checkout. Existing installs are unaffected: the plugin keeps
1497
+ working where it is already installed, and the four managed sites running it hold licences issued
1498
+ before the move. Published npm tarballs stay published, so a version you already have is a version
1499
+ you keep - only newer versions are withheld. Git history is not rewritten; the code has been public
1500
+ in released tarballs and a rewrite would buy little.
1501
+
1502
+ Design: `docs/superpowers/specs/2026-09-22-dcms-marketplace-design.md`.
1503
+
1504
+ ## [0.66.0] - 2026-09-22
1505
+
1506
+ ### Added
1507
+ - **The whole invoice list as one PDF.** CSV and Excel describe the invoices; this exports the
1508
+ invoices themselves, a page each, for whatever the filters are currently showing. The hazard
1509
+ was CSS rather than layout: every print template is a whole HTML document, and they fight when
1510
+ merged - `modern` hides `.top`, `classic-nologo` hides `.top .party .name`. A site picks one
1511
+ style, but the logo and no-logo variants are chosen per invoice, so a batch spanning an issuer
1512
+ with a logo and one without mixes two templates, and unscoped the no-logo rule would delete the
1513
+ trading name from the invoices that do have a logo. Each stylesheet is confined with `@scope`,
1514
+ and identical ones are emitted once and shared, so the ordinary single-template case collapses
1515
+ to one block.
1516
+
1517
+ The invoice ids do not travel in the download token: a hundred uuids is four kilobytes of JWT in
1518
+ a query string, so the token carries a one-shot claim ticket and the list stays on the server,
1519
+ deleted on redemption. Capped at 100, with the render timeout growing with the batch.
1520
+
1521
+ - **The floating search button can be sized and shaped.** `iconSize` (32-96px) sets its width and
1522
+ height, with the glyph keeping its share of the button rather than staying 20px inside it, and
1523
+ `iconRadius` takes it from a hard square to a circle. The two are coupled, because at half its
1524
+ width a square is already a circle: the radius ceiling is half the size, and the admin slider's
1525
+ maximum moves with it. Icon mode only - the navbar trigger has no surface to round and the
1526
+ labelled variant is a pill.
1527
+
1528
+ ### Changed
1529
+ - **Every slideover resizes, and remembers.** Domma sizes a slideover from a fixed set of presets
1530
+ and `admin.css` then pins it, which suits a confirmation and not a working panel: the width
1531
+ someone wants for a table of issuers or a menu tree depends on their screen, and a panel that
1532
+ forgets it on every open is barely better than a fixed one. Done by patching `E.slideover` once
1533
+ at boot rather than at the forty-odd call sites that open one - a call-site sweep drifts, misses
1534
+ new panels, and cannot reach the ones plugins open, since a plugin view runs inside the same SPA
1535
+ and calls the same `E`.
1536
+
1537
+ - **A plugin view gets the screen.** The 1200px `.view-container` cap is a measure for reading -
1538
+ right for a settings form, wrong for a table of invoices with nine columns, and with no
1539
+ centring it left the panel pinned left with half a wide display empty. Lifted for anything
1540
+ carrying the plugin shell, so it reaches every plugin without the author doing anything.
1541
+
1542
+ - **The mail admin is built from Domma rather than from `createElement`.** The settings sections
1543
+ declared their own inputs, laid themselves out with `style.cssText` on every node, and set
1544
+ status colours inline - which works, and is what the house rules exist to prevent. An inline
1545
+ colour cannot read a `--dm-*` token that a theme override redefines, so those screens were the
1546
+ same shade in every theme; and five sections each inventing a form is five copies of the same
1547
+ bugs.
1548
+
1549
+ Sections now declare a Forma schema and `_lib/admin/mail/section-kit.js` renders it, handing
1550
+ back the model's tracked proxy - so a card's title following what is typed into it, or the Away
1551
+ tab's state line following its dates, is a subscription rather than a set of callbacks somebody
1552
+ has to remember to call. Cards, badges, list groups and switches are Domma's components, and
1553
+ every layout and colour decision is a Domma class or one in `mail.css`.
1554
+
1555
+ There is no inline styling left anywhere in the mail admin. The rule builder is the one screen
1556
+ still assembled directly, for the reason given in its module comment: its rows change shape as
1557
+ you build them, and a schema re-rendered on each keystroke throws away focus.
1558
+
1559
+ ## [0.65.0] - 2026-09-20
1560
+
1561
+ ### Added
1562
+ - **An away message**, with the refusals that make one safe. It never answers something that
1563
+ announced itself as automatic, a mailing list, a bounce, a no-reply address, its own mailbox, or
1564
+ anything it was merely Bcc'd on, and it tells one person once per period. Its own replies carry
1565
+ `Auto-Submitted: auto-replied` so the responder at the far end refuses them in turn - one half
1566
+ without the other is a mail loop. The replies are deliberately not filed in Sent, since one
1567
+ absence makes a great many; a log on the Away tab is the record instead.
1568
+
1569
+ It also carries a preview, because every interesting thing an auto-responder does is a refusal
1570
+ and a refusal is invisible: "it did not answer the mailing list" and "it is broken" look
1571
+ identical. The preview runs the real decision over real mail and prints the reason for each.
1572
+
1573
+ - **A sidebar badge for unread mail**, and the mechanism behind it: a plugin's `admin.sidebar`
1574
+ entry may now declare `countUrl`, and the admin sidebar fetches it. The core counts were a fixed
1575
+ list a plugin could not join.
1576
+ - **A mailbox can have more than one face.** The Identity section is now a list rather than a
1577
+ form: each entry has its own display name, optional alias address, Reply-To and signature, and
1578
+ the first is the default. Compose grows a **From** picker when there is more than one, and
1579
+ changing it swaps the signature already in the message - where the block can still be found.
1580
+ An alias sends as itself in the header *and* the SMTP envelope, because SPF and DMARC are
1581
+ checked against the envelope and an alias that fails alignment lands in spam.
1582
+
1583
+ A mailbox configured before this migrates to a one-entry list on read, so nothing needed
1584
+ changing and nothing was lost.
1585
+
1586
+ - **Templates.** Canned responses with a name, an optional subject and a rich-text body, inserted
1587
+ from the compose window. Placeholders (`{{to.first}}`, `{{subject}}`, `{{my.name}}` and five
1588
+ more) are filled in on insertion; anything unrecognised is left visible, so a typo is found
1589
+ while the window is still open rather than after "Dear ," has gone to a customer. A template is
1590
+ added *above* what is already written and never overwrites a subject the message has.
1591
+
1592
+ - **Filing rules.** Conditions on sender, recipient, subject, mailing list, attachment, priority
1593
+ or size; actions to move, copy, mark read, flag, treat as spam, move to Trash, or stop. They run
1594
+ as part of a sync pass, in order, over **new mail only** - never during a full resync, and never
1595
+ over anything older than the day the rules were written. Every rule has a **Test** that runs it
1596
+ against the messages already mirrored and lists exactly what it would have caught, moving
1597
+ nothing: a rule's failure mode is not an error message, it is mail quietly going somewhere
1598
+ nobody looks.
1599
+
1600
+ `matches` is a wildcard rather than a regular expression - a stored regex is a pattern the
1601
+ server evaluates on every message that arrives, which is a denial of service waiting for a
1602
+ nested quantifier.
1603
+
1604
+ - **Send later, undo send and snooze**, all on one durable queue. Send holds a message for a
1605
+ configurable few seconds (ten by default) with an Undo, or schedules it for a time you pick;
1606
+ snooze moves a message out of the inbox and brings it back, unread, when it is due. The hold is
1607
+ written to the store before you are told the message was accepted, so a deploy landing in the
1608
+ middle of it loses nothing, and a queued send is claimed with a conditional update so it cannot
1609
+ go twice. Cancelling hands the draft back whole and reopens it.
1610
+
1611
+ - **Conversations.** The message list can group into threads, reconstructed from `Message-ID`,
1612
+ `In-Reply-To` and `References` - never from the subject. Grouping happens in the database so a
1613
+ page is a page of conversations, with a repair pass over the page for clients that put only the
1614
+ immediate parent in `References`. Mirrors built by an earlier version are backfilled the first
1615
+ time a folder is listed this way.
1616
+
1617
+ - **One-click unsubscribe** where a list advertises it (RFC 8058), the link opened in a tab where
1618
+ it does not, and a prefilled message where the list only offers `mailto:`. An `http://`
1619
+ unsubscribe link is ignored: it carries a token identifying the recipient.
1620
+
1621
+ - **Mail into the CMS.** An attachment saves straight to the media library; a message becomes a
1622
+ draft page or a collection entry. The attachment allowlist is deliberately narrower than the
1623
+ CMS's own - no SVG and no HTML, because `/media/` is same-origin and this file came from a
1624
+ stranger - and every name is de-duplicated, since `saveMedia` overwrites silently. Collections
1625
+ whose required fields a message cannot fill are not offered, rather than failing on submit.
1626
+
1627
+ - **Read receipts**, requested per message and sending both conventional headers or neither.
1628
+ **Mailbox quota**, where the server implements it. **Folder export as mbox**, streamed, so a
1629
+ mailbox mirrored here is not a mailbox held hostage here.
1630
+
1631
+ ### Security
1632
+ - **An attachment's filename could defeat the media allowlist.** `/media/` is served with a
1633
+ `Content-Type` derived from the file extension, and the declared MIME type and the filename are
1634
+ two independent fields - both chosen by whoever sent the message. An attachment declared
1635
+ `image/png` and named `logo.svg` therefore passed the type check and landed on disk as a script
1636
+ container, served from the site's own origin. The extension is now derived from the allowed
1637
+ type and never from the sender.
1638
+ - **One-click unsubscribe could be pointed at the internal network.** It is the only place the
1639
+ server fetches a URL a stranger chose. Redirects are no longer followed, and the host must
1640
+ resolve to a public address - checking the literal is not enough, since a hostname can resolve
1641
+ to 127.0.0.1.
1642
+ - **`POST /messages/:uid/to-page` could overwrite any page.** `createPage()` writes
1643
+ unconditionally; the check the CMS's own route performs was missing here, so a caller holding
1644
+ only `pages.create` could replace the home page with the text of an email.
1645
+
1646
+ ### Fixed
1647
+ - **Snooze could lose a message on a server without UIDPLUS.** `moveMessage` returning no new UID
1648
+ means the server cannot report where the message went - not that it stayed put. The route said
1649
+ "the message has not been moved" after it had, leaving it parked with no queue row and no way
1650
+ back. The Message-ID is now read before the move and searched for on wake.
1651
+ - **A queue row claimed when the process restarted was stranded forever.** Nothing matched
1652
+ `sending` or `waking` again, so the message was neither sent nor reported as failed, appeared in
1653
+ no list, and had no button. A later tick now reclaims a stale claim, failed sends can be
1654
+ retried, and the snoozed list shows everything unfinished rather than only what is pending.
1655
+ - **A failed wake was terminal.** A snooze coming due while the mail server was briefly
1656
+ unreachable was abandoned, and then hidden by the same filter. It retries like a send.
1657
+ - **Rules could fire on a message that was already there.** In an IMAP UID range `*` is the
1658
+ highest EXISTING uid, so `101:*` on a folder topping out at 100 is read as `100:101`. Harmless
1659
+ for the mirror; not harmless for a rule re-filing mail somebody had deliberately left alone.
1660
+ - **The conversation view could exceed the 16MB document limit.** A folder of automated mail
1661
+ sharing one thread root built a single aggregation document larger than a document may be.
1662
+ - **A signature containing `$'` pasted the rest of the draft into the message** when the sender
1663
+ was changed mid-compose: `String.replace` expands `$&`, `` $` `` and `$'` in a string
1664
+ replacement.
1665
+ - **A retry after a post-acceptance SMTP failure sent a second message.** It cannot be made
1666
+ exactly-once - a dropped connection between the terminating dot and the `250` is
1667
+ indistinguishable from one that never arrived - so a queued send now carries a fixed Message-ID
1668
+ and the duplicate is one the receiver can detect.
1669
+ - **The Undo strip expired before the hold it was offering to cancel**, on any mailbox set to
1670
+ 20 or 30 seconds.
1671
+ - **Filing a message as a collection entry did not fire `collection:entryCreated`**, so a plugin
1672
+ listening for new entries never learned about one that arrived from the mail reader.
1673
+ - **`fromLine()` mangled an address with a display name.** Stripping spaces and angle brackets
1674
+ turned `Sales <s@e.com>` into `Saless@e.com` - an mbox separator naming an address that does
1675
+ not exist. It goes through `bareAddress()` now.
1676
+ - **An absent undo-send setting read as "send immediately".** `Number(null)` is `0`, and `0` is a
1677
+ legal setting meaning exactly that, so a mailbox that had never been asked was treated as one
1678
+ that had opted out of the hold.
1679
+
1680
+
1681
+ ## [0.64.3] - 2026-09-20
1682
+
1683
+ ### Added
1684
+ - **A mailbox can say who it is and sign off.** Mail Settings gains an **Identity** section: a
1685
+ display name, an optional Reply-To, a rich-text signature, and control over which of new
1686
+ messages, replies and forwards get it - plus whether it sits above the quoted message or at the
1687
+ very bottom. The signature is added at the one point all four ways of starting a message meet,
1688
+ so a reopened draft never accumulates a second one.
1689
+
1690
+ The engine ships the shared half. `_lib/admin/mail/identity.js` is pure and used by both sides:
1691
+ the send path builds the From header with it, the compose window adds the signature with it. It
1692
+ lives under `admin/` because only `{plugin}/admin/**` and `{plugin}/public/**` are served, and a
1693
+ module the browser must import cannot sit beside the credential store.
1694
+
1695
+ ### Fixed
1696
+ - **The sealed SMTP password was sent to the browser.** `GET /accounts` returned each mailbox
1697
+ through `toPublic()`, whose destructure removes the top-level `passwordSealed` but does not
1698
+ reach into the nested `smtp` object - so the outgoing password's ciphertext shipped to every
1699
+ admin that listed its mailboxes. Nothing read it, and it was sealed rather than plaintext, but
1700
+ it had no business leaving the server. The nested object is now rebuilt through `smtpToPublic()`.
1701
+ - **Discarding an untouched reply asked about losing changes nobody had made.** The compose
1702
+ window compared the editor's text against the draft's plain-text part, and those already
1703
+ differed on every HTML reply because the editor is seeded from the HTML. It now compares against
1704
+ what the window actually opened showing. The redundant "or the subject is non-empty" clause is
1705
+ gone too - it caught nothing the change test did not, and flagged every reply and forward, which
1706
+ open with `Re:` / `Fwd:` prefilled.
1707
+
1708
+ ## [0.64.2] - 2026-09-20
1709
+
1710
+ ### Fixed
1711
+ - **A block template's own documentation could break its render.** The bundled-plugins grid on the
1712
+ home page came out as one empty `<div>` with the block's stylesheet printed underneath it as
1713
+ prose. Two causes, both general.
1714
+
1715
+ Every block ships with a header comment documenting its parameters and showing an example call,
1716
+ and that comment is still live markup when the shortcode processor runs. `plugin-card`'s example
1717
+ reads `[collection slug="plugins" … block="plugin-card"]` with no `/]`, so the processor took it
1718
+ for an unclosed tag and swallowed the rest of the render - four cards became one empty wrapper.
1719
+ Comments are now stripped in `loadBlockTemplate`, the single point every render path loads
1720
+ through.
1721
+
1722
+ Separately, a blank line ends an HTML block as far as Markdown is concerned. A block used inside
1723
+ another shortcode's wrapper (`[reveal]`, a `[col]`) arrives mid-HTML-block, so everything after
1724
+ the first blank line in its stylesheet came back wrapped in `<p>`. `buildBlockStyleTag` emits the
1725
+ CSS without blank lines now.
1726
+
1727
+ - **No Code button on a plugin whose source the server refuses.** A licensed plugin
1728
+ (`closedSource: true`) is distributed to run on a site, not to be read there, and the source
1729
+ endpoints refuse it - but the Plugins screen offered a Code button on every card, so the only way
1730
+ to learn the rule was to press it. The card leaves the button off now and shows a "Licensed"
1731
+ badge instead. `GET /api/plugins` carries `closedSource` and `licence` so the screen can tell.
1732
+ The header also wraps rather than squeezing the plugin name into a 46px column when three badges
1733
+ are present.
1734
+
1735
+ ### Changed
1736
+ - **Search is part of the CMS now, not a plugin.** `site-search` was already force-loaded as a core
1737
+ feature - it could not be switched off, and the Plugins screen offered a toggle whose result the
1738
+ loader ignored. It is now what it always was in practice: a built-in tool.
1739
+
1740
+ Everything moved rather than changed. Scoring and indexing are `server/services/search.js` (same
1741
+ `stripMarkdown`, same weights), the endpoint is `GET /api/search?q=`, the overlay is
1742
+ `public/js/search.js` + `public/css/search.css`, and the settings screen is **System > Search**
1743
+ instead of a plugin page. Settings live in `config/search.json` and now include `enabled`, so a
1744
+ site that does not want search emits no trigger, stylesheet or script at all.
1745
+
1746
+ One real improvement fell out of the move: the renderer embeds the settings in the page as
1747
+ `window.__CMS_SEARCH__` instead of the client fetching `/settings` before it can draw anything, so
1748
+ the trigger is there on the first paint. The settings object is whitelisted on save - it reaches
1749
+ the source of every public page, so "whatever was PUT last" was the wrong contract for it.
1750
+
1751
+ `window.DommaSiteSearch.open()` keeps its name, so authored context menus and any site script
1752
+ calling it are unaffected, as are existing `.site-search-*` CSS overrides.
1753
+
1754
+ Existing sites are migrated on the next start: settings are lifted out of `plugins.json`, the
1755
+ plugins.json entry is dropped, and `plugins/site-search/` is deleted. That last part is the one
1756
+ that matters - the updater never removes a plugin that has gone away upstream, and a leftover copy
1757
+ switched back on would run a second search UI on every public page.
1758
+
1759
+
1760
+ ## [0.62.0] - 2026-09-20
1761
+
1762
+ ### Changed
1763
+ - **An enabled plugin now appears under Tools, not Plugins.** The sidebar already synthesised two
1764
+ folders, but it split them by whether a plugin was *core* - so only `analytics` ever reached
1765
+ Tools and everything else you switched on sat under Plugins next to the Manage link, reading as
1766
+ an optional extra forever.
1767
+
1768
+ Only an enabled plugin registers a sidebar item at all, so anything arriving at the grouper is a
1769
+ feature the site actually has. All of it goes to **Tools**. **Plugins** keeps exactly one thing:
1770
+ the Manage Plugins link, which is where you go to turn things on and off. Turning something on
1771
+ now moves it from the thing you manage to the thing you use, which is what enabling it meant.
1772
+
1773
+ No change to what is installed or where a plugin lives on disk, and the `core` flag still travels
1774
+ on each item for anything else that wants it.
1775
+
1776
+ ## [0.61.0] - 2026-09-19
1777
+
1778
+ ### Added
1779
+ - **"Mark as spam" in the context menu vocabulary.** An authored menu item can now flag the entry
1780
+ it was right-clicked on, defaulting to a `spam` field and taking an optional field name. The id
1781
+ comes from the wrapper the collection renderer puts round every entry and the collection slug
1782
+ from the display's own `data-ctx`, so the action needs no configuration to work.
1783
+
1784
+ It posts to a new `POST /api/collections/:slug/entries/:id/spam` rather than the generic entry
1785
+ update, because `updateEntry` **replaces** `data` wholesale - the obvious client-side PUT of
1786
+ `{spam: true}` would wipe every other field on the record. The merge happens server-side, under
1787
+ the same `collections.update` permission as any other write, so a logged-out visitor gets a 401
1788
+ and is told so rather than failing silently. The flag field need not be declared in the schema;
1789
+ validation only checks required fields, so an undeclared key is carried through untouched.
1790
+
1791
+ Three files in step as the vocabulary demands - `ACTION_TYPES` for server validation, the handler
1792
+ in `public/js/context-menus.js`, and `ACTION_GROUPS` in the editor picker - plus tests that read
1793
+ the other two files rather than trusting they were remembered. Miss the handler and an editor can
1794
+ save an item that silently does nothing; miss the picker and the action cannot be chosen.
1795
+
1796
+ ## [0.60.4] - 2026-09-19
1797
+
1798
+ ### Changed
1799
+ - Plugin release dates on the admin Plugins screen render as **dd.mm.yyyy** rather than the raw
1800
+ manifest string. Formatted with Domma's `D()` like every other date in the admin, and anything
1801
+ that is not a real date is dropped rather than printed as "Invalid Date" - `plugins/_template`
1802
+ ships the literal placeholder `PLUGIN_DATE`, and a scaffolded plugin keeps it until its author
1803
+ edits it.
1804
+
1805
+ ### Changed
1806
+ - **The mail read surface is now shared, not owned by the free plugin.** The routes, the
1807
+ three-pane view, the folder tree, the pane sizing, the template, the stylesheet and the
1808
+ configuration defaults all moved into `plugins/_lib/mail/` and `plugins/_lib/admin/mail/`. The
1809
+ Pro edition registers the same surface and adds to it, rather than forking roughly two thousand
1810
+ lines that would then need every IMAP quirk and sanitiser decision fixed twice. `mail-reader` is
1811
+ now 48 lines of plugin and 17 of view; nothing about how it behaves has changed.
1812
+
1813
+ UI preferences moved from per-plugin keys to a shared `domma-mail.*` prefix. The editions never
1814
+ run together, so per-plugin keys would only have meant that upgrading to Pro silently forgot
1815
+ your pane widths and which folders you had folded.
1816
+
1817
+ ## [0.60.3] - 2026-09-19
1818
+
1819
+ ### Fixed
1820
+ - **The navbar hover no longer inverts the label, so it cannot go invisible.** `elements.css`
1821
+ paints a hover as white text on a dark slate fill: two properties that must BOTH land, and if the
1822
+ fill does not, white text is left on a cream page and the item reads as empty. Three attempts to
1823
+ make the pair land together - killing the dropdown's background-only transition, then the
1824
+ top-level links' crossfade, then the cache-buster so the fixes actually shipped - each measured
1825
+ clean in Chromium and each was still reported broken, because the reporter is on **Firefox** and
1826
+ every check had been Chromium. Rather than keep chasing an engine difference I cannot reproduce
1827
+ locally, the inversion itself is gone: hover now keeps the label its normal dark colour, tints
1828
+ the background with `--dm-hover-bg`, and underlines it. A tint that fails to paint costs a hover
1829
+ cue instead of the word, and the text colour never approaches the page background, so this class
1830
+ of bug cannot recur on any engine.
1831
+
1832
+ ## [0.60.2] - 2026-09-19
1833
+
1834
+ ### Fixed
1835
+ - **The two hover fixes reached nobody, because their cache-buster was never bumped.**
1836
+ `page.html` links `site.css` with a hand-written `?v=` token, and it still read
1837
+ `20260905-menu-highlight`. A browser keys its cache on the full URL, so a returning visitor kept
1838
+ the September stylesheet and never fetched either fix - while a cold browser, which is what every
1839
+ check of mine used, saw them immediately and passed. Same for `collection-context.js`, still on
1840
+ `20260819-export`. The tokens are now `20260919-nav-hover` and `20260919-ctx-tokens`.
1841
+
1842
+ This is why the hover bug survived two releases, a nine-site rollout and repeated headless
1843
+ verification: the fix was correct on disk, correct on the server, and invisible to anyone who had
1844
+ visited before.
1845
+
1846
+ ### Added
1847
+ - Test pinning each versioned asset's content to its `?v=` token. Shipping a changed file under an
1848
+ unchanged token is silent in every other way - the file is right, the server is right, the suite
1849
+ passes - so it now fails loudly and says which token to bump.
1850
+
1851
+ ## [0.60.1] - 2026-09-19
1852
+
1853
+ ### Fixed
1854
+ - **The same hover fade on the top-level nav links, which is the one people actually notice.**
1855
+ 0.60.0 fixed `.navbar-dropdown-item`, where `elements.css` transitions only the background while
1856
+ the hover rule sets the colour instantly. The top-level `.navbar-link` has the opposite spelling,
1857
+ `transition: all 0.15s`, so both properties animate - but the crossover from dark-text-on-light
1858
+ to white-on-dark passes through a washed-out middle, measured at roughly 2:1 around 30ms. Since
1859
+ the whole navbar is hoverable, the label appears to fade out every time the cursor crosses it,
1860
+ which is what "I can't see the nav text I'm on" meant. Colour and background now land together;
1861
+ transform, shadow and the highlight effects keep their 150ms, because a later entry in a
1862
+ transition list overrides an earlier one for that property alone. Scoped to the exact condition
1863
+ of the legacy rule that paints the dark fill, and `make verify-menus` still passes 158/158.
1864
+
1865
+ ### Added
1866
+ - **Plugins can supersede one another.** A plugin declaring `"supersedes": ["other"]` in its
1867
+ manifest pushes that other aside at load time: the superseded plugin registers no routes, no
1868
+ sidebar item and no collections, so a paid edition and the free one it replaces never appear
1869
+ side by side. The claim lives on the replacement, so a bundled plugin needs no knowledge of the
1870
+ paid one that may displace it.
1871
+
1872
+ It is resolved every boot and **never written to `config/plugins.json`**, which is the whole
1873
+ point: a licence is withdrawn by disabling a plugin and leaving its files, so persisting the
1874
+ supersession would cost a lapsed customer both the replacement and the thing it replaced,
1875
+ leaving them worse off than someone who never paid. Derived at load time, the superseded plugin
1876
+ simply returns on the next restart. Mutual claims, cycles, self-claims and claims on core
1877
+ plugins are all refused with a warning rather than applied silently.
1878
+
1879
+ - **The Mail Reader shows the real folder hierarchy.** IMAP returns flat paths plus a delimiter
1880
+ ('.' on Dovecot, '/' elsewhere), so `Work.Contracts` arrived as one entry and was drawn as a
1881
+ top-level "Contracts" with nothing tying it to Work - and two folders sharing a leaf name were
1882
+ indistinguishable. The pane now rebuilds the tree, indents by depth, and synthesises any
1883
+ intermediate level the server did not list rather than orphaning its children.
1884
+
1885
+ - **Nested mail folders fold away, and the panes are adjustable.** Any folder with children gets
1886
+ a disclosure control, remembered per mailbox. The reader now fills the window rather than
1887
+ stopping at a fixed height that guessed at admin chrome, and the two dividers between the panes
1888
+ can be dragged (double-click resets them); widths are remembered per user.
1889
+
1890
+ - **Deleting a user removes their stored mailboxes.** `deleteUser()` now emits a `user:deleted`
1891
+ hook, and the mail core clears that user's saved mailboxes and closes their connections when it
1892
+ fires. A sweep at startup catches anyone deleted while the plugin happened to be disabled, so
1893
+ stored credentials cannot outlive the account that owned them.
1894
+
1895
+ ### Changed
1896
+ - **The IMAP core moved to `plugins/_lib/mail/`.** Crypto, account store, connection pool and
1897
+ renderer are shared with the forthcoming Pro edition, so they belong to neither plugin. State
1898
+ moved with it, to `plugins/_lib/data/` - the plugin-root `data` directory is the only path
1899
+ `bin/update.js` preserves, and a nested `_lib/mail/data` would have been deleted on every
1900
+ engine update, taking every saved mailbox with it. Existing installs are migrated on first
1901
+ read: the old encryption key is adopted before any new one is generated, so stored passwords
1902
+ keep working and nobody is asked to retype them. The folder-pane logic and the pane sizing moved
1903
+ to `plugins/_lib/admin/mail/` for the same reason - the Pro edition draws the same three panes -
1904
+ and the store location is overridable via `DOMMA_MAIL_DATA_DIR` so tests never operate on a
1905
+ real one.
1906
+
1907
+ ### Fixed
1908
+ - **A plugin's admin stylesheet and template were never cache-busted.** View entries have been
1909
+ auto-stamped with their mtime for a while, but the stylesheet declared in `admin.css[]` and the
1910
+ template fetched by `templateUrl` were not, so after an update a browser kept serving the old
1911
+ ones. The visible result is a plugin that looks half-broken - a template cached without the
1912
+ markup a new feature needs reads as "that feature doesn't work" rather than as a stale file, and
1913
+ only a hard refresh explains it. Both are stamped now; the template reuses the token the loader
1914
+ put on the view module, so it refreshes exactly when the plugin does.
1915
+ - **The Mail Reader did not use the full width.** The admin caps its content at `max-width: 1200px`,
1916
+ which suits forms and tables and leaves a dead strip down the side of a mail client on a wide
1917
+ screen. The view now lifts that cap while it is mounted and puts it back on the way out, so no
1918
+ other screen is affected.
1919
+ - **Only the open mailbox's header pins now.** Every mailbox row was `position: sticky; top: 0`, so
1920
+ with more than one mailbox they all stacked at the same place and silently overlapped, the last
1921
+ one painted winning. The folders scrolling past belong to the open mailbox, so its name is the
1922
+ one that has to stay visible; the others scroll normally.
1923
+ - **The Mail Reader did not fill the window.** It sized itself against `window.innerHeight`, but
1924
+ the admin puts a view inside `main.dashboard-main`, which scrolls on its own and sits inside a
1925
+ padded container. Measuring against the window therefore overshot by that padding and the
1926
+ container scrolled instead of the panes fitting. It now measures the nearest scrolling ancestor
1927
+ and subtracts the padding in between.
1928
+ - **"Load images" could never work.** The reader rendered a message with `srcdoc`, and a `srcdoc`
1929
+ document **inherits the embedding page's Content-Security-Policy** - the admin's, whose
1930
+ `img-src` is `'self' data: blob:`. So the button flipped the image back to its real URL, the
1931
+ server returned it correctly, and the browser then refused to fetch it, silently, without ever
1932
+ making a request. Nothing in the server was wrong and no amount of testing it would have shown
1933
+ the problem.
1934
+
1935
+ The body is now served from its own route and loaded with `src`, so it carries its own policy
1936
+ instead of inheriting one. That route cannot read an `Authorization` header - an `<iframe src>`
1937
+ sends none - so it is authorised by a short-lived signed token naming one user, one mailbox and
1938
+ one message, in the same shape as the CMS's existing preview links. The token covers the
1939
+ load-images decision too, so it cannot be escalated by editing a URL. Scripts, frames and
1940
+ everything else stay refused, and a message whose images have not been loaded is served a policy
1941
+ that permits no remote images at all.
1942
+ - **The Mail Reader's folder list did not scroll.** `.mail-folders-list` had `overflow-y: auto`
1943
+ but, as a flex child, kept the default `min-height: auto` - "at least as tall as my content" -
1944
+ so a mailbox with more folders than fitted pushed the pane taller instead of scrolling inside
1945
+ it. The scrolling children now set `min-height: 0`. The mailbox header is sticky, so scrolling a
1946
+ long list never leaves you looking at indented folders with nothing to say which mailbox they
1947
+ belong to, and the message-list heading names the full path and the mailbox - an "Archive" under
1948
+ Work and another under Personal were previously both just "Archive".
1949
+ - **`plugins/_lib/` shipped in no published package.** It matches none of the plugin copy
1950
+ patterns in `scripts/build.js` - no `plugin.js`, no `services/`, no `views/` - so the shared
1951
+ helper library was absent from every tarball. A plugin built against its documented API worked
1952
+ in development and broke on an installed copy. Nothing bundled imported it yet, which is why it
1953
+ went unnoticed.
1954
+ - **Plugin runtime state was copied into the build.** `plugins/**/*.json` swept each plugin's
1955
+ `data/` directory into `_publish/`, so the build artifact carried the analytics counters - and
1956
+ would have carried sealed mailbox passwords once the mail core landed. The npm `files` list
1957
+ excluded them from the published tarball, but that is one typo away from shipping someone's
1958
+ credentials, and the artifact should never have contained them.
1959
+
1960
+ ## [0.60.0] - 2026-09-19
1961
+
1962
+ ### Added
1963
+ - **Mail Reader - a read-only IMAP mailbox in the admin.** `plugins/mail-reader/` connects to a
1964
+ standard IMAP server and displays mail: folders, paged envelopes, message bodies, attachments.
1965
+ A user may hold several mailboxes and sees only their own. It ships **disabled by default**, so
1966
+ no site stores a credential until someone opts in.
1967
+
1968
+ Read-only is enforced at the protocol level rather than merely by leaving routes out. Mailboxes
1969
+ open with `EXAMINE` and fetches use `BODY.PEEK[]`, so reading a message never sets `\Seen` - a
1970
+ reader that silently marks mail as read on someone's phone is worse than no reader at all.
1971
+ There is no route in the plugin that writes to a mailbox.
1972
+
1973
+ Message bodies are hostile content by default and are treated as such: scripts, styles, frames,
1974
+ forms and event handlers are stripped, `javascript:` URLs are dropped, and the result renders in
1975
+ an iframe sandboxed with neither `allow-scripts` nor `allow-same-origin`, so a hole in the
1976
+ sanitiser has no origin to abuse. Remote images are blocked unless the reader asks for them per
1977
+ message, because loading one reports the open - and who opened it - back to the sender; that
1978
+ choice is deliberately never persisted.
1979
+
1980
+ Passwords are sealed with AES-256-GCM into a gitignored, owner-only file rather than a
1981
+ collection: a plugin-owned collection is still browsable from the admin Collections screen and
1982
+ reachable through `/api/v1`, which is not where a mailbox credential belongs. The key resolves
1983
+ `MAIL_SECRET` -> `INSTANCE_SECRET` -> a generated plugin-local key, because `INSTANCE_SECRET`
1984
+ only exists on sites that ran setup or registered with a manager, and deriving from `JWT_SECRET`
1985
+ would mean rotating it bricked every stored password at the same time as logging everyone out.
1986
+
1987
+ Access control is ownership: account lookups take the owner's id as well as the account's, so a
1988
+ route cannot hand one user another's mailbox, and an id that is not the caller's answers 404
1989
+ rather than 403.
1990
+ - Test covering theme-token references: every `--dm-*` used from `public/` must be defined in a
1991
+ shipped stylesheet or set at runtime by our own code. An undefined token does not warn and does
1992
+ not throw, it silently uses whatever literal was typed as its fallback, which is how a
1993
+ dark-theme colour reaches every light theme unnoticed. Reintroducing either fix above fails the
1994
+ suite.
1995
+
1996
+ ### Fixed
1997
+ - **Every npm publish since 0.55.1 failed, so the registry sat four minor versions behind the
1998
+ fleet.** The workflow ran `npm test` before the step that vendors the Domma bundle, and
1999
+ `public/dist/` is gitignored - so on a fresh CI checkout the theme service had no
2000
+ `domma-themes.css` to read theme and token names out of, and the admin smoke test drove a page
2001
+ with no bundle. Ten tests failed on every run and the job never reached the npm step, which is
2002
+ why the failure looked like a publishing problem and was not one. The bundle is now vendored
2003
+ before the tests. `npm latest` was 0.55.1 while every site ran 0.59.3, so a routine
2004
+ `npx domma-cms@latest update` would have silently downgraded the whole fleet.
2005
+ - **The published package carried a real contact record.** `plugins/contacts/data/contacts.json`
2006
+ is tracked, and `files` excluded `plugins/analytics/data` and `plugins/mail-reader/data` but not
2007
+ this one, so every tarball since the contacts plugin landed shipped a live name and mobile
2008
+ number to a public registry. The file is now an empty seed, and `files` excludes the data
2009
+ directory of every bundled plugin rather than naming them one at a time as each leak is noticed.
2010
+ `_lib/dataStore` returns a default on a missing file and creates the directory before writing,
2011
+ so shipping no `data/` changes nothing at runtime.
2012
+ - **A plugin's admin stylesheet never reached the published package.** `scripts/build.js`
2013
+ minified `plugins/*/public/**/*.css` but had no pattern for `plugins/*/admin/css/**/*.css`, so
2014
+ an `admin.css` declared in a plugin's `plugin.json` was dropped from the tarball. It is
2015
+ invisible in development, where the file is served straight off disk, and only shows up as an
2016
+ unstyled admin screen on an installed copy - the same shape as the missing `.mjs` that shipped
2017
+ 0.22.1. The `invoice` plugin has been shipping unstyled since it was added.
2018
+ - **A collection sorted by a number scrambled itself past nine entries.**
2019
+ `FileAdapter.list()` compared sort values with a plain
2020
+ `String(a).localeCompare(String(b))`, so an `order` field came back 1, 10, 11,
2021
+ 2, 20, 3. Nine entries look perfect, which is why this survived so long: the
2022
+ tenth entry is what breaks it, and the breakage looks like someone mis-typed
2023
+ the order values rather than like a bug. `MongoAdapter` hands the sort to the
2024
+ database, which compares numbers numerically, so the two adapters had been
2025
+ disagreeing about identical data - as had the server and the browser, since
2026
+ the public right-click menu sorts through `collection-sort.mjs`, which has
2027
+ always had a proper numeric ladder. The comparison now uses numeric
2028
+ collation. Dates and text are unaffected: ISO dates still order
2029
+ chronologically and text still orders alphabetically, both covered by tests.
2030
+ - **A navbar dropdown flashed its labels invisible on every hover.** Domma's `elements.css`
2031
+ transitions only the background on a dropdown item (`transition: background 0.15s ease`), while
2032
+ its light-mode hover rule sets `color: #ffffff !important` with no transition. The label
2033
+ therefore turned white on the first frame and then waited 150ms for the dark background to fade
2034
+ in behind it: white text on a near-white panel, about **1.02:1**, on every hover. Running the
2035
+ cursor down a menu flashed each row blank in turn. Transitioning the colour as well does not
2036
+ help, because the crossover from dark-on-light to light-on-dark passes through roughly 1.2:1
2037
+ whichever way it is animated, so both now land together the way a native menu does.
2038
+ `#site-navbar` outranks the framework selector, so no `!important` is needed. Measured on a
2039
+ real page frame by frame; the first frame goes from 1.02:1 to 14.6:1.
2040
+ - **The public right-click menu had no visible hover at all on light themes.**
2041
+ `collection-context.js` styled three surfaces with `var(--dm-surface-2, ...)`, and
2042
+ `--dm-surface-2` is defined in **none of the 32 shipped themes**, so every site fell back to the
2043
+ hardcoded literal sitting beside it: `rgba(255,255,255,.06)` and friends, white washes written
2044
+ while looking at a dark theme. On a light theme that is white on white, so hovering a row, and
2045
+ the search box and the filter chips, were invisible. They now use `--dm-hover-bg` and
2046
+ `--dm-input-bg`, which exist, with neutral-gray fallbacks that degrade sanely in either mode.
2047
+ `--dm-surface-alt` in `site.css` was the same mistake pointing the other way - a dark wash,
2048
+ invisible on dark themes - and is gone too.
2049
+
2050
+ ## [0.59.3] - 2026-09-19
2051
+
2052
+ ### Fixed
2053
+ - **Public pages carry cache headers.** They went out with no `Cache-Control`, no `ETag` and no
2054
+ `Last-Modified` at all, so a browser applied heuristic caching and could keep showing an edited
2055
+ page's old content on an ordinary refresh while the server served the new one - which looks
2056
+ exactly like a CMS bug and was reported as one. Pages now send
2057
+ `Cache-Control: private, no-cache` plus an `ETag`: store it, but check before reusing it. A
2058
+ repeat visit with nothing changed costs a 304 with no body; an edit changes the validator and the
2059
+ visitor gets the new page. `private` because page visibility is role-dependent, so a shared proxy
2060
+ must never hand one visitor a page rendered for another's permissions. Draft previews keep their
2061
+ stricter `no-store`, and 404s keep their status - the helper deliberately sets no status code, so
2062
+ adding headers could not turn a not-found into a soft 200.
2063
+
2064
+ ## [0.59.2] - 2026-09-19
2065
+
2066
+ ### Fixed
2067
+ - **The Context Menu editor's preview showed the menu you edited before, not the one on screen.**
2068
+ `onUnmount` called `closeAll()`, which hides the open panel but leaves the instance registered
2069
+ against `#ctx-preview`. Opening a second menu's editor bound a second instance to the same
2070
+ element; Domma collects every instance matching a right-click and sorts by priority, both were 0,
2071
+ the sort is stable - so the FIRST registered won, and the old menu's items appeared under the new
2072
+ menu's heading. The instance is destroyed on unmount now, and any stray still bound to the
2073
+ preview element is cleared on mount, for the case where unmount never ran.
2074
+
2075
+ ## [0.59.1] - 2026-09-19
2076
+
2077
+ ### Fixed
2078
+ - **Editing a context menu did not reach the front end on a live site.** The invalidation call was
2079
+ wrong twice over: it named `invalidateTag`, which the cache module does not export, and the tag
2080
+ `pages`, which nothing is ever stored with - so even the corrected spelling would have matched
2081
+ nothing. A `try/catch` marked "cache is optional" swallowed the resulting TypeError. Because the
2082
+ response cache is **off in development and on in production**, the edit worked perfectly on the
2083
+ machine it was written on and the live site served the old menu for up to an hour. Pages are
2084
+ tagged `site`, so that is what is cleared now, and a failed invalidation is logged instead of
2085
+ hidden.
2086
+
2087
+ ### Changed
2088
+ - **Any authenticated admin change now clears the response cache.** The targeted invalidations stay
2089
+ - they are what stops one collection edit clearing every unrelated page - but they are no longer
2090
+ the only thing standing between an edit and the live site. Runs after the reply is sent, and is
2091
+ scoped to authenticated mutations so a public form submission does not cold-start the cache on a
2092
+ busy site.
2093
+ - The theme service passed a dead `pages` tag alongside `site`; dropped.
2094
+
2095
+ ## [0.59.0] - 2026-09-19
2096
+
2097
+ ### Added
2098
+ - **The Theme editor now shows what adopting a theme actually changes.** Picking a theme for an
2099
+ override lists how many tokens it moves here, the first few with before/after swatches, and -
2100
+ the point of it - warns when the adopted theme shares the current theme's surface colour:
2101
+
2102
+ Changes 70 of 119 tokens here.
2103
+ Both themes use the same surface colour, so this region keeps the fill it has now.
2104
+ Set --dm-card-bg (a card) or --dm-surface (most other regions) below to change it.
2105
+
2106
+ Adopting one light theme onto a region inside another changes its accent and text and leaves the
2107
+ fill exactly as it was, because most light themes use a white surface. That reads as a broken
2108
+ feature when it is doing precisely what it says. The check deliberately ignores
2109
+ `--dm-background`: that is the page canvas, which a card never paints from, and counting it
2110
+ would silence the warning for the very case that prompted this.
2111
+ - `GET /api/theme/themes/:id` returns one theme's token values, fetched on demand rather than
2112
+ folded into `/catalog` - 32 themes by ~245 tokens is a payload nobody needs on a page load.
2113
+
2114
+ ## [0.58.2] - 2026-09-19
2115
+
2116
+ ### Added
2117
+ - **Page picker for URL fields** (`admin/js/lib/page-picker.js`). A browse button beside the URL
2118
+ box lists the site's own pages with search, and marks anything not published. Typing still wins:
2119
+ the picker fills the field in rather than replacing it, because plenty of legitimate targets are
2120
+ not pages at all - an external site, an anchor, a file in the media library. Wired into the
2121
+ Context Menu editor's `navigate` action; `makePageInput()` returns the same `{el, input}` shape
2122
+ `makeIconInput()` does, so any other URL field can take it.
2123
+
2124
+ ### Fixed
2125
+ - The Breadcrumbs tab in Site Settings used `col-sm-*`, which Domma's grid does not define, so its
2126
+ offset fields had been stacking full-width instead of sitting side by side. Same dead-class
2127
+ problem as the Context Menu editor in 0.58.1; these were the last of them in the admin.
2128
+
2129
+ ## [0.58.1] - 2026-09-19
2130
+
2131
+ ### Fixed
2132
+ - **The Context Menu editor's preview left headings out.** A menu with two headings listed six
2133
+ entries in the Items tab and previewed four. The preview now uses the same mapping the public
2134
+ renderer does, so what you see is what the page gets - a preview that quietly omits things is
2135
+ worse than none, because you trust it.
2136
+ - **The editor's two-column layouts never worked.** Every column was marked `col-md-*`, which
2137
+ Domma's grid does not define - only `col-1` to `col-12` exist - so every "column" had been a
2138
+ full-width block and the page was one long stack. That was the real cause of the editor reading
2139
+ as a mess.
2140
+
2141
+ ### Changed
2142
+ - The preview pane now sits to the **right of the tabs**, visible from whichever tab you are on,
2143
+ and sticks as a long Items list scrolls past it. One instance rather than one per tab, because
2144
+ four copies would be four elements sharing `#ctx-preview` and the menu binds by selector.
2145
+ Domma's columns go side by side from 576px, which is far too narrow for a two-pane editor, so
2146
+ this workspace stacks below 1100px instead.
2147
+ - The item **Icon** field is the admin's icon picker rather than a bare text box.
2148
+
2149
+ ## [0.58.0] - 2026-09-19
2150
+
2151
+ ### Added
2152
+ - **"Specials" in the Context Menu builder** - the things a browser's own right-click menu offers:
2153
+ Reload, Back, Forward, Copy, Cut, Paste, Select all, plus **Open site search**. They take no
2154
+ arguments and each decides whether it applies to the click that opened the menu: Back greys out
2155
+ with no history behind the page, Paste outside an editable field, Copy with nothing selected,
2156
+ Site search when that plugin is disabled. That is `disabled` rather than hidden, so the menu keeps
2157
+ a stable shape and reads the way a native one does. Forward is the one the browser cannot report
2158
+ at all - there is no `canGoForward` - so it keys off whether this page load came from a
2159
+ back/forward navigation, which lights it up exactly when you have gone back.
2160
+ - The action picker is **grouped** (This page / Clipboard and selection / Collection displays /
2161
+ This site). Flat, it had grown past the point where anything could be found in it, and the
2162
+ self-disabling actions now say so under the picker rather than in the docs.
2163
+ - `window.DommaSiteSearch.open()` - the one handle the Site Search plugin exports, so the rest of
2164
+ the site can open it without reaching for its trigger button's class.
2165
+ - First tests for the context-menu action vocabulary, which shipped in 0.56.0 with none.
2166
+
2167
+ ## [0.57.1] - 2026-09-19
2168
+
2169
+ ### Added
2170
+ - Count pill for **Theme** in the admin sidebar, showing how many element overrides are
2171
+ configured. Theme answers with a config object rather than a list, so it needs its own
2172
+ accessor rather than the shared array-length one the other counts use.
2173
+
2174
+ ## [0.57.0] - 2026-09-19
2175
+
2176
+ ### Added
2177
+ - **Theme has its own home: Content > Theme.** The front-end theme, admin theme, automatic
2178
+ day/night pair and typography moved out of Site Settings into `config/theme.json`, lifted
2179
+ there by a one-shot migration on first boot. The matching `site.json` keys are still
2180
+ rewritten on every save as a one-way mirror, so plugins, the Theme Roller and fleet tooling
2181
+ that read them keep working; nothing in the CMS reads them any more.
2182
+ - **Per-element theme overrides.** An override is a CSS selector plus either a theme to adopt
2183
+ wholesale or individual `--dm-*` values, compiled server-side into one `<style>` block that
2184
+ is injected ahead of `content/custom.css`. A Domma theme is only a class-scoped block of
2185
+ custom properties, so re-declaring those properties on a selector re-themes everything
2186
+ inside it - including components rendered later, which is what hand-written CSS always
2187
+ misses, and without the specificity fight that made the navbar hover rule unbeatable. The
2188
+ theme and token pickers are built from the shipped `domma-themes.css`, so an override cannot
2189
+ name a token that does not exist. Values that could close their own declaration, and
2190
+ selectors that would re-theme the whole document, are refused rather than escaped.
2191
+ - **Form Triggers.** A trigger hangs off a field and acts on the whole form when the answers
2192
+ meet a condition: show a banner or toast, hide, disable or require other fields, refuse the
2193
+ submit, jump to a wizard step, celebrate, redirect, or run a CMS action. State actions apply
2194
+ while the condition holds and are reverted when it stops; event actions fire once on the
2195
+ transition into a branch, and never on the first render. Everything a trigger decides about
2196
+ submission is re-enforced in the submit route from the same engine, so a blocked form stays
2197
+ blocked when the button is re-enabled in dev tools. Authored in each field's Triggers panel,
2198
+ beside Conditional Logic, whose condition engine it shares. See
2199
+ `docs/guides/form-triggers.md`.
2200
+
2201
+ ### Changed
2202
+ - Em dashes swept back out of the source, docs, tests and the seed script in favour of plain
2203
+ hyphens. `scripts/seed.js` carried 68 of them, so a freshly seeded site's demo content was the
2204
+ biggest offender.
2205
+
2206
+ ### Fixed
2207
+ - **The Context Menu editor never loaded the menu you opened.** Domma's router calls
2208
+ `onMount($container)` with one argument, so the editor's `params.slug` was always undefined
2209
+ and every "edit" opened a blank new-menu form - which also left the live preview with no
2210
+ items to register, so right-clicking the Try it panel did nothing at all. It now reads the
2211
+ slug from the hash, as the menu and form editors do.
2212
+ - **The inline `[form]` embed leaked the form's actions into the page.** The whole form record
2213
+ was base64-encoded into `data-form-inline`, including the notification email recipients and
2214
+ the webhook URL. `GET /forms/:slug/public` has always stripped those; this path never did.
2215
+ The browser never read them - the submit route resolves them server-side.
2216
+ - **One malformed record 500'd the notification bell on every admin page load.** The admin polls
2217
+ the unread count on every screen, and all four notification handlers read `entry.data.x`
2218
+ without checking there was an `entry.data` - so a single flat record in the collection (left
2219
+ behind by a form that once pointed its submissions at `notifications`) threw
2220
+ `Cannot read properties of undefined (reading 'expiresAt')` every time. Entries that are not
2221
+ notifications are now skipped, with a one-time warning naming the id, and the rest of the
2222
+ inbox is still served. Mark-read and dismiss answer 404 for such a record rather than
2223
+ crashing.
2224
+ - **The theming docs listed design tokens that do not exist** (`--dm-color-primary` and
2225
+ friends). A `var()` on an undefined token silently resolves to nothing, which is how a popup
2226
+ ends up with no background; the list now matches the shipped stylesheet.
2227
+
2228
+ ## [0.56.1] - 2026-09-18
2229
+
2230
+ ### Fixed
2231
+ - **Context Menus was invisible to the `admin` role.** 0.56.0 added the
2232
+ `context-menus` permission family to the registry but never added it to the base
2233
+ `admin` seed, so only `super-admin` could see the sidebar item - on every site in
2234
+ a fleet, the entry was present in `admin-sidebar.json` and then filtered out
2235
+ again by permission. `roles.js seed()` self-heals base roles at boot, so this
2236
+ lands on restart with no migration. Custom roles are still not back-filled and
2237
+ need the grant adding by hand, as with Menus, Projects and API Tokens.
2238
+
2239
+ ## [0.56.0] - 2026-09-18
2240
+
2241
+ ### Added
2242
+ - **Context Menus are authorable in the admin** (Content > Context Menus). A list, an editor,
2243
+ and the storage and API behind them. Until now a right-click menu could only be added by
2244
+ editing JavaScript. Storage mirrors menus - one file per menu under `config/context-menus/` -
2245
+ and `binding` is deliberately the same shape menus already use, so per-page and per-project
2246
+ scoping behaves identically.
2247
+ - **Authored menus reach the public site.** Resolved per page server-side, so a visitor receives
2248
+ only the menus that apply - a menu bound to `/shop/*` is not in the source of `/about`. Both
2249
+ render paths do it. `public/js/context-menus.js` translates the records into Domma
2250
+ registrations; which menu answers a right-click stays Domma's decision.
2251
+ - Item actions come from a fixed vocabulary (navigate, filter, search, sort, export, copy-link,
2252
+ print, open-modal, run-action), never stored JavaScript: `admin/` is replaced wholesale by the
2253
+ updater, and a stored script on a public page is a cross-site scripting hole with extra steps.
2254
+ Anything bespoke goes through `run-action`, naming an Actions artefact with its own permissions.
2255
+ - Permission family `context-menus.{read,create,update,delete}`. Custom roles are not back-filled.
2256
+
2257
+ ### Changed
2258
+ - **Requires `domma-js` 0.43.0** for `E.contextMenu`.
2259
+
2260
+
2261
+ ### Changed
2262
+ - **Both context menus register with Domma instead of listening for themselves.** The collection
2263
+ filter panel and the page editor's shortcode menu each owned a `contextmenu` listener - one on
2264
+ `document`, one on the textarea. That works while a page has exactly one context menu. Add a
2265
+ second and the winner is decided by which script registered first rather than by which menu is
2266
+ nearer the click, which is a bundling accident and differs between dev and production. Domma
2267
+ 0.43.0 keeps one document listener and a registry of bound containers and resolves by walking
2268
+ outward from the click, so depth decides and load order cannot. Requires `domma-js` 0.43.0; both
2269
+ menus fall back to their original listener below that, so behaviour is unchanged until the bump.
2270
+ - **The collection menu now owns its display outright.** It registers with `exclusive: true`, so a
2271
+ right-click menu bound deeper - on `[data-entry-id]`, or on any selector that happens to match
2272
+ inside a display - is not offered the click. Filter, search, sort, group-by and export are the
2273
+ point of a collection display and are not replaced by accident. It is not a veto: when the menu
2274
+ declines because there is no usable model, it steps aside completely and the click reaches
2275
+ whatever encloses the display.
2276
+ - Neither panel is an item list, so both hand off through Domma's `render` callback and keep their
2277
+ own rendering and open/close handling unchanged. A rendered panel is opaque in both directions:
2278
+ no ancestor items merge in, and it contributes none upward.
2279
+
2280
+ ## [0.55.1] - 2026-09-18
2281
+
2282
+ ### Fixed
2283
+ - **A menu's selected-item effect was wiped by its own `hover: none`.** `highlight.active` did nothing
2284
+ on any menu that also set `highlight.hover` to "none" - which is most menus that configure
2285
+ highlighting at all. `hover: none` declares its rest and hover states together, on purpose, so the
2286
+ two are provably identical; that includes `background-color: transparent !important`, and since
2287
+ 0.54.1 it carries a `#site-navbar`-scoped copy at specificity (1,4,0). The current-page effects had
2288
+ no such copy and sat at (0,4,0), so the rest-state rule won: `darken`, `background` and `pill` lost
2289
+ their background entirely, and `underline`/`overline` lost their line. Every current-page effect now
2290
+ carries the navbar-scoped twin. Per-item overrides already had theirs, which is why only the
2291
+ menu-wide effects were affected.
2292
+
2293
+ ---
2294
+
2295
+ ## [0.55.0] - 2026-09-18
2296
+
2297
+ ### Added
2298
+ - **Per-item highlighting.** `highlight: {hover, active, accent}` can now sit on any individual menu
2299
+ item, not just on the menu. The menu's block is the default; an item's own replaces either half of
2300
+ it, so one entry can glow orange and become an orange pill while the rest of the bar keeps a plain
2301
+ colour shift. Edited under **Highlighting** in the Menus item editor: blank means "inherit the
2302
+ menu's", `None` means "switch this off for this item only" - different things, and the editor says
2303
+ so. Applies on every render path: the public navbar, footer columns, `[menu]`, overlay panels and
2304
+ the admin sidebar.
2305
+ - **Menu alignment.** `align: left | centre | right | spread` on a menu, as **Alignment** on the
2306
+ Menus Behaviour tab. Horizontal menus only.
2307
+
2308
+ ### Fixed
2309
+ - **Navbar items were never actually centred.** Domma's stylesheet centres `.navbar-nav`, but inside
2310
+ the space left over once the brand and the actions block have taken theirs - so a wide brand shunts
2311
+ the items right by half its width, and a search icon pulls them back left by half of its own. They
2312
+ lined up on the bar's real centre only when those two happened to be the same width. The bar is now
2313
+ a three-track grid whose outer tracks are equal, so the items land on the centre line whatever
2314
+ flanks them. Measured at 1440px with a 161px brand: the list centre moved from 80px right of the
2315
+ bar centre to 0. This is the default; `align` overrides it.
2316
+ - **The "when selected" effect looked like it did nothing.** `public/css/site.css` still carried the
2317
+ pre-menus current-page marker - bold plus a 2px `::after` bar, with `!important` - and nothing in
2318
+ `menu-highlight.css` can reset a pseudo-element it does not know about. So the rule was additive
2319
+ rather than a default: picking "Filled pill" drew a pill *and* the old bar, picking "None" left both
2320
+ the bar and the bold. It now stands down for any menu-level or per-item effect, and still draws for
2321
+ menus that have chosen nothing.
2322
+ - **New sites ship with SMTP configured.** `bin/cli.js` wrote no `smtp` key at all, and an empty
2323
+ `smtp.host` is not an error - it diverts every message to a throwaway Ethereal test account that
2324
+ accepts the mail and reports success. Password resets and form notifications appeared to send, the
2325
+ Health panel's "Last email send" went green, and nothing was delivered. A scaffolded site now gets
2326
+ a complete block from `bin/lib/smtp-defaults.js`: `SMTP_HOST`/`PORT`/`SECURE`/`USER`/`PASS`/`FROM`
2327
+ from the environment, falling back to a local MTA on port 25, with the port defaulting to 587 when
2328
+ a real host is supplied. Credentials are read from the environment, never compiled in. The fleet
2329
+ manager already did this for the sites it provisions (`composeSmtp` in `siteScaffolder.js`); this
2330
+ is the equivalent for a site created straight from the CLI.
2331
+ - **A site with no SMTP host now says so.** Three surfaces, because the failure is invisible
2332
+ otherwise: a warning banner on Settings → Email (live as you edit the field), a new `SMTP host`
2333
+ check in Health at `warn` - deliberately separate from `Last email send`, which is green on an
2334
+ Ethereal send - and a `console.warn` naming the consequence instead of the previous friendly info
2335
+ line. The fallback itself is unchanged, so existing sites keep working.
2336
+ - `npm run setup` now renames `smtp.fromName` along with the site title, so a fresh install stops
2337
+ signing its email "My Site" - but only while the two still agree, leaving a deliberately chosen
2338
+ From name alone.
2339
+
2340
+ ### Changed
2341
+ - The two effect dropdowns on the Menus Behaviour tab are built from `HIGHLIGHT_HOVER` /
2342
+ `HIGHLIGHT_ACTIVE` in `menu-decor.mjs` rather than from hand-written `<option>` markup, so they and
2343
+ the per-item ones cannot offer different effects. The menu-wide "Current page" field is relabelled
2344
+ **When selected**, matching the per-item control.
2345
+
2346
+ ---
2347
+
2348
+ ## [0.54.2] - 2026-09-06
2349
+
2350
+ ### Fixed
2351
+ - **Preset colours degrade to a literal instead of silently changing.** `colourToCss` emitted a bare
2352
+ `var(--dm-success)`. A token that a theme does not define makes the custom property
2353
+ guaranteed-invalid, and what happens next depends on the call site: a menu `highlight.accent`
2354
+ quietly fell through to the theme primary, so the accent an editor picked was not the one drawn;
2355
+ a badge or pill background, which uses the value directly with no fallback, became transparent and
2356
+ took its white text with it. Presets now emit `var(--dm-success, #198754)` - the literal each token
2357
+ carries in Domma's core theme - the way Domma's own CSS does it. This covers every consumer:
2358
+ menu item colours, badges, pills, floating-panel accents and highlight accents.
2359
+ - The stylesheet's own `var(--dm-menu-highlight, var(--dm-primary))` chain ended in a bare token for
2360
+ the same reason and now ends in `#0d6efd`.
2361
+
2362
+ ### Changed
2363
+ - `PRESET_COLOUR_VARS` / `colourToCss` are no longer duplicated between `server/services/menus.js`
2364
+ and `public/js/menu-decor.mjs`. The server imports and re-exports the browser module's copy, as it
2365
+ already does for the highlight helpers, so the two can no longer drift. `PRESET_COLOUR_FALLBACKS`
2366
+ joins them, and a test asserts every preset has both a token and a literal.
2367
+
2368
+ ---
2369
+
2370
+ ## [0.54.1] - 2026-09-06
2371
+
2372
+ ### Fixed
2373
+ - **Menu hover effects did nothing on a transparent navbar.** Every `highlight.hover` value was
2374
+ inert on any navbar without a `navbar-light`/`navbar-dark` variant class while the site was in
2375
+ light mode: "None" did not switch hover off, "Colour change" changed no colour, "Background tint"
2376
+ showed the theme's legacy slate instead of the accent, and "Uppercase" and "Grow" were swamped by
2377
+ a background that never moved. Domma's `elements.css` carries
2378
+
2379
+ [data-mode="light"] .navbar:not(.navbar-light):not(.navbar-dark) .navbar-link:hover
2380
+ { color: #fff !important; background-color: #1e293b !important }
2381
+
2382
+ at specificity (0,5,0). The effect rules were (0,3,0), and with `!important` on both sides
2383
+ specificity decides the winner - so the legacy rule owned the hover colour and background and the
2384
+ chosen effect was invisible. Each hover effect is now declared twice, the second scoped to
2385
+ `#site-navbar` at (1,3,0), which wins. `transform`, `text-transform`, `letter-spacing`,
2386
+ `animation` and `text-shadow` also gained the `!important` they needed for the same reason.
2387
+ A test now asserts the paired selectors exist for every effect in the vocabulary.
2388
+ - **The footer's motion switch is honoured.** The reduced-motion rules keyed only off the OS
2389
+ `prefers-reduced-motion` media query, but the site's own switch stores the visitor's choice and
2390
+ stamps `dm-reduced-motion` on `<html>` (it patches `matchMedia` for JS, which CSS never sees). A
2391
+ visitor who had switched motion off still got `grow` and `breathe`.
2392
+
2393
+ ---
2394
+
2395
+ ## [0.54.0] - 2026-09-05
2396
+
2397
+ ### Added
2398
+ - **Menu item highlighting is configurable.** A menu can now say what its items do under the
2399
+ pointer and how the item for the current page is marked, through a new `highlight` block in
2400
+ `config/menus/<slug>.json` (Menus > Behaviour in the admin):
2401
+ `hover` - `none`, `colour`, `underline`, `background`, `uppercase`, `grow`, `breathe` or `glow`;
2402
+ `active` - `none`, `bold`, `underline`, `overline`, `background`, `darken` or `pill`;
2403
+ plus an optional `accent` (preset key or `#rrggbb`) the effects draw with. Previously the hover
2404
+ colour shift and the current-page underline were fixed by the framework theme with no way to
2405
+ change or switch them off. Both fields are optional and a menu without the block looks exactly
2406
+ as it did, so nothing moves on an existing site until an editor picks an effect.
2407
+ The effects apply on every path a menu is drawn on - the public navbar, both footer columns, the
2408
+ `[menu]` shortcode, overlay panels and the admin sidebar. `grow` and `breathe` are dropped for
2409
+ visitors whose browser asks for reduced motion.
2410
+ - **The current page is now marked in menus.** Menu items pointing at the page being viewed carry
2411
+ `aria-current="page"` (and `.active` / `.is-active`), on the navbar, the footer and the `[menu]`
2412
+ shortcode. Nothing marked them before - Domma's navbar only ever read an `active` flag the CMS
2413
+ never set - so the current page was announced to nobody and `highlight.active` would have had
2414
+ nothing to style. Matching is exact, ignoring query string, fragment and trailing slash, so an
2415
+ item pointing at `/` is only marked on the home page and a parent is not marked because you are
2416
+ somewhere below it.
2417
+
2418
+ ### Changed
2419
+ - The blanket `.page-body a` link styling in `site.css` now steps aside for a `[menu]` whose menu
2420
+ declares a `highlight` - its rest-state underline would otherwise pre-empt a hover underline.
2421
+ Menus without a highlight block are untouched.
2422
+ - Supersedes the first cut of this feature (`style.hover` / `style.active`, never released). Those
2423
+ keys are still read and translated onto `highlight`, so a menu saved under the previous admin
2424
+ keeps rendering. Two defects went with it: nothing in the CMS ever set `.active` or
2425
+ `aria-current` on a public menu, so the whole selected-item half was inert outside the admin
2426
+ sidebar; and the navbar's generated `<style>` tag appended `:hover` / `.active` to only the last
2427
+ selector of a comma-joined list, so those rules applied to every navbar item unconditionally.
2428
+ - `aria-current` is allowed through the markdown sanitiser (previously only `aria-hidden` was), so
2429
+ the marking survives on the `[menu]` shortcode path.
2430
+
2431
+ ---
2432
+
2433
+ ## [0.53.0] - 2026-08-31
2434
+
2435
+ ### Added
2436
+ - **The log level is configurable.** `config/server.json` gains an optional
2437
+ `logging.level` (`fatal`, `error`, `warn`, `info`, `debug`, `trace`, `silent`), also
2438
+ settable through the `LOG_LEVEL` environment variable. Previously it was hardcoded to
2439
+ `info` in development and `warn` otherwise, and `warn` hides every 4xx response and every
2440
+ `log.info` a plugin writes - so a rejected admin action left no trace at all and could not
2441
+ be diagnosed after the event. Defaults are unchanged, so nothing moves unless you set it.
2442
+ An unrecognised value warns and falls back rather than stopping the server booting.
2443
+
2444
+ ---
2445
+
2446
+ ## [0.52.0] - 2026-08-31
2447
+
2448
+ ### Fixed
2449
+ - **Preview renders no longer record analytics page views.** The preview iframe reports its own
2450
+ location to the page-view beacon, and inside a `srcdoc` document that is the literal string
2451
+ `srcdoc` - so opening a preview invented a page called `srcdoc` and inflated the counts for it.
2452
+ Present since the Live Preview tab shipped in 0.51.0. Preview renders now set
2453
+ `window.__CMS_PREVIEW__` before any other injection and the bundled tracker returns immediately
2454
+ when it sees it. This covers all three preview paths - editor preview, draft preview and share
2455
+ links. Real page views are unaffected. **Plugin authors:** any client-side beacon or outbound
2456
+ tracker must check this flag; a preview is not a visit, and it renders at a URL that is not a
2457
+ real page.
2458
+ - **The editor's split-view preview now shows the page's real colours.** It rendered the markdown
2459
+ body as a fragment inside the admin document, where the site's background could never apply -
2460
+ the page background comes from `body { background-color: var(--dm-background) }`, and a fragment
2461
+ has no body to attach it to - while `site.css` cannot be loaded into the admin without restyling
2462
+ the admin itself. The pane is now an iframe carrying the real rendered page, so background,
2463
+ theme, layout preset colours and page-level `theme:` are exactly what the public site shows.
2464
+ - **Live components now work in the split-view preview.** `[countdown]`, `[carousel]`, tooltips,
2465
+ `[timeline]`, forms and effects never initialised there: they are wired by `site.js` against
2466
+ `.page-body`, which the old fragment had no equivalent of, and `site.js` never ran in the admin
2467
+ at all. Only icons and tabs were ever initialised. All of them now behave as they do on the
2468
+ live page.
2469
+
2470
+ ### Added
2471
+ - **The editor panes can be resized.** Drag the divider between the writing and preview panes to
2472
+ give either one more room. The ratio persists with the other editor preferences. The handle is
2473
+ keyboard-operable (arrow keys, Shift for larger steps, Home/End for the extremes), double-click
2474
+ restores an even split, and the split is clamped so neither pane can be squeezed away. Below the
2475
+ mobile breakpoint the panes still stack, where a horizontal ratio has no meaning.
2476
+
2477
+ ---
2478
+
2479
+ ## [0.51.0] - 2026-08-31
2480
+
2481
+ ### Added
2482
+ - **The editor's Live Preview shows the whole page, from unsaved changes.** It renders the current
2483
+ editor state through the server's real page pipeline - the same parse and render the public site
2484
+ runs on save - so navbar, footer, theme, menus, `dconfig` and custom CSS are exactly what
2485
+ publishing would produce. It previously pointed at the saved URL, so it showed the last saved
2486
+ version rather than the work in progress. Re-renders shortly after typing stops, and only while
2487
+ the tab is on screen. Width buttons check tablet (1024px) and mobile (390px) layout, and scroll
2488
+ position survives a re-render. The markdown split-view pane is unchanged and still renders the
2489
+ body alone. Backed by `POST /api/pages/preview/full`.
2490
+ - **Revocable share links for unpublished pages.** *Share link* in the Live Preview tab mints a URL
2491
+ that renders one unpublished page to someone with no account - a client, an external designer, a
2492
+ copywriter. The page is named inside the signed token, so a holder cannot repoint the link.
2493
+ Expires after 1 hour, 24 hours, 7 days or 30 days, and can be revoked at any moment: every token
2494
+ carries an id that must still be present and unrevoked in `content/preview-links.json`, because a
2495
+ bare signed token cannot be withdrawn once it is out. Links are dropped automatically when the
2496
+ page is deleted or renamed. Recipients render as anonymous, so menus and role-gated shortcodes
2497
+ show them the least rather than the most. Responses carry `no-store`, `noindex, nofollow` and
2498
+ `Referrer-Policy: no-referrer`, since the credential is in the URL and must not leak through
2499
+ `Referer`. Minting requires `pages.update`, not merely `pages.read` - handing an unauthenticated
2500
+ stranger a working URL is an act of publishing, however narrow.
2501
+
2502
+ ---
2503
+
2504
+ ## [0.50.0] - 2026-08-31
2505
+
2506
+ ### Added
2507
+ - **Draft pages can be previewed on the public site.** A page with `status: draft` still returns
2508
+ 404 to the public, but a signed-in user whose role holds `pages.read` is now served the real
2509
+ page - real layout, navbar, footer, theme, menus and custom CSS - at its real URL. Designers and
2510
+ developers can verify a page before it is released instead of guessing from the editor's
2511
+ body-only preview. A draft banner pinned to the bottom of the viewport names the viewer, links
2512
+ to the page in the admin editor, and offers a one-click Publish. Nothing leaks: an anonymous
2513
+ visitor, or a signed-in user without the permission, gets the same 404 as a page that does not
2514
+ exist. Draft renders bypass the response cache entirely (`Cache-Control: no-store, private`) so
2515
+ a preview can never be replayed to the public, carry `X-Robots-Tag: noindex, nofollow`, stay out
2516
+ of `sitemap.xml`, and emit no `content:pageViewed` hook, so previewing does not pollute
2517
+ analytics. A disabled project still 404s for everyone, previewers included.
2518
+
2519
+ ### Fixed
2520
+ - **Role-gated pages were unreachable from a browser.** `visibility:` was only ever enforceable
2521
+ against a client that could set an `Authorization` header, because the token lived in `S` storage
2522
+ and no cookie was ever issued - so a signed-in person clicking a link to a page gated to their own
2523
+ role got a 403. Login now also sets a `dm_session` cookie (HttpOnly, SameSite=Lax, Secure over
2524
+ https, expiring with the access token), and the public renderer accepts either channel. The
2525
+ cookie is read **only** when rendering public pages; every API route stays Bearer-only, so it
2526
+ carries no write authority and the CSRF surface is unchanged. Logout clears it.
2527
+
2528
+ ---
2529
+
2530
+ ## [0.49.2] - 2026-08-29
2531
+
2532
+ ### Fixed
2533
+ - **The floating panel's hamburger button was empty.** It asked the icon set for `bars`, which is
2534
+ not an icon - that name belongs to Domma's chart renderer. The scanner marked the span processed
2535
+ and drew nothing, so every collapsible panel since 0.49.0 had an invisible control: the button
2536
+ worked, but there was nothing to see or aim at. It is now drawn in CSS from one element and its
2537
+ two pseudo-elements, the same shape Domma uses for `.navbar-toggle-icon`, so it depends on no
2538
+ icon set at all. Both render paths lose their `data-icon` reference.
2539
+
2540
+ ---
2541
+
2542
+ ## [0.49.1] - 2026-08-29
2543
+
2544
+ ### Fixed
2545
+ - **A floating navbar's Collapsible and Movable did nothing.** The panel chrome - drag grip, title
2546
+ and hamburger - was only ever drawn server-side by `buildFloatChrome()`, which runs for the
2547
+ `[menu]` shortcode and overlay panels. The public navbar is built in the browser by Domma from
2548
+ `window.__CMS_NAV__` and never passes through that code, so it arrived with the behaviour
2549
+ attributes and no controls at all: `wireCollapse`/`wireDrag` found no toggle and no handle and
2550
+ returned silently. `public/js/menu-float.js` now builds the bar itself when a floating panel
2551
+ declares the behaviour but has no chrome, so the navbar, the shortcode and an overlay all behave
2552
+ the same.
2553
+ - **"Start collapsed" was ignored on a floating navbar** for the same reason - the initial
2554
+ `is-collapsed` class is stamped by the server renderer. `applyFloat()` now carries the author's
2555
+ default across as `data-float-collapsed`, and the module normalises it to the class on init.
2556
+ - **Collapsing hid the wrong thing on a navbar.** The rule targeted the panel's direct `<ul>`,
2557
+ which a server-rendered menu has and the navbar does not - its body is `.navbar-container`. It
2558
+ now hides everything except the bar, and restates that at id specificity, since a vertical rail
2559
+ sets `#site-navbar.dm-nav-vertical .navbar-container { display: flex }` and outranked the
2560
+ class-only rule. A collapsed navbar was left full size behind the hamburger.
2561
+ - **The "transparent" menu variant was not transparent.** The Menus editor has offered it since
2562
+ the variant dropdown existed, but nothing ever implemented it - Domma ships `.navbar-dark` and
2563
+ `.navbar-light` only, so `navbar-transparent` matched no rule. It now clears the background,
2564
+ border and shadow, with selectors specific enough to beat the `var(--dm-surface)` that
2565
+ `.dm-menu-floating` paints behind a floating panel. The collapsed hamburger keeps a surface of
2566
+ its own, so a transparent panel does not mean an invisible button.
2567
+
2568
+ ---
2569
+
2570
+ ## [0.49.0] - 2026-08-29
2571
+
2572
+ ### Added
2573
+ - **Floating menu panels can now be collapsed, moved and resized.** A menu with
2574
+ `position: floating` was a fixed decal on the page - wherever the author pinned it was where it
2575
+ stayed. Its `float` block takes four new flags: `collapsible` (a hamburger folds the panel down
2576
+ to a single round button), `collapsed` (start folded), `movable` (drag the panel by its bar) and
2577
+ `resizable` (drag its bottom-right corner), plus `title` for the bar's label. Any of the first
2578
+ two or `movable` makes the server draw a chrome bar - drag grip, title, hamburger - and
2579
+ `public/js/menu-float.js` wires the behaviour. All four are checkboxes on the Menus editor's
2580
+ Behaviour tab.
2581
+ - **The config is a starting point, not a state.** Where a visitor drags a panel, what size they
2582
+ leave it and whether they folded it are stored per visitor under `dm_menu_float_<slug>` and win
2583
+ over `float` on the next load; double-clicking the bar throws that away and restores the
2584
+ authored pinning. Nothing about one visitor's layout reaches the server or another visitor.
2585
+ - A panel keeps tracking its `anchor` until it is actually moved or resized - only then is its
2586
+ right/bottom/transform pinning converted to plain left/top, so an untouched panel still
2587
+ re-pins itself as the viewport changes. Once freed it is clamped to stay reachable on screen.
2588
+ - **Menus can now contain spacers.** `{"type": "spacer"}` joins `{"type": "separator"}` as a
2589
+ structural item: blank room with no line, no text and no URL. `size` is `xs|sm|md|lg|xl`
2590
+ (mapped to the theme's `--dm-space-*` tokens, default `md`) or `flex`, which claims all the
2591
+ leftover room and so pushes everything after it to the far end - the right of a navbar, the foot
2592
+ of a vertical rail or the admin sidebar. An **Add spacer** button and a per-row size picker sit
2593
+ beside the existing separator controls in the Menus editor.
2594
+ - Spacers work in every render path: the `[menu]` shortcode, floating and overlay panels, the
2595
+ public navbar, the footer and the admin sidebar. The axis follows the menu - height in a
2596
+ vertical list, width in a horizontal one - and they are `aria-hidden`, so they never read as
2597
+ empty items.
2598
+
2599
+ ### Changed
2600
+ - Below 768px floating panels already return to the document flow, so moving and resizing are
2601
+ inert there; collapsing still works, which is the hamburger behaviour a phone wants anyway.
2602
+ - The markdown sanitiser now allows `aria-hidden` on any element. It was stripping the attribute
2603
+ from spacers and the floating panel's drag grip, leaving decorative markup announced as empty
2604
+ items. Presentational only - `on*` handlers and `javascript:` URLs are still stripped globally.
2605
+
2606
+ ---
2607
+
2608
+ ## [0.48.0] - 2026-08-29
2609
+
2610
+ ### Added
2611
+ - **Site Settings > Footer can now show or hide the "Reduce motion" switch.** The footer's
2612
+ accessibility switch was unconditional - every site got it whether or not it suited the design.
2613
+ A new checkbox on the Footer tab writes `footer.showMotionToggle` to `site.json`; the renderer
2614
+ already spreads the whole `footer` block onto `window.__CMS_SITE__`, so `public/js/site.js` gates
2615
+ the control on it.
2616
+ - The key **defaults to shown**. `site.js` gates on `!== false`, so an absent key behaves exactly
2617
+ as before and no existing site loses its switch on update.
2618
+ - Hiding the switch removes the control, not the capability: a visitor who already flipped it keeps
2619
+ their stored `domma:reduced_motion` preference, and anyone whose browser sends
2620
+ `prefers-reduced-motion: reduce` still gets the calmer site - they just cannot override it from
2621
+ the footer. Documented in `docs/configuration.md` under a new Footer section.
2622
+
2623
+ ---
2624
+
2625
+ ## [0.47.2] - 2026-08-29
2626
+
2627
+ ### Fixed
2628
+ - **Slideovers now scroll instead of clipping.** Domma sets `overflow: hidden` inline on the
2629
+ slideover panel, so whichever wrapper holds the content is the only place overflowing content
2630
+ can scroll. Content projected as a *sibling* of `.dm-slideover-body` - the light-DOM mount the
2631
+ Insert dialogs use so their inputs and buttons stay interactive - had padding but no overflow
2632
+ handling, so anything taller than the viewport was simply cut off with no way to reach it. The
2633
+ embed builder, the effects builder and the menu item editor were all affected. That wrapper is
2634
+ now a scroll container in its own right.
2635
+ - The empty `.dm-slideover-body` those dialogs leave behind carried `flex: 1` and so claimed half
2636
+ the panel height, squashing the real content into the other half. An empty body now takes no
2637
+ space at all.
2638
+ - Both scrolling wrappers get a thin, themed scrollbar (`--dm-border` thumb) rather than the
2639
+ browser default, matching the admin sidebar, and `overscroll-behavior: contain` so scrolling
2640
+ past the end of a slideover no longer scrolls the page behind it.
2641
+ - The public site's `[slideover]` body was hardened the same way: explicit `min-height: 0`,
2642
+ overflow on both axes so a wide table or code block scrolls rather than being clipped, and the
2643
+ same scrollbar treatment.
2644
+
2645
+ ---
2646
+
2647
+ ## [0.47.1] - 2026-08-28
2648
+
2649
+ ### Fixed
2650
+ - **Shortcodes nested inside a container lost every attribute.** `[text size="lg" color="red"]`
2651
+ inside a `[tab]`, or inside a `[card]` inside a `[hero]`, rendered unstyled; `[button href="/x"]`
2652
+ in the same position came out with `href="true"`. Containers hand their nested body to marked,
2653
+ and marked HTML-escapes the quotes in any `[tag attr="x"]` still sitting in the text, so the
2654
+ tag's own processor received `attr=&quot;x&quot;` and every value missed its lookup.
2655
+ - The top-level pipeline has always avoided this by running the inline tags (badge, text, button,
2656
+ link, cta) before the container tags. Nested bodies did not: `processHeroBlocks` ran
2657
+ `processCardBlocks` first and the other containers never ran the inline pass at all. They now
2658
+ share one `processInlineShortcodes()` helper that mirrors the top-level order, applied at every
2659
+ site that renders a nested body.
2660
+ - Seven contexts were affected and are now covered by tests: hero > card, tabs > tab,
2661
+ tabs > tab > card, accordion > item, carousel > slide, timeline > event and listgroup > item.
2662
+ Directly nested cases (a tag straight inside `[card]`, `[hero]`, `[banner]`, `[center]`,
2663
+ `[slideover]` or `[grid]`) were already correct and stay that way.
2664
+
2665
+ ---
2666
+
2667
+ ## [0.47.0] - 2026-08-28
2668
+
2669
+ ### Changed
2670
+ - **One shortcode dialog for every entry point.** Inserting a link used to produce three
2671
+ different things depending on where you started: the toolbar's Insert menu emitted
2672
+ `[link href]text[/link]`, the right-click menu's `Insert…` dropped the registry template
2673
+ `[link href label /]`, Ctrl+K wrote a Markdown link or a `[button]`, and editing an
2674
+ existing tag happened in a 210px popup. The two Insert menus were separately maintained
2675
+ lists as well - a hardcoded array of 25 items in `markdown-toolbar.js` against a
2676
+ registry-generated list in the context menu - so they had already drifted apart by six
2677
+ shortcodes.
2678
+ - Both menus are now generated from the shortcode registry through `shortcode-insert.js`,
2679
+ and every insert opens the same generated slideover (`shortcode-form.js`): fields built
2680
+ from the registry's attribute list, repeatable rows for container children (tabs, grid
2681
+ columns, accordion items, carousel slides, list-group items), and a live preview of the
2682
+ exact snippet before it is inserted. `Edit <tag>…` on the right-click menu opens the same
2683
+ form for an existing tag; the inline quick-toggles stay where they were for one-click
2684
+ changes.
2685
+ - The 22 hand-written insert dialogs in `page-editor.js` are gone (about 1,500 lines),
2686
+ along with the toolbar's parallel `on*(cb)` registration surface. Four dialogs that do
2687
+ more than fill in attributes remain and are registered with the insert router instead:
2688
+ the media picker, the card builder, the timeline builder and the scribe composer, plus
2689
+ the lazily loaded component gallery, embed builder and colour picker.
2690
+ - Consequences worth knowing: the Insert menus now also list Row, Slideover, Centre,
2691
+ Countdown, Table and Menu, which only the right-click menu offered before; every form
2692
+ exposes the full attribute set from the registry rather than the subset its old dialog
2693
+ happened to implement; block-level snippets are given their own line when inserted
2694
+ mid-paragraph; and editing a tag writes back only the attributes actually changed, so
2695
+ opening a form and pressing Apply leaves the tag byte-for-byte as it was.
2696
+ - The Link form offers both spellings - a Markdown link (still the default, as Ctrl+K
2697
+ always produced) and the `[link]` shortcode - and falls back to the shortcode as soon as
2698
+ an attribute Markdown cannot carry is set. The preview shows which one will be inserted.
2699
+ Inserting a *button* is now the Button entry rather than a checkbox inside the link
2700
+ dialog.
2701
+ - The Style Text dialog's live styled preview survives the move: a registry entry can name
2702
+ a rendered preview (`preview: 'text'`), and the form shows the styled sample above the
2703
+ snippet. Its `point-size` and `style` attributes were added to the registry too, so
2704
+ nothing that dialog could write is lost.
2705
+ - The `[text]` attribute → CSS mapping now lives in `public/js/text-style.mjs` and is
2706
+ imported by both `server/services/markdown.js` and the editor preview, the same way
2707
+ `collection-export.mjs` is shared. The preview is no longer an approximation of the
2708
+ rendered output - it is computed by the code that renders it, and a test asserts the two
2709
+ agree. Two consequences of the shared mapping: `bold="false"` / `italic="false"` now mean
2710
+ off rather than on (the word-match they used to hit read them as on), and a double quote
2711
+ in a free-form value can no longer break out of the generated `style` attribute.
2712
+ - Inserting `[text]` starts unstyled, as its dialog always did - the registry template's
2713
+ style attributes are a syntax example, not a default, so they no longer prefill the form.
2714
+ - The toolbar-anchored icon and spacer pickers are replaced by the form's icon and number
2715
+ fields, with the layout's configured spacer height still supplying the default.
2716
+
2717
+ ---
2718
+
2719
+ ## [0.46.4] - 2026-08-22
2720
+
2721
+ ### Fixed
2722
+ - **Hover-to-open nav dropdowns closed on the way to themselves.** With a menu's
2723
+ `appearOnHover` on, `site.js` reimplemented the behaviour with `mouseenter`/`mouseleave`
2724
+ toggling Domma's `.open` class directly. `.navbar-dropdown-menu` hangs 4px below its
2725
+ toggle, and that 4px belongs to neither box, so `mouseleave` fired the instant the
2726
+ pointer entered it and closed the menu with no grace period. Whether it survived came
2727
+ down to whether the pointer happened to jump the gap between two events - open on a
2728
+ quick flick, gone on a normal one. It also fought the navbar's own click toggle, so
2729
+ clicking a menu to keep it open was undone by the next mousemove.
2730
+ - Domma's navbar has had its own `appearOnHover` since 0.38.0 - delegated so it survives
2731
+ a re-render, reconciling the whole open chain so flyouts keep their parents open, and
2732
+ holding a grace period on leave. The option is now passed through and the local
2733
+ reimplementation deleted. Reproduced and verified in headless Chrome against a running
2734
+ site: slow crossing of the gap fails on the old code and passes on the new.
2735
+
2736
+ ### Changed
2737
+ - **Pinned `domma-js` to 0.42.0**, and moved the vendored Domma Tools pair with it - both
2738
+ banners read v0.42.0. That release carries the matching fixes: `hoverCloseDelay` on the
2739
+ navbar (250ms), a click that *pins* a dropdown open rather than being reconciled away,
2740
+ a toggle that closes its siblings, and `Navbar.collapseAt` moved from 768 to **993** to
2741
+ match the breakpoint `elements.css` switches the bar to its desktop layout at. Sites
2742
+ that restyled the collapse breakpoint should now pass `collapseAt` explicitly.
2743
+
2744
+ ---
2745
+
2746
+ ## [0.46.3] - 2026-08-22
2747
+
2748
+ ### Changed
2749
+ - **Pinned `domma-js` to 0.41.0**, and moved the vendored Domma Tools pair with it.
2750
+ npm does not ship `domma-tools.*`, so without that copy `/dist/domma/domma.min.js`
2751
+ would have gone to 0.41.0 while `admin/dist/domma/domma-tools.min.js` sat on 0.40.0,
2752
+ and the tools bundle is built against a specific core. Both banners read v0.41.0.
2753
+
2754
+ ### Fixed
2755
+ - **The navbar brand was invisible on every dark theme.** `.navbar-dark` painted the
2756
+ bar from `--dm-background` and the brand from `--dm-text-inverse`, which resolve to
2757
+ the same slate in every dark theme - 1.00 contrast. `site.css` compounded it by
2758
+ re-applying the same token at ID specificity. Fixed at source in domma-js 0.41.0
2759
+ along with three more rules of the same shape (`.card-primary .card-body` at 1.19,
2760
+ `.navbar-link.active` at 1.72, and the `.navbar-dark .navbar-dropdown-*` overrides
2761
+ at 1.00), so the interim overrides carried here have been **removed** rather than
2762
+ left to drift from the fix they duplicated.
2763
+ - **`.navbar-actions` still needs its own rule** - the site-search icon is a CMS
2764
+ addition, not a Domma element, so the upstream token fix does not reach it. It had
2765
+ the same defect for the same reason.
2766
+ - **The seven generic hero gradients ignored the theme.** They were fixed pastels with
2767
+ fixed dark text, so `gradient-forest` painted a mint band across the top of a navy
2768
+ site - the loudest reason the seed pages read as unthemed. The hue now mixes into
2769
+ `--dm-background` with `--dm-text` over it, so the band follows the theme in both
2770
+ modes. Worst case across 15 themes and 7 hues: 9.65. `night` and the 26
2771
+ theme-specific classes are unchanged.
2772
+ - **`[card variant="primary"]` header** keeps the primary identity as an 18% tint plus
2773
+ the accent border rather than a solid fill, because no single token reads reliably on
2774
+ `--dm-primary` across the theme set.
2775
+
2776
+ All eight seed pages now scan clean at the 4.5 AA threshold with computed styles in a
2777
+ real browser. One pre-existing miss is left alone: inline `<code>` on /resources at
2778
+ 3.94, which is a theme-palette issue rather than a token misuse.
2779
+
2780
+ ---
2781
+
2782
+ ## [0.46.2] - 2026-08-22
2783
+
2784
+ ### Fixed
2785
+ - **Vertical navbar layout and rail rendering.** Fixed a bug where `Domma.elements.navbar()`
2786
+ overwrote `#site-navbar`'s class list, stripping `.dm-nav-vertical` on initialisation
2787
+ and causing vertical menus to render as standard horizontal navbars.
2788
+ - **Vertical navbar container styling.** Added column flex styling to `.navbar-container`
2789
+ inside `#site-navbar.dm-nav-vertical` so that brand identity and navigation items
2790
+ stack vertically inside the rail instead of laying out in a compressed horizontal row.
2791
+ - **Server-rendered navbar classes.** Stamped the initial vertical navbar class directly
2792
+ onto `<nav id="site-navbar">` in `page.html` and `renderer.js` to match the body gutter
2793
+ on first paint.
2794
+ - **Floating navbar configuration and body gutter handling.** Fixed an issue where `navMenu.float`
2795
+ was omitted from `__CMS_NAV__` in `renderer.js`, and prevented vertical floating navbars
2796
+ from erroneously applying a full-height body gutter (`dm-vnav`).
2797
+ - **Asset cache busting.** Bumped cache-busting version query parameters on `site.css` and
2798
+ `site.js` in `page.html` to ensure updated stylesheets and scripts load immediately.
2799
+
2800
+ ---
2801
+
2802
+ ## [0.46.1] - 2026-08-21
2803
+
2804
+ ### Changed
2805
+ - **The Domma pin moves to 0.40.0.** Two ancestor binding-context names arrive
2806
+ with it - `$parents` and `$parentContext` - along with a warning for a nested
2807
+ `data-each` that previously failed quietly, and a binding write to a frozen
2808
+ target that now warns instead of throwing. Nothing in the CMS uses them yet;
2809
+ the pin moves so the admin is not served a core two minor versions behind the
2810
+ one it is documented against. The vendored Domma Tools pair
2811
+ (`admin/dist/domma/domma-tools.min.js` and its CSS) moves with it, because npm
2812
+ does not ship `domma-tools.*` and the tools bundle is built against a specific
2813
+ core - leaving it on 0.39.2 while the core went to 0.40.0 is exactly the
2814
+ mismatch that breaks the admin.
2815
+
2816
+ ---
2817
+
2818
+ ## [0.46.0] - 2026-08-21
2819
+
2820
+ ### Fixed
2821
+ - **`[form]` on a page renders through `public/js/site.js`, and that renderer
2822
+ ignored the grid on every step.** The shortcode does not emit `data-form` -
2823
+ it emits `data-form-inline` with the form definition encoded into the
2824
+ attribute, hydrated by a separate implementation in `site.js` that called
2825
+ `F.wizard(el, {schema, onSubmit})` with no `layout` and no `columns`. It
2826
+ carried its own copy of step splitting, its own `columns || 1` and
2827
+ `layout || 'stacked'` defaults, decorated only the first step, and injected
2828
+ the spam honeypot into a step the visitor never submits from. It now uses
2829
+ the shared splitter and matches the other two renderers.
2830
+
2831
+ - **Multi-page forms ignored the form's grid setting.** `Domma.forms.wizard()`
2832
+ forwards its leftover options to the form renderer for each step, and the
2833
+ renderer only lays out a grid when it is told `layout: 'grid'`. Neither the
2834
+ public renderer nor the builder's preview passed `layout` or `columns`, so
2835
+ every page of every wizard rendered stacked whatever the author chose. The
2836
+ single-page path passed both, which is why only wizards were affected.
2837
+
2838
+ - **Everything applied after the first render was lost on page two.** The
2839
+ wizard rebuilds its container with `innerHTML` on each navigation, and the
2840
+ CMS decorated the rendered form exactly once. From page two onwards: date
2841
+ fields fell back to plain text inputs, icon and image fields lost their
2842
+ Browse buttons and previews, spacers vanished, and conditional logic was
2843
+ bound to elements that no longer existed. Each page is now decorated as it
2844
+ is rendered.
2845
+
2846
+ - **Conditional logic could not see answers from an earlier page.** The logic
2847
+ runtime reads values out of the DOM, and a wizard only has one page in the
2848
+ DOM at a time, so a rule on page two testing an answer from page one
2849
+ evaluated against nothing and silently decided the wrong way.
2850
+ `FormLogicRuntime` takes a `baseValues` option; the wizard seeds it with the
2851
+ answers collected so far.
2852
+
2853
+ - **Files chosen before the last page were dropped.** The submitter collected
2854
+ `<input type="file">` elements from the DOM, which by then held only the
2855
+ final page. A CV attached on page one never left the browser. File choices
2856
+ are now retained as the visitor advances.
2857
+
2858
+ - **Spam protection did nothing on multi-page forms.** The honeypot pair was
2859
+ injected into page one's form and destroyed on the first navigation, so the
2860
+ submission arrived with both values empty - which the server reads as "not a
2861
+ bot". Both are now present on the page that submits, carrying the *original*
2862
+ timestamp rather than a fresh one (re-stamping per page would measure only
2863
+ the last page's dwell time, and the server discards anything submitted
2864
+ within 2s of the stamp).
2865
+
2866
+ - **A form with its own field named `website` discarded every submission.**
2867
+ The honeypot decoy is an input named `website`, and the submitter read it
2868
+ back by name - picking up the visitor's real answer, sending it as `_hp`,
2869
+ and having the server treat the submission as a bot: success message shown,
2870
+ nothing stored. Silent data loss on single-page forms too, not only wizards.
2871
+ The decoy now steps aside to `_website` when the name is taken, and is read
2872
+ from inside its own wrapper rather than by name.
2873
+
2874
+ - **A page break at the end produced an empty final page** holding nothing but
2875
+ the button, and one at the start produced an empty first page. The builder's
2876
+ preview dropped the trailing one but the live form kept it, so the author
2877
+ and the visitor saw different forms.
2878
+
2879
+ - **A full-width submit button only worked in the preview.** The builder
2880
+ applied `col-span-full` to its own preview and the public renderer never
2881
+ did, so the setting looked right to the author and did nothing for the
2882
+ visitor.
2883
+
2884
+ ### Changed
2885
+ - **Grid is now the default layout for forms.** Forms were created as
2886
+ `stacked`, so a wizard that correctly honoured its layout setting still had
2887
+ no grid to honour. Grid is applied wherever a layout is not explicitly set:
2888
+ new forms from both creation paths, the builder's Layout control, and the
2889
+ renderer reading a form saved before the change. An explicit Stacked or
2890
+ Inline choice is still respected.
2891
+ - **The Columns control only offers what the renderer can produce.** Domma
2892
+ emits `grid-cols-2` or `grid-cols-3` and nothing else, but the control
2893
+ accepted 1 to 6 - four of the six choices quietly rendered as two columns.
2894
+ It is now a 2/3 select, and stored values outside that range are clamped.
2895
+ - Wizard step splitting moved to `public/js/form-steps.mjs`, imported by all
2896
+ three renderers - the `[form]` embed hydrator, the `data-form` renderer and
2897
+ the builder preview. Each carried its own copy, and they had drifted apart.
2898
+ - The wizard's final button honours the form's Submit Text setting.
2899
+ - The five stored forms still marked `stacked` from the old default
2900
+ (`categories`, `comments`, `notes`, `posts`, `to-do`) were switched to grid.
2901
+ - `site.js` imported `forms.js` under a different cache-busting query string
2902
+ from the one in `page.html`, so a page fetched the module twice and ran its
2903
+ top-level scan twice. Both now use one URL.
2904
+
2905
+ ### Added
2906
+ - `docs/guides/multi-page-forms.md` - building wizards with page breaks, and
2907
+ what carries across pages.
2908
+ ## [0.45.1] - 2026-08-21
2909
+
2910
+ ### Fixed
2911
+ - **Carousels with text slides did not appear at all.** The `[carousel]`
2912
+ shortcode's own default snippet - three `[slide]`s with titles and body text
2913
+ and no images - rendered as a gap between the paragraphs either side of it.
2914
+ The cause is in domma-js: `.carousel-slide-content` was unconditionally
2915
+ `position: absolute`, being the caption overlay for a slide's background
2916
+ image. On a slide with no `<img>` that caption was the only child, so
2917
+ nothing was left in flow, the slide computed to 0px tall, and the track and
2918
+ carousel collapsed with it. Arrows, indicators and slide transitions were
2919
+ all present and working the whole time, on an element with no height - which
2920
+ is why the markup and the JS both looked correct in isolation.
2921
+
2922
+ Fixed in **domma-js 0.39.2**: slide content sits in flow by default and
2923
+ inherits its colour, with the overlay stated as the enhancement for
2924
+ `.carousel-slide:has(> img)`, so a browser without `:has()` puts an image
2925
+ caption under the picture rather than hiding the slide. In-flow content is
2926
+ also padded clear of the arrows and indicator dots, which are positioned
2927
+ over the slide and otherwise sat on top of the words.
2928
+
2929
+ ---
2930
+
2931
+ ## [0.45.0] - 2026-08-21
2932
+
2933
+ ### Fixed
2934
+ - **The page editor could not save.** 0.43.1's un-minifying refactor renamed
2935
+ the router global `R` to `Router` in `page-editor.js` (8 references).
2936
+ `Router` is not a global, so the editor threw `ReferenceError` partway
2937
+ through mounting, and every listener wired after that point - Save, Cancel,
2938
+ the unsaved-changes navigation guard - was never attached. The button did
2939
+ nothing at all. The rename checker (`tools/check-rename.mjs`) could not see
2940
+ it: the mapping IS one-to-one, it just points at a name that does not exist.
2941
+ - **An admin smoke test now runs the editor in a real browser.** Nothing in
2942
+ the suite loaded the admin - node:test has no DOM - so any browser-only
2943
+ error shipped silently, which is how the above got out. The new test boots
2944
+ the server, drives the page editor in headless Chrome, and asserts a save
2945
+ round-trips through the API, failing on any uncaught browser error. Verified
2946
+ against the bug: reintroducing `Router` makes it fail. It skips when no
2947
+ headless Chrome is present (set `DOMMA_CHROME` to point at one).
2948
+ - **A page created inside a project vanished, but kept its URL.** `createPage`
2949
+ built its frontmatter from a fixed whitelist, so `project`, `visibility`,
2950
+ `tags`, `category` and `theme` were all discarded on the FIRST save of a
2951
+ page. A page created from `#/projects/<slug>/pages` therefore landed in
2952
+ core: missing from that project's list and totals, while still owning the
2953
+ path - so re-creating it answered "already exists" for a page the admin
2954
+ could not see anywhere. Create now writes every key through, the same
2955
+ contract `updatePage` has always had.
2956
+ - **Project context survives "New Page".** The per-project list linked to the
2957
+ shared `#/pages/new` route, which dropped the slug. The link now carries
2958
+ `?project=<slug>` and the editor pre-selects it (`carryProjectContext`).
2959
+ The Project field's empty option is "Inherit from URL" and now genuinely
2960
+ inherits: it omits the key so the rootUrl resolver runs, instead of writing
2961
+ `project: null`, which pinned the page to core forever.
2962
+ - **A trailing slash on a project's `rootUrl` no longer switches inheritance
2963
+ off.** The prefix test built `rootUrl + '/'`, so `/shop/` produced `/shop//`
2964
+ and matched nothing: one stray keystroke in the project editor and every
2965
+ page under that project quietly fell through to core. Note that `/` itself
2966
+ stays reserved for core, so a whole-site project inherits nothing by URL and
2967
+ its pages carry an explicit `project:` tag - which is now automatic for
2968
+ pages created from inside a project.
2969
+ - **A create conflict names the project.** `POST /api/pages` returns
2970
+ "A page already exists at /x (project: core)" rather than a bare
2971
+ "already exists".
2972
+ - **Wizard forms lost their conditional logic on every step change.**
2973
+ `F.wizard` replaces the container's innerHTML per step, so the logic
2974
+ runtime's listeners pointed at dead nodes and its hidden classes were gone:
2975
+ stepping to page 2 and back brought every conditionally hidden field back
2976
+ into view. The runtime, date patching, media/icon pickers and the honeypot
2977
+ are all re-applied per step now, in both render paths (`site.js` inline
2978
+ embeds and `forms.js` `[data-form]` targets).
2979
+ - **Next now says what is missing.** An empty required field made the wizard's
2980
+ Next button silently do nothing. A capture-phase guard runs a
2981
+ conditional-aware check (`FormLogicEngine.validateFields`) and, on failure,
2982
+ flags the offending fields, scrolls to the first, and names them in a
2983
+ dialog. Fields whose requirement or visibility is conditional are no longer
2984
+ statically `required` in the blueprint - a field the logic had hidden used
2985
+ to block the form for good.
2986
+ - **Forms stopped collapsing on narrow screens.** Domma's `grid-cols-N` are
2987
+ fixed counts with no built-in collapse, so a 2 or 3 column form kept all its
2988
+ columns down to 320px. Below 700px a CMS form is one column and `col-span-N`
2989
+ collapses with it.
2990
+ - **Radio buttons no longer render as choosers.** Switching a chooser field to
2991
+ Radio buttons / Dropdown / Checkbox group left the chooser's visual config
2992
+ (`variant`, `density`, `columns`, `glow`) on the field, and both renderers
2993
+ promote "a select/radio with a variant" back to a chooser - so every option
2994
+ type looked identical. The builder drops chooser-only keys the moment the
2995
+ type stops being a chooser.
2996
+
2997
+ ### Changed
2998
+ - **A project-filtered list now admits what it is hiding.** Entering a list
2999
+ from a project (`#/projects/<slug>/pages`) drops every artefact belonging to
3000
+ another project and said nothing about it: on a site with 14 pages, 10 of
3001
+ which are tagged, the table footer read "10 of 10" while four pages sat one
3002
+ route away. Anything resolving to core was therefore indistinguishable from
3003
+ something that had never been created - the exact reading a freshly created
3004
+ page invites, since it lands in core and still owns its slug. Filtered lists
3005
+ now carry a line above the table naming the count held back, with a link to
3006
+ the unfiltered view. Applied to pages, collections, forms, actions, views,
3007
+ blocks, components, users and API endpoints.
3008
+ - **Admin lists show 25 rows before paginating, not 10.** Every admin table
3009
+ took Domma's default `pageSize: 10`, so a site with 14 pages showed 10 of
3010
+ them and put the rest behind a pagination control. A newly created page
3011
+ sorts to the end (default `sortOrder: 99`), which means it landed on page 2
3012
+ the moment a site passed ten pages - indistinguishable from the page not
3013
+ existing. Applied to all 16 admin list tables; the page-size control is
3014
+ still there for larger sets.
3015
+ - **The admin API is never cached.** Everything under `/api` is served
3016
+ `Cache-Control: no-store`, except the two externally consumable surfaces
3017
+ (`/api/v1/*`, `/api/x/*`) which have their own caching contracts. A stale
3018
+ list is indistinguishable from a missing artefact.
3019
+ - **New forms start as a 2-column grid** instead of a single stacked column,
3020
+ and the column/span controls are visible on mount rather than after the
3021
+ layout select is touched.
3022
+ - **Themes are listed A-Z** in every admin picker, the login theme swatches
3023
+ and the theme-switcher plugin's dots. Admin-chrome palettes sort last as a
3024
+ block.
3025
+ - **Cancel sits clear of Save.** Admin view headers, every admin modal footer
3026
+ and public form button rows moved from a 0.5rem gap to 1rem, with an extra
3027
+ half-rem before the primary action.
3028
+ - **The Form Builder's conditional logic sections are laid out as blocks** -
3029
+ Visibility, Requirement, Validation and Cascade each get a bordered panel,
3030
+ and each "when ... then ..." rule sits on its own tinted strip instead of
3031
+ running together as one stack of selects.
3032
+ - **Em dashes are gone.** 5,800 of them across source, docs, admin copy and
3033
+ demo content, replaced with plain hyphens.
3034
+
3035
+ ---
3036
+
3037
+ ## [0.44.0] - 2026-08-20
3038
+
3039
+ No functional change over 0.43.1. This release exists to resolve a version
3040
+ number that means two different things.
3041
+
3042
+ `0.43.1` was published to npm from a second environment while the de-mangling
3043
+ work was still local, so the registry's `0.43.1` is the 0.43.0 feature set with
3044
+ the admin views still minified - different code under the same number as the
3045
+ `v0.43.1` tag here, and the registry will not accept a re-publish. `0.43.0` was
3046
+ never published at all; npm goes `0.42.0` → `0.43.1`.
3047
+
3048
+ Publishing as `0.44.0` gives the current tree a number that is free on both
3049
+ sides. The npm package therefore gains, relative to its `0.43.1`:
3050
+
3051
+ - the right-click sort, group, export, copy, print and share-view additions to
3052
+ the public collection context menu, and the `field: value` search syntax
3053
+ (released here as 0.43.0)
3054
+ - readable admin view sources and the setup wizard fix (released here as 0.43.1)
3055
+
3056
+ Nothing published under `0.43.1` is lost - that tarball's contents are an
3057
+ ancestor of this one.
3058
+
3059
+ ### Fixed
3060
+ - **Releases no longer carry the build machine's local config.** `config/site.json`
3061
+ is a working file on a development install, so the copy staged into `_publish/`
3062
+ held whatever SMTP host and site URL were last set on the machine cutting the
3063
+ release - every published tarball up to and including 0.43.1 put a LAN address
3064
+ (`192.168.1.27`) on the public registry, and handed anyone installing without
3065
+ the scaffolder a mail host that does not exist for them. `scripts/build.js` now
3066
+ resets `smtp` and `url` to neutral defaults in the staged copy and drops the
3067
+ `.bak` migration leftovers. `config/connections.json` was already excluded by
3068
+ `.npmignore` and never shipped.
3069
+
3070
+ ## [0.43.1] - 2026-08-20
3071
+
3072
+ ### Fixed
3073
+ - **The setup wizard could not run at all.** `scripts/setup.js` declared
3074
+ `const existingSecret` twice at module scope - once for `JWT_SECRET`, again
3075
+ for `INSTANCE_SECRET` - which is a hard `SyntaxError`, so `npm run setup`
3076
+ failed before printing anything. Present since 2026-04-08 and shipped in
3077
+ every release since; only fresh installs were affected, which is why it went
3078
+ unnoticed.
3079
+
3080
+ ### Changed
3081
+ - **Seven admin view files are readable source again.** An accidental in-place
3082
+ minify run in March 2026 overwrote them with their own build output and was
3083
+ committed; five months of work then landed on the damaged versions, so this
3084
+ is a rename-and-reformat pass rather than a revert. `view-editor.js` and
3085
+ `action-editor.js` were single lines of 18,824 and 13,248 bytes; `page-editor.js`
3086
+ carried a 23,541-character line. Identifiers were recovered from the last
3087
+ readable ancestor where one existed, and inferred from behaviour for
3088
+ `block-editor.js`, which has never existed in readable form.
3089
+
3090
+ Every file is proved to be a pure rename of what shipped: `tools/check-rename.mjs`
3091
+ compares token streams and passes only if the two are identical except for a
3092
+ one-to-one identifier mapping. Where the original minifier reused one letter
3093
+ for different kinds of value across scopes, the letter was left alone rather
3094
+ than given a name that would be false in some of its occurrences.
3095
+
3096
+ No behaviour changes. Nothing needed minified source here in the first place:
3097
+ `scripts/build.js` treats `admin/js` and `public/js` as inputs and writes
3098
+ minified copies to `_publish/`.
3099
+
3100
+ ### Added
3101
+ - `tools/check-rename.mjs` - verifies that a change to a JavaScript file is a
3102
+ pure identifier rename, for use on any future refactor of untested code.
3103
+ - A `pre-commit` hook that refuses to commit minified output over source,
3104
+ keyed on average bytes per line (healthy source here runs 43-67; the damaged
3105
+ files ran 419-18,824). The existing `commit-msg` hook is now tracked rather
3106
+ than local to one machine, and `npm run setup` enables `core.hooksPath` so a
3107
+ fresh clone gets both.
3108
+
3109
+ ---
3110
+
3111
+ ## [0.43.0] - 2026-08-19
3112
+
3113
+ ### Added
3114
+ - **The right-click menu on collection displays now sorts, groups, exports and
3115
+ copies, not just filters.** Rows are organised into drill-in panels rather
3116
+ than one growing list: filtering stays a single click, while `Sort…`,
3117
+ `Group by…`, `Copy…` and `Export…` open a sub-panel with a Back row. Sort
3118
+ works on every display type and labels its directions by detected value type
3119
+ (`A → Z`, `oldest → newest`, `low → high`). Grouping inserts headings between
3120
+ entries and is offered only where entries render as siblings - cards, list,
3121
+ listgroup, accordion and block; it is hidden on table, timeline and carousel,
3122
+ which re-render or sequence their entries and have nowhere to put a heading.
3123
+ - **Export and copy of what is on screen.** Export writes the filtered and
3124
+ sorted rows as CSV or JSON; Copy puts a single value, a row as JSON, or the
3125
+ whole visible set as a Markdown table on the clipboard. Both are limited to
3126
+ the fields the display actually rendered, so neither can emit a column the
3127
+ page did not show.
3128
+ - **Export permissions in the collection schema**, editable from the collection
3129
+ editor's new "API & Export" tab: `enabled`, `access` (`public` or a role) and
3130
+ `formats`. Absent config means signed-in users only. An existing
3131
+ `[collection exportable="true"]` is honoured as a public opt-in, so no page
3132
+ that already offered export loses it. The gate is a convenience affordance,
3133
+ not an enforcement boundary - entries already ship in the page's `data-ctx`
3134
+ attribute - and `docs/configuration.md` says so plainly.
3135
+ - **`field: value` search syntax** in the menu's search box, with `=`, `!=`,
3136
+ `>` and `<` as well as `:`. Field references match on name or label, case
3137
+ insensitively. Anything that does not name a known field falls back to a
3138
+ plain search, so `10:30` and `Re: proposal` keep working.
3139
+ - **Print, "Copy link to this view", and a unified "Clear all"** that resets
3140
+ filters, sort and grouping together and names what it will clear. Sort and
3141
+ grouping now travel in the URL hash alongside filters, so a shared link
3142
+ restores the whole view; links shared before this release still restore.
3143
+
3144
+ ### Fixed
3145
+ - Object-valued collection fields exported as the literal string
3146
+ `[object Object]`. CSV export now shares one dialect with the browser,
3147
+ including a UTF-8 BOM so Excel reads accented characters correctly.
3148
+ - Values beginning with a tab or carriage return bypassed CSV formula-injection
3149
+ sanitising, because `Number()` trims whitespace and stripped the very
3150
+ character that flagged them. Only `+` and `-` may now take the numeric
3151
+ exemption.
3152
+ - `>` and `<` filters treated two dates in the same calendar year as equal:
3153
+ `parseFloat` read `2026-06-15` as the bare year and short-circuited before
3154
+ the date comparison. The comparison ladder now tries whole-string numbers,
3155
+ then dates, then prefix numbers, then strings.
3156
+ - The export `access` role was not enforced. Every role behaved identically
3157
+ because the check only asked whether anyone was signed in.
3158
+ - `CLAUDE.md` referenced `server/services/userTypes.js`, which does not exist;
3159
+ those functions live in `roles.js`.
3160
+
3161
+ ---
3162
+
3163
+ ## [0.42.0] - 2026-08-17
3164
+
3165
+ ### Added
3166
+ - **Menus can be placed anywhere, not just the header, footer and admin
3167
+ sidebar.** A new core `overlay` slot: any menu bound to it renders on
3168
+ matching pages positioned by its own `float` config - no navbar or footer
3169
+ region, and no `[menu]` shortcode in the page body. Unlike the other slots it
3170
+ is not one-menu-wins, so a page can carry several overlays at once (a left
3171
+ rail and a corner widget, say); mapping a menu to `overlay` in
3172
+ `menu-locations.json` makes it site-wide instead. Overlays compose with
3173
+ everything already there - bindings decide *which pages*, `float` decides
3174
+ *where on the screen*, `orientation` decides *how it flows* - and an overlay
3175
+ left with no visible items after role filtering is dropped rather than
3176
+ leaving an empty box on the page. The slot appears automatically in the
3177
+ menu editor's Binding tab and in Menu locations.
3178
+
3179
+ ### Changed
3180
+ - Menu → HTML now lives in one place (`server/services/menuRender.js`), shared
3181
+ by the `[menu]` shortcode and overlay rendering, so the same menu produces
3182
+ identical markup however it reaches the page.
3183
+
3184
+ ## [0.41.1] - 2026-08-17
3185
+
3186
+ ### Added
3187
+ - **Enabling or disabling a plugin restarts the server.** A plugin's routes,
3188
+ hooks and admin views only register at boot, so a toggle used to be inert
3189
+ until someone restarted by hand. The process now exits cleanly after the
3190
+ toggle is saved and the supervisor brings it back, with the admin UI waiting
3191
+ and reporting when it is live. Guarded two ways: only where a supervisor is
3192
+ detected (pm2's `pm_id`/`PM2_HOME`, or the fleet manager's IPC channel - a
3193
+ bare `node server/server.js` is told to restart manually instead of being
3194
+ killed), and only when the enabled flag actually changed, so a settings save
3195
+ never bounces the site. Rapid toggles debounce into one restart. Opt out with
3196
+ `restartOnPluginToggle: false` in `config/server.json`.
3197
+ - The `[menu]` shortcode's `orientation` and `float` attributes are now offered
3198
+ in the editor's shortcode menu.
3199
+
3200
+ ### Fixed
3201
+ - **`[menu]` no longer disappears without explanation.** An unknown slug, or a
3202
+ tag with no slug or location at all, rendered as nothing - a blank space with
3203
+ no clue what went wrong. It now renders a visible note naming the problem, the
3204
+ way `[form]` and `[block]` already did. An unmapped `location=` slot stays
3205
+ silent (a legitimate config state) but logs a warning server-side.
3206
+ - **Unquoted shortcode attributes.** `[menu slug=main /]` parsed as two
3207
+ meaningless flags rather than a value, so the shortcode lost its argument and
3208
+ silently rendered nothing. Unquoted `key=value` pairs now work in every
3209
+ shortcode.
3210
+
3211
+ ## [0.41.0] - 2026-08-17
3212
+
3213
+ ### Added
3214
+ - **Menu bindings.** A menu can now claim a slot for part of the site instead
3215
+ of being mapped to it everywhere: `binding: {slot, projects, pages}` makes it
3216
+ take over (say) the navbar on the pages of one project, or on a list of URL
3217
+ patterns (`/pricing`, `/docs/*`, `/*`). The most specific match wins - exact
3218
+ page, then wildcard (longest first), then project, then the
3219
+ `menu-locations.json` mapping. Honoured by the public navbar, the footer
3220
+ columns and `[menu location="…" /]`. New **Binding** tab in the menu editor,
3221
+ with a page picker and a live plain-English summary of what the binding
3222
+ resolves to; the menus list gains a "Bound to" column.
3223
+ - **Vertical menus.** `orientation: horizontal | vertical` (plus
3224
+ `side: left | right`) on any menu. In the navbar slot a vertical menu docks
3225
+ as a fixed side rail from 993px up, with the page body given a matching
3226
+ gutter server-side so there is no layout shift; below that breakpoint the
3227
+ existing mobile drawer is unchanged. The `[menu]` shortcode gains an
3228
+ `orientation` attribute to override a menu for one placement. Menus that set
3229
+ no orientation render exactly as before.
3230
+ - **Drag-and-drop menu items.** The menu editor's tree reorders by dragging a
3231
+ row's grip: dropping on the top or bottom quarter of a row places the item
3232
+ above or below it as a sibling, and dropping in the middle nests it as that
3233
+ row's last child. Dropping a row into its own subtree is refused. The
3234
+ existing indent / outdent / up / down buttons are untouched.
3235
+ - **Floating menu surface options.** `float` now also takes `radius`,
3236
+ `shadow` (`none | sm | md | lg | xl`), `accent` (preset colour key or hex -
3237
+ tints the border and draws a stripe down the panel's leading edge) and
3238
+ `opacity` (20-100, translucent blurred surface with fully opaque text). All
3239
+ resolve to theme tokens, so a floating panel still tracks the active theme.
3240
+ - **Grouped font pickers.** Site settings and the navbar font picker group
3241
+ families into Sans-serif / Serif / Dyslexia-friendly / System, adding
3242
+ Lexend, Atkinson Hyperlegible and Andika as an accessibility group.
3243
+
3244
+ ### Fixed
3245
+ - **Font families with digits or a non-sans fallback.** The renderer emitted
3246
+ `font-family: Source Sans 3, sans-serif` - an unquoted identifier cannot
3247
+ start with a digit, so browsers dropped the whole declaration and the font
3248
+ silently never applied. Family names are now quoted and sanitised, and
3249
+ serif/monospace families get the matching generic fallback instead of
3250
+ `sans-serif`.
3251
+
3252
+ ## [0.37.0] - 2026-07-22
3253
+
3254
+ ### Added
3255
+ - **Forms: `icon` and `image` field types.** The form builder offers two new
3256
+ field types that store plain strings (an icon name / a media URL) but are
3257
+ filled with pickers instead of typed by hand. Icon fields get a live
3258
+ preview chip and a Browse button opening a searchable overlay of the full
3259
+ Domma icon set; image fields get a thumbnail preview and a Browse button
3260
+ opening the site media library (authenticated with the visitor's JWT +
3261
+ one refresh retry - anonymous visitors keep manual URL entry with a
3262
+ sign-in hint). Pickers work in all render paths: inline embeds
3263
+ (`site.js`), the legacy `[data-form]` renderer and its no-Domma manual
3264
+ fallback (`forms.js`), and the form builder's live preview
3265
+ (`enhancePickerFields` is shared from `public/js/forms.js`). Submissions
3266
+ store the chosen string as-is, so collections, actions, email and webhook
3267
+ payloads see a normal text value.
3268
+
3269
+ ## [0.36.19] - 2026-07-16
3270
+
3271
+ ### Fixed
3272
+ - **Ticker-tape inserts as a self-closing tag.** The effects builder and the
3273
+ editor context menu inserted `[ticker-tape]Content[/ticker-tape]`; the effect
3274
+ is now marked `selfClosing` so both insert `[ticker-tape … /]` (the full-page
3275
+ overlay form), matching `[celebrate]`. The wrapping container-scoped form is
3276
+ still rendered by the server and can be hand-written.
3277
+
3278
+ ## [0.36.18] - 2026-07-04
3279
+
3280
+ ### Changed
3281
+ - **Plugin code editor, properly.** The plain textarea is now the shared
3282
+ simple-editor with a new zero-dependency syntax-highlight layer (JS, JSON,
3283
+ CSS, HTML, Markdown - output HTML-escaped by construction, unit-tested for
3284
+ XSS), line numbers, auto-indent on Enter, and Ln/Col in the status bar.
3285
+ The file tree groups by directory; unsaved edits survive switching files
3286
+ (per-file buffers with dirty dots), Save All / Ctrl+Shift+S flushes them,
3287
+ files can be renamed, and closing the tab with unsaved changes warns first.
3288
+ simple-editor's new `language` / `autoIndent` / `onCursor` options are
3289
+ opt-in - the Block and Component editors are unchanged.
3290
+
3291
+ ## [0.36.17] - 2026-07-04
3292
+
3293
+ ### Added
3294
+ - **In-admin plugin code editor.** Every plugin card gains a Code button opening
3295
+ `#/plugins/code/<name>` - file tree, monospace editor with Ctrl+S, new/delete
3296
+ file, and a Restart Server button so server-side edits apply without leaving
3297
+ the browser. Saves are flagged per file: `admin/` files need only a hard
3298
+ refresh, everything else needs the restart. All I/O is confined to the plugin
3299
+ directory (traversal/symlink escapes refused, text extensions only, JSON
3300
+ validated, required files undeletable) and gated by the new `plugins.develop`
3301
+ permission - separate from `plugins.update` because plugin source is code
3302
+ execution. Base admin roles receive it automatically; custom roles need an
3303
+ explicit grant.
3304
+
3305
+ ## [0.36.16] - 2026-07-04
3306
+
3307
+ ### Added
3308
+ - **Plugin scaffolder.** New Plugin button on the admin Plugins view opens a slideover
3309
+ (display name with auto-derived slug, description, author, icon) that instantiates
3310
+ `plugins/_template` via the new `POST /api/plugins/scaffold` endpoint (guarded by the
3311
+ existing `plugins.create` permission). The plugin is created pre-enabled - server
3312
+ routes, admin view, sidebar entry and CLAUDE.md all wired - and goes live on the next
3313
+ server restart. Scaffolding documented in `docs/plugin-development.md`.
3314
+
3315
+ ## [0.36.15] - 2026-07-04
3316
+
3317
+ ### Changed
3318
+ - Converted the last remaining modal - the page editor's "Save Named Version" dialog -
3319
+ to a slideover, completing the modal→slideover sweep.
3320
+
3321
+ ## [0.36.14] - 2026-07-04
3322
+
3323
+ ### Changed
3324
+ - **All admin modals are now slideovers.** Create Form, Send Test Email, New Collection,
3325
+ Add Role, New API Token / Token created, Entry add/edit/details/import, Submission
3326
+ Details, and the image editor (75% viewport at full height) all open as right-hand
3327
+ slideovers with identical behaviour - light-DOM content projection, focus handling,
3328
+ and button wiring preserved.
3329
+
3330
+ ## [0.36.13] - 2026-07-04
3331
+
3332
+ ### Fixed
3333
+ - **Form builder crashed for any form with option fields (regression in 0.36.6).** The
3334
+ cascade help-tooltips wired `E.tooltip` on a not-yet-attached node; Domma's tooltip
3335
+ wraps elements via `parentNode.insertBefore`, so it threw and killed the entire
3336
+ field-card render - the builder showed nothing for forms containing select/chooser
3337
+ fields while simple forms were unaffected. Tooltip wiring is now deferred a frame,
3338
+ guarded, and falls back to a native title.
3339
+
3340
+ ## [0.36.12] - 2026-07-04
3341
+
3342
+ ### Added
3343
+ - **Duplicate form.** The Forms list gained a copy action that clones a form's fields,
3344
+ settings, and email/webhook actions under a new slug - the backing collection is
3345
+ freshly provisioned for the copy so its submissions never land in the source form's
3346
+ collection.
3347
+
3348
+ ### Fixed
3349
+ - **Page cache now self-invalidates on file changes.** The response-cache key includes
3350
+ the page file's mtime, so out-of-band edits (scripts, git pulls, deploys) serve fresh
3351
+ content without relying on service-layer hooks having fired. Previously a directly
3352
+ edited page could be served stale indefinitely per host/role variant.
3353
+ - **Row delete buttons are theme-correct.** All list/table delete buttons switched from
3354
+ solid `btn-danger` blocks (whose background varies per theme - the "white bin icon")
3355
+ to quiet ghost buttons whose trash icon carries the `--dm-danger` accent, visible on
3356
+ all 26 themes.
3357
+
3358
+ ## [0.36.11] - 2026-07-04
3359
+
3360
+ ### Fixed
3361
+ - **Table action icons no longer vanish after sort/search/pagination.** T.create
3362
+ re-renders its cells, wiping the injected icon SVGs and any per-button click
3363
+ listeners - most visibly the Forms list's delete button. A shared observer
3364
+ (`admin/js/lib/table-icons.js`) now re-scans icons and re-wraps tooltips after every
3365
+ table re-render across all 14 table views, per-button delete/export/run bindings are
3366
+ replaced with delegated handlers that survive re-renders, and all row-action icons
3367
+ are sized to the standard 14px (they rendered at a 24px default, overflowing their
3368
+ buttons). Collection entries' row-click detail modal now also survives sorting
3369
+ (resolved by entry id instead of row index).
3370
+
3371
+ ## [0.36.10] - 2026-07-04
3372
+
3373
+ ### Fixed
3374
+ - **One broken artefact can no longer blank a whole type in Projects.** The artefact
3375
+ resolver re-reads menus/forms/collections per item; a single unreadable one (e.g. a
3376
+ form whose internal slug didn't match its filename) threw and emptied that entire
3377
+ type across every project - "Forms disappeared from Core". Each item is now guarded
3378
+ individually.
3379
+ - **`listForms()` normalises the slug to the filename** (the canonical key used by
3380
+ read/delete/save and the `[form]` shortcode), so a stale internal slug can't desync
3381
+ the list from the files again.
3382
+
3383
+ ## [0.36.9] - 2026-07-04
3384
+
3385
+ ### Fixed
3386
+ - **Admin sidebar type scale normalised.** Section headings, links, and nested items all
3387
+ rendered at an identical 16px with no hierarchy (and larger than the brand header).
3388
+ Now: items 0.875rem at every depth, top-level sections as small uppercase muted labels,
3389
+ nested section headers item-weighted, and both badge styles unified. Menu-data style
3390
+ overrides still take precedence.
3391
+
3392
+ ## [0.36.8] - 2026-07-04
3393
+
3394
+ ### Fixed
3395
+ - **Conditional logic now triggers from chooser fields.** Domma choosers (chips/cards)
3396
+ check their underlying radio programmatically without dispatching change/input, so any
3397
+ visibility/requirement condition keyed on a chooser never re-evaluated - a "reveal on
3398
+ yes" question after a chip picker simply never appeared. The logic engine now also
3399
+ delegates clicks on chooser containers and re-evaluates the field after the selection
3400
+ lands.
3401
+
3402
+ ---
3403
+
3404
+ ## [0.36.7] - 2026-07-04
3405
+
3406
+ ### Fixed
3407
+ - **Chooser styling on select/radio/checkbox-group fields now renders.** The form
3408
+ builder's chooser section configures variant/density/columns/accent on any option
3409
+ field, but all three renderers (public inline embed, public `[data-form]` embed,
3410
+ admin preview) only honoured those props on `chooser`-type fields - a chip-styled
3411
+ select silently collapsed to a plain dropdown. Option fields carrying a chooser
3412
+ variant are now promoted to chooser blueprints (identical value contract;
3413
+ checkbox-groups map to multi-select choosers).
3414
+
3415
+ ---
3416
+
3417
+ ## [0.36.6] - 2026-07-04
3418
+
3419
+ ### Changed
3420
+ - **Form builder polish.** Field cards gained definition - card background, subtle shadow,
3421
+ softer radius, more separation - with roomier body padding, row gaps, and clearer
3422
+ section headings.
3423
+ - **Cascade Options explained in place.** Help tooltips on the Cascade heading, the
3424
+ Mapping JSON label, and Default options describe exactly how the cascade behaves:
3425
+ the source field's value picks the matching mapping entry as this field's options,
3426
+ falling back to the defaults when nothing matches.
3427
+
3428
+ ---
3429
+
3430
+ ## [0.36.5] - 2026-07-04
3431
+
3432
+ ### Changed
3433
+ - Form builder layout rebalanced to a 60/40 builder/preview split - the preview pane now
3434
+ scales with the viewport instead of capping at a fixed 460px.
3435
+
3436
+ ---
3437
+
3438
+ ## [0.36.4] - 2026-07-04
3439
+
3440
+ ### Added
3441
+ - **Form builder: drag-to-reorder fields.** Field, spacer, and page-break cards reorder by
3442
+ dragging (grip handle / card header), mirroring the collection editor's behaviour and
3443
+ drop-highlight. The per-card up/down arrow buttons are gone - the drag grip plus a
3444
+ tidier header (type pill, label, edit, remove) matches the collection editor exactly.
3445
+ Chooser options keep their small arrows (they don't drag).
3446
+ - **Form builder uses the full workspace width** - the fields + preview layout is no
3447
+ longer squeezed into the default 1200px column, and the preview pane gained room.
3448
+
3449
+ ### Fixed
3450
+ - **Admin-wide consistency sweep.** Every remaining unicode text-glyph button across the
3451
+ admin now uses the standard Domma icon set at consistent sizes: card/timeline/effects
3452
+ builders (dialog closes, browse/star buttons, drag grips, row arrows/removes, Replay),
3453
+ markdown-toolbar caret, shortcode-modal icon-browse, the collection editor's drag grip,
3454
+ disclosure chevron and oversized remove icon, page-editor's fold-all toggle and
3455
+ ListGroup-builder remove. Editor-folding's per-line gutter chevrons stay as text for
3456
+ performance but are normalised to one glyph pair.
3457
+ - **Every data-fetching admin view now shows the Domma loader** while loading (22 views:
3458
+ actions, API builder/endpoints/tokens, blocks, components, effects, layouts, menus ×3,
3459
+ profile, notifications, projects ×4, users, views ×3, collection entries, view preview,
3460
+ login's setup check) - all with guaranteed teardown on error paths.
3461
+
3462
+ ---
3463
+
3464
+ ## [0.36.3] - 2026-07-04
3465
+
3466
+ ### Added
3467
+ - **Context menu: data-driven dropdowns.** Attributes that bind to CMS data are now live
3468
+ pickers instead of type-the-slug text fields: `[form]` lists your forms, `[collection]`
3469
+ your collections (and its Sort/Title-field dropdowns read the selected collection's
3470
+ schema), `[view]`, `[block]`, `[menu]` (slugs + location slots) and `[cta]` actions
3471
+ likewise. Values that no longer exist are flagged "(missing)". The Insert menu drills
3472
+ into the same pickers, so Insert → Form inserts `[form name="…"]` with the chosen form
3473
+ baked in - no placeholder slug to fix up.
3474
+ - **Form builder: always-on live preview.** The Fields tab is now a two-column layout with
3475
+ a sticky preview pane on the right that re-renders (debounced) as fields and settings
3476
+ change; the Preview toolbar toggle is gone. Test mode still posts real submissions.
3477
+
3478
+ ### Fixed
3479
+ - **Form builder icon standardisation.** All unicode text-glyph buttons (↑ ↓ ✕ ⋯ ⚡ → ▲ ▼ ▾)
3480
+ replaced with the site-standard `data-icon` set at a consistent 14px: `arrow-up/down`
3481
+ for reordering, quiet ghost `x` for per-row removes (matching the collection editor -
3482
+ no more bold red ✕ buttons), `more-horizontal` for edit toggles, `zap` for the logic
3483
+ badge, `chevron-*` for disclosure. Chooser option rows and the Add-option button now
3484
+ match the rest of the builder.
3485
+ - Forms list, form editor, and submissions views show the Domma loader during fetches.
3486
+
3487
+ ---
3488
+
3489
+ ## [0.36.2] - 2026-07-04
3490
+
3491
+ ### Fixed
3492
+ - **Theme-compliance sweep: every popup, panel, and component now honours the active theme.**
3493
+ The admin and public UI referenced ~300 CSS variables that were never defined anywhere
3494
+ (`--card-bg`, `--border-color`, `--primary`, and invented `--dm-*` names such as
3495
+ `--dm-surface-subtle`, `--dm-bg`, `--dm-muted`, `--dm-accent`). Undefined variables silently
3496
+ fall back to hardcoded colours tuned for a single dark theme, which made popups blank or
3497
+ illegible on other themes - most visibly the form editor's Add ▾ menu and new-field card.
3498
+ All references are canonicalised to real Domma tokens (verified against
3499
+ `domma-themes.css`/`domma.css`: zero undefined names remain).
3500
+ - **Hardcoded admin-chrome colours tokenised (~230 sites).** Editor toolbar/dropdown hover
3501
+ states, API-doc method badges, card/timeline/effects-builder dialogs, the API Builder
3502
+ try-it JSON palette, form-editor pills and test-submit banners, project-detail artefact
3503
+ accents, sidebar badges, and role/stat badges now use semantic theme tokens
3504
+ (`--dm-hover-bg`, `--dm-primary-text`, `color-mix(...)` tints, etc.).
3505
+ - **Public `site.css`/`forms.css` tokenised (~100 sites).** Inline code/pre blocks, card
3506
+ variants (accent/glow/gradient body), block-template cards that were hardcoded
3507
+ white-on-light, marketing components, menu badges, and form success/error messages now
3508
+ follow the site theme. Deliberate design colours (gradient presets, always-dark code
3509
+ panels, image-overlay scrims, rating stars) are intentionally unchanged.
3510
+ - Menu badge text (`makeBadgeEl`) no longer hardcodes white on themed backgrounds.
3511
+
3512
+ ---
3513
+
3514
+ ## [0.36.1] - 2026-07-04
3515
+
3516
+ ### Fixed
3517
+ - **Page editor: Ctrl+Z now undoes toolbar, Insert-menu, and context-menu edits.** Every
3518
+ programmatic insert wrote `textarea.value` directly, which clears the browser's native undo
3519
+ buffer - so nothing inserted (or even typed beforehand) could be undone. All content-edit
3520
+ paths now route through a new `replaceRange()` helper built on
3521
+ `document.execCommand('insertText')`, which participates in native undo/redo: formatting
3522
+ buttons, headings/lists, Tab indent, all Insert-menu items, the `[text]` and Insert Component
3523
+ slideovers, the Effects builder, the card builder, the CRUD tutorial snippets, and the
3524
+ right-click context menu's live attribute edits and Wrap-in. Falls back to the old direct
3525
+ splice where `execCommand` is unavailable.
3526
+ - **Context menu now mirrors the actual tag state.** Checkboxes read bare flags, `="true"`,
3527
+ and `="1"` alike and show the attribute's default when absent (e.g. Full-width is now
3528
+ checked when `fullwidth="true"` is present); writes emit the form every server-side check
3529
+ accepts and drop attributes that match their default. `[collection]` `search`/`sortable`
3530
+ are recognised as default-on, so unchecking writes `="false"` instead of silently
3531
+ re-enabling them. Selects highlight the effective value (explicit or default), inputs show
3532
+ defaults as placeholders, and injected-effect detection matches the server's
3533
+ presence check (`reveal="true"` counts).
3534
+ - Preview/autosave `input` listeners no longer fire twice per inserted snippet.
3535
+
3536
+ ---
3537
+
3538
+ ## [0.36.0] - 2026-07-03
3539
+
3540
+ ### Added
3541
+ - **`[embed]` shortcode** - embed video and external pages from one tag. YouTube/Vimeo
3542
+ links render via privacy-friendly `youtube-nocookie.com` / `player.vimeo.com`;
3543
+ self-hosted `.mp4`/`.webm`/`.ogg` render as a native `<video>`; any other URL renders
3544
+ as a generic iframe. Attributes: `ratio` (16:9 default, plus 4:3/1:1/21:9/9:16/3:4),
3545
+ `title` (caption + accessibility), `width`, `align`, and `autoplay`/`loop`/`muted`.
3546
+ Generic iframes are gated by a host allow-list in `config/embeds.json`, enforced twice
3547
+ (at render time and again by the HTML sanitiser's `allowedIframeHostnames`). New
3548
+ **Insert → Embed** builder with a URL field and a self-hosted-video picker.
3549
+ - **Right-click context menu in the Page Editor** - right-click inside any shortcode to
3550
+ edit that tag's options live and inline (checkboxes toggle instantly, values edit in
3551
+ place, selects open sub-menus, and the shortcode rewrites as you go); right-click
3552
+ anywhere for **Insert…** and **Wrap in…**. Registry-driven across all 32 shortcodes
3553
+ (plus child tags) and all 17 effects. Effects reach full parity: edit standalone effect
3554
+ tags, edit or remove effects injected onto host tags, and **Add effect…** to inject into
3555
+ a compatible host or insert a standalone effect.
3556
+
3557
+ ### Fixed
3558
+ - **Effects button** - the toolbar Effects panel now reopens on repeat presses; its
3559
+ re-open guard previously latched until a full page reload.
3560
+ - **Unsaved-changes navigation** - confirming "Leave this page?" now actually changes the
3561
+ page; the guard previously discarded the navigation, forcing a hard reload. (The router
3562
+ awaits the guard's returned confirm promise.)
3563
+
3564
+ ---
3565
+
3566
+ ## [0.35.0] - 2026-07-02
3567
+
3568
+ ### Added
3569
+ - **Butterflies effect** - `[butterflies /]` (full-page overlay) or `[butterflies]…[/butterflies]`
3570
+ (container-scoped): procedurally drawn butterflies wander and rise with flapping wings.
3571
+ Thirteen palettes plus density, speed, wander, rise/flap speed, wingspan range, two-tone,
3572
+ and burst-mode controls. Injectable into `[hero]` (`butterflies butterflies-palette="pastel"`).
3573
+ - **Strobe lighting effect** - `[strobe /]` / wrapping form: light beams sweep from the corners
3574
+ and brighten where they cross. Six presets (club, concert, police, searchlight, scanner, mood)
3575
+ with motion, speed, arc, beam width, intensity, flicker + Hz, custom colours, and auto-stop
3576
+ overrides. Injectable into `[hero]`.
3577
+ - Both effects appear in the Page Editor **Insert → Effects** catalogue (Celebrations group)
3578
+ with full parameter forms and live preview. The strobe form carries a photosensitivity hint
3579
+ mirroring the framework's own >5 Hz flicker warning.
3580
+
3581
+ ### Fixed
3582
+ - Docs: the effects shortcode table now lists `[ticker-tape]` (previously missing) alongside
3583
+ the new `[butterflies]` and `[strobe]` rows.
3584
+
3585
+ Both new effects respect `prefers-reduced-motion` and degrade silently on sites running an
3586
+ older `domma-js` bundle (feature-detected before initialisation).
3587
+
3588
+ ---
3589
+
3590
+ ## [0.34.1] - 2026-07-02
3591
+
3592
+ ### Fixed
3593
+ - `listCollections()` no longer crashes when a hand-rolled schema lacks `title` -
3594
+ the sort falls back to the slug. (Previously a single title-less schema 500'd
3595
+ `/api/collections` for the entire site.)
3596
+ - Deleting a MongoDB-backed collection now **drops** the backing `cms_<slug>`
3597
+ Mongo collection (new `MongoAdapter.drop()`); previously only the documents
3598
+ were cleared, leaving the namespace and its unique index behind forever.
3599
+
3600
+ ---
3601
+
3602
+ ## [0.34.0] - 2026-07-02
3603
+
3604
+ ### Added
3605
+ - **Views are now innate** - available on every install with no MongoDB requirement.
3606
+ File-backed collections run a new built-in JS pipeline evaluator (`server/services/viewPipeline.js`)
3607
+ covering all ten allow-listed aggregation stages; MongoDB-backed collections keep using native
3608
+ aggregation. The same view definition works identically on both storage adapters.
3609
+ - One-time automatic migration of legacy view configs from the MongoDB `cms__views` collection to
3610
+ `content/views/*.json` (read-only on the Mongo side).
3611
+ - Views section in the API reference (previously undocumented).
3612
+
3613
+ ### Changed
3614
+ - View configs are now stored as JSON files in `content/views/` (always file-based, like collection
3615
+ schemas and forms), regardless of storage adapter.
3616
+
3617
+ ### Removed
3618
+ - The Pro/MongoDB requirement for Views: the "pro mode" error, the admin MongoDB notice on the Views
3619
+ screen, and the *(Pro)* framing in the docs.
3620
+
3621
+ ---
3622
+
3623
+ ## [0.33.1] - 2026-07-01
3624
+
3625
+ ### Fixed
3626
+
3627
+ - **Custom API endpoints now honour `limit`, `page`, `sort` and `order` query params.** These
3628
+ reserved keys were correctly kept out of ad-hoc field filtering but were never wired to the
3629
+ query, so `?limit=5` did nothing. In list mode `?limit=N` now shrinks the page - **capped at the
3630
+ endpoint's own configured limit**, so a caller can request fewer results but never more than the
3631
+ endpoint intends - `?page=N` walks the results, `?sort=field` sorts (restricted to fields the
3632
+ endpoint exposes) and `?order=asc|desc` sets direction. `search` remains reserved.
3633
+
3634
+ ---
3635
+
3636
+ ## [0.33.0] - 2026-07-01
3637
+
3638
+ ### Added
3639
+
3640
+ - **Custom API endpoints now accept ad-hoc query filters.** A request's query string is applied
3641
+ as additional filters *on top of* an endpoint's declared filters (which always win on conflict).
3642
+ Ad-hoc filters are restricted to the fields the endpoint actually exposes - its read-field
3643
+ allowlist, or the collection's fields when none is set - so a caller can never probe a field
3644
+ hidden from the response, and `page`/`limit`/`sort`/`order`/`search` are reserved as envelope
3645
+ controls. Declared `{{query.x}}` clauses are unchanged.
3646
+ - **API Builder "Try it" console overhaul.** Dedicated, self-labelling inputs for each
3647
+ `{{query.x}}` an endpoint reads; the full request rendered as copy-paste cURL alongside the
3648
+ response status, headers, and body; a syntax-coloured, collapsible JSON viewer for the response
3649
+ (with a Tree⇄Raw toggle and copy); per-endpoint test history you can click to re-run (tokens are
3650
+ never persisted); and keyboard shortcuts - `Cmd/Ctrl+S` to save, `Cmd/Ctrl+Enter` to send.
3651
+ - **Tabbed, full-width endpoint editor.** The editor's left column is now split into **Definition**,
3652
+ **Query** and **Response fields** tabs (Enabled folded into Definition), and the view uses the full
3653
+ browser width in a three-column layout - definition · Try-It · results - so the response lands
3654
+ *beside* the request instead of below the fold. Collapses to two columns then one on narrower
3655
+ viewports.
3656
+
3657
+ ### Fixed
3658
+
3659
+ - **Hardened Try-it path-parameter substitution** so a `:id` placeholder can no longer clobber a
3660
+ `:idx` in the same path, and a parameter value that contains `:name` is not mistaken for a
3661
+ placeholder.
3662
+ - **`Cmd/Ctrl+S` no longer wrongly reports "already exists".** The editor mounts into a reused
3663
+ container, so the save shortcut's `keydown` handler was never being torn down - handlers stacked
3664
+ up on `document`, and the one closing over a *new* (unsaved) endpoint kept running `create` after
3665
+ the endpoint had been saved, tripping the duplicate-path guard. Exactly one editor shortcut
3666
+ handler is now active at a time.
3667
+
3668
+ ---
3669
+
3670
+ ## [0.32.4] - 2026-07-01
3671
+
3672
+ ### Fixed
3673
+
3674
+ - **Hover-triggered dropdown menus in the admin are now reliable.** Bumped `domma-js` to **0.29.3**,
3675
+ which fixes a dropdown that opened inconsistently on hover and would not stay open when the pointer
3676
+ moved from the trigger onto the menu - which in turn destabilised the click trigger. Two root causes
3677
+ in Domma: `close()` scheduled menu removal via a timer that read the live menu reference at fire time,
3678
+ so a rapid re-open could have its fresh menu torn down and leave the open-state flag desynced; and the
3679
+ menu (a `document.body` sibling) had an offset/margin dead-zone the pointer had to cross, which fired
3680
+ the close. Domma now detaches state synchronously with the removal timer owning its own node, and adds
3681
+ a transparent hover bridge so the trigger-to-menu path is one continuous hover surface. The committed
3682
+ admin tools bundle was synced to 0.29.3 to keep core and tools in lockstep.
3683
+
3684
+ ---
3685
+
3686
+ ## [0.32.3] - 2026-06-30
3687
+
3688
+ ### Fixed
3689
+
3690
+ - **Action-button tooltips no longer vanish in admin tables.** Bumped `domma-js` to **0.29.2**,
3691
+ which fixes a tooltip double-wrap regression introduced by 0.29.1's table re-render fix. The
3692
+ `E.tooltip` factory physically wraps each element in a `<domma-tooltip>` but was not idempotent,
3693
+ so when the framework's per-render tooltip wiring and the admin views both wired the same action
3694
+ button, it got nested in two `<domma-tooltip>` wrappers and showed nothing. The wrapper is now
3695
+ idempotent (refreshes instead of nesting) and falls back to `data-tooltip`/`title` for content.
3696
+ The committed admin tools bundle was synced to 0.29.2 to keep core and tools in lockstep.
3697
+
3698
+ ---
3699
+
3700
+ ## [0.32.2] - 2026-06-30
3701
+
3702
+ ### Fixed
3703
+
3704
+ - **Surveys: the non-responders reminder no longer drops recipients in tests, and the send path is
3705
+ now testable without real SMTP.** The `/surveys/:slug/send` route always built a live SMTP
3706
+ transport (or, with no SMTP configured, an Ethereal account over the network), so in a sandboxed
3707
+ test environment every send timed out and was marked `bounced` - which `ledger.outstanding()`
3708
+ excludes, making the `non-responders` reminder resolve to zero recipients. The plugin now accepts
3709
+ an injectable `email` transport (`createTransport` / `sendEmail`) that **defaults to the real
3710
+ service** - no production behaviour change - so tests can supply a deterministic, network-free
3711
+ stub. The surveys send suite drops from ~240s to ~0.2s and the full test suite goes green
3712
+ (404/404, ~8s).
3713
+
3714
+ ---
3715
+
3716
+ ## [0.32.1] - 2026-06-30
3717
+
3718
+ ### Fixed
3719
+
3720
+ - **Data tables no longer lose action-button icons and tooltips after pagination, search or
3721
+ sort.** Bumped `domma-js` to **0.29.1**, which fixes `Table.render()` to re-scan icons and
3722
+ re-wire tooltips on every re-render. Previously only the *initial* render was processed, so
3723
+ paging, searching or sorting any admin or public-site table stripped the icons and tooltip
3724
+ popovers from rendered cells - the action buttons appeared to "fall apart". The committed admin
3725
+ tools bundle (`admin/dist/domma/domma-tools.*`) was synced to 0.29.1 to keep core and tools in
3726
+ lockstep.
3727
+
3728
+ ---
3729
+
3730
+ ## [0.32.0] - 2026-06-29
3731
+
3732
+ ### Added
3733
+
3734
+ - **Surveys - a Pro plugin for targeted email surveys, built on the Forms system.** Create a survey
3735
+ campaign that reuses an existing form definition, target one or more contact groups, and send each
3736
+ recipient a personalised invitation carrying a unique link. Responses are recorded against the form's
3737
+ own collection, tagged to the originating survey, and surfaced in a results dashboard - response rate,
3738
+ per-question distributions, free-text answers - with CSV and JSON export.
3739
+ - **Audience.** Shared `surveys-contacts` and `surveys-groups` collections, with a one-way import from
3740
+ the Contacts plugin so existing contacts can be reused without duplication.
3741
+ - **Identified or anonymous per survey.** Anonymous mode de-links responses from the contact who
3742
+ submitted them.
3743
+ - **Hosted survey page** at `/survey/:slug`, with a Markdown intro and footer and a custom thank-you
3744
+ message; plus a `[survey slug="…"]` shortcode to embed a survey inside any page.
3745
+ - **Editor-triggered sending.** Reminders re-issue fresh tokens to non-responders only, and a generic
3746
+ due-work contract lets an external scheduler drive reminders without bespoke wiring.
3747
+ - MongoDB-backed (Pro) storage for campaign, audience, and invite data.
3748
+ - **Six new admin chrome themes** (Sharp and Smooth variants in Indigo, Steel, and Teal), selectable in
3749
+ Site Settings.
3750
+
3751
+ ## [0.31.0] - 2026-06-25
3752
+
3753
+ ### Changed
3754
+
3755
+ - **Domma Docs now render at request time from the engine's templates** instead of being baked into
3756
+ per-site Markdown by a one-shot porter. Enabling the `domma-docs` project publishes the handbook at
3757
+ `/domma-docs/…` exactly as before - but a domma-cms version bump now updates the published docs on
3758
+ every site automatically. There is no longer any per-site provisioning step to run, nothing that can
3759
+ go stale, and no baked pages to drift from the shipped templates. The project's enable/disable flag
3760
+ remains the public kill-switch.
3761
+ - Section landing pages (`/domma-docs` and the Usage / Tutorials / API indexes) are now first-class -
3762
+ the previous porter linked to them from its nav but never generated them.
3763
+
3764
+ ### Removed
3765
+
3766
+ - `scripts/migrate-docs.js` (the one-shot porter) and the baked `content/pages/domma-docs/*` pages.
3767
+ Existing installs can delete their baked `content/pages/domma-docs/` and the managed Domma Docs block
3768
+ in `content/custom.css`; the request-time renderer supersedes both.
3769
+
3770
+ ## [0.30.0] - 2026-06-25
3771
+
3772
+ ### Added
3773
+
3774
+ - **Domma Docs - a built-in, toggleable documentation project.** Every site is now seeded with a
3775
+ default **Domma Docs** project (`domma-docs`, disabled by default, undeletable). Enable it and the
3776
+ full CMS documentation is published as public pages under `/domma-docs/…`; disable it and those
3777
+ pages leave the public site entirely (the project's enable/disable acts as a hard public
3778
+ kill-switch).
3779
+ - Pages are built from native Domma components - full-width `[hero]`, `[card]` grids,
3780
+ `[tabs]`/`[accordion]` sections, `[button]`s and list-groups - with a persistent section nav,
3781
+ breadcrumbs and prev/next paging between pages.
3782
+ - Generated from the admin documentation screens by `scripts/migrate-docs.js`; doc-specific
3783
+ styling lives in `content/custom.css`.
3784
+
3785
+ ### Changed
3786
+
3787
+ - **Projects: `protected` now means "undeletable", not "un-disable-able".** Protected projects
3788
+ (core and Domma Docs) cannot be deleted or untagged at both the service and route layers, but -
3789
+ unlike before - non-core protected projects can still be enabled and disabled. Only the core
3790
+ project can never be disabled.
3791
+ - **Disabled projects now appear (greyed, with an "off" badge) in the admin Projects sidebar**
3792
+ instead of being hidden, so a disabled-by-default project stays discoverable.
3793
+
3794
+ ### Fixed
3795
+
3796
+ - **The Manage Projects enable/disable toggle now shows for protected non-core projects.** It was
3797
+ suppressed for every protected project, which left Domma Docs impossible to switch on from the UI.
3798
+
3799
+ ## [0.25.15] - 2026-06-15
3800
+
3801
+ ### Fixed
3802
+
3803
+ - **Navbar `appearOnHover` did nothing - it was only half-wired across the stack.** Toggling a
3804
+ menu's "open dropdowns on hover" had no visible effect, and a hard-refresh couldn't help because
3805
+ three separate links were missing:
3806
+ - **Server:** `renderer.js` now injects `appearOnHover` into `window.__CMS_NAV__` for both the
3807
+ page and blog render paths (it was being dropped, so the flag never reached the client).
3808
+ - **Client:** `public/js/site.js` now acts on the flag. The Domma navbar opens dropdowns on
3809
+ click only and has no hover option, so when `appearOnHover` is set we toggle Domma's own
3810
+ `.open` class on `mouseenter`/`mouseleave`. Desktop only (≥993px) - the mobile drawer keeps
3811
+ tap-to-expand.
3812
+ - **Cache:** menu saves now invalidate the response cache. `menus.js` never imported the cache
3813
+ module, so on a production site (response cache on) editing *any* menu - content, behaviour,
3814
+ style, or slot mapping - left the `nav`/`site`-tagged public pages stale until TTL or restart.
3815
+ Create/update/delete and `setLocations` now call `cache.invalidateTags(['nav', 'site'])`.
3816
+
3817
+ Site-owned `public/css/site.css` is unchanged; existing sites need `server/` + `public/js/site.js`
3818
+ refreshed and the site process restarted.
3819
+
3820
+ ---
3821
+
3822
+ ## [0.25.14] - 2026-06-14
3823
+
3824
+ ### Fixed
3825
+
3826
+ - **Admin Dashboard showed "No Traffic" despite analytics recording, and the traffic chart grew
3827
+ unbounded.** The analytics plugin decorates `fastify.analytics` inside its own encapsulated
3828
+ (prefixed) scope, so the core dashboard route could never read it - `daily`/`journeys` were always
3829
+ empty. The dashboard now reads the analytics counters (`plugins/analytics/data/{daily,journeys}.json`,
3830
+ with a pre-2.1 root fallback) straight off disk and computes traffic/top-pages/realtime/spikes/journeys
3831
+ with logic ported from the analytics plugin (kept dependency-free). The traffic chart canvas is now
3832
+ wrapped in a fixed-height container so Chart.js (`maintainAspectRatio:false`) can't grow it to full
3833
+ page height.
3834
+
3835
+ ---
3836
+
3837
+ ## [0.25.12] - 2026-06-13
3838
+
3839
+ ### Fixed
3840
+
3841
+ - **Analytics counters are now stored under `plugins/analytics/data/`** (plugin v2.1.0). They
3842
+ previously lived in the plugin root, which meant a normal plugin push - the manager's
3843
+ `siteCmsUpdater`/`copyPlugin` and `npx domma-cms update` both replace plugin code wholesale and
3844
+ preserve only the `data/` carve-out - reset each site's live analytics to the distributed copy on
3845
+ every update. Moving the counters into `data/` makes both update paths preserve them
3846
+ automatically. A one-shot boot migration (`migrateToDataDir`) relocates any pre-2.1 root files on
3847
+ first start; `.gitignore`, the new-project template, the `update` self-heal and the `files`
3848
+ allowlist all point at `plugins/analytics/data/`.
3849
+
3850
+ ---
3851
+
3852
+ ## [0.25.11] - 2026-06-13
3853
+
3854
+ ### Fixed
3855
+
3856
+ - **Analytics/dashboard data died on every update and was never tracked reliably.** The analytics
3857
+ plugin's runtime counters (`daily.json`, `lifetime.json`, `journeys.json`, legacy `stats.json`)
3858
+ were treated as source: new projects committed them, the npm package shipped them, and
3859
+ `domma-cms update` overwrote each site's live counters with the bundled copies - so accumulated
3860
+ analytics were repeatedly reset to a stale seed. This recurred because an earlier fix only
3861
+ gitignored the old single-file `stats.json`, and the later split into three files never updated
3862
+ the ignore list.
3863
+ - `bin/cli.js`: the new-project `.gitignore` template now ignores all four counter files.
3864
+ - `bin/update.js`: the `.gitignore` self-heal (previously `.domma-backups/` only) now also ensures
3865
+ the four files are ignored - existing installs are fixed automatically on their next update.
3866
+ `scaffold.reset` in the analytics manifest lists all four, so the updater now **preserves** live
3867
+ `journeys.json`/`stats.json` instead of wiping them.
3868
+ - `package.json` `files`: `!plugins/analytics/*.json` negations exclude the counters from the
3869
+ published package (also stops shipping real visitor paths/counts).
3870
+
3871
+ ---
3872
+
3873
+ ## [0.25.10] - 2026-06-13
3874
+
3875
+ ### Fixed
3876
+
3877
+ - **Components (and API endpoints) never appeared in a project's sidebar submenu.** 0.25.8 added
3878
+ Components as a project artefact type to the project Overview tiles, the per-project routes, and
3879
+ the server-side enumeration - but the admin sidebar's per-project sub-tree (`buildProjectsSubtree`
3880
+ in `sidebar-renderer.js`) carries its **own** hardcoded artefact-type list, which still listed only
3881
+ 9 types. Components and APIs were missing from it, so even a project with components tagged (e.g.
3882
+ 4 tagged to a project) showed no "Components" row under it in the sidebar. Added both to that list;
3883
+ the submenu now matches the Overview tiles. (As with every type, a row only shows when the project
3884
+ has ≥1 of that artefact - the always-visible entry point is the Overview tile.)
3885
+
3886
+ ---
3887
+
3888
+ ## [0.25.9] - 2026-06-13
3889
+
3890
+ ### Fixed
3891
+
3892
+ - **Navbar dropdown child items rendered at the wrong (smaller) font size.** Only the dropdown
3893
+ *toggle* (the top-level item that opens a submenu) was normalised to match the regular nav-link
3894
+ size at each breakpoint; the first-level child items (`.navbar-dropdown-item`) were left at
3895
+ Domma's base component size, so dropdown children visibly mismatched the rest of the menu. The
3896
+ per-breakpoint normalisation now covers the child items too (scoped to `#site-navbar`), on both
3897
+ the desktop dropdown and the mobile accordion drawer. Site-owned file - existing sites need
3898
+ `public/css/site.css` refreshed.
3899
+
3900
+ ---
3901
+
3902
+ ## [0.25.8] - 2026-06-13
3903
+
3904
+ ### Added
3905
+
3906
+ - **Components are now a project artefact type.** Domma `.dmc` components can be filed under a
3907
+ project just like blocks: the component editor gains a **Project** field, the project Overview
3908
+ shows a **Components** count tile, and `#/projects/:slug/components` lists a project's components.
3909
+ Membership is stored in each component's `.meta.json` sidecar (`meta.project`), resolved the same
3910
+ way as every other artefact (untagged → core). The components API list/get endpoints now enforce
3911
+ project access-scope (`canSeeArtefact`), and the project delete-guard and "untag all" cover
3912
+ components too.
3913
+
3914
+ ### Changed
3915
+
3916
+ - **List-view action buttons are now icon + tooltip** across Collections, Actions, Forms, Views,
3917
+ Roles, Users, Blocks, Components, and Collection Entries - replacing the text buttons for a more
3918
+ compact, consistent action column.
3919
+
3920
+ ---
3921
+
3922
+ ## [0.25.7] - 2026-06-13
3923
+
3924
+ ### Fixed
3925
+
3926
+ - **Mobile navbar drawer was unclickable / dimmed (regression from 0.25.6)** - the slide-in
3927
+ drawer opened but appeared masked out and swallowed every click. The sticky navbar is
3928
+ `z-index:100`, which makes it its own stacking context; the drawer (`.navbar-collapse`)
3929
+ lives inside it while the backdrop is a `<body>` child at `z-index:1090`. Because the
3930
+ navbar's context sat *below* the backdrop, the entire navbar subtree - drawer included -
3931
+ painted under the backdrop regardless of the drawer's own z-index. While the drawer is
3932
+ open we now lift the navbar above the backdrop (`body.dm-drawer-open #site-navbar`), so
3933
+ the drawer sits on top and is fully interactive. Existing sites need `public/css/site.css`
3934
+ refreshed (carried automatically by a normal `update`).
3935
+
3936
+ ---
3937
+
3938
+ ## [0.25.6] - 2026-06-13
3939
+
3940
+ ### Fixed
3941
+
3942
+ - **Mobile navbar submenus hung off the screen** - parent (dropdown) items in the mobile
3943
+ drawer rendered their submenus as absolutely-positioned floating cards, so a long list
3944
+ (e.g. a World Cup menu) overflowed past the edge of the narrow drawer and could only be
3945
+ reached by awkward scrolling. Submenus now expand **inline as an accordion**: the panel
3946
+ is full-width, children are indented, and the drawer scrolls as one. The parent caret is
3947
+ parked on the right edge of a full-width tap row so it clearly reads as expandable.
3948
+ The drawer breakpoint is also raised to `992px` to match Domma's desktop cutover (993px),
3949
+ closing the 769-992px gap where the hamburger appeared but the old default collapse (with
3950
+ the same overflow bug) leaked through. Existing sites need `public/css/site.css` refreshed
3951
+ (site-owned file).
3952
+
3953
+ ---
3954
+
3955
+ ## [0.25.5] - 2026-06-12
3956
+
3957
+ ### Added
3958
+
3959
+ - **Per-row insertion in the menu editor** - every row (items and separators) gains "Add item
3960
+ below" and "Add separator below" buttons that insert directly beneath that row, at that row's
3961
+ level. Building a dropdown divider is now one click on the child you want it under - no more
3962
+ add-at-bottom, arrow-walk, indent.
3963
+
3964
+ ### Fixed
3965
+
3966
+ - **Separators were invisible on dark themes** - the editor's separator line, the admin
3967
+ sidebar divider, and the navbar dropdown divider all relied on black (or surface-colour)
3968
+ fallbacks. The editor row now shows a labelled "SEPARATOR" line; all divider fallbacks are
3969
+ neutral gray. Existing sites need `public/css/site.css` refreshed for the navbar part
3970
+ (site-owned file).
3971
+
3972
+ ---
3973
+
3974
+ ## [0.25.4] - 2026-06-12
3975
+
3976
+ ### Fixed
3977
+
3978
+ - **Menu editor separator rows wrapped across three lines** - 0.25.3 added the movement buttons
3979
+ but not the matching CSS: the separator row's grid was still sized for grip/line/remove only.
3980
+ The grid now accommodates the full control set, and rows sharpen on hover.
3981
+
3982
+ ---
3983
+
3984
+ ## [0.25.3] - 2026-06-12
3985
+
3986
+ ### Fixed
3987
+
3988
+ - **Menu separators are now movable and actually render.** Three stacked issues:
3989
+ - The menu editor rendered separator rows with only a Remove button - and "Add separator"
3990
+ appends at the bottom, so separators were stuck there. Separator rows now carry the same
3991
+ up/down/indent/outdent controls as items (indenting *under* a separator is refused - they
3992
+ can't hold children).
3993
+ - The public navbar stripped separators entirely. Separators inside dropdown submenus now
3994
+ translate to the navbar component's native divider (`navbar-dropdown-divider`, styled by
3995
+ Domma's elements.css). Top-level separators are still stripped - a horizontal navbar has no
3996
+ divider representation.
3997
+ - The `[menu]` shortcode and footer renderers emitted separator markup with no CSS - the
3998
+ `.dm-menu-sep` / `.footer-sep` divider styles now ship in `public/css/site.css`.
3999
+ - Note for existing sites: `public/js/site.js` and `public/css/site.css` are site-owned and
4000
+ not touched by rolling updates - refresh them per site to pick up the navbar/divider parts.
4001
+
4002
+ ---
4003
+
4004
+ ## [0.25.2] - 2026-06-10
4005
+
4006
+ ### Fixed
4007
+
4008
+ - **Inline code and fenced blocks inside `[reveal]`/effects shortcodes rendered as literal
4009
+ `undefined`** (or raw backticks). Two root causes in the markdown pipeline: code-region scrub
4010
+ placeholders were not namespaced per call, so a nested processor's restore consumed the outer
4011
+ call's placeholders and looked them up in the wrong store; and the effects handlers parsed the
4012
+ still-scrubbed body, so code regions missed markdown rendering. Placeholders now carry a
4013
+ per-call id, and effects handlers restore the body before their nested parse (the existing
4014
+ tabs-handler pattern). Regression-tested for nesting, fences, and placeholder leakage.
4015
+
4016
+ ---
4017
+
4018
+ ## [0.25.1] - 2026-06-10
4019
+
4020
+ ### Changed
4021
+
4022
+ - **API endpoints are now collection-scoped to their project** - an endpoint may only expose
4023
+ collections from its own project or core. Previously a definition could query any collection on
4024
+ the site, which let a project-scoped user publish another project's data through their own URL
4025
+ namespace. The builder's collection picker now filters to the selected project (+ core, marked
4026
+ as such), and the server enforces the same rule (400 on save). Pre-existing cross-project
4027
+ endpoints keep serving but cannot be re-saved until corrected - the editor flags them.
4028
+ - **Scoped users must be able to see a collection to expose it** - endpoint create/update now
4029
+ 403s when the chosen collection is outside the caller's project access scope.
4030
+ - **The base `admin` role now includes `api-tokens` and `api-endpoints`** - seeded on fresh
4031
+ installs, and the boot self-heal now covers all base roles (append-only against their seed
4032
+ definitions), so existing fleet admins get API Builder and API Tokens after updating without a
4033
+ manual grant. Custom roles are never touched; note a *seeded* permission removed from a base
4034
+ role is re-added at boot - use a custom role to run with less than the base set.
4035
+
4036
+ ---
4037
+
4038
+ ## [0.25.0] - 2026-06-10
4039
+
4040
+ ### Added
4041
+
4042
+ - **API Builder - custom REST endpoints over collection data** (`/api/x/*`). Build named, curated
4043
+ endpoints like `GET /api/x/world-cup/fixtures-day/:date` without writing code: a definition binds
4044
+ a clean URL path to a fixed collection query and is served by a single catch-all route.
4045
+ - **Definitions are data** - entries in a new `api-endpoints` preset collection (file-based,
4046
+ undeletable), project-scoped: the project is the endpoint's URL namespace and access boundary.
4047
+ - **Path params + query refinements** - filter values may embed `{{params.name}}` (from a
4048
+ `:name` path segment; missing → 400) or `{{query.name}}` (optional - the clause is dropped
4049
+ when absent). Values only ever feed the filter engine; there is no way to execute code.
4050
+ - **`list` and `single` modes**, sort/order/limit, and a response field allowlist.
4051
+ - **Auth reuses the collections API machinery** - `public`, `token` (project-bound API token,
4052
+ scopes enforced), or a role name. Same strictness rules as `/api/v1`.
4053
+ - **Static paths beat `:param` paths**; duplicate path shapes per project are refused (409);
4054
+ system-managed collections (e.g. `api-tokens`) cannot be exposed.
4055
+ - **Cached** - public-auth responses are cached and invalidated automatically when the
4056
+ collection's entries or the definition change.
4057
+ - **Admin: Data → API Builder** - list view plus a builder editor with live URL preview,
4058
+ schema-driven filter rows and field checkboxes, and a **try-it console** that sends a raw,
4059
+ credential-free request to the saved definition and renders status, pretty JSON, and a
4060
+ copy-as-curl line.
4061
+ - **Projects integration** - endpoints are the tenth artefact type: APIs tile on the project
4062
+ overview, Quick-add, and `/projects/:slug/apis` filtered list. Untag-all skips endpoints
4063
+ (their project is their URL namespace); a project with endpoints refuses deletion.
4064
+ - **Scaffolder** - recipes may declare `apiEndpoints: [...]`; created at apply time under the
4065
+ recipe's project, idempotent on re-apply.
4066
+ - New permission family `api-endpoints.{read, create, update, delete}` - the root role
4067
+ self-heals at boot; grant to other roles via the Roles editor.
4068
+
4069
+ ### Fixed
4070
+
4071
+ - **Revoked API tokens could be resurrected** by an in-flight throttled `lastUsedAt` write racing
4072
+ the revoke (FileAdapter read-modify-write). The token service now serialises its own writes and
4073
+ drains them before revoke/update - revoke is final.
4074
+
4075
+ ---
4076
+
4077
+ ## [0.24.0] - 2026-06-10
4078
+
4079
+ ### Added
4080
+
4081
+ - **External collections API with project-scoped API tokens** - collections are now first-class
4082
+ externally consumable resources, secured by machine credentials instead of public-or-human-JWT.
4083
+ - **`/api/v1/:slug[/:id]`** - a stable, versioned alias of the public collection endpoints
4084
+ (`/api/collections/:slug/public[...]`); same handlers, same query parameters. The documented
4085
+ surface for third-party integrations.
4086
+ - **API tokens** - a new `api-tokens` preset collection (always file-based, undeletable). Tokens
4087
+ are `dcms_` + 64 hex; only a SHA-256 hash is stored and the plaintext is shown **once**, at
4088
+ creation. Each token is bound to one project and only works on collections resolving to that
4089
+ project; optional `scopes: [{collection, verbs}]` narrow it to specific collections/verbs.
4090
+ Disable, expiry (`expiresAt`), and `lastUsedAt` tracking included.
4091
+ - **`access: 'token'` mode** - a third per-verb access mode in `schema.api` alongside `'public'`
4092
+ and role names. Strict: a token is only accepted in token mode, never as a role substitute,
4093
+ and a JWT is never accepted in token mode. Entries created via token auth are attributed
4094
+ `meta.createdBy: 'token:<id>'`.
4095
+ - **Read field allowlist** - `schema.api.read.fields: []` restricts which `data` fields the
4096
+ public/external read endpoints return (admin endpoints unaffected).
4097
+ - **Admin UI** - new System → API Tokens page (create with copy-once reveal, enable/disable,
4098
+ revoke); the collection editor's API tab gains the Token access mode and a read-fields input.
4099
+ - **Token management API** - `/api/api-tokens` CRUD guarded by a new `api-tokens.{read, create,
4100
+ update, delete}` permission family. Existing roles are **not back-filled** (grant via the Roles
4101
+ editor) - but `roles.js seed()` now self-heals the level-0 root role with any missing registry
4102
+ resources at boot, so super-admins always see newly added permission families.
4103
+ - **Scaffolder** - recipes may declare `apiTokens: [{name, scopes?}]`; tokens are generated at
4104
+ apply time under the recipe's project and surfaced once in the apply summary. Re-applying never
4105
+ re-issues an existing token.
4106
+ - **`ensureSidebarItem()`** - sidebar migration gains an append-if-absent helper so updates can
4107
+ surface new admin pages on existing installs without clobbering admin edits (no-op when the
4108
+ item's URL exists anywhere in the persisted menu).
4109
+
4110
+ ### Changed
4111
+
4112
+ - `admin/js/views/collection-editor.js` was de-minified (esbuild one-liner → readable source;
4113
+ identifiers remain mangled) so it can be reviewed and edited like the rest of `admin/js`.
4114
+
4115
+ ### Fixed
4116
+
4117
+ - **Project Quick-add navigation did nothing for forms, views, users, roles, and actions** - the
4118
+ Domma router compares raw hash segments, so `#/forms/new?project=x` never matched `/forms/new`
4119
+ and rendered not-found. The admin's router patch now strips query strings before matching.
4120
+ - **Core project lists showed empty despite non-zero count tiles** - the client-side project
4121
+ filter only matched explicit tags and never applied core's ownership-by-exclusion. It now
4122
+ mirrors the server's `resolveArtefactProject()` fallback (untagged → core) in both
4123
+ `filterByProject()` and the roles view's inline filter.
4124
+
4125
+ ---
4126
+
4127
+ ## [0.23.0] - 2026-06-10
4128
+
4129
+ ### Added
4130
+
4131
+ - **Built-in Core project** - every site now has a protected `core` project representing the core site itself. It
4132
+ owns **everything not assigned to another project** - by resolution, not by tagging: any artefact without a
4133
+ `meta.project` tag, and any page no other project's `rootUrl` claims, belongs to Core automatically. Nothing is
4134
+ ever stamped; "no tag" *is* Core membership, so artefacts created later (by plugins, the API, or hand-edited
4135
+ files) can never become strays.
4136
+ - Seeded at boot (create-if-absent) - **existing sites gain Core automatically on first start after updating**,
4137
+ with no migration and no content rewrites. A pre-existing user-created project with the slug `core` is adopted
4138
+ rather than overwritten.
4139
+ - **Not removable** - delete and untag-all are refused at both service and API level (403); the admin UI shows a
4140
+ *System* badge, hides the delete button, and locks the Root URL field. Name, description, icon and sort order
4141
+ remain editable.
4142
+ - **Never an access boundary** - Core artefacts are visible to all authenticated users regardless of their
4143
+ `projects: []` scope, so scoped users always keep the site's pages, menus and blocks. Core always appears in
4144
+ every user's project list, so the sidebar's Projects section now renders on every site.
4145
+ - Page frontmatter `project: null` (the old "untagged" opt-out) is deprecated and now resolves to `core`.
4146
+ Longest-prefix `rootUrl` matching is unchanged - scaffolded projects still win over Core for their own URLs.
4147
+ - **`npm test`** - the repo gains a canonical test script (`node --test --test-concurrency=1`); files run serially
4148
+ because test processes share the live `content/` and `config/` directories.
4149
+
4150
+ ### Fixed
4151
+
4152
+ - **Form submissions lost when the backing collection was missing** - a form whose collection action pointed at a
4153
+ slug that was never provisioned was permanently broken: the admin submissions list 404'd and every public
4154
+ submission was rejected with a 500 - and lost. Forms now **self-heal**: the missing collection is provisioned
4155
+ from the form's own fields on first submission (admin-only API access, since submissions can contain PII), the
4156
+ submission is saved instead of dropped, and the submissions list shows empty rather than erroring. Explicitly
4157
+ disabled collection storage is still respected.
4158
+
4159
+ ## [0.22.6] - 2026-06-08
4160
+
4161
+ ### Changed
4162
+
4163
+ - **Analytics is now a core plugin** - always-on and un-disableable, so page-view recording can never be silently
4164
+ switched off. The analytics Top Pages filter now uses the Domma form input (was a foreign Bootstrap class).
4165
+ - **Admin sidebar auto-groups plugin nav** - core plugins under a **Tools** folder, optional plugins under a
4166
+ **Plugins** folder led by *Manage Plugins*. The standalone System → Plugins item is relocated at render time - no
4167
+ menu migration needed. Grouping logic extracted to `admin/js/lib/sidebar-grouping.js` with unit tests.
4168
+ - **Admin role seed grants the `plugins` permission** so child-site admins can see and manage their own plugins.
4169
+
4170
+ ## [0.22.5] - 2026-06-08
4171
+
4172
+ ### Fixed
4173
+
4174
+ - **Plugin sidebar items missing** - plugins that declare their navigation in `plugin.json` (`admin.sidebar`) were not
4175
+ appearing in the admin sidebar. The renderer reads plugin nav from `/api/sidebar/registered-items`, but manifest
4176
+ `admin.sidebar` entries were never registered there. They are now bridged into the registry at plugin load, so
4177
+ plugin sections (e.g. Site Manager) render again. Regression introduced when the sidebar moved to the registry
4178
+ mechanism in 0.22.4.
4179
+
4180
+ ## [0.22.4] - 2026-06-08
4181
+
4182
+ ### Added
4183
+
4184
+ - **Create artefacts straight from a project's Overview** - every artefact card now carries a **+**. Pages,
4185
+ collections, menus and blocks open a slide-over and are created already tagged to the project *without leaving the
4186
+ page* (the card's count ticks up immediately). Forms, actions, views, roles and users open their editor with the
4187
+ project pre-selected.
4188
+ - **Project Overview redesign** - a hero header with the project's identity, colour-coded artefact cards, and
4189
+ **Created / Creator / Updated** metadata badges. Projects now record who created them.
4190
+
4191
+ ### Fixed
4192
+
4193
+ - **Projects → Pages listed nothing** - the pages list API dropped each page's resolved project, so the per-project
4194
+ filter matched no rows. The resolved project is now included in the listing.
4195
+ - **Collections and blocks created under a project weren't tagged** - their create routes silently discarded the
4196
+ `meta.project` tag, so the new artefact never appeared under the project. The tag is now persisted.
4197
+ - **Block "untag all" / project block counts** - the projects service referenced helpers that don't exist
4198
+ (`readBlock` / `writeBlock`), making block untagging a silent no-op. It now uses the real block read/save helpers.
4199
+
4200
+ ## [0.22.3] - 2026-05-29
4201
+
4202
+ ### Changed
4203
+
4204
+ - **Menu editor - Visibility is now a dropdown** - the per-item Visibility field was a free-text box (you had to know
4205
+ to type a role name). It is now a select populated from the site's roles: *Everyone / Public / `<role>` and above /
4206
+ Private*, preserving any existing custom value. Short help text was added under the Label, URL, Visibility,
4207
+ Permission, Badge-count and Render-as fields so the item editor is self-explanatory.
4208
+
4209
+ ## [0.22.2] - 2026-05-29
4210
+
4211
+ ### Fixed
4212
+
4213
+ - **Build dropped `.mjs` modules from the published package** - the minify globs in `scripts/build.js` only matched
4214
+ `*.js`, so browser ESM modules using the `.mjs` extension (e.g. `public/js/menu-decor.mjs`) were silently excluded
4215
+ from `_publish/`. This shipped 0.22.1 with the menu decoration code importing `/public/js/menu-decor.mjs` but
4216
+ without the file itself, 404'ing the import and breaking every page. The globs now include `*.mjs`.
4217
+
4218
+ ### Added
4219
+
4220
+ - **Asset-integrity build gate** (`scripts/verify-assets.mjs`) - after building `_publish/`, every local ESM import
4221
+ under `admin/` and `public/` is statically resolved against the files that actually ship; any dangling import fails
4222
+ the build, so an incomplete release can no longer be packaged or published. Also runnable standalone:
4223
+ `node scripts/verify-assets.mjs [root]`.
4224
+
4225
+ ## [0.6.21] - 2026-03-25
4226
+
4227
+ ### Added
4228
+
4229
+ - **Page editor - CSS Editor toggle** - new `</>` toolbar button (left of the Split View icon) swaps the left pane
4230
+ between the Markdown editor and the Custom CSS editor. CSS changes save automatically alongside the page in a single
4231
+ operation with a combined "Page and CSS saved." toast.
4232
+ - **Page editor - inline preview honours Custom CSS** - the split-pane preview now injects the site's Custom CSS so
4233
+ the inline preview matches the live page appearance.
4234
+ - **Page editor - batch history delete** - the History tab now shows a checkbox per version row plus a Select All
4235
+ checkbox. Selecting one or more entries reveals a "Delete Selected" button which bulk-deletes via a single
4236
+ `POST /api/versions/bulk-delete/*` request.
4237
+ - **Page editor - IDE-style keybindings** - the Markdown and CSS editor textareas now support:
4238
+ `Tab` (indent 2 spaces), `Shift+Tab` (dedent 2 spaces), `Ctrl+X` (cut line / selection), `Ctrl+C` (copy line /
4239
+ selection), `Ctrl+Z` (undo), `Ctrl+Y` (redo). Each action pops a brief info toast.
4240
+ - **Page editor - Colour Picker** - new Insert → Colour Picker entry opens a centred overlay with a native colour
4241
+ picker, an editable hex input, a Copy button (copies hex to clipboard), and an Insert button (drops the hex code
4242
+ at the cursor). Click the backdrop to dismiss.
4243
+ - **`[collection]` / `[view]` / `[block]` shortcodes - `class` and `id` support** - all three shortcodes now accept
4244
+ `class` and `id` attributes. For `[collection]` and `[view]` the attributes are applied to an outer wrapper div;
4245
+ for `[block]` they are applied directly to the `dm-static-block` div.
4246
+ - **Page editor help - keybindings reference** - the Editor Reference slideover (Basics tab) now includes a full
4247
+ keyboard shortcuts table.
4248
+ - **Page editor help - CSS Editor section** - the Editor Reference slideover (Advanced tab) now documents the CSS
4249
+ Editor toggle, view mode buttons, and combined save behaviour.
4250
+ - **Page editor help - Colour Picker section** - the Editor Reference slideover (Advanced tab) documents the new
4251
+ Insert → Colour Picker workflow.
4252
+ - **Page editor help - copy code buttons** - every code snippet in the Editor Reference slideover now has a copy icon
4253
+ button; clicking copies the snippet to the clipboard and toasts "Copied!".
4254
+ - **Versions API - bulk delete endpoint** - `POST /api/versions/bulk-delete/*` accepts `{ filenames: [...] }` and
4255
+ deletes all listed versions in a single read/write of `_meta.json`.
4256
+
4257
+ ### Changed
4258
+
4259
+ - **Page editor help - Data tab** - `[collection]` and `[view]` attribute tables updated to include `class` and `id`.
4260
+
4261
+ ---
4262
+
4263
+ ## [0.6.20] - 2026-03-25
4264
+
4265
+ ### Added
4266
+
4267
+ - **`[block]` shortcode** - renders a single block template with static inline data, no Collection required.
4268
+ Usage: `[block template="contact-card" full_name="Jane Doe" email_address="jane@example.com" /]`. Any attribute
4269
+ beyond `template` is passed as data for `{{placeholder}}` substitution. Missing templates are silently removed;
4270
+ missing placeholder values render as empty strings; all values are HTML-escaped before substitution.
4271
+ - **Insert → Block in the page editor** - new guided modal in the markdown toolbar's Insert menu. Select a block
4272
+ template, fill in dynamically generated input fields (auto-detected from the template's `{{placeholders}}`), and
4273
+ see a live DOMPurify-sanitised preview before inserting. Builds the `[block ...]` shortcode and drops it at the
4274
+ cursor position.
4275
+ - **`[text]` shortcode** - inline text styling with granular control over size, weight, colour, font, transform,
4276
+ decoration, and letter-spacing. Full guided modal with live preview in the page editor Insert menu.
4277
+
4278
+ ---
4279
+
4280
+ ## [0.6.5] - 2026-03-17
4281
+
4282
+ ### Added
4283
+
4284
+ - **`cols` attribute for `[collection display="block"]`** - block display mode now accepts a `cols` attribute (values
4285
+ 2-6) to wrap entries in a Domma CSS Grid (`grid grid-cols-N gap-4`). Invalid or omitted values fall back to the
4286
+ existing stacked layout. Usage: `[collection slug="..." display="block" block="post-card" cols="3" /]`.
4287
+ - **`[header]` and `[footer]` sub-tags in `[card]`** - cards now support `[header]...[/header]` and
4288
+ `[footer]...[/footer]` sub-tags for rich Markdown content (bold, links, icons) in card headers and footers.
4289
+ - **Wedding and Christmas themes** - two new Domma themes (`wedding` and `christmas`) added to the theme selector in
4290
+ Site Settings.
4291
+ - **Hero gradient variants expanded** - `[hero]` shortcode `variant` attribute now supports 8 generic gradients (
4292
+ `gradient-purple`, `gradient-blue`, `gradient-green`, `gradient-sunset`, `gradient-ocean`, `gradient-rose`,
4293
+ `gradient-forest`, `gradient-night`) and 26 theme-specific variants (`gradient-{theme}-light/dark` for all 13 themes).
4294
+ Reference page at `/hero-gradients`.
4295
+ - **Hero scroll-reveal animation** - hero sections on public pages now slide up on entry with re-animation support on
4296
+ scroll.
4297
+ - **`npx domma-cms update` command** - new `bin/update.js` script for updating an existing project via npx; includes
4298
+ smart config merge (`bin/lib/config-merge.js`) that preserves user config values.
4299
+ - **Server port in setup script** - `npm run setup` now prompts for a custom port and writes it to `config/server.json`;
4300
+ CLI respects the configured port dynamically.
4301
+ - **CLAUDE.md for site developers** - AI assistant guide committed to the project root for use with Claude Code.
4302
+ - **Cookie and privacy policy pages** - `cookies.md` and `privacy.md` seed pages added (UK GDPR / PECR compliant);
4303
+ seeded via `npm run seed`.
4304
+ - **"Display as button" option in page editor** - checkbox + colour picker in the page editor to render the page CTA as
4305
+ a styled button.
4306
+
4307
+ ### Changed
4308
+
4309
+ - **Collection schema format** - select field options now use `{ "value": "...", "label": "..." }` objects consistently
4310
+ across all bundled collection schemas; field type `"text"` normalised to `"string"`.
4311
+ - **`public/css/site.css` expanded** - additional utility classes and theme-aware CSS variables added for hero
4312
+ gradients, navbar dropdown icons, and scroll-reveal effects.
4313
+ - **`domma-js` upgraded** to `^0.20.2`.
4314
+ - **Shortcode documentation updated** - `docs/markdown-shortcodes.md` extended with `[header]`/`[footer]` card sub-tag
4315
+ examples, hero gradient reference table, and grid usage clarifications.
4316
+
4317
+ ---
4318
+
4319
+ ## [0.5.0] - 2026-03-13
4320
+
4321
+ ### Added
4322
+
4323
+ - **Initial public release** of Domma CMS on npm.
4324
+ - **Admin SPA** - full single-page application built with the Domma framework; hash-based routing, role-aware sidebar, auto-refreshing JWT auth.
4325
+ - **File-based pages** - content stored as Markdown files with YAML frontmatter in `content/pages/`; filename maps directly to public URL.
4326
+ - **Server-side rendering** - public pages assembled server-side via Fastify and served as complete HTML; supports `{{headInject}}` and `{{bodyEndInject}}` plugin slots.
4327
+ - **Plugin system** - three-file structure (`plugin.json`, `plugin.js`, `config.js`); plugins can add backend routes, inject HTML into public pages, and register admin panel views and sidebar entries.
4328
+ - **Bundled plugins** - Form Builder (visual form creation, submissions, email/webhook actions), Analytics (flat-file page-view tracking), Back to Top button, Cookie Consent banner.
4329
+ - **Role-based permissions** - four built-in roles (`admin`, `manager`, `editor`, `subscriber`) stored as a preset Collection; roles and permissions fully manageable from the admin panel.
4330
+ - **Dynamic Collections** - schema-driven content collections with a file-based storage adapter by default; optional MongoDB adapter for Pro deployments.
4331
+ - **Markdown shortcodes** - server-side shortcode processing before Markdown parsing:
4332
+ - `[card]` - Domma card component with optional title.
4333
+ - `[grid]` / `[row]` / `[col]` - CSS Grid layout blocks.
4334
+ - `[slideover]` - slide-over panel triggered by a button.
4335
+ - `[dconfig]` - declarative page config (maps to `$.setup()`).
4336
+ - **Password reset flow** - time-limited reset tokens with email delivery via Nodemailer; configurable SMTP in `config/site.json`.
4337
+ - **Media management** - file upload (multipart), image processing via Sharp, media library in admin panel.
4338
+ - **Navigation editor** - drag-and-drop nav builder with dropdown support; persisted to `config/navigation.json`.
4339
+ - **Layouts and presets** - admin-managed layout presets stored in `config/presets.json`.
4340
+ - **Setup wizard** - interactive `npm run setup` script; creates admin account, sets site title and theme; safe to re-run.
4341
+ - **Utility scripts** - `seed` (sample content), `reset` (factory reset), `fresh` (reset + seed), `pro` (Pro upgrade helper).
4342
+ - **CLI scaffolder** - `npx domma-cms my-site` scaffolds a new project from the npm package.
4343
+ - **Configuration architecture** - all config in `config/*.json`; secrets-only `.env`; `getConfig()` / `saveConfig()` API for safe in-process reads and writes.
4344
+ - **Domma dist via npm** - `domma-js` resolved from `node_modules` at runtime; no copy step required.