@actuate-media/cms-admin 0.42.1 → 0.42.4

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 (37) hide show
  1. package/CHANGELOG.md +1508 -0
  2. package/README.md +35 -0
  3. package/dist/__tests__/lib/post-editor-service.test.js +120 -1
  4. package/dist/__tests__/lib/post-editor-service.test.js.map +1 -1
  5. package/dist/__tests__/views/post-section-editor.test.d.ts +2 -0
  6. package/dist/__tests__/views/post-section-editor.test.d.ts.map +1 -0
  7. package/dist/__tests__/views/post-section-editor.test.js +96 -0
  8. package/dist/__tests__/views/post-section-editor.test.js.map +1 -0
  9. package/dist/lib/collection-schema.d.ts +29 -0
  10. package/dist/lib/collection-schema.d.ts.map +1 -0
  11. package/dist/lib/collection-schema.js +54 -0
  12. package/dist/lib/collection-schema.js.map +1 -0
  13. package/dist/lib/page-editor-service.d.ts.map +1 -1
  14. package/dist/lib/page-editor-service.js +2 -32
  15. package/dist/lib/page-editor-service.js.map +1 -1
  16. package/dist/lib/post-editor-service.d.ts +1 -0
  17. package/dist/lib/post-editor-service.d.ts.map +1 -1
  18. package/dist/lib/post-editor-service.js +50 -9
  19. package/dist/lib/post-editor-service.js.map +1 -1
  20. package/dist/lib/posts-service.d.ts.map +1 -1
  21. package/dist/lib/posts-service.js +3 -1
  22. package/dist/lib/posts-service.js.map +1 -1
  23. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
  24. package/dist/views/page-editor/PageSectionEditor.js +5 -2
  25. package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
  26. package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
  27. package/dist/views/post-editor/PostSectionEditor.js +27 -4
  28. package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
  29. package/package.json +5 -4
  30. package/src/__tests__/lib/post-editor-service.test.ts +136 -0
  31. package/src/__tests__/views/post-section-editor.test.tsx +147 -0
  32. package/src/lib/collection-schema.ts +62 -0
  33. package/src/lib/page-editor-service.ts +3 -35
  34. package/src/lib/post-editor-service.ts +54 -8
  35. package/src/lib/posts-service.ts +4 -1
  36. package/src/views/page-editor/PageSectionEditor.tsx +5 -2
  37. package/src/views/post-editor/PostSectionEditor.tsx +49 -2
package/CHANGELOG.md ADDED
@@ -0,0 +1,1508 @@
1
+ # @actuate-media/cms-admin
2
+
3
+ ## 0.42.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 0c3a6eb: Posts UX improvements from external-consumer feedback:
8
+ - New `admin.excludeFromPostsAggregate: true` collection flag keeps a post-shaped collection out of the admin's aggregate Posts area (cross-collection list, type tabs, New Post picker) while leaving its own admin list and API routes fully accessible. Enforced server-side in `GET /posts` and mirrored in the admin client.
9
+ - `blogPostCollection()` now satisfies the admin Post Editor's full field contract out of the box: it gains `sections` (json), `category`, and `publishDate` (replacing `publishedDate` — update seeds/queries that referenced the old key), and `body` is no longer hard-required so section-driven posts can publish.
10
+ - `create-actuate-cms` scaffolds now define `posts: blogPostCollection()` instead of an inline collection, so new projects always start with an editor-compatible posts schema.
11
+
12
+ ## 0.42.3
13
+
14
+ ### Patch Changes
15
+
16
+ - 854152d: Developer experience improvements from external-consumer feedback:
17
+ - Every published package now ships its `CHANGELOG.md` in the npm tarball, and packages that were missing a `README.md` (cli, cms-admin, cms-core, import, platform-vercel, all plugins, sections-react) now have one — so release notes and usage docs are visible on npmjs.com and in `node_modules`.
18
+ - New client-safe form validation entry point: `@actuate-media/cms-core/forms/client` exports `validateFormFieldClient()`, `validateFormValuesClient()`, visibility/conditional helpers, and the exact `FORM_EMAIL_RE` / `FORM_PHONE_RE` regexes the server enforces — zero Node dependencies, so public-site forms can mirror server rules without reimplementing them. The server validator now imports the same primitives, so client and server can never drift.
19
+
20
+ ## 0.42.2
21
+
22
+ ### Patch Changes
23
+
24
+ - 39a2972: Bring the Post editor to parity with the Page editor's data-safety guarantees. Posts and post-type templates now preserve sections whose type has no registered definition (flagged unmanaged, round-tripped verbatim on save) instead of silently dropping them on open; publish validation skips unmanaged sections instead of hard-failing; an "externally managed sections" banner mirrors the Page editor's. Added a schema guard that refuses to open a post whose collection is missing the fields the editor writes (preventing silent content loss on save), and the Page editor's role check now fails closed (read-only) when no role is identifiable, matching the Post editor.
25
+
26
+ ## 0.42.1
27
+
28
+ ### Patch Changes
29
+
30
+ - 211a9ee: Fix unstyled Radix overlays: all admin theme tokens are scoped under `.actuate-admin`, but Radix portals (dialogs, dropdown menus, drawers, selects) defaulted to `document.body` — outside the scope — so portaled surfaces rendered with no tokens (transparent card backgrounds, colorless destructive buttons, broken dark mode). Most visible on the "Block this production site from search engines?" confirmation in Settings. Every `*.Portal` now mounts into the admin root via a shared `adminPortalContainer()` helper, restoring theming, dark mode, and density for all overlays.
31
+
32
+ ## 0.42.0
33
+
34
+ ### Minor Changes
35
+
36
+ - 915f97c: Pluggable admin canvas renderers for custom section types. Pass `sectionRenderers` to `<AdminRoot>` to render custom (config-declared) section types with real React components in the page/post editor canvas and the eye preview, instead of structural placeholders. The renderer signature structurally mirrors `SectionRenderer` from `@actuate-media/sections-react`, so client-safe public-site renderers can be reused as-is. Throwing renderers fall back to the placeholder per section; types with a registered renderer no longer trigger the "Structure view" notice.
37
+
38
+ ## 0.41.0
39
+
40
+ ### Minor Changes
41
+
42
+ - f192376: Page/post editor UX for custom-section installs and revision history:
43
+ - The canvas now shows a "Structure view" notice when a document contains section types rendered as placeholders (custom/unmanaged types), making clear the canvas is structural — not WYSIWYG.
44
+ - "View on site" is promoted to a labeled button (the primary design preview) when custom section types are present, and the eye preview is relabeled "Preview structure".
45
+ - Version history (with restore) is now accessible directly from the page and post editor top bars via a History button. Restoring reloads the editor with the restored content.
46
+
47
+ ## 0.40.1
48
+
49
+ ### Patch Changes
50
+
51
+ - db4c47e: Settings → General clarity fixes for the robots toggles: the "Noindex Non-Production Environments" row now shows an inline note when the current deployment is the production environment (where the toggle cannot change served pages, pointing to Default No Index instead), and the settings save bar shows an "Unsaved changes" hint so a flipped toggle is never mistaken for an applied change.
52
+
53
+ ## 0.40.0
54
+
55
+ ### Minor Changes
56
+
57
+ - 8e73cf5: The admin now loads section types from `GET /page-sections/types` on boot (with the config as a synchronous fallback), so custom types registered server-side are always editable without a separate bundled registry.
58
+
59
+ The section inspector renders a unified `RepeaterField` for `repeater`, `stats`, and `gallery` fields, with drag-and-drop row reordering via `@dnd-kit`, replacing the bespoke stats/gallery editors.
60
+
61
+ ## 0.39.0
62
+
63
+ ### Minor Changes
64
+
65
+ - 99565a1: Page Editor: stop dropping externally-managed sections, support custom types, fix breadcrumb ids (Opal feedback).
66
+ - The flat Page Editor now reads with `preserveUnknown` and round-trips sections whose type isn't registered (e.g. seed/code-owned `opal-*`) on every save, so editing a page never silently overwrites them with `[]`. A banner lists the externally-managed types, the Sections panel shows them locked (no drag/duplicate/delete/visibility), the inspector shows a read-only notice, and the canvas renders a neutral placeholder instead of a blank gap.
67
+ - `AdminRoot` registers `config.sections.customTypes` into the client section registry, so config-declared custom types appear in the Add Section gallery and get a field-driven inspector (editable like built-ins). Publish validation skips unmanaged sections.
68
+ - Breadcrumbs `isId()` now recognizes CUID/CUID2 ids (Prisma `@default(cuid())`), so document routes render as "Edit ..." instead of showing the raw id.
69
+
70
+ - 0158abe: Settings → Appearance branding: add a "Media library" picker to every logo/favicon field so you can reference an existing media asset instead of only uploading. Selecting from the library points the brand field at the existing asset id (no re-upload, no duplicate media). The shared `MediaPickerModal` gained a `libraryOnly` mode, real image thumbnails, and a normalized `url` on the selected item.
71
+
72
+ ## 0.38.1
73
+
74
+ ### Patch Changes
75
+
76
+ - 74662c0: Fix Next.js 16 admin routing and the client/server bundle boundary.
77
+ - The admin router now marks `history.pushState` state with `__NA: true` so Next.js 16's patched `pushState` passes it through natively instead of throwing "Invalid URL" on in-admin navigation.
78
+ - Page-builder runtime utilities are now imported from the client-safe `@actuate-media/cms-core/page-builder` subpath instead of the main barrel, so bundlers (Turbopack) no longer pull server-only dependencies (`pg`, `sharp`, codegen) into the client admin bundle. A regression test guards against reintroducing bare-barrel value imports.
79
+
80
+ ## 0.38.0
81
+
82
+ ### Minor Changes
83
+
84
+ - aa4bd1a: Unify the page content model at write time (ADR 0002). Visual Page-Builder pages now compile their `_layout` tree to the canonical `data.sections[]` on every save, so every reader (SDK, MCP, search, content graph, `/resolve`) sees one contract instead of the lazy read-time bridge.
85
+ - **cms-core**: `createDocument`/`updateDocument` derive `sections` from a `layout` envelope; the `/page-sections/*` write routes refuse a builder-owned page (one with `_layout`) with HTTP 409 to prevent silently-overwritten edits; the `/resolve` bridge is now a legacy fallback for un-migrated pages. New `migratePageBuilderSections()` export backfills legacy pages (idempotent).
86
+ - **cms-admin**: the flat Page Editor opens a builder-owned page read-only with an "Open in Page Builder" action; `EditorPage` gains a `builderOwned` flag.
87
+ - **cli**: new `actuate migrate:sections` command (with `--dry-run` / `--batch-size`) to backfill canonical sections for legacy page-builder documents.
88
+
89
+ ## 0.37.2
90
+
91
+ ### Patch Changes
92
+
93
+ - 2f18c56: WS-E (E2): public form rendering — a built site can now render and submit a CMS form end to end.
94
+ - **sections-react:** new `<ActuateForm>` Client Component fetches the public form
95
+ schema (`GET /api/cms/public/forms/:slug`), renders the fields (text/email/
96
+ url/tel/number/date, textarea, select, checkbox, radio), and submits to the
97
+ schema's `submitPath`. Spam protection is wired automatically: a hidden
98
+ honeypot (keyed from `spam.honeypotFieldKey`), an `elapsedMs` "time to submit"
99
+ timer, and an optional consumer-supplied `captchaToken`. Pre-fetch a schema via
100
+ the `form` prop to skip the round-trip (SSR/streaming). Adds a `baseUrl`
101
+ `RenderOptions` field (defaults to same-origin). Exposes pure, tested helpers
102
+ (`coercePublicForm`, `buildSubmitBody`, `fieldControl`, `firstMissingRequired`).
103
+ - **cms-core:** new canonical `form` section type. Set `content.formSlug` (plus
104
+ optional heading/intro) on any page or post and the matching renderer embeds
105
+ the form. The registry-drift guard now covers it (renderer ↔ type).
106
+ - **cms-admin:** the form Embed panel now shows the real public endpoints
107
+ (`/api/cms/public/forms/:slug` + `/submit`) and a copy-paste `<ActuateForm>`
108
+ snippet, replacing the previously-advertised `/api/forms/*`, iframe, and
109
+ `embed.js` routes that were never implemented. Adds a `mail` section icon.
110
+
111
+ ## 0.37.1
112
+
113
+ ### Patch Changes
114
+
115
+ - a4f7b5d: The login screen now shows the same brand logo as the admin sidebar. Previously
116
+ the login page only read the static `config.admin.branding.logo`, so a logo
117
+ uploaded in Settings → Appearance appeared in the sidebar but not on sign-in.
118
+ `Login` now resolves its logo with the same precedence as the sidebar: the
119
+ uploaded appearance brand (from the cached admin brand the sidebar paints from,
120
+ then the unauthenticated `GET /public/brand` endpoint for fresh browsers), then
121
+ `config.admin.branding.logo`, then the bundled Actuate wordmark. Passing
122
+ `branding.logo === null` still hides the logo entirely for whitelabel installs.
123
+
124
+ The brand lockup's logo-less fallback also changed. The sidebar previously
125
+ rendered a filled site-initials badge when no logo was set, which read as a user
126
+ avatar (the real account avatar already sits top-right). That badge is replaced
127
+ with a neutral "logo slot" placeholder — a dashed outline with a muted image
128
+ icon — shared by the sidebar and login via a new `LogoPlaceholder` component. It
129
+ is automatically superseded the moment a logo is uploaded. The bundled Actuate
130
+ wordmark/mark remains the default for a fully un-branded install; the
131
+ placeholder only appears once the install is branded (a site/brand name is set)
132
+ but has no logo yet. On a fresh browser with no cached brand, the login also
133
+ reads the site initials from `GET /public/brand` so a site-title-only install
134
+ (no static `branding.name`) is still detected as branded pre-auth.
135
+
136
+ ## 0.37.0
137
+
138
+ ### Minor Changes
139
+
140
+ - 7d11b8d: Production-ready Settings → Updates tab with real release intelligence.
141
+ - **cms-core**: Fix a critical permission bug where `GET/PUT /updates/config` and
142
+ `POST /updates/apply` rejected real admins (they compared `session.role` to the
143
+ lowercase `'admin'` while roles are stored uppercase). They now use
144
+ `requireAdminScope`, so OWNER/ADMIN sessions (and admin-scoped API keys) can
145
+ manage updates. `GET /updates/check` no longer leaks the GitHub repo to
146
+ non-admins and reports a `canManage` flag. Update checks now run through a new
147
+ hybrid resolver `getUpdateStatus()` that queries the Actuate update server
148
+ first (rich, real changelog/severity/migration/breaking-change data derived
149
+ from the published CHANGELOG) and transparently falls back to a direct npm
150
+ registry check. New exports: `getUpdateStatus`, `checkForUpdatesViaServer`, and
151
+ the `ChangeType` / `ChangelogItem` / `PluginCompatibilityInfo` types; richer
152
+ `VersionInfo`. `fetchChangelog()` now targets the real `/api/changelog`
153
+ endpoint (overridable via `ACTUATE_UPDATE_SERVER_URL`).
154
+ - **cms-admin**: Rebuild the Updates tab as a dedicated `UpdatesTab` + `useUpdates`
155
+ hook (extracted from `Settings.tsx`). The GitHub integration now uses the shared
156
+ `SettingsControlRow` + sticky `SettingsSaveBar` dirty-state pattern, and the tab
157
+ surfaces real release notes (categorized changelog), a security-update badge,
158
+ "migrations required" and breaking-change callouts, and plugin compatibility.
159
+ Write actions are permission-aware (hidden for non-admins).
160
+ - **cli**: `update-check` / `upgrade` now resolve the update-server base URL from
161
+ `ACTUATE_UPDATE_SERVER_URL` (defaulting to the production host), so the
162
+ `/api/versions` endpoint is configurable for self-hosted update servers.
163
+
164
+ ### Patch Changes
165
+
166
+ - 5ac1d16: Make the Appearance → Branding section visually uniform with the rest of
167
+ Settings. Logo fields (Primary logo, Favicon, and the advanced/admin variants)
168
+ now render as consistent rows with the Upload/Replace control aligned to the
169
+ right edge instead of stacked beneath the description, and the admin override is
170
+ presented as a labelled "Brand source" row with the toggle on the right. Add a
171
+ shared `SettingsControlRow` primitive (leading visual + label/description on the
172
+ left, control on the right) so this rhythm is reusable, route the Branding card
173
+ through `SettingsCard`/`SettingsSubsection`, and sentence-case the Appearance
174
+ section titles ("Admin theme", "Typography & motion", "Brand preview") so the
175
+ whole tab is consistent.
176
+
177
+ ## 0.36.1
178
+
179
+ ### Patch Changes
180
+
181
+ - e92dd1e: Unify the Settings design language across every tab. Introduce a shared
182
+ `SettingsCard` section primitive (title + description + optional header actions)
183
+ plus a `SettingsSubsection` helper, and route the General, Security, SEO, AI,
184
+ Layout, and Updates tabs through it so each section matches the Appearance tab:
185
+ `p-6` cards, a `text-lg` medium title, and a muted `text-sm` description. Tokenize
186
+ the Settings shell header and the Updates panel so they use semantic theme tokens
187
+ (no raw gray/blue/green/red palette classes) and render correctly in dark mode.
188
+ The persistent save bar behavior is unchanged.
189
+ - e92dd1e: Bring the Users, API Keys, and Profile screens onto the shared Settings design
190
+ language. The API Keys view is now wrapped in the `SettingsCard` primitive
191
+ (title + description + header action) and fully tokenized — every raw
192
+ gray/blue/red/green/amber palette class in the table, status pills, empty state,
193
+ and both dialogs was replaced with semantic theme tokens so it renders correctly
194
+ in dark mode. The Users "Team members" section title is bumped to the shared
195
+ `text-lg` section scale, and the Profile page header now uses `text-foreground` /
196
+ `text-muted-foreground` tokens with the standard medium weight.
197
+
198
+ ## 0.36.0
199
+
200
+ ### Minor Changes
201
+
202
+ - 4e6ead2: Add Brand Voice. Admins can define a reusable brand voice (tone, audience, guidelines, and
203
+ sample copy) in Settings > AI. When enabled, the governed AI runtime injects it into the system
204
+ prompt of content-generation features — co-author and SEO titles/descriptions — so every AI
205
+ surface writes in one consistent, on-brand voice.
206
+ - `cms-core`: new `BrandVoiceProfile` type on `AISettings`, a `brand-voice` module
207
+ (`buildBrandVoiceSystemPrompt`, `applyBrandVoice`, `getActiveBrandVoice`, `normalizeBrandVoice`),
208
+ a `useBrandVoice` option on `generateTextForFeature`, and `PUT /ai/settings` persistence
209
+ (length-clamped, no secrets).
210
+ - `cms-admin`: a Brand Voice card in the AI settings tab wired through `useAiSettings`.
211
+
212
+ ## 0.35.0
213
+
214
+ ### Minor Changes
215
+
216
+ - fcede90: Settings > AI: per-feature model routing. Each AI feature can now run on a
217
+ specific model (e.g. a cheap model for alt-text, a frontier model for content
218
+ writing) or inherit the default. Adds `resolveFeatureModel` /
219
+ `resolveAllFeatureModels` to cms-core as the authoritative feature→model
220
+ resolver (capability-aware, with capability-typed and fallback defaults), and
221
+ extends `PUT /ai/settings` to persist + validate per-feature `modelId`/
222
+ `providerId` overrides. The admin AI Features card gains a per-feature model
223
+ picker that filters to capability-compatible models and warns when a routed
224
+ model is missing a required capability (e.g. a text-only model on a vision
225
+ feature).
226
+
227
+ ## 0.34.0
228
+
229
+ ### Minor Changes
230
+
231
+ - 89eebe0: Add Settings > AI (Phases 1-2): DB-backed provider connections + dynamic model catalog.
232
+
233
+ **cms-core**
234
+ - New `src/ai` module: provider connections (OpenAI, Anthropic, xAI, custom OpenAI-compatible), a normalized model catalog, feature flags, and budget settings, all stored in an `__ai_config__` config document (no Prisma migration, mirroring the SEO config-store).
235
+ - Provider API keys are encrypted at rest with `encryptSecret` and never returned to the client — responses expose only a masked `apiKeyLast4` and a source flag. When a provider's env var (`OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / `XAI_API_KEY`) is set, that key wins and is surfaced as env-managed/read-only.
236
+ - SSRF-safe adapters: built-in providers use fixed hostnames; custom base URLs are validated and DNS-resolved via the existing webhook guards.
237
+ - Dynamic catalog merges live `/v1/models` listings with a curated capability/pricing map, with a curated fallback when live listing is unavailable and `unavailable` marking for configured-but-missing models.
238
+ - Monthly usage summary aggregated from existing audit-log token totals (real counts; 0 until per-run metering lands).
239
+ - New ADMIN-gated, audited routes: `GET/PUT /ai/settings`, `POST/PUT/DELETE /ai/providers(/:id)`, `POST /ai/providers/:id/test`, `POST /ai/providers/:id/sync-models`, `GET /ai/models`, `GET /ai/usage/summary`.
240
+
241
+ **cms-admin**
242
+ - Replaced the read-only AI status panel with a full `AISettingsTab`: AI Assistant card (default provider + model selector with capability badges and Refresh), provider connection management (add/test/disconnect, masked or env-managed key handling with confirm dialogs), global AI feature toggles, and a real monthly usage meter with a budget limit.
243
+ - Feature enforcement across modules and per-run usage metering are deferred to later phases; toggles and budget persist now.
244
+
245
+ ## 0.33.0
246
+
247
+ ### Minor Changes
248
+
249
+ - 8dfeb94: Add a **Settings > SEO** tab for site-wide SEO defaults, backed by the shared SEO configuration service (no duplicate state with `/seo` or Settings > General).
250
+ - **cms-core**: extend `SiteSEOConfig` with `titleTemplate`, `defaultMetaDescription`, `defaultSocialImageAssetId`, and a `verification` block; add title-template token validation/application, search-engine verification meta-tag rendering, and wire all three into the public page-meta renderer. New `notifySitemapOnPublish` service performs production-gated, SSRF-safe sitemap regeneration + Google/Bing pings, fired as a best-effort effect when public content is published, updated, unpublished, or deleted. Cache invalidation runs on every change while the timestamp write + outbound ping are debounced (default 60s) so bursts of publishes don't hammer the shared `__seo_config__` row or spam search engines. `PUT /seo/config` and `PUT /seo/crawl-settings` now validate new fields and write field-level audit logs (verification token values redacted).
251
+ - **cms-admin**: new `SeoSettingsTab` (Meta Defaults with SERP/social preview, Search Engine Verification, Sitemap Defaults, full-workspace callout, Advanced disclosure) driven by a `useSeoSettings` hook that reads/writes the shared `/seo/config` and `/seo/crawl-settings` endpoints with dirty tracking, validation, permission-aware read-only states, and dangerous-change confirmation. `MediaPickerModal` gains an `onSelectItem` callback so the default social image stores its asset id.
252
+
253
+ ## 0.32.0
254
+
255
+ ### Minor Changes
256
+
257
+ - 7336b5f: Add content-ownership transfer to Settings > Users.
258
+
259
+ cms-core: new `transferContentOwnership` / `countOwnedContent` helpers reassign
260
+ a member's authored documents, uploaded media, and created templates to another
261
+ active user (immutable version/audit history is never touched). New admin-only,
262
+ audited `POST /users/:id/transfer-content` endpoint validates the recipient
263
+ (must exist and be active) and applies the same Owner/last-Owner guards as the
264
+ other per-user actions. `contentOwnedCount` in the user detail now reflects all
265
+ three content types, not just documents.
266
+
267
+ cms-admin: the user detail drawer Overview now offers "Transfer content" with an
268
+ active-member picker before revoking access. Auxiliary dialogs (transfer,
269
+ confirm, reauth) now render inside the Drawer's Radix focus scope so they are
270
+ keyboard-reachable and no longer marked `aria-hidden` behind the drawer.
271
+
272
+ ## 0.31.0
273
+
274
+ ### Minor Changes
275
+
276
+ - 951963a: Build the Settings > Users access-control system: real invites, roles, seats, and access review.
277
+
278
+ **@actuate-media/cms-core**
279
+ - Schema: add `OWNER` and `VIEWER` to the `UserRole` enum, a new `InviteStatus` enum, a new `Invite` model (`actuate_invites`), and additive user-lifecycle columns (`suspendedAt`, `lockedAt`, `forcePasswordReset`, `mfaRequired`, `failedLoginCount`, `lastActiveAt`, `invitedById`, `acceptedInviteAt`). Two hand-authored migrations (`0011_user_roles_owner_viewer`, `0012_invites_and_user_lifecycle`) — enum values are added in their own migration so Postgres can use them as defaults afterward. All user columns are nullable/defaulted so existing rows migrate cleanly.
280
+ - Add a `roles` module (also exported as the `@actuate-media/cms-core/roles` subpath): display mapping (`OWNER/ADMIN/EDITOR/VIEWER` plus legacy `AUTHOR→Editor`, `CLIENT→Viewer`), assignable roles, privilege ranking, and per-role permission derivation.
281
+ - Add an `Invite` lifecycle service (`createInvite`, `listInvites`, `resendInvite`, `cancelInvite`, `updateInviteRole`, `extendInvite`, `expireStaleInvites`, `markInviteAcceptedForEmail`). Invite tokens are hashed at rest, single-use, and expire (7/14/30 days). Completing the password-reset flow for an invited email now marks the invite `ACCEPTED` atomically.
282
+ - Add a `team` module (`listTeamMembers`, `getUserStats`, `computeAccessReview`) that derives seat usage and access-review recommendations (inactive 90d+, external domain, admin/owner without MFA, stale invite) from real user/security signals.
283
+ - New admin endpoints: enriched `GET /users` (search/role/status filters), `GET /users/stats`, `PATCH /users/:id/role`, hardened `DELETE /users/:id` (revoke = suspend, last-Owner protection), `GET /invites` + `POST /invites/:id/resend|cancel` + `PATCH /invites/:id`, and `GET /access-review` + `POST /access-review/:id/dismiss`. Role assignment/removal of Owner is Owner-only, self-escalation is blocked, role downgrades revoke sessions, and seat limits are enforced. All sensitive actions are audit-logged.
284
+
285
+ **@actuate-media/cms-admin**
286
+ - Rebuild the Settings > Users tab to match the design: dynamic summary cards (members, pending invites, admins, seats used), an actionable access-review banner, a combined team table (active members + pending invites) with role-filter pills and search, per-row role dropdowns and revoke/resend/cancel actions, and an invite modal with role radio cards and advanced options (custom message, expiry). Includes loading, empty, filtered-empty, and error states; destructive and Owner-assignment actions require confirmation.
287
+
288
+ **create-actuate-cms**
289
+ - Sync the scaffold's Prisma schema and migrations with the new Users-management schema so newly created projects start with the `Invite` model, new roles, and user-lifecycle columns.
290
+
291
+ - 951963a: Settings > Users phase 2: per-user detail, sessions, activity, and security actions.
292
+
293
+ **@actuate-media/cms-core**
294
+ - Add a `user-admin` read-model module (`getUserDetail`, `listUserSessions`, `listUserActivity`, `getUserPermissionView`, `parseUserAgent`) that shapes a single user's overview, security posture (MFA / password / lock / SSO-only), sessions (with device parsing + "this device" marker), audit activity (events where the user is actor or target), and derived permissions.
295
+ - New admin-only, audited per-user endpoints: `GET /users/:id`, `GET /users/:id/sessions`, `DELETE /users/:id/sessions/:sessionId`, `POST /users/:id/sessions/revoke-all`, `GET /users/:id/activity`, `GET /users/:id/permissions`, `POST /users/:id/password-reset`, `POST /users/:id/force-password-reset`, `POST /users/:id/mfa-reset`, `POST /users/:id/require-mfa`, `POST /users/:id/suspend`, `POST /users/:id/reactivate`, `POST /users/:id/lock`, `POST /users/:id/unlock`. Owner actions are Owner-only, self-actions and last-Owner removal are blocked, MFA reset and Admin/Owner password resets require password reauthentication, and session-affecting actions revoke sessions.
296
+ - Login now rejects suspended (`suspendedAt`) and locked (`lockedAt`) accounts so an admin Suspend/Lock takes effect on the next sign-in attempt — distinct from full deactivation (`isActive=false`), which preserves the account's content and history.
297
+
298
+ **@actuate-media/cms-admin**
299
+ - Add the User detail drawer opened from a Team members row (and the row "View details" action): Overview, Security, Sessions, Activity, and Permissions tabs. Security actions (send/force password reset, reset/require MFA, lock/unlock, suspend/reactivate, revoke one/all sessions) are wired to the new endpoints with confirmation dialogs and an inline re-authentication prompt that retries with the `x-reauth-password` header when the server requires it. Includes loading, empty, and error states across all tabs.
300
+
301
+ ## 0.30.0
302
+
303
+ ### Minor Changes
304
+
305
+ - f043e18: Fix day-one auth dead-ends and wire up transactional email + user invites.
306
+
307
+ **@actuate-media/cms-core**
308
+ - Add a transactional email sender registry (`setEmailSender`, `getRegisteredEmailSender`, `resolveEmailSender`). Password-reset and form-notification email now resolve a sender via this registry, so apps that configure email purely through `emailPlugin()` actually send (previously only `config.platform.email` was consulted, so reset/notification mail silently never sent).
309
+ - Password reset links are now built from the configured admin path instead of a hardcoded `/admin`, so resets work on apps mounted elsewhere.
310
+ - Add an admin-only `POST /users/invite` endpoint plus `createUserInvite` / `normalizeInviteRole` / `VALID_INVITE_ROLES`. Invites provision the user with an unusable password and a single-use, 7-day token, then email a "set your password" link (reusing the existing `/reset-password` page). When no email sender is configured the response returns the link so an admin can share it manually. Rate-limited per admin.
311
+
312
+ **@actuate-media/cms-admin**
313
+ - `AdminRoot` gains an `onLogout` prop. The account-menu "Logout" item now revokes the server session (`POST /auth/logout`), clears local session state, and returns to the login screen — previously it navigated to a non-existent `/logout` route (404) and left the session valid.
314
+ - The "Invite User" dialog is now a real flow backed by `POST /users/invite` (loading state, error handling, valid roles, optional name) instead of a fake success toast.
315
+
316
+ **create-actuate-cms**
317
+ - The scaffolded admin page now wires `onVerifyTotp` (so MFA users can complete the second-factor step) and `onLogout`, matching the dev app.
318
+
319
+ **@actuate-media/plugin-email**
320
+ - `emailPlugin()` now registers its resolved provider with cms-core's email sender registry, so configuring email via the plugin is sufficient for password-reset, invite, and form-notification email.
321
+
322
+ - 05537a8: Replace the decorative AI settings tab with an honest, read-only status panel.
323
+
324
+ The previous AI tab shipped a fake API-key field (the key was never saved), a no-op "Verify" button, a misleading "encrypted at rest" claim, hardcoded zero usage metrics, and nine feature toggles that were read by nothing. None of it configured AI — that happens server-side via `@actuate-media/plugin-ai` and environment variables.
325
+ - `cms-core`: new admin-only `GET /ai/status` endpoint reporting `{ installed, configured, provider }`. It detects whether the AI plugin is installed and a provider is configured, and hints at which provider env var is present — never exposing key values.
326
+ - `cms-admin`: the AI tab now renders a truthful status panel (active / installed-but-unconfigured / not-installed) plus setup guidance, and no longer pretends to store credentials.
327
+
328
+ ### Patch Changes
329
+
330
+ - e6f0257: Fix the misleading global "Save Changes" footer in Settings. It previously rendered on every tab not in a denylist — including the SEO and Updates tabs, which manage their own persistence. Clicking it there toasted "Settings saved successfully!" while saving none of the tab's edits.
331
+
332
+ The footer is now gated by an explicit allowlist (`GLOBAL_SAVE_TABS`) so it only appears on the Layout tab, which is the only tab that actually persists through the shared `handleSave`. New tabs default to self-managed.
333
+
334
+ ## 0.29.0
335
+
336
+ ### Minor Changes
337
+
338
+ - 008544f: Complete the MFA loop: account enrollment, login challenge, and backup-code recovery.
339
+
340
+ Previously the TOTP backend existed but had no user-facing plumbing — there was no
341
+ way to enroll from the admin, the login screen couldn't complete a second-factor
342
+ challenge, and backup codes were generated but never consumed anywhere (a lost
343
+ authenticator meant permanent lockout). The "Require MFA" policy was therefore
344
+ impossible to satisfy.
345
+
346
+ **cms-core**
347
+ - `POST /auth/totp/login` now accepts a one-time backup code as well as a TOTP code.
348
+ Matches are constant-time and single-use (the code is removed and remaining codes
349
+ re-encrypted); `login_success` audit records whether a backup code was used.
350
+ - New pure helpers `normalizeBackupCode` / `findBackupCodeMatch` in `auth/totp`.
351
+ - `GET /auth/me` now returns `totpEnabled` so the admin can show enrollment state.
352
+
353
+ **cms-admin**
354
+ - New `/profile` view (fixes the previously dead account-menu link) with an
355
+ **Account security** card: password reauth → QR code + manual key + downloadable
356
+ one-time backup codes → verify, plus a reauth-gated disable flow.
357
+ - The login screen now renders a **two-factor challenge** step (authenticator code
358
+ with a backup-code fallback). `AdminRoot`/`Login` gain an `onVerifyTotp` prop and
359
+ `onLogin` may now return `{ requiresTOTP, mfaPendingToken }`.
360
+ - The Security tab's "MFA not set up" warning now links to the enrollment flow.
361
+ - Adds `qrcode.react` for QR rendering.
362
+
363
+ ## 0.28.0
364
+
365
+ ### Minor Changes
366
+
367
+ - 06940a1: Add the Settings → Security tab as a real Security Center.
368
+
369
+ **cms-core**
370
+ - New `@actuate-media/cms-core/security-center` subpath export with pure, client-safe
371
+ helpers: `computeSecurityStatus`, `parseSecuritySettings`, `validateSecuritySettings`,
372
+ `validateIpRange`, `mfaEnableLockoutError`, `summarizeUsers`, `summarizeApiKeys`, and types.
373
+ - New API routes: `GET /security` (computed posture + cross-module summaries +
374
+ secret/encryption verdicts, never secret values), `PUT /security` (persists enforced
375
+ MFA + session policy with server-side lockout-safe MFA enabling, audited as
376
+ `security_settings_changed`), and `POST /security/sessions/revoke-all` (revokes the
377
+ caller's other sessions, audited).
378
+ - Concurrent-session enforcement now honors the stored `security.session.maxConcurrentSessions`
379
+ policy (config/default precedence preserved when unset; `0` = unlimited).
380
+ - Login responses include `mfaSetupRequired` when org-wide MFA is required but the account
381
+ has not enrolled past its grace window.
382
+ - `settings` global writes now audit nested `security` changes.
383
+
384
+ **cms-admin**
385
+ - New Security tab with a computed Security Status card, Authentication & MFA, Session
386
+ Policy (with revoke-other-sessions), Access & Request Protection, and Secrets/Encryption/Audit
387
+ cards. Wired to the real `/security` endpoints with source badges, dangerous-change
388
+ confirmations, lockout-safe MFA toggle, validation, and read-only states for non-admins.
389
+ Replaces the previous decorative security toggles.
390
+
391
+ ## 0.27.0
392
+
393
+ ### Minor Changes
394
+
395
+ - 3667ac2: Add the Settings → Appearance tab: a production-ready brand + admin-theme system.
396
+
397
+ **cms-core**
398
+ - New `@actuate-media/cms-core/appearance` subpath module with pure utilities:
399
+ logo-shape classification, WCAG contrast checks, admin accent-token
400
+ generation, and brand/favicon resolution (admin + public fallback chains).
401
+ - `GET /public/brand` (unauthenticated) resolves public logos + favicon
402
+ metadata for the public frontend; `GET /appearance` (authenticated) returns
403
+ stored appearance/brand settings plus resolved admin/public/favicon views and
404
+ per-asset URLs for editor previews.
405
+ - `PUT /globals/:slug` audit logging now records nested `appearance`/`brand`
406
+ changes.
407
+
408
+ **cms-admin**
409
+ - New Appearance tab (Branding, Admin Theme, Typography & Motion, live Brand
410
+ Preview) wired to the Media Center for logo/favicon uploads (assets referenced
411
+ by ID, never raw paths), with shape warnings, dark-logo warnings, accent
412
+ contrast validation, source badges, and read-only/permission states.
413
+ - `ThemeProvider` now applies color mode, accent color, sidebar density, content
414
+ font, and reduce-motion live, and feeds the resolved admin brand to the
415
+ sidebar logo (with site-initials fallback).
416
+
417
+ ## 0.26.0
418
+
419
+ ### Minor Changes
420
+
421
+ - 58eff9b: Build a production-ready General Settings tab with a computed Site Index Status card.
422
+
423
+ **cms-core**
424
+ - Add `getSiteIndexStatus()` / `computeSiteIndexStatus()` (exported from the SEO module) that derives the effective crawl/index state (indexable / protected / blocked / partially_blocked / unknown) from the SAME effective SEO config the public renderer uses — environment, site URL, robots, sitemap, and last audit — plus a `robotsTxtBlocksEntireSite()` helper.
425
+ - Add `GET /seo/index-status` (ADMIN) returning the computed status, per-check rows, and actions.
426
+ - Audit `PUT /globals/settings` with a per-key before/after diff (`settings_changed`, including source + environment).
427
+ - Migrate the `GET /seo/meta/:documentId` preview to read robots defaults from the effective SEO config instead of the legacy `settings` global, so preview, public render, and General Settings share one source of truth.
428
+
429
+ **cms-admin**
430
+ - Replace the General tab with a fully wired implementation: Site Index Status card, Site Information (with config-managed source badges + live Site URL validation), Language & Region, and SEO & Robots Defaults — the SEO toggles now read/write the shared `/seo/config` (no duplicate store).
431
+ - Add dirty-state Save/Reset bar, validation summary, confirmation dialogs for dangerous indexing changes (production noindex on; non-production noindex off), loading skeletons, error/retry, and read-only gating for non-admins.
432
+ - Use semantic theme tokens (purple `brand` accent, status colors) so the tab matches the design system in light and dark modes.
433
+
434
+ ## 0.25.0
435
+
436
+ ### Minor Changes
437
+
438
+ - 8e38837: Integrate the form block into the page builder (PR7).
439
+ - **Form picker**: block `relationship` fields (the form block's `formId`) now render a real dropdown populated from the linked collection via the admin API, with loading/empty states and a graceful fallback to an id input when the collection can't be loaded. Extracted as a reusable `RelationshipField` component.
440
+ - **Live canvas previews**: the page-builder canvas now renders the rich block-preview registry (with a labelled fallback for unknown block types) instead of a generic dashed placeholder, so editors see what each block will look like.
441
+ - **Form preview**: the form block preview fetches the linked form's name and fields and renders a realistic preview, with a clear "No form selected" empty state prompting the editor to pick a form in block settings.
442
+
443
+ ## 0.24.0
444
+
445
+ ### Minor Changes
446
+
447
+ - b328ec8: Add the global **All Entries** inbox (`/forms/entries`) — a cross-form
448
+ submissions view with a Form column, per-form filter, the same search / status
449
+ pills (All / Unread / Starred / Archived), bulk actions, CSV export, pagination,
450
+ and detail drawer as the per-form Submissions view. The shared submissions
451
+ table is now extracted into `components/forms/EntriesTable.tsx` and reused by
452
+ both views.
453
+
454
+ Expand the **Forms sidebar item into a submenu**: an "All Entries" child plus
455
+ one child per active form, each carrying its own unread badge, with the total
456
+ unread surfaced on the parent (and as a dot on the collapsed rail / in the
457
+ hover flyout).
458
+
459
+ ## 0.23.0
460
+
461
+ ### Minor Changes
462
+
463
+ - 3a641fd: Rebuild the per-form Submissions view as a table (was a card list). Entries
464
+ now load from the real entries API via `forms-service.ts` instead of mock
465
+ attribution data, with summary chips (total / unread / starred / this week),
466
+ search, status pills (All / Unread / Starred / Archived), row selection with
467
+ bulk actions (mark read, star, archive, delete), per-row star toggle, and CSV
468
+ export. Clicking a row opens a token-only detail drawer that renders the
469
+ submission against the schema version it was captured with, surfaces sender
470
+ and attribution, marks the entry read on open, and supports star / archive /
471
+ mark-unread / delete. Adds entries client helpers (`fetchEntries`,
472
+ `fetchEntryCounts`, `fetchEntry`, `updateEntry`, `deleteEntry`,
473
+ `bulkUpdateEntries`) and replaces the previous raw-palette styling with theme
474
+ tokens throughout.
475
+
476
+ Add a reusable token-only `FormsPagination` footer (matching the Posts/Pages
477
+ list controls) and wire it into both Forms tables: the Submissions view now
478
+ paginates server-side (25 per page), and the Forms list paginates the filtered
479
+ results (10 per page). Both reset to page 1 when search or status filters
480
+ change.
481
+
482
+ The per-form notification toggles (Forms list "Notify" column and the schema
483
+ drawer Notifications tab) now read green when enabled and red when disabled
484
+ using the semantic success / destructive tokens.
485
+
486
+ ## 0.22.0
487
+
488
+ ### Minor Changes
489
+
490
+ - 9ed05b2: Forms admin — list view, schema drawer, and live sidebar badge (PR 3/9).
491
+ - Rebuilt the Forms list view to match the design: dynamic summary chips
492
+ (forms / active / total entries / unread) sourced from `/forms/stats`,
493
+ search across name/slug/description/field labels, Active/Draft/Archived
494
+ filter pills, and a compact table (Form / Fields / Status / Entries / Last
495
+ Entry / Notify / Schema) with field-type and status badges. The notify
496
+ toggle persists optimistically and rolls back on error.
497
+ - Added the right-side Form Schema drawer with Fields, Embed, and
498
+ Notifications tabs. The Fields tab is a keyboard-accessible drag-to-reorder
499
+ (dnd-kit) editor; saving creates a new schema version server-side so
500
+ historical submissions are never rewritten. Embed exposes copyable
501
+ public/schema endpoints, iframe/script snippets, and domain restrictions.
502
+ Notifications persists per-form recipients, templates, field inclusion, and
503
+ autoresponder settings (delivery wiring lands in a later phase).
504
+ - Added a typed `forms-service.ts` client (re-using `@actuate-media/cms-core`
505
+ domain types) and a tiny `forms-events` bus so the sidebar refreshes counts
506
+ after a mutation.
507
+ - The Forms sidebar item now shows a live unread badge (total unread across
508
+ active forms), rendered as a pill when expanded and a dot in the collapsed
509
+ rail. `NavItem` gained an optional `badge` field.
510
+
511
+ ## 0.21.1
512
+
513
+ ### Patch Changes
514
+
515
+ - 8891547: Scope the SEO surfaces to real front-end content and keep the admin out of search indexes.
516
+ - **cms-core**: The Content SEO list, audit, scan, internal-link candidates, **and the XML sitemap** now only include documents from collections explicitly typed `page` or `post` (visible and sitemap-eligible). Structural/utility collections (navigation menus, footers, tags, templates) — which have no `type` — and ad-hoc rows whose collection isn't declared in the config (e.g. benchmark/test data) are excluded, so a menu can no longer leak into the sitemap as an indexable URL. Adds the `frontEndContentCollectionTypes(config)` helper and threads a `collectionTypes` option through `gatherAuditEntities` / `runAndPersistAudit`; `processSeoScan` gains an `allowedCollections` option. The `POST /seo/scan` route now reuses `processSeoScan` (also fixing its case-sensitive `alt=`/`<h1` checks).
517
+ - **cms-admin**: `AdminRoot` now ensures a `noindex, nofollow` robots meta tag while the admin is mounted, so private admin UI is never indexed regardless of how the consumer renders the admin page.
518
+
519
+ - 97df7f4: Improve sidebar submenu UX. When the sidebar is collapsed, parent items with children (Posts, Templates) now reveal their sub-pages in a hover/focus flyout (portaled via Radix so it escapes the rail's overflow), keeping the 80px rail clean while staying fully reachable by mouse and keyboard. When expanded, child rows now connect to their parent with curved-elbow tree connectors for clearer hierarchy. Expanded parent sections are now collapsible — a chevron toggle contracts/expands the submenu (with `aria-expanded`/`aria-controls`), while the active route's section stays open so the current page is never hidden.
520
+
521
+ ## 0.21.0
522
+
523
+ ### Minor Changes
524
+
525
+ - aa9797d: Consolidate Users and API Keys into the Settings view as dedicated tabs. The
526
+ sidebar Settings group now shows only "Settings"; Users and API Keys are
527
+ reachable as tabs (`/settings?tab=users`, `/settings?tab=api-keys`). The legacy
528
+ `/users` and `/api-keys` routes still resolve and land on the matching tab, and
529
+ Settings tabs are now deep-linkable via the `?tab=` query param (with
530
+ browser back/forward support). The `Users` and `ApiKeys` views gain an optional
531
+ `embedded` prop that drops page padding and demotes the heading when rendered
532
+ inside the Settings shell.
533
+
534
+ ### Patch Changes
535
+
536
+ - a8976e2: SEO admin UI reliability and accessibility fixes:
537
+ - `useSeoResource` now ignores stale, out-of-order fetch responses (request-id
538
+ guard + unmount guard), so rapidly switching SEO views no longer flashes data
539
+ from a superseded request.
540
+ - `SeoEditorDrawer` Field wrapper auto-associates each `<label>` with its child
541
+ control via `useId`, fixing WCAG label/`for` associations for the SEO meta
542
+ inputs.
543
+
544
+ - 1bd4a49: SEO production-readiness Polish tier — post-launch nice-to-haves from the SEO review.
545
+
546
+ cms-core:
547
+ - **Markdown-aware heading analysis** — `analyzeContent` and `countH1` now detect
548
+ Markdown ATX (`#`/`##`) and Setext (`===`/`---`) headings in addition to HTML
549
+ tags, so markdown-bodied posts are no longer wrongly flagged "No H1 found" /
550
+ "no subheadings".
551
+ - **Correct default OG image URL** — the auto-generated `og:image` now points at
552
+ `/api/cms/og.png` (where the endpoint is actually mounted) instead of `/og.png`,
553
+ which 404'd for social crawlers.
554
+ - **Consistent environment resolution** — `resolveSeoEnvironment()` centralises the
555
+ production gate, preferring `VERCEL_ENV` over `NODE_ENV` everywhere (preview
556
+ deploys report `NODE_ENV=production`), so `noIndexNonProduction` reliably
557
+ noindexes preview deployments.
558
+ - **Admin route boundary** — the CMS middleware matches `/admin` and `/api/cms` on
559
+ a path-segment boundary, so sibling public routes like `/administrators` or
560
+ `/admin-news` are no longer treated as admin routes.
561
+ - **AI status semantics** — SEO AI endpoints now return `503` when the provider is
562
+ installed-but-unconfigured (vs `501` when the plugin is genuinely missing).
563
+ - Removed a duplicated `gradeForScoreBand` helper in favour of the canonical
564
+ `gradeForSeoScore`.
565
+
566
+ cms-admin:
567
+ - SEO filter chips use accessible toggle-button semantics (`aria-pressed`) instead
568
+ of misused `role="tab"`.
569
+ - Crawl-settings toggles use the Radix `Switch` primitive and are disabled while a
570
+ save is in flight.
571
+ - Score progress bars expose an accessible name and `aria-valuetext`; drawers
572
+ without a description no longer trigger Radix's missing-description warning.
573
+ - Redirect CSV export defers object-URL revocation (and mounts the anchor) so the
574
+ download isn't cancelled before it starts.
575
+ - The SEO AI service treats both `501` and `503` as the graceful "AI unavailable"
576
+ state.
577
+
578
+ - 90f53a6: Fix SEO Operations Center production-readiness blockers.
579
+ - **SSRF (`GET /seo/summary`)**: the link-health probe now uses `safeFetch`
580
+ (blocks private/loopback IPs, no redirect following), only probes same-origin
581
+ internal links, skips all probing when `NEXT_PUBLIC_SITE_URL` is unset, and is
582
+ gated to EDITOR+ — matching `/seo/link-health`.
583
+ - **`GET /llms.txt` leak**: now excludes soft-deleted documents and
584
+ internal/system collections (`__seo_config__`, `__*`, etc.) via the shared
585
+ `isInternalCollection` helper, so "deleted" and internal content can no longer
586
+ appear on a public surface.
587
+ - **`PUT /seo/content` data-layer bypass**: SEO field edits now route through the
588
+ new `updateDocumentSeoFields` action, which enforces the collection's
589
+ `access.update` rule, snapshots a `Version` row, recomputes the search index,
590
+ and fires the `afterUpdate`/cache-invalidate effects — while preserving the
591
+ undeclared SEO convention keys the generic field-access layer would strip.
592
+ Inbound SEO fields are now type- and length-validated.
593
+ - **cms-admin**: SEO read fetchers in `seo-service` now re-throw on `cmsApi`
594
+ errors so the error/retry UI works instead of rendering a misleading empty
595
+ fallback (e.g. a fake score of 0 / grade "Critical") on a backend failure.
596
+
597
+ ## 0.20.0
598
+
599
+ ### Minor Changes
600
+
601
+ - ea35f7b: Ship only the canonical **Blog** Quick Post Type preset on the New Post Type screen. The previous demo-oriented presets (Case Studies, News, Portfolio, Events, Team) were specific to the reference site and are not appropriate npm defaults — equivalent content types remain available as opt-in collection factories in `@actuate-media/cms-core` (`collections/presets.ts`).
602
+ - 5fa84d3: Media Library improvements:
603
+ - Grid cards now show a persistent footer (filename, type · size, SEO-status dot) instead of a hover-only overlay, and rows pack to their natural height (fixes the large vertical gaps).
604
+ - Alt Tag and Title "AI Generate" now call the real `/ai/media-metadata` endpoint, falling back to a basic filename-derived suggestion when AI isn't configured (replacing the previous fake `setTimeout` generators).
605
+ - Removed the focal-point picker from the detail drawer to reduce clutter (the `focalPointX/Y` columns are retained in the schema).
606
+
607
+ - 5fa84d3: SEO Operations Center — a full detect → prioritize → explain → route → fix → audit → generate workflow.
608
+
609
+ **cms-core**
610
+ - Additive `0009_seo` migration (mirrored to `apps/dev`): `SEOAuditRun`, `SEOIssue` (+ severity/status/category enums), `Redirect` extensions (`hits`, `lastHitAt`, `enabled`, `priority`, `preserveQuery`, `source`), `Redirect404Hit`, and `RedirectSuggestion`. All columns are nullable or defaulted, so it is safe to apply to an existing database.
611
+ - Pure, explainable scoring model (`seo/score.ts`): `calculatePageSeoScore` / `calculateSiteSeoScore` with documented weights and breakdowns.
612
+ - Persistent audit engine (`seo/audit-engine.ts`) with cross-run issue lifecycle (open / resolved / ignored / recurring) and a redirect engine (chain/loop detection, safe flatten, hit tracking, 404 logging, gated AI 404 recovery).
613
+ - New endpoints: `POST /seo/audit`, `GET /seo/audits[/:id]`, `GET /seo/issues[/:id]`, issue `resolve`/`ignore`/`reopen`/`fix`, `GET /seo/overview`, redirect CRUD + import/export + chains/loops/flatten/suggest, technical settings (`/seo/sitemap/*`, `/seo/crawl-settings`, `/seo/robots`), and approval-gated `/ai/seo/*` (generate-field, explain-issue, generate-schema, summarize) that degrade gracefully with 501 when `plugin-ai` is unconfigured.
614
+ - `/resolve` now returns Next.js-ready `metadata` plus validated JSON-LD; public `robots.txt` honors the admin-edited body.
615
+
616
+ **cms-admin**
617
+ - Rebuilt `SEO.tsx` into a five-tab Operations Center (Overview / Content / Technical / Redirects / Audit) with `?tab=` deep-link routing, dynamic badge counts, a typed `seo-service.ts` client, reusable token-only primitives and a focus-trapped drawer, editor/fix/copilot drawers, and full loading / empty / error / permission states.
618
+
619
+ **mcp-server**
620
+ - New SEO tools: `seo_overview`, `run_seo_audit`, `list_seo_audit_runs`, `list_seo_issues`, `get_seo_issue`, `set_seo_issue_status`, `list_redirect_rules`, `seo_redirect_overview`, `seo_redirect_chains`.
621
+
622
+ **create-actuate-cms**
623
+ - Scaffolded projects now ship a redirect-resolving `middleware.ts` backed by the database redirect table.
624
+
625
+ ## 0.19.0
626
+
627
+ ### Minor Changes
628
+
629
+ - 9c38720: Remove the unrouted legacy `Posts` and `PostEditor` view exports. These were superseded by the section-based `PostsListView` / `PostSectionEditor` (wired into `AdminRoot`), were never reachable through the admin router, and their internal navigation was broken. The live Posts surface is unaffected.
630
+
631
+ Note: although removing public exports is technically breaking, these components were pre-1.0 and non-functional in practice, so this is released as a minor rather than forcing a 1.0. Consumers importing `Posts` / `PostEditor` directly (none expected) should switch to the routed admin shell.
632
+
633
+ ### Patch Changes
634
+
635
+ - d95c8e7: Post editor: edit the body as rich text, pick a featured image from the media library, and keep the body WYSIWYG.
636
+ - The post details modal now edits the `richText` Body with the shared rich-text editor (was a plain textarea, which lost formatting/newlines since the body renders as HTML) and picks the `media` Featured image via the media library browser (was a raw URL field only).
637
+ - The admin Article Body section now renders the body as sanitized HTML, matching the public site, so the editor canvas and preview are true WYSIWYG.
638
+ - The preview tab's Close button now falls back to the editor when the tab can't be closed programmatically (in-app navigation), instead of doing nothing.
639
+
640
+ - 5567daf: Post templates: seed sensible defaults and route the admin through the dedicated endpoint.
641
+ - A post type with no saved template now returns the default starting template (centered header + article body + related posts) instead of an empty one, so new posts and the template editor are never blank. Applied on the server (`GET /post-templates/:type`) and defended client-side.
642
+ - The admin post-editor service now reads/writes templates through the dedicated, audited `/post-templates/:type` endpoints (scoped + logged) instead of the raw `post-templates` collection API.
643
+
644
+ - 1768a8a: Render the post type's template header on the public site so the live page matches the editor canvas and preview.
645
+ - `GET /resolve` now returns `template.header` (a normalized `PostHeaderConfig`) for post-type documents, resolved from the post type's `post-templates` doc at read time. The template stays the single source of truth, so editing it restyles every post of that type without re-saving each post.
646
+ - New `computeReadingTime()` export drives the header's "min read" figure from the post body (200 wpm, HTML stripped, min 1 minute) instead of the previous hardcoded "5 min read".
647
+
648
+ - 01bba26: Fix posts list bulk actions silently no-op'ing across pages. Selection is now scoped to the rows visible on the current page and is cleared on page change, so the bulk-action bar count always matches the posts a publish/delete will actually touch.
649
+ - bf799ac: Posts editor polish: the post editor's permission gate now fails closed when no
650
+ role is present (the server already enforces access independently); typed the
651
+ `config` / `session` props on `PostSectionEditor` instead of `any`; removed a
652
+ dead category branch and an arbitrary brand color (now the `primary` token) in
653
+ the post header / empty-state; and corrected stale doc comments in
654
+ `NewPostPicker` and `PostTypesView`.
655
+ - d85e89e: Add a server-side aggregate `GET /posts` endpoint for the admin Posts area.
656
+ Filtering, sorting and pagination now run at the database level (one query plus
657
+ one count) and respect per-collection read scope, so the Posts list scales past
658
+ the old ~500-row-per-type client cap instead of pulling every document into the
659
+ browser. The admin `fetchPosts` service now calls this endpoint; its public
660
+ `Post` / `PostType` / `PostAuthor` shapes are unchanged.
661
+
662
+ ## 0.18.0
663
+
664
+ ### Minor Changes
665
+
666
+ - d649ba4: Pages production-readiness hardening:
667
+ - **Accurate counts:** the Pages list header now reports the true page total (`grandTotal`) instead of the sum of tag counts, which silently excluded untagged pages.
668
+ - **Fewer/lighter fetches:** the list fetches the dataset once and applies search, sort, group, and paging in memory, so typing or re-sorting no longer re-downloads every page document (also removes a double-fetch on tag toggle). New `fetchPagesDataset()` / `PagesDataset` exports back this.
669
+ - **Data-loss guard:** the page editor detects a misconfigured `pages` collection (missing `sections`/`path`/`metaTitle`/`metaDescription`) and refuses to operate with a clear message instead of silently dropping content on save. New `missingPageFields()` export.
670
+ - **Accessibility:** `aria-sort` moved from the sort button to the column header (`<th>`); the bulk tag menu is now a Radix `DropdownMenu` (keyboard nav, Escape, focus return) instead of a hand-rolled dropdown.
671
+ - **Reliability:** bulk publish/draft/tag/delete now run with bounded concurrency (no longer one-at-a-time), tolerate per-row failures, and surface a real error toast on total failure.
672
+ - **Preview:** the standalone preview "Close" button falls back to the editor when the tab wasn't script-opened.
673
+
674
+ Adds regression tests for the API mapping/write payloads, `fetchPages` counts/pagination, bulk concurrency, the schema guard, and the list view.
675
+
676
+ ## 0.17.0
677
+
678
+ ### Minor Changes
679
+
680
+ - 4fb0097: Add semantic theme tokens and migrate the Dashboard off raw palette colors.
681
+ - New theme tokens in `theme.css` (light + dark + `@theme inline`): `success`, `warning`, `info` (each with a `-foreground`), and a `brand` accent capturing the admin's violet. Values are tuned so the token reads as accessible TEXT on both the card surface and its own `/10` tint, and as a solid fill (dots, bars). This is the foundation for migrating the rest of the admin off hard-coded Tailwind palette colors.
682
+ - `Dashboard.tsx` now uses these tokens for all status, content-health, delivery, banner, and brand-accent styling, and maps the decorative stat-card accents to the sanctioned `chart-1..5` categorical tokens. Error states intentionally remain on raw `red-*` because the existing `--destructive` dark value is too dark to use as text — promoting error to a token is a separate, admin-wide change.
683
+
684
+ No intended visual change to semantic/brand styling; the stat-card icon accents adopt the chart palette.
685
+
686
+ - 4fb0097: Dashboard performance: cache the expensive SEO compute and reduce refetch churn.
687
+ - **cms-core:** `/stats` now caches its average-SEO-score computation per content-scope for a short TTL (60s). That score otherwise reads and scans the `data` JSON of up to 200 published documents on every dashboard load — the heaviest part of the endpoint. On a warm cache the read is skipped entirely; transient DB failures are not cached. Module-level so it survives across warm requests; a cold start recomputes once.
688
+ - **cms-admin:** `useApiData` gains an opt-in `cacheMs` option. When set, successful responses are cached in-memory by endpoint and reused on the next mount (cache-first), so views that remount frequently don't re-issue identical requests. `refetch()` always bypasses the cache. Default behaviour is unchanged for existing callers. The Dashboard enables a 30s cache on its four requests, so navigating away and back no longer re-fires them; first load and independent streaming are preserved.
689
+
690
+ ## 0.16.1
691
+
692
+ ### Patch Changes
693
+
694
+ - 8a51783: Dashboard production-readiness fixes.
695
+ - **Fix:** Recent Activity and Publishing Queue rows now open documents in the correct editor. Post-type documents route to the section-based post editor (`/posts/:type/:id/edit`) instead of the legacy generic document editor, which has no concept of the section model and would silently strip a post's `sections` on save. Pages route to the full page editor (`/pages/:id/edit`).
696
+ - **Accessibility:** Clickable Recent Activity, Publishing Queue, and Content Health rows are now real `<button>` elements — keyboard-reachable and focusable with a visible focus state — instead of click-only `<li>`s.
697
+ - **UX:** The Recent Activity "View all" control is relabeled to "Show more / Show less" (it expands in place, capped at the server's 20-item limit) and is hidden when there's nothing more to show. The greeting and date now refresh on a 60s ticker so a long-open dashboard no longer reads "Good morning" all afternoon, and relative activity timestamps stay current.
698
+ - A document opened from the dashboard via the bare `/pages/:id` route now receives the preview handler, matching the `/pages/:id/edit` route.
699
+
700
+ Adds `useApiData` unit tests (load / null endpoint / exponential-backoff retry + exhaustion / refetch recovery) and Dashboard render tests covering loading, empty, and exhausted-error states plus the editor-routing regression above.
701
+
702
+ ## 0.16.0
703
+
704
+ ### Minor Changes
705
+
706
+ - 91885de: Add a section-based visual Page Editor for the Pages area.
707
+
708
+ `/pages/new` and `/pages/:id/edit` now open a flat, section-based editor (a
709
+ left **Sections** panel + scaled preview canvas + slide-over inspector)
710
+ instead of the tree-based page builder. Pages are an ordered list of
711
+ `PageSection`s persisted as JSON on the page document, so there is no schema
712
+ change.
713
+
714
+ Highlights:
715
+ - Sections panel with accessible drag-and-drop reorder (dnd-kit, keyboard
716
+ support), visibility toggle, duplicate, and delete-with-confirmation.
717
+ - Top bar with breadcrumb, live status indicator, real viewport presets
718
+ (4K / 1080p / Laptop / Tablet / Mobile) that scale the preview canvas,
719
+ Save Draft, and Publish.
720
+ - Shared `SectionRenderer` (Hero Banner, Text, Image + Text, By the Numbers,
721
+ CTA Banner) used by both the editor canvas and the chrome-free
722
+ `/pages/:id/preview` route, so the preview matches what ships.
723
+ - Schema-driven inspector for content/appearance/advanced settings, media
724
+ picking, draft/publish validation, unsaved-changes guards, permission
725
+ gating, and loading/empty/error states.
726
+
727
+ - 91885de: Add the Pages management area and restructure the sidebar.
728
+ - **Pages list** (`/pages`): table matching the design reference — search, group-by (None / Tag / Status / Author), status filter, sortable columns (Title / Author / Status / Last Modified / SEO), a tag-chip filter bar (OR semantics), bulk actions (publish, draft, add/remove tag, delete with homepage protection), and pagination. Loading / empty / filtered-empty / error states included.
729
+ - **Page tags**: DB-backed tag system. Tags are documents in a hidden `page-tags` collection; pages reference them by slug in `data.tags` with an optional `data.primaryTag`. New `pages-service` (`fetchPages`, `fetchPageTags`, `applyPagesQuery`, `groupPages`, bulk page/tag mutations, tag CRUD + merge) keeps filtering/grouping logic pure and testable.
730
+ - **Tag manager** (`/pages/tags`): create, rename, recolor, merge, and delete tags (detaching them from pages on delete).
731
+ - **Sidebar**: Templates is now its own top-level Content item with **Saved Sections** and **Widgets** children; Pages no longer nests those. Adds a `/widgets` placeholder view.
732
+ - Reserves the `/pages` and `/widgets` route namespaces and wires `/pages/new`, `/pages/:id/edit`, and `/pages/tags*`.
733
+
734
+ Note: page seed/mock data lives only in the private `apps/dev` package and is never published.
735
+
736
+ - 91885de: Add the Posts admin area (All Posts + Post Types).
737
+ - `/posts` aggregates every collection marked `type: 'post'` (or
738
+ `admin.group: 'Posts'`) into a single sortable, filterable table
739
+ with type tabs, search, status/author filters, bulk actions
740
+ (publish / move to draft / delete), and pagination. Counts come
741
+ from the data layer, not constants.
742
+ - `/posts/types` shows a card grid per post-type collection with
743
+ accent border, icon, description, post count, field count, and
744
+ URL base — plus a dashed "+ New Post Type" card linking to setup
745
+ instructions.
746
+ - `/posts/new` lands on a type picker before opening the existing
747
+ per-collection editor.
748
+ - `/posts/types/new` offers a **Quick Post Type** gallery (Case
749
+ Studies, News, Portfolio, Events, Team) that pre-fills a tailored
750
+ field set, icon, and accent color, plus a manual form — both feed a
751
+ paste-ready `actuate.config.ts` snippet generator. Collections
752
+ remain code-defined because the read-only serverless filesystem
753
+ rules out runtime config writes.
754
+ - Sidebar collapses every post-type collection under a single
755
+ "Posts" parent with fixed `All Posts` / `Post Types` children.
756
+ - `/api/cms/collections` now surfaces `admin.description`, `icon`,
757
+ `color`, and `group` so the admin client can render type cards
758
+ and tabs without a separate fetch.
759
+ - New `admin.color` field on `CollectionDefinition` (accent token).
760
+
761
+ V1 deliberately skips an in-app Post Type editor / field builder —
762
+ the cards click through to the existing per-collection list and
763
+ new Post Types are added via the snippet generator. A follow-up PR
764
+ introduces a DB-overlay for visual metadata (icon / color /
765
+ description) editable from the admin UI.
766
+
767
+ - 91885de: Add a design-driven Posts template system that mirrors the Pages section editor.
768
+ - **cms-core**: tag section types with a `scope` (`page` | `post`), add post-oriented section types (`article-body`, `quote`, `author-bio`, `related-posts`, `gallery`), `PostHeaderConfig` / `PostTemplate` types, the `getSectionTypesForScope` helper, and new field types (`richText`, `boolean`, `relationship`, `gallery`). Adds `GET/PUT /post-templates/:type` API routes and a `?scope=` filter on `GET /page-sections/types`.
769
+ - **cms-admin**: new `PostSectionEditor`, `PostTemplateEditor` (with header panel), `PostPreview`, post section renderers, and a `post-editor-service` data layer. The Add Section gallery now filters by scope, and post type cards expose an "Edit template" action.
770
+ - **mcp-server**: scope-aware `list_section_types`, plus `get_post_template` and `save_post_template` tools so an external AI agent can read and build a post type's template.
771
+
772
+ - 91885de: Page and Post previews now open in a new browser tab as standalone, chrome-free pages.
773
+ - The editor "Preview" action opens `/{basePath}/…/preview` in a new tab (popup-safe: the tab is opened synchronously within the click and pointed at the URL after any pending draft save resolves), so the preview reflects the latest saved content.
774
+ - Preview routes render without the admin sidebar/header, so a previewed page looks like the real published page.
775
+ - The slim preview bar is now tab-scoped: "Close" (closes the tab) and "Open editor".
776
+ - `useAdminRouter` exposes `buildHref(path)` for constructing absolute, basePath-aware admin URLs without navigating.
777
+
778
+ - 91885de: Add a "View on site" editor action that previews a draft through the real public front-end.
779
+ - Page and Post editors gain a "View on site" button (shown once the document is saved). It mints a signed, time-boxed preview token (`POST /preview/token`), then opens the document's real public URL — `<urlPrefix>/<slug>?preview=<token>` — in a new tab (popup-safe, save-first when dirty). The public route drops the `PUBLISHED` filter for just that document, so unpublished drafts render at their canonical URL.
780
+ - New `lib/preview-link.ts` helpers: `createPreviewToken`, `buildPublicPreviewUrl`, and `resolveSiteUrl` (config `seo.siteUrl` → current origin).
781
+ - The public site URL is taken from `config.seo.siteUrl`, falling back to the current origin, so same-origin installs work with no extra config.
782
+
783
+ This builds on the new-tab preview: the in-admin `/preview` route remains the always-available fidelity preview, while "View on site" renders through the consumer's actual page components when a public route exists.
784
+
785
+ ### Patch Changes
786
+
787
+ - 91885de: Add a section-aware bridge so AI agents (e.g. in Cursor / VS Code) can build pages section-by-section through MCP, producing real, editable, publishable Page Editor sections rather than disposable markup.
788
+ - **cms-core**: new `@actuate-media/cms-core/page-sections` subpath exporting the flat Page Editor section registry, pure section-array helpers, and a deterministic `validateSectionContent`. This is now the single source of truth shared by the admin editor, the REST API, and MCP. Adds REST routes: `GET /page-sections/types`, `POST /page-sections/validate`, and section CRUD over a page document's `data.sections` (`GET|POST /page-sections/:collection/:id`, `PATCH|DELETE /page-sections/:collection/:id/:sectionId`, `POST /page-sections/:collection/:id/reorder`). Mutations go through the audited `updateDocument` path (versions, hooks, cache invalidation, live-preview push) and reuse collection scope checks. Writes are rejected on collections that don't declare a `sections` JSON field.
789
+ - **mcp-server**: nine new tools — `list_section_types`, `get_section_schema`, `validate_section_content`, `list_page_sections`, `add_page_section`, `update_page_section`, `reorder_page_sections`, `remove_page_section`, `toggle_page_section_visibility`.
790
+ - **cms-admin**: the Page Editor section registry and section helpers now re-export from `@actuate-media/cms-core/page-sections`; per-section publish validation is delegated to the shared `validateSectionContent`. No behavior change — internal refactor for a single source of truth.
791
+
792
+ - 91885de: Sidebar: map the `navigation`, `menu`, and `list-tree` `admin.icon`
793
+ identifiers to Lucide icons so navigation/menu collections (e.g.
794
+ `@actuate-media/plugin-navigation`'s `navigations` collection) render
795
+ with a meaningful icon instead of the generic document fallback. Any
796
+ collection tagged `admin.group: 'Content'` already surfaces as its own
797
+ top-level Content nav item; this just gives "Menus"-style collections the
798
+ right glyph.
799
+
800
+ ## 0.15.0
801
+
802
+ ### Minor Changes
803
+
804
+ - 560d1de: Dashboard redesign + real-data backends.
805
+
806
+ `@actuate-media/cms-admin`:
807
+ - Sidebar now uses explicit Content and Settings group headers with nested
808
+ Pages children (Saved Sections, Templates). The collapse control moved
809
+ to the sidebar footer.
810
+ - Header hoists primary page actions (e.g. "+ New Post", "View Site")
811
+ into the top utility bar so the page hero is no longer the source of
812
+ truth for those affordances. The notification bell is wired to the
813
+ real `NotificationBell` component.
814
+ - Dashboard greeting picks the user's first name, and the "Delivery API"
815
+ - "Content Health" cards now read live data from new endpoints.
816
+
817
+ `@actuate-media/cms-core`:
818
+ - New `ApiRequestMetric` table (minute-bucketed counter + log-scale
819
+ latency histogram) and `recordApiRequest` instrumentation wired into
820
+ `handler-factory.ts`. Writes are fire-and-forget — request paths are
821
+ never blocked on the metric DB.
822
+ - New `ContentIssue` table + nightly `processContentHealthScan` cron
823
+ (`/api/cms/cron/content-health`) that runs a pure-logic scanner for
824
+ missing meta descriptions, missing alt text, outdated content
825
+ (>90 days), and broken internal links via HEAD probes.
826
+ - New endpoints `GET /api/cms/stats/api-delivery` and
827
+ `GET /api/cms/stats/content-health` return the dashboard's
828
+ aggregates with stable, never-undefined response shapes.
829
+ - Migration `0007_dashboard_metrics` adds both tables with the indexes
830
+ the new queries need. Safe to apply to populated DBs (no backfill).
831
+
832
+ `create-actuate-cms`:
833
+ - Scaffolded `vercel.json` now includes the nightly content-health
834
+ cron entry, and the deployment guide documents it.
835
+
836
+ ## 0.14.1
837
+
838
+ ### Patch Changes
839
+
840
+ - 40d48da: Two flake fixes from the OSS maintenance pass:
841
+ - **`NotificationBell` SSE/REST race.** The `seenIdsRef` lookup index
842
+ was reconciled with `items` via `useEffect([items])`, which runs
843
+ after React commits. An SSE push that arrived between a REST
844
+ refresh resolving and the effect running would read a stale ref,
845
+ fall through the de-dup gate, and append a duplicate row. The ref
846
+ is now maintained synchronously at every items-mutation call site,
847
+ and the SSE handler also re-checks the latest snapshot inside the
848
+ `setItems` updater as a final safety net.
849
+ - **`compareToDummyHash` eager-init test determinism.** Exported a
850
+ test-only `_waitForDummyHashReady()` so the timing-parity test can
851
+ measure verify-only latency (rather than racing the eager hash
852
+ precompute on a cold module). Added a paired test that asserts the
853
+ eager-init promise resolves with no work pending on the consumer
854
+ side — catches future refactors that accidentally drop the
855
+ top-level invocation.
856
+
857
+ ## 0.14.0
858
+
859
+ ### Minor Changes
860
+
861
+ - a0f6eba: Rich `@`-mention picker for comments (last Phase 7 carry-over).
862
+
863
+ **`cms-core`**: New `GET /api/cms/users/search?q=<query>` endpoint —
864
+ authenticated (any role), case-insensitive `contains` matching against
865
+ `name` + `email`, capped at 10 results, returns only `{ id, name,
866
+ email }` (no role / hash / audit metadata). Empty / whitespace
867
+ queries return an empty array so the endpoint can't be used as a
868
+ directory-enumeration tool.
869
+
870
+ **`cms-admin`**: New `<MentionableTextarea>` component plus pure
871
+ helpers in `lib/mention-picker.ts` (`detectMentionContext`,
872
+ `insertMentionToken`, `searchMentionUsers`). Wired into the three
873
+ comment composer surfaces in `CommentSidePanel`: the new-thread
874
+ textarea, the reply input, and the in-place edit textarea. The
875
+ picker is keyboard-driven (Arrow Up / Down, Enter / Tab to commit,
876
+ Escape to close) with a Radix-style listbox + ARIA roles, and
877
+ gracefully handles slow responses (drops stale requests when typing
878
+ quickly).
879
+
880
+ **`mcp-server`**: New `search_users(query)` tool exposes the same
881
+ lookup so AI agents can resolve teammates by name fragment before
882
+ drafting a comment. Wire format is unchanged — the picker writes
883
+ `@<userId>` tokens that the existing `extractMentionedUserIds`
884
+ parser already understands, so no notification schema migration is
885
+ required.
886
+
887
+ No breaking changes.
888
+
889
+ ## 0.13.0
890
+
891
+ ### Minor Changes
892
+
893
+ - 4732184: Per-user SSE push channel for the notification bell (Phase 7 carry-over).
894
+
895
+ `@actuate-media/cms-core` exposes a new authenticated SSE endpoint:
896
+
897
+ GET /api/cms/notifications/stream
898
+
899
+ The stream pushes each `comment_reply`, `comment_mention`, and
900
+ `comment_resolved` notification the instant it's persisted to
901
+ `DocumentNotification`. The wire format is `event: ready` (once on
902
+ connect, with the subscribed `userId`) followed by `event:
903
+ notification` frames carrying the full DTO, plus a 25 s heartbeat
904
+ keepalive and a 4 min server-initiated close so `EventSource`
905
+ reconnects before platform max-duration caps fire.
906
+
907
+ `@actuate-media/cms-admin` switches `NotificationBell` over to an
908
+ `EventSource`-backed subscription with the existing REST endpoints as
909
+ a convergence backstop (the in-process bus is single-node by design;
910
+ the poller now runs every 60 s instead of 30 s). The new
911
+ `subscribeNotificationStream` helper is exported for any consumer that
912
+ wants to drive its own UI off the same channel.
913
+
914
+ Both packages bump `minor`: the cms-core change adds a new public
915
+ route and a new realtime export surface; the cms-admin change adds a
916
+ new `NotificationsApi.subscribe` member and exports the
917
+ `NotificationStreamHandle` / `NotificationStreamOptions` /
918
+ `EventSourceLike` types. No breaking changes — callers who don't pass
919
+ a custom `api` continue to work unchanged.
920
+
921
+ ## 0.12.1
922
+
923
+ ### Patch Changes
924
+
925
+ - a3be694: Security: bump `happy-dom` from `^15.7.4` to `^20.9.0` to close all five open Dependabot alerts (2 critical + 3 high). The package is a vitest test-environment polyfill only (no production runtime exposure), but the advisories cover real attack surfaces against dev / CI machines:
926
+ - `GHSA-37j7-fg3j-429f` (critical, fixed in 20.0.0): VM Context Escape → RCE through the script-execution sandbox.
927
+ - `GHSA-w4gp-fjgq-3q4g` (high, fixed in 20.8.9): `fetch` credentials use page-origin cookies instead of target-origin cookies, leaking session cookies to cross-origin endpoints.
928
+ - `GHSA-6q6h-j7hj-3r64` (high, fixed in 20.8.8): `ECMAScriptModuleCompiler` interpolates unsanitised export names as executable code.
929
+
930
+ The bump is a five-major-version jump (15 → 20) but happy-dom is consumed only via the `// @vitest-environment happy-dom` directive in three test files — no direct API usage exists, so the major-version surface change is transparent. All 264 cms-admin tests and 26/26 workspace test tasks pass on `happy-dom@20.9.0`.
931
+
932
+ ## 0.12.0
933
+
934
+ ### Minor Changes
935
+
936
+ - 02df678: Phase 4, PR #2 — admin form generator + cross-file type resolution.
937
+
938
+ `@actuate-media/cms-admin` adds a new `componentBlock` field renderer:
939
+ - `<ComponentBlockField>` picks a component from the manifest's allow-list and renders a typed props form via a recursive `<PropInput>` dispatcher. Inputs vary per `PropType.kind` (string, number, boolean, enum, literal, object, array; unsupported kinds fall back to a JSON textarea with a hint).
940
+ - Picking a new component seeds the props object from each prop's `defaultValue`, with structural defaults for required props that lack a literal default.
941
+ - Live structural validation mirrors `validateComponentBlockValue` from cms-core and surfaces per-prop errors under the correct input on every keystroke.
942
+ - The new field is wired into `FieldRenderer`'s `FIELD_MAP` under `componentBlock` and exported from the package's main entry.
943
+
944
+ `@actuate-media/component-blocks` extends the extractor:
945
+ - Cross-file `import type { Foo } from './bar'` is now followed during extraction (with cycle detection + per-run caching). Renamed imports (`{ Foo as Bar }`) are stored under their local alias.
946
+ - TypeScript-ESM-style `.js` import specifiers resolve to their `.ts`/`.tsx` source siblings.
947
+ - Manifest `filePath` is now a POSIX-relative path from the `rootDir` instead of an absolute disk path, so a manifest generated on Windows is identical to one generated on Linux.
948
+
949
+ Bug fixes from Bugbot review:
950
+ - `<EnumInput>` placeholder option now clears the field to `undefined` instead of silently selecting the first enum value (`Number("")` → `0` regression).
951
+ - `getAllowedComponents` and `buildClientValidator` now agree on the meaning of `allow={[]}`: both treat an empty allow array as "no filter". Previously the picker showed every component while validation rejected every selection.
952
+ - Import scanner bail-out no longer skips 6 extra characters after `import` (the keyword-match index already points past `import`), so subsequent imports after a side-effect import are no longer silently swallowed.
953
+ - `defaultForType` now returns `null` (not `undefined`) for `union`, `reference`, and `unknown` kinds so the documented "never `undefined` in an array" invariant holds. `JsonFallback` treats both `null` and `undefined` as empty so the textarea doesn't show the literal string `null`. `seedPropsForComponent` skips seeding required props of those kinds so the validator's "Missing required prop" message doesn't fire the instant an editor picks the component (the prop stays absent until the editor provides a value via the JsonFallback).
954
+ - `safeJsonStringify` no longer short-circuits for strings; the JsonFallback textarea now round-trips strings through `"hello"` instead of bare `hello`, so users can keep editing without the field "locking" into a non-JSON form.
955
+ - `clientShapeError` now recurses into object props with required children. Passing `{}` for an object prop with required nested fields used to slip past the client validator and fail server-side; the validator now reports `Prop 'cta.label' is required but missing.` inline.
956
+
957
+ - 12e042c: Phase 4, PR #4 — discriminated-union picker + marketing-grade docs.
958
+ Closes Phase 4 of the competitive gaps roadmap.
959
+
960
+ The headline UX win: a `union` PropType whose variants share a single
961
+ required literal field (the discriminator) no longer falls through
962
+ to the JSON textarea. The admin form renders a variant `<select>` and
963
+ a recursive object form for the chosen variant's remaining fields,
964
+ keyed automatically off the TypeScript shape — no manifest
965
+ annotation, no schema language, no configuration.
966
+
967
+ **New `@actuate-media/component-blocks` exports:**
968
+ - `detectDiscriminator(propType)` — pure function. Inspects a union
969
+ PropType; returns `{ field, kind, variants[] }` when every variant
970
+ is an object sharing a required-literal field with distinct values,
971
+ or `null` otherwise. The rule is strict by design — a half-
972
+ discriminated union would silently accept invalid states.
973
+ - `findVariant(value, detected)` — locate the variant whose
974
+ discriminator value matches a stored value. Used by the form to
975
+ keep the picker in sync, and by the validator + future migrator to
976
+ route checks into the correct variant's schema.
977
+ - New types: `DiscriminatedUnion`, `DiscriminatedVariant`,
978
+ `DiscriminatorValue`.
979
+
980
+ **`@actuate-media/cms-admin` updates:**
981
+ - `<PropInput>` `union` branch now calls `detectDiscriminator` first;
982
+ on a hit it renders the new `DiscriminatedUnionInput` (variant
983
+ picker + recursive object form for the chosen variant's remaining
984
+ fields). Non-discriminated unions still fall back to the JSON
985
+ textarea with a clearer hint.
986
+ - Variant switching preserves fields that exist on both variants
987
+ (so `{ kind: 'link', label: 'Go' }` → `{ kind: 'modal', label: 'Go',
988
+ modalId: '' }` keeps the label), drops variant-exclusive fields,
989
+ and seeds new required fields from their declared defaults.
990
+ - `defaultForType` now returns a structural default for discriminated
991
+ unions (first variant + its required field defaults). A required
992
+ discriminated-union prop arrives on screen as a fully-editable form
993
+ rather than a `Missing required prop` error.
994
+ - `seedPropsForComponent` carries that forward — required
995
+ discriminated-union props seed cleanly; non-discriminated unions
996
+ / references / unknowns still stay absent so the validator's
997
+ message and the data state agree honestly.
998
+ - `clientShapeError` routes discriminated-union values into the
999
+ matching variant's schema. New error messages: `cta.kind must be
1000
+ one of: "link", "modal"`, `cta.modalId is required but missing`,
1001
+ `cta must be an object with a 'kind' discriminator`. Same
1002
+ precision as plain object props.
1003
+
1004
+ **Example component upgraded:**
1005
+ - `examples/Hero.tsx` now ships a discriminated `cta` prop
1006
+ (`{ kind: 'link', href, label } | { kind: 'modal', modalId, label }`).
1007
+ The `/component-blocks-demo` live playground doubles as the
1008
+ marketing artifact and exercise the picker end-to-end.
1009
+
1010
+ **Docs:**
1011
+ - New `docs/component-blocks-intro.md` — marketing-grade landing page
1012
+ with the comparison matrix vs schema-first CMSs and a 60-second
1013
+ hands-on tour. The technical reference doc links to it from the top.
1014
+ - `docs/component-blocks.md` gains a dedicated "Discriminated-union
1015
+ picker" section covering the detection rule + programmatic API.
1016
+ Known limits refreshed; "variant picker ships in PR #4" notes
1017
+ removed.
1018
+ - `docs/roadmap-status.md` marks Phase 4 as feature-complete /
1019
+ in-review.
1020
+
1021
+ **Tests:** 18 new unit tests in `discriminated-union.test.ts`
1022
+ (positive + negative cases for the detector, `findVariant` matching);
1023
+ 7 new helper tests covering `defaultForType` / `seedPropsForComponent`
1024
+ / `clientShapeError` for discriminated unions; existing extractor
1025
+ test updated to assert the new Hero CTA shape via the public detector.
1026
+ Workspace test counts: component-blocks 141 → 159, cms-admin 82 → 89.
1027
+
1028
+ This PR closes Phase 4. The next session can start fresh on
1029
+ Phase 3 (real-time collaboration) or Phase 6 (importers).
1030
+
1031
+ ## 0.11.0
1032
+
1033
+ ### Minor Changes
1034
+
1035
+ - 72db484: Ship the Actuate Media brand mark as the default admin/login logo, and extend the existing `admin.branding` override to the login screen.
1036
+
1037
+ **Sidebar**
1038
+ - The default expanded-sidebar wordmark now renders the real Actuate Media lockup (inlined as a base64 data URL so it works under every consumer bundler with no extra asset config). The collapsed state keeps the lightweight inline SVG mark since the raster asset gets fuzzy at 32×32.
1039
+ - `config.admin.branding.logo` still overrides the default — unchanged behaviour for integrators who already configured a custom logo.
1040
+
1041
+ **Login**
1042
+ - The login screen now renders the same brand logo at the top by default (the generic Shield icon is gone). It reads from the same `config.admin.branding` block as the sidebar, so swapping logos is a one-line change.
1043
+ - New `branding?: LoginBrandingConfig` prop on `<Login />` exposes `logo`, `name`, and `tagline`. Pass `logo: null` to opt out of any logo on whitelabel deployments.
1044
+ - `AdminRoot` wires `config.admin.branding` through to `Login` automatically.
1045
+
1046
+ Docs updated in `docs/admin-guide.md` ("Admin Branding") with the override snippet.
1047
+
1048
+ - 8f2d3f8: Redesign the admin Dashboard and fix the recurring "content cut off on the left" layout bug.
1049
+
1050
+ **Dashboard (`@actuate-media/cms-admin`)**
1051
+ - New `Dashboard.tsx` matching the production design: time-of-day greeting, hero action row (New Post / View Site), horizontally-scrollable quick actions, five stat cards, two-column main grid (Recent Activity + Publishing Queue + Content Health), and a four-tile Content Delivery footer (Total Documents, Scheduled, Form Responses, Active Webhooks).
1052
+ - All data derived from the existing `/stats` and `/health` endpoints — no additional network round-trips. Recent activity, publishing queue, content health, and delivery tiles are computed client-side via `useMemo` so the backend never blocks the frontend.
1053
+ - Fully responsive: stat cards collapse from 5 → 3 → 2 columns at `lg` / `sm` / mobile breakpoints, the main grid stacks below `lg`, and quick actions scroll horizontally on narrow screens.
1054
+ - Deterministic per-author avatar colors (hash-based) so the activity feed is stable across renders without state or extra fetches.
1055
+
1056
+ **Layout cutoff fix (`@actuate-media/cms-admin`)**
1057
+ - Added `lg:shrink-0` to the desktop Sidebar wrapper. Without it, on viewports between `lg` (1024px) and ~1280px the flex sidebar column could compress below the Sidebar's intrinsic 256px width, causing dashboard content to overflow into and visually overlap the sidebar. Combined with the existing `min-w-0` on the main column, the row now reliably renders as `[256px Sidebar][1fr Main]` at every desktop width.
1058
+ - Dashboard is wrapped in `max-w-[1400px] mx-auto w-full` so on ultra-wide displays the content stays in a comfortable reading column instead of stretching edge-to-edge.
1059
+
1060
+ **Stats endpoint extension (`@actuate-media/cms-core`)**
1061
+ - `GET /stats` now returns `webhookCount` and `webhookActiveCount`, calculated via two cheap `safeCount` calls in the existing `Promise.allSettled` batch. The dashboard's "Active Webhooks" tile uses these instead of issuing a second request, keeping page load to a single stats fetch.
1062
+
1063
+ - d3d06bf: Add layout primitives package (Phase 0.3 of the best-in-class roadmap).
1064
+
1065
+ Introduces a sanctioned set of layout components every admin view must
1066
+ compose against, so layout bugs (sidebar overlap, breakpoint mismatches,
1067
+ content blowout) cannot recur in new views:
1068
+ - `AdminShell` — the CSS-Grid based admin chrome. Owns the
1069
+ desktop/mobile decision, the sidebar overlay, and the scroll lanes.
1070
+ `<Layout>` now delegates to it.
1071
+ - `PageContainer` — the canonical per-page wrapper. Owns max-width,
1072
+ padding, and the title/description/actions header.
1073
+ - `Stack` — vertical stack with a single `space` token.
1074
+ - `Cluster` — horizontal wrap-friendly group with `align` / `justify`.
1075
+ - `Grid` — explicit-column or `autoFit` responsive grid.
1076
+ - `Split` — two-pane primary+aside layout that stacks on mobile.
1077
+ - `Box` — lowest-level forwardRef'd container (semantic `as` prop).
1078
+ - `tokens` — single source of truth for the spacing / radius /
1079
+ breakpoint scales (mirrors Tailwind).
1080
+
1081
+ All primitives are exported from `@actuate-media/cms-admin`. Existing
1082
+ views continue to render the same markup — the primitives package
1083
+ formalizes the patterns rather than reskinning the app.
1084
+
1085
+ - 5d18097: Wire per-type SEO defaults into the admin Settings panel — the last loose end from Slice A (SEO Completeness).
1086
+
1087
+ **`cms-core`**
1088
+ - New `seo/config-store` module persists admin edits in a single reserved-slug document (`__seo_config__`) and merges them on top of the static `actuate.config.ts` `seo` block at runtime, field-by-field.
1089
+ - New endpoints `GET /seo/config` and `PUT /seo/config` (admin-only). Saving uses partial-update semantics so editing the site-wide section never wipes per-collection overrides (and vice versa).
1090
+ - `/sitemap.xml`, `/sitemaps/:slug.xml`, `/robots.txt`, `/og.png`, and `/resolve` now load the merged config per request via `loadEffectiveConfig()`, so admin changes are live immediately with no redeploy.
1091
+ - Empty strings in PUT payloads are treated as "unset" (fall back to static config) — the panel is purely additive.
1092
+
1093
+ **`cms-admin`**
1094
+ - New **Settings → SEO** tab backed by `<SEOConfigPanel />`. Edits site-wide defaults (siteUrl, siteName, OG image, Twitter handle, change-freq, priority, robots toggles, AI-bot blocking) and per-collection defaults (Schema.org type, archive path, sitemap priority/changefreq, noindex/nofollow, exclude-from-sitemap).
1095
+ - Static `actuate.config.ts` values render as input placeholders so admins always see what they're overriding.
1096
+
1097
+ 12 new unit tests cover the merge semantics, unset behaviour, DB roundtrip, upsert (no duplicate rows), and DB-error fallback.
1098
+
1099
+ - e021937: Add typed component variants via a built-in `cv()` helper
1100
+ (class-variance-authority pattern, no runtime dependency).
1101
+
1102
+ Refactors `Button` and `Badge` to use typed variants, and introduces three
1103
+ new variant-typed primitives: `Card` (with `CardHeader` / `CardTitle` /
1104
+ `CardDescription` / `CardContent` / `CardFooter`), `Input`, and `Select`.
1105
+
1106
+ All existing props (Button: `variant`, `size`, `loading`; Badge: `status`)
1107
+ continue to work — the new API is purely additive. New options:
1108
+ - Button: `variant: 'outline' | 'link'`, `size: 'xs' | 'icon'`,
1109
+ `leftIcon`, `rightIcon`, `full`.
1110
+ - Badge: `tone`, `size`, `outline` for generic non-status badges.
1111
+ - Card / Input / Select: typed variants for size, padding, elevation,
1112
+ validation state.
1113
+
1114
+ Also exports each component's variant function (`buttonVariants`,
1115
+ `badgeVariants`, ...) so consumers can compose the same class strings
1116
+ outside the JSX (e.g. for headless components or third-party renderers).
1117
+
1118
+ ### Patch Changes
1119
+
1120
+ - 3115d90: Two follow-ups to the recent brand-logo addition:
1121
+
1122
+ **1. Inline SVG logo instead of base64 JPEG.** The Actuate Media wordmark now ships as a transparent inline SVG (`<ActuateBrandLogo />`) using the official 2021 brand asset. Drops the surrounding dark pill on both the sidebar and the login screen — the SVG sits naturally on any surface. Bundle size for the logo is ~3.5 KB of paths vs the ~40 KB base64 JPEG it replaced.
1123
+
1124
+ **2. Fix dashboard content cutoff at small desktop widths.** The main column in `Layout.tsx` was missing `min-w-0`, so its flex `min-width: auto` default prevented it from shrinking below the intrinsic width of wider content (the dashboard's `xl:grid-cols-5` stat row, long document titles, tables). On viewports just above the `lg` breakpoint this caused the column to overflow under the sidebar and visibly clip the leftmost cards. Added `min-w-0` and a comment explaining why it's load-bearing.
1125
+
1126
+ - 848309f: Improve admin dev tooling (Phase 0.6).
1127
+ - `pnpm dev` in `packages/cms-admin` now runs `tsc --watch` and the
1128
+ Tailwind CLI `--watch` in parallel via `concurrently`, so JS _and_ CSS
1129
+ rebuild on save. Previously you had to remember to run `pnpm build:css`
1130
+ manually, which is what allowed the stale-precompiled-CSS class of
1131
+ layout bugs to slip in.
1132
+ - Adds `prettier-plugin-tailwindcss` so Tailwind class lists are
1133
+ auto-sorted in canonical order on save / `pnpm format`. The plugin is
1134
+ configured to recognize the in-house `cv()`, `cn()`, and `clsx()`
1135
+ helpers so classes inside variant maps are sorted too.
1136
+
1137
+ - a815027: Refactor admin shell layout to CSS Grid (with inline-styled grid template
1138
+ columns) and a JS-driven `matchMedia` breakpoint, replacing the brittle
1139
+ `fixed` ↔ `md:static` positioning toggle that repeatedly let dashboard
1140
+ content render under the sidebar when the precompiled CSS bundle was
1141
+ stale or browser-cached.
1142
+
1143
+ The new layout is bulletproof against missing utility classes: even if a
1144
+ single Tailwind class went missing, the grid column reserved by the
1145
+ parent guarantees the main content never overlaps the sidebar. Mobile
1146
+ overlay behavior is preserved.
1147
+
1148
+ Also adds a `dev:css` watch script for parallel CSS rebuilds during
1149
+ development.
1150
+
1151
+ - 8a87aac: Add Playwright visual regression infrastructure (Phase 0.5).
1152
+
1153
+ `e2e/visual-regression.spec.ts` snapshots four canonical admin screens
1154
+ (login, dashboard, posts list, settings) at four breakpoints
1155
+ (375 / 768 / 1024 / 1440 px), giving every PR a pixel-level safety net
1156
+ for the kinds of layout regressions that caused recurring "content under
1157
+ the sidebar" bugs.
1158
+
1159
+ New scripts:
1160
+ - `pnpm test:visual` — run the suite.
1161
+ - `pnpm test:visual:update` — refresh baselines after an intentional
1162
+ design change.
1163
+
1164
+ Docs: `docs/visual-regression.md`.
1165
+
1166
+ Baselines will be captured on the first CI run after this PR merges.
1167
+
1168
+ ## 0.10.0
1169
+
1170
+ ### Minor Changes
1171
+
1172
+ - 39f5941: Slice E — Preview & Publishing Polish.
1173
+
1174
+ **Draft preview links**
1175
+ - Preview tokens now accept a configurable `ttlSeconds` (clamped to
1176
+ `[60, 2,592,000]`, default 7 days). The old 5-minute hard-coded TTL
1177
+ was too short for sharing with clients.
1178
+ - `POST /preview/token` now verifies the document exists before
1179
+ issuing a token, includes the doc's `slug` in the response so the
1180
+ admin can build a friendly URL, and records the issuing user in the
1181
+ JWT `sub` claim for audit logs.
1182
+ - `GET /resolve?preview=<token>` drops the `status: PUBLISHED` filter
1183
+ when a valid token is presented, returning draft content at the
1184
+ document's real URL. The response includes a `preview: { active,
1185
+ expiresAt }` block so frontends can render a "preview mode" banner.
1186
+ Tokens are bound to a single `(collection, documentId)` — using one
1187
+ against a different URL returns `403`.
1188
+
1189
+ **Scheduled publishing**
1190
+ - New `POST /collections/:slug/:id/schedule` endpoint sets
1191
+ `scheduledAt` and/or `scheduledUnpublishAt` (validated as future
1192
+ ISO 8601 timestamps with `unpublish > publish`) and flips status to
1193
+ `SCHEDULED`. The existing scheduling cron picks the doc up at the
1194
+ scheduled time.
1195
+ - New `DELETE /collections/:slug/:id/schedule` cancels a pending
1196
+ schedule. `SCHEDULED` docs revert to `DRAFT`; `PUBLISHED` docs with
1197
+ a pending unpublish stay live.
1198
+
1199
+ **Admin UI**
1200
+ - New **Share preview** toolbar button on `DocumentEdit` opens a
1201
+ dialog to generate signed preview URLs with selectable TTL
1202
+ (1 hour / 1 day / 7 days / 30 days) and a copy-to-clipboard control.
1203
+ - New **Schedule** button in the Status panel opens a datetime picker
1204
+ for setting both publish and (optionally) unpublish times. Active
1205
+ schedules render an inline blue pill with the times. "Reschedule"
1206
+ and "Cancel schedule" supported.
1207
+ - New exported components: `SharePreviewLinkDialog`,
1208
+ `SchedulePublishDialog`.
1209
+
1210
+ **Tests & docs**
1211
+ - 20 new tests: preview adapter TTL clamping, JWT round-trip, secret
1212
+ isolation; `/preview/token` + `/resolve?preview=` happy/sad paths;
1213
+ schedule create/cancel/validation paths.
1214
+ - New `docs/preview-and-scheduling.md` walkthrough; `docs/api-reference.md`
1215
+ expanded with the new endpoints.
1216
+
1217
+ ## 0.9.0
1218
+
1219
+ ### Minor Changes
1220
+
1221
+ - e87471d: Phase 1: AI access. API key authentication (act*sk*\* bearer tokens) with scope-based authorization across collections, globals, media, and the page builder. New /api-keys admin endpoints + UI for creating, listing, and revoking keys with password reauth. CSRF is skipped for API-key requests. New /page-builder/create one-shot endpoint that generates an AI page and persists it as a document in a single call. New @actuate-media/mcp-server package exposes the CMS to AI agents (Claude, Cursor, ChatGPT, etc.) via the Model Context Protocol — full CRUD, globals, media, page generation, and one-shot create tools. Adds 44 new tests covering the API key auth path, scope enforcement, CSRF skip, and MCP client/tools.
1222
+
1223
+ ## 0.8.2
1224
+
1225
+ ### Patch Changes
1226
+
1227
+ - 6d2ea6e: Fix admin rendering offscreen when wrapped in a `<div className="actuate-admin">` by the consumer. `theme.css` was applying `contain: layout style` to `.actuate-admin`, which made every such element a containing block for `position: fixed` descendants. AdminRoot's own fixed overlay then resolved relative to the wrapper instead of the viewport, and if the wrapper had no other content the entire admin rendered into a 0-height box. Switched to `contain: style` (keeps the style-scoping benefit, drops the positioning footgun).
1228
+
1229
+ Symptom on the consumer side: clicks on admin buttons mysteriously fail with Playwright's `<body> intercepts pointer events`, even though the DOM is fully rendered.
1230
+
1231
+ - 6d2ea6e: Two fixes that emerged while stabilizing the e2e suite:
1232
+ - **cms-core**: `createRateLimiter()` now respects `ACTUATE_DISABLE_RATE_LIMIT=1` and returns a permissive limiter that always allows requests. This is for test harnesses where every test shares the same client IP and exhausts the strict per-IP buckets within the first few login attempts. Production never sets this var; the deploy guide and Vercel templates omit it. Previously the env var bypass only existed at the per-handler `checkRateLimitAsync` wrapper in `handlers.ts`, so the global API rate limit in `handler-factory.ts` (which uses `rateLimiter.check()` directly) ignored it and broke retries with HTTP 429.
1233
+ - **cms-admin**: `<SEO />` view crashed with "M.filter is not a function" the first time it rendered. The `/seo/link-health` endpoint returns `{ truncated, checked, issues: [...] }` (an object), but `useApiData<any[]>` typed it as an array and the `linkHealth = linkHealthData ?? []` coercion silently put the wrapper object into the `linkHealth` variable. Calls like `linkHealth.filter(l => l.status === 404)` then threw on every render — including when the user was on the (default) Pages tab, because Radix `<Tabs.Content>`'s children are evaluated eagerly even when the tab is unmounted. Fixed the typing and switched to `linkHealthData?.issues ?? []`.
1234
+
1235
+ ## 0.8.1
1236
+
1237
+ ### Patch Changes
1238
+
1239
+ - 22c314b: Fix React Rules of Hooks violation in `AdminShell` that caused
1240
+ "Rendered more hooks than during the previous render." after login.
1241
+
1242
+ The `useMemo` for the collection→type map (added in v0.8.0 to power
1243
+ PageBuilder routing) was placed _after_ the early-return guards for
1244
+ `setupRequired` and `!session`. The pre-login render therefore called
1245
+ N hooks; the post-login render called N+1 — which crashes React. The
1246
+ hook is now hoisted above the early returns so the count is stable
1247
+ across renders. Caught by the e2e Playwright suite (`auth.spec.ts →
1248
+ login redirects to dashboard`).
1249
+
1250
+ ## 0.8.0
1251
+
1252
+ ### Minor Changes
1253
+
1254
+ - 6db988e: Major engineering quality + integration upgrades.
1255
+
1256
+ **Security & reliability (cms-core)**
1257
+ - Audit log: fixed runtime crash from `orderBy: createdAt` → `timestamp`; never throws on insert; allows null `details`.
1258
+ - IPv4 + IPv6 + CIDR allowlist with canonical comparison; reject `unknown` IPs against non-empty allowlists.
1259
+ - Centralised trusted client IP extraction via `x-vercel-forwarded-for` / `x-real-ip` / `x-forwarded-for` (with explicit `trustProxy`).
1260
+ - Atomic Upstash `INCR` + `EXPIRE NX` rate limiter, fail-open with logging.
1261
+ - Encryption at rest for OAuth tokens, TOTP secrets, backup codes, and webhook secrets via `CMS_ENCRYPTION_KEY`.
1262
+ - Hardened TOTP login: `mfa_pending` JWT + per-IP/per-user rate limit + request fingerprint binding (no userId leak between steps).
1263
+ - Reauth required for TOTP setup/disable; revoke other sessions on TOTP changes.
1264
+ - OAuth state nonce bound to a cookie; `allowSelfSignup` flag; never silently link to password-protected accounts.
1265
+ - SSRF-safe `safeFetch()` for webhooks and `/seo/link-health`; concurrency cap + URL count cap; private IPs/loopback/link-local rejected.
1266
+ - Magic-byte + MIME validation + DOMPurify SVG sanitisation on `/media/upload`; tightened WebP/AVIF/GIF/SVG detectors.
1267
+ - Webhook + plugin hook dispatch moved outside the DB transaction so a rollback never fires events for a doc that no longer exists.
1268
+ - AI: per-user rate limit + token/cost budget + input length cap + balanced-brace JSON extractor + `validateTree` after every step + secret redaction in audit prompts.
1269
+ - CSRF exemption tightened to exact-prefix match on the rewritten path (no more `pathname.includes`).
1270
+ - `refreshSession`: validate session row exists & is active before re-issuing.
1271
+ - Public SEO endpoints require auth; `/llms.txt` filters PUBLISHED + non-deleted.
1272
+ - `/forms`, `/forms/:id/submissions`, `/search/global` users restricted to EDITOR+.
1273
+ - Open-redirect host allowlist for `/redirects`; folder routes validate scope on move/delete; password-reset email enumeration blocked.
1274
+ - `/script-tags/resolve` returns proper public cache headers.
1275
+
1276
+ **Integration fixes (consumer feedback)**
1277
+ - **`createServerSiteClient(prisma, config)`** — new server-only Site client that reads from Prisma directly. Use in Server Components, `generateMetadata`, route loaders. Avoids the `Invalid URL` crash that `createSiteClient()` throws under bare Node, and skips the self-fetch overhead.
1278
+ - **`createSiteClient()`** now auto-resolves `baseUrl` from `NEXT_PUBLIC_SITE_URL` or `VERCEL_URL` when not explicitly provided.
1279
+ - **`/public/globals/:slug`** defaults to public when no `access.read` is set (was a 403 footgun).
1280
+ - **`cmsApi`** auto-bootstraps the `actuate_csrf` cookie before non-GET requests; `AdminRoot` also primes it on mount when a session exists. Fixes the silent 403 every consumer hit when signing in via a custom Server Action.
1281
+ - **AdminShell** routes collections with `type: 'page'` through `<PageBuilder>` instead of `<DocumentEdit>`. Built-in admin paths (`/users`, `/media`, etc.) are reserved before collection routes so a user-defined `users` collection can't shadow the Users view.
1282
+ - **`BlockCatalog`** now reads custom blocks from `globalThis.__actuateConfig.pageBuilder.blocks` automatically. Also accepts an explicit `customBlocks` option and an `includeCore: false` flag.
1283
+ - **Page Builder envelope** unified at the top level — `layout` and `pageSettings` are accepted on POST/PUT and returned at top-level on GET. Storage shape is `data._layout` / `data._pageSettings`; both wire shapes accepted on input for backwards compatibility.
1284
+ - **`/media/upload`** now uses the configured platform storage adapter (e.g. `platform-vercel`) when set; honors `token`/`prefix` options. Falls back to direct `@vercel/blob` import for legacy installs.
1285
+ - **BlockEditor** field types fleshed out: real TipTap editor for `richText`, working media picker for `media`, JSON editor with validation for `json`, plus `slug`, `date`, and an array editor for primitive arrays.
1286
+ - **`pageBuilder.previewTheme`** now applied to the canvas via scoped CSS variables.
1287
+ - OpenAPI: documented `/public/globals/:slug` and `/resolve`.
1288
+
1289
+ **DX**
1290
+ - `.env.example` documents required formats for `CMS_ENCRYPTION_KEY` (64-hex), `BLOB_READ_WRITE_TOKEN`, and `RESEND_*`.
1291
+ - Tests: 266 passing (was 230); regression tests for IP allowlist, client-ip, redact, secret-storage, audit, upload-magic, server-site, custom block discovery.
1292
+
1293
+ ## 0.7.3
1294
+
1295
+ ### Patch Changes
1296
+
1297
+ - 58bf2e6: Add site-wide SEO robots defaults with per-page index/follow override controls.
1298
+
1299
+ ## 0.7.2
1300
+
1301
+ ### Patch Changes
1302
+
1303
+ - e2fabee: Add AI-friendly deployment diagnostics, align generated schemas with first-run admin features, and harden setup guidance for design-first deployments.
1304
+
1305
+ ## 0.7.1
1306
+
1307
+ ### Patch Changes
1308
+
1309
+ - b66800c: Align admin-facing API response shapes with cms-admin consumers, add Vercel Blob image configuration, and harden admin views against partial data.
1310
+
1311
+ ## 0.7.0
1312
+
1313
+ ### Minor Changes
1314
+
1315
+ - c3c55c7: Add AI-powered page generation pipeline with prompt-to-page workflow.
1316
+
1317
+ **cms-core**: AI pipeline types (GenerationContext, GenerationStep, StepResult, GenerationResult), prompt template builders for structure/content/SEO/accessibility steps, deterministic accessibility auto-fix engine (auditAccessibility, fixAccessibility), and API endpoints for /page-builder/generate, /page-builder/generate-block, /page-builder/audit-a11y, and /page-builder/fix-a11y.
1318
+
1319
+ **plugin-ai**: Multi-step generatePage() pipeline that runs structure → content → SEO → accessibility steps sequentially via AIProvider, with validation retry, graceful fallback, progress callbacks, and token tracking. Single-block generateBlockContent() for context-aware content generation.
1320
+
1321
+ **cms-admin**: AI Generate Dialog with prompt input, tone selection, step toggles, business context fields, real-time generation progress, and step-by-step review with accept/regenerate flow. AI Block Assist button for one-click content generation per block. AI button integrated into the builder toolbar.
1322
+
1323
+ - 2ef3505: Add SEO analysis and design scoring for the page builder. cms-core gains `analyzeSEO()` (content extraction, readability, 15+ SEO checks, per-block hints), `analyzeDesign()` (6-category scoring for visual hierarchy, spacing consistency, content density, color usage, image quality, and mobile readiness), and semantic block intelligence with smart suggestions. cms-admin replaces the placeholder SEO and Design tabs in the page builder ContextPanel with fully functional `BuilderSEOPanel` (score ring, checks, readability grid, SERP/social previews, inline editing) and `DesignScorePanel` (score ring, category progress bars, expandable checks, severity-ranked suggestions).
1324
+ - 2269f10: Add page templates and saved sections for the page builder. cms-core gains 6 built-in page templates (blank, landing, about, contact, location, blog-post) with full container tree structures, plus CRUD API endpoints for both page templates and saved sections with tree validation, auto-seeding, and audit logging. cms-admin adds a TemplatePicker modal for new page creation, a SavedSections CRUD management view with category filtering, and admin routing for the new views.
1325
+ - b68358b: Add visual page builder UI with three-zone layout (toolbar, canvas, context panel), 10 core block preview renderers, block picker modal, structural node editors, and admin routing integration.
1326
+
1327
+ ## 0.6.0
1328
+
1329
+ ### Minor Changes
1330
+
1331
+ - b5de8dd: Add password reset flow for admin login. Users click "Forgot Password?" on the login page, enter their email, and receive a time-limited reset link via the configured email adapter (Resend). The reset token is hashed before storage and expires after 1 hour. On successful reset, all existing sessions are revoked.
1332
+ - New PasswordResetToken Prisma model with migration 0005_password_reset_tokens.
1333
+ - POST /auth/forgot-password and POST /auth/reset-password API endpoints with rate limiting.
1334
+ - Core auth module (reset.ts) with token generation, email dispatch, and password update logic.
1335
+ - ForgotPassword and ResetPassword admin views with full form validation.
1336
+ - AdminRoot routes /forgot-password and /reset-password in the unauthenticated shell.
1337
+
1338
+ ### Patch Changes
1339
+
1340
+ - b5de8dd: Fix ESM module resolution and dependency duplication issues reported during consumer integration.
1341
+ - Add "default" export condition to all platform and plugin packages so they resolve correctly from require() contexts (fixes ERR_PACKAGE_PATH_NOT_EXPORTED in next.config.ts and serverless functions).
1342
+ - Move @actuate-media/cms-core from direct dependencies to peerDependencies across all packages to prevent duplicate installations when consumer version ranges don't exactly match the pinned version.
1343
+ - Add "prepack" lifecycle script to create-actuate-cms to guarantee dist/ is built before npm publishes the package (fixes empty dist/ in v0.4.0).
1344
+
1345
+ ## 0.5.0
1346
+
1347
+ ### Minor Changes
1348
+
1349
+ - 54c7a3b: Add Script Tags management system for injecting tracking codes, analytics, and custom scripts into public site pages. Admins create tags with HTML/JS code, choose placement (head, body open, body close), set scope (entire site, parent pages + descendants, or exact URLs), and assign priority order. Includes new ScriptTag database model, CRUD + public resolve API endpoints, admin list and editor views, and updated scaffolder site layout with automatic tag rendering.
1350
+
1351
+ ### Patch Changes
1352
+
1353
+ - Updated dependencies [54c7a3b]
1354
+ - @actuate-media/cms-core@0.6.0
1355
+
1356
+ ## 0.4.0
1357
+
1358
+ ### Minor Changes
1359
+
1360
+ - c08cf8b: Add layout variants system for customizable per-page headers, footers, and other layout regions. Layout regions are defined in config, stored as regular collection documents, and assigned to pages with automatic parent-to-child URL path inheritance. The /resolve endpoint returns resolved layout variants alongside page data. Admin UI includes layout assignment on document edit, collection grouping in sidebar, and default layout settings.
1361
+
1362
+ ### Patch Changes
1363
+
1364
+ - Updated dependencies [c08cf8b]
1365
+ - Updated dependencies [80aa939]
1366
+ - @actuate-media/cms-core@0.5.0
1367
+
1368
+ ## 0.3.0
1369
+
1370
+ ### Minor Changes
1371
+
1372
+ - f2cf7d7: Redesign Dashboard and Pages views: extended /stats API with statusCounts, formCount, avgSeoScore, and author names; new /seo/summary endpoint; recharts donut chart for content overview; SEO performance panel; folder color badges and SEO score column in Pages view.
1373
+
1374
+ ### Patch Changes
1375
+
1376
+ - Updated dependencies [f2cf7d7]
1377
+ - @actuate-media/cms-core@0.4.0
1378
+
1379
+ ## 0.2.1
1380
+
1381
+ ### Patch Changes
1382
+
1383
+ - 0621037: Make Dashboard collection-aware instead of hardcoding Posts/Pages labels.
1384
+
1385
+ **cms-core:**
1386
+ - `/stats` API now returns `collectionCounts` (per-collection document counts via groupBy)
1387
+ - Recent documents include `collection` field in response
1388
+
1389
+ **cms-admin:**
1390
+ - Dashboard reads `config.collections` to derive stat card labels, counts, routes, and quick actions
1391
+ - "Edit" links use `/{doc.collection}/{doc.id}` instead of hardcoded `/posts/{id}`
1392
+ - "New" quick actions are generated from configured collections
1393
+ - Falls back to generic "Documents" / "Pages" labels when no config is provided
1394
+
1395
+ - Updated dependencies [0621037]
1396
+ - @actuate-media/cms-core@0.3.1
1397
+
1398
+ ## 0.2.0
1399
+
1400
+ ### Minor Changes
1401
+
1402
+ - 4e3a5eb: RC2-RC4 and hardening release.
1403
+
1404
+ **cms-core:**
1405
+ - Replace isomorphic-dompurify with sanitize-html (eliminates Vercel ESM crash)
1406
+ - Remove cms-core from serverExternalPackages, move to transpilePackages
1407
+ - Add GET /api/cms/health endpoint with model probing and diagnostics
1408
+ - Harden db() Proxy with try/catch on property access and rawDb()
1409
+ - Harden safeCount/safeFindMany against Proxy throws
1410
+ - Add startup validation for missing CMS_SECRET (503 instead of 500)
1411
+ - Bypass auth on /stats when secret is missing (returns zero-data)
1412
+ - Loosen NextConfigLike types (experimental/images/webpack as unknown)
1413
+ - Implement login anomaly detection (new IP, brute force, unusual hours)
1414
+ - Implement webhook DNS resolution with private range checking
1415
+ - Implement TOTP reauth verification
1416
+ - Add Zod validation for secret in defineConfig (min 32 chars)
1417
+ - Read CMS_CORE_VERSION from package.json at runtime instead of hardcoding '0.1.0'
1418
+
1419
+ **cms-admin:**
1420
+ - Ship pre-compiled CSS (dist/actuate-admin.css) with all 48 responsive classes
1421
+ - New export: @actuate-media/cms-admin/styles/precompiled.css
1422
+ - Add CSS containment (`contain: layout style`) on .actuate-admin
1423
+ - Fix ShortcutHelp modal using React state instead of DOM manipulation
1424
+ - Add fixed-position layout isolation wrapper to AdminRoot
1425
+ - Add useApiData retry limits (maxRetries=3) with exponential backoff
1426
+ - Dashboard shows health-aware "Database Setup Required" banner
1427
+
1428
+ **cli:**
1429
+ - Add `actuate db:init` command (injects CMS models into Prisma schema)
1430
+ - Add `actuate db:status` command (checks model presence and DB connectivity)
1431
+
1432
+ **create-actuate-cms:**
1433
+ - Embed full CMS schema (9 models + enum) in generated schema.prisma
1434
+ - Add @actuate-media/cli to scaffolded devDependencies
1435
+ - Use pre-compiled CSS import in scaffolded admin layout
1436
+
1437
+ ### Patch Changes
1438
+
1439
+ - Updated dependencies [4e3a5eb]
1440
+ - @actuate-media/cms-core@0.3.0
1441
+
1442
+ ## 0.1.4
1443
+
1444
+ ### Patch Changes
1445
+
1446
+ - Fix /stats endpoint: use Promise.allSettled so missing Prisma models return 0 instead of 500. Dashboard now shows a soft warning instead of a red error banner when stats are unavailable. Add !important to font-family on .actuate-admin to prevent consumer site fonts from cascading into the admin panel.
1447
+ - Updated dependencies
1448
+ - @actuate-media/cms-core@0.2.3
1449
+
1450
+ ## 0.1.3
1451
+
1452
+ ### Patch Changes
1453
+
1454
+ - Updated dependencies
1455
+ - @actuate-media/cms-core@0.2.2
1456
+
1457
+ ## 0.1.2
1458
+
1459
+ ### Patch Changes
1460
+
1461
+ - Updated dependencies
1462
+ - @actuate-media/cms-core@0.2.1
1463
+
1464
+ ## 0.1.1
1465
+
1466
+ ### Patch Changes
1467
+
1468
+ - 682dac7: Fix consumer integration issues discovered during MaidPro.co deployment
1469
+ - **cms-core**: API handlers gracefully degrade when CMS Prisma models are missing (returns 501 instead of 500)
1470
+ - **cms-core**: Accept CMS secret via `actuate.config.ts` (`secret` field) in addition to `CMS_SECRET` env var
1471
+ - **cms-core**: `withActuateCMS` now accepts `Partial<ActuateCMSConfig>` (no more forced type casts)
1472
+ - **cms-core**: `withActuateCMS` auto-injects all `@actuate-media/*` packages into `transpilePackages`
1473
+ - **cms-core**: Expanded Prisma schema fragment with all CMS models for consumer merge
1474
+ - **cms-admin**: Removed duplicate Tailwind import from admin styles (consumers already load Tailwind)
1475
+ - **platform-vercel**: Email adapter accepts both `defaultFrom` and `from` for sender address
1476
+ - **plugin-email**: Email plugin accepts both `defaultFrom` and `from` for consistency
1477
+
1478
+ - 3fd38a2: Fix all 13 integration issues discovered during MaidPro.co deployment
1479
+
1480
+ **cms-core:**
1481
+ - Add .js extensions to all ESM relative imports (58 files)
1482
+ - Replace workspace:\* with versioned deps in all packages
1483
+ - Add sideEffects:false for proper tree-shaking
1484
+ - Subpath exports (/config, /api, /middleware, /next) avoid barrel side effects
1485
+ - Stats/search routes use safeCount/safeFindMany for missing Prisma models
1486
+ - Ship prisma/cms-schema.prisma with all required CMS models
1487
+ - Accept both CMS_SECRET and CMS_SESSION_SECRET with clear error messages
1488
+ - Fix hardcoded /admin paths in OAuth flow (now uses getAdminPath())
1489
+ - Loosen withActuateCMS and createCMSMiddleware parameter types
1490
+ - Auto-inject transpilePackages and serverExternalPackages in withActuateCMS
1491
+ - Unify PluginDefinition with ActuatePlugin interface
1492
+
1493
+ **cms-admin:**
1494
+ - Scope all 40+ CSS theme variables to .actuate-admin instead of :root
1495
+ - ThemeProvider applies dark class to .actuate-admin, not document.documentElement
1496
+ - Scaffold uses (admin)/(site) route groups for full CSS/script isolation
1497
+ - Sidebar branding supports custom logo via config.admin.branding
1498
+
1499
+ **platform-vercel:**
1500
+ - Email adapter accepts both `from` and `defaultFrom`
1501
+
1502
+ **All plugin/platform packages:**
1503
+ - Fix ESM .js extensions in compiled output
1504
+ - Replace workspace:\* with ^0.1.0 for publishable deps
1505
+
1506
+ - Updated dependencies [682dac7]
1507
+ - Updated dependencies [3fd38a2]
1508
+ - @actuate-media/cms-core@0.2.0