@actuate-media/cms-admin 0.42.2 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,1514 @@
1
+ # @actuate-media/cms-admin
2
+
3
+ ## 0.43.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 6027f23: Branding uploads now accept WebP, JPEG, GIF, and AVIF in addition to SVG and PNG. Favicons accept SVG, PNG, and WebP (lossy photographic formats stay excluded for crisp small-size rendering). The file-picker accept strings are derived from the validation allow-lists so they can't drift, validation errors show readable format names (e.g. "SVG, PNG, WebP"), and logo shape detection can no longer block an upload if image decoding fails.
8
+
9
+ ## 0.42.4
10
+
11
+ ### Patch Changes
12
+
13
+ - 0c3a6eb: Posts UX improvements from external-consumer feedback:
14
+ - 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.
15
+ - `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.
16
+ - `create-actuate-cms` scaffolds now define `posts: blogPostCollection()` instead of an inline collection, so new projects always start with an editor-compatible posts schema.
17
+
18
+ ## 0.42.3
19
+
20
+ ### Patch Changes
21
+
22
+ - 854152d: Developer experience improvements from external-consumer feedback:
23
+ - 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`.
24
+ - 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.
25
+
26
+ ## 0.42.2
27
+
28
+ ### Patch Changes
29
+
30
+ - 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.
31
+
32
+ ## 0.42.1
33
+
34
+ ### Patch Changes
35
+
36
+ - 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.
37
+
38
+ ## 0.42.0
39
+
40
+ ### Minor Changes
41
+
42
+ - 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.
43
+
44
+ ## 0.41.0
45
+
46
+ ### Minor Changes
47
+
48
+ - f192376: Page/post editor UX for custom-section installs and revision history:
49
+ - 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.
50
+ - "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".
51
+ - 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.
52
+
53
+ ## 0.40.1
54
+
55
+ ### Patch Changes
56
+
57
+ - 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.
58
+
59
+ ## 0.40.0
60
+
61
+ ### Minor Changes
62
+
63
+ - 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.
64
+
65
+ 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.
66
+
67
+ ## 0.39.0
68
+
69
+ ### Minor Changes
70
+
71
+ - 99565a1: Page Editor: stop dropping externally-managed sections, support custom types, fix breadcrumb ids (Opal feedback).
72
+ - 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.
73
+ - `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.
74
+ - Breadcrumbs `isId()` now recognizes CUID/CUID2 ids (Prisma `@default(cuid())`), so document routes render as "Edit ..." instead of showing the raw id.
75
+
76
+ - 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.
77
+
78
+ ## 0.38.1
79
+
80
+ ### Patch Changes
81
+
82
+ - 74662c0: Fix Next.js 16 admin routing and the client/server bundle boundary.
83
+ - 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.
84
+ - 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.
85
+
86
+ ## 0.38.0
87
+
88
+ ### Minor Changes
89
+
90
+ - 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.
91
+ - **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).
92
+ - **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.
93
+ - **cli**: new `actuate migrate:sections` command (with `--dry-run` / `--batch-size`) to backfill canonical sections for legacy page-builder documents.
94
+
95
+ ## 0.37.2
96
+
97
+ ### Patch Changes
98
+
99
+ - 2f18c56: WS-E (E2): public form rendering — a built site can now render and submit a CMS form end to end.
100
+ - **sections-react:** new `<ActuateForm>` Client Component fetches the public form
101
+ schema (`GET /api/cms/public/forms/:slug`), renders the fields (text/email/
102
+ url/tel/number/date, textarea, select, checkbox, radio), and submits to the
103
+ schema's `submitPath`. Spam protection is wired automatically: a hidden
104
+ honeypot (keyed from `spam.honeypotFieldKey`), an `elapsedMs` "time to submit"
105
+ timer, and an optional consumer-supplied `captchaToken`. Pre-fetch a schema via
106
+ the `form` prop to skip the round-trip (SSR/streaming). Adds a `baseUrl`
107
+ `RenderOptions` field (defaults to same-origin). Exposes pure, tested helpers
108
+ (`coercePublicForm`, `buildSubmitBody`, `fieldControl`, `firstMissingRequired`).
109
+ - **cms-core:** new canonical `form` section type. Set `content.formSlug` (plus
110
+ optional heading/intro) on any page or post and the matching renderer embeds
111
+ the form. The registry-drift guard now covers it (renderer ↔ type).
112
+ - **cms-admin:** the form Embed panel now shows the real public endpoints
113
+ (`/api/cms/public/forms/:slug` + `/submit`) and a copy-paste `<ActuateForm>`
114
+ snippet, replacing the previously-advertised `/api/forms/*`, iframe, and
115
+ `embed.js` routes that were never implemented. Adds a `mail` section icon.
116
+
117
+ ## 0.37.1
118
+
119
+ ### Patch Changes
120
+
121
+ - a4f7b5d: The login screen now shows the same brand logo as the admin sidebar. Previously
122
+ the login page only read the static `config.admin.branding.logo`, so a logo
123
+ uploaded in Settings → Appearance appeared in the sidebar but not on sign-in.
124
+ `Login` now resolves its logo with the same precedence as the sidebar: the
125
+ uploaded appearance brand (from the cached admin brand the sidebar paints from,
126
+ then the unauthenticated `GET /public/brand` endpoint for fresh browsers), then
127
+ `config.admin.branding.logo`, then the bundled Actuate wordmark. Passing
128
+ `branding.logo === null` still hides the logo entirely for whitelabel installs.
129
+
130
+ The brand lockup's logo-less fallback also changed. The sidebar previously
131
+ rendered a filled site-initials badge when no logo was set, which read as a user
132
+ avatar (the real account avatar already sits top-right). That badge is replaced
133
+ with a neutral "logo slot" placeholder — a dashed outline with a muted image
134
+ icon — shared by the sidebar and login via a new `LogoPlaceholder` component. It
135
+ is automatically superseded the moment a logo is uploaded. The bundled Actuate
136
+ wordmark/mark remains the default for a fully un-branded install; the
137
+ placeholder only appears once the install is branded (a site/brand name is set)
138
+ but has no logo yet. On a fresh browser with no cached brand, the login also
139
+ reads the site initials from `GET /public/brand` so a site-title-only install
140
+ (no static `branding.name`) is still detected as branded pre-auth.
141
+
142
+ ## 0.37.0
143
+
144
+ ### Minor Changes
145
+
146
+ - 7d11b8d: Production-ready Settings → Updates tab with real release intelligence.
147
+ - **cms-core**: Fix a critical permission bug where `GET/PUT /updates/config` and
148
+ `POST /updates/apply` rejected real admins (they compared `session.role` to the
149
+ lowercase `'admin'` while roles are stored uppercase). They now use
150
+ `requireAdminScope`, so OWNER/ADMIN sessions (and admin-scoped API keys) can
151
+ manage updates. `GET /updates/check` no longer leaks the GitHub repo to
152
+ non-admins and reports a `canManage` flag. Update checks now run through a new
153
+ hybrid resolver `getUpdateStatus()` that queries the Actuate update server
154
+ first (rich, real changelog/severity/migration/breaking-change data derived
155
+ from the published CHANGELOG) and transparently falls back to a direct npm
156
+ registry check. New exports: `getUpdateStatus`, `checkForUpdatesViaServer`, and
157
+ the `ChangeType` / `ChangelogItem` / `PluginCompatibilityInfo` types; richer
158
+ `VersionInfo`. `fetchChangelog()` now targets the real `/api/changelog`
159
+ endpoint (overridable via `ACTUATE_UPDATE_SERVER_URL`).
160
+ - **cms-admin**: Rebuild the Updates tab as a dedicated `UpdatesTab` + `useUpdates`
161
+ hook (extracted from `Settings.tsx`). The GitHub integration now uses the shared
162
+ `SettingsControlRow` + sticky `SettingsSaveBar` dirty-state pattern, and the tab
163
+ surfaces real release notes (categorized changelog), a security-update badge,
164
+ "migrations required" and breaking-change callouts, and plugin compatibility.
165
+ Write actions are permission-aware (hidden for non-admins).
166
+ - **cli**: `update-check` / `upgrade` now resolve the update-server base URL from
167
+ `ACTUATE_UPDATE_SERVER_URL` (defaulting to the production host), so the
168
+ `/api/versions` endpoint is configurable for self-hosted update servers.
169
+
170
+ ### Patch Changes
171
+
172
+ - 5ac1d16: Make the Appearance → Branding section visually uniform with the rest of
173
+ Settings. Logo fields (Primary logo, Favicon, and the advanced/admin variants)
174
+ now render as consistent rows with the Upload/Replace control aligned to the
175
+ right edge instead of stacked beneath the description, and the admin override is
176
+ presented as a labelled "Brand source" row with the toggle on the right. Add a
177
+ shared `SettingsControlRow` primitive (leading visual + label/description on the
178
+ left, control on the right) so this rhythm is reusable, route the Branding card
179
+ through `SettingsCard`/`SettingsSubsection`, and sentence-case the Appearance
180
+ section titles ("Admin theme", "Typography & motion", "Brand preview") so the
181
+ whole tab is consistent.
182
+
183
+ ## 0.36.1
184
+
185
+ ### Patch Changes
186
+
187
+ - e92dd1e: Unify the Settings design language across every tab. Introduce a shared
188
+ `SettingsCard` section primitive (title + description + optional header actions)
189
+ plus a `SettingsSubsection` helper, and route the General, Security, SEO, AI,
190
+ Layout, and Updates tabs through it so each section matches the Appearance tab:
191
+ `p-6` cards, a `text-lg` medium title, and a muted `text-sm` description. Tokenize
192
+ the Settings shell header and the Updates panel so they use semantic theme tokens
193
+ (no raw gray/blue/green/red palette classes) and render correctly in dark mode.
194
+ The persistent save bar behavior is unchanged.
195
+ - e92dd1e: Bring the Users, API Keys, and Profile screens onto the shared Settings design
196
+ language. The API Keys view is now wrapped in the `SettingsCard` primitive
197
+ (title + description + header action) and fully tokenized — every raw
198
+ gray/blue/red/green/amber palette class in the table, status pills, empty state,
199
+ and both dialogs was replaced with semantic theme tokens so it renders correctly
200
+ in dark mode. The Users "Team members" section title is bumped to the shared
201
+ `text-lg` section scale, and the Profile page header now uses `text-foreground` /
202
+ `text-muted-foreground` tokens with the standard medium weight.
203
+
204
+ ## 0.36.0
205
+
206
+ ### Minor Changes
207
+
208
+ - 4e6ead2: Add Brand Voice. Admins can define a reusable brand voice (tone, audience, guidelines, and
209
+ sample copy) in Settings > AI. When enabled, the governed AI runtime injects it into the system
210
+ prompt of content-generation features — co-author and SEO titles/descriptions — so every AI
211
+ surface writes in one consistent, on-brand voice.
212
+ - `cms-core`: new `BrandVoiceProfile` type on `AISettings`, a `brand-voice` module
213
+ (`buildBrandVoiceSystemPrompt`, `applyBrandVoice`, `getActiveBrandVoice`, `normalizeBrandVoice`),
214
+ a `useBrandVoice` option on `generateTextForFeature`, and `PUT /ai/settings` persistence
215
+ (length-clamped, no secrets).
216
+ - `cms-admin`: a Brand Voice card in the AI settings tab wired through `useAiSettings`.
217
+
218
+ ## 0.35.0
219
+
220
+ ### Minor Changes
221
+
222
+ - fcede90: Settings > AI: per-feature model routing. Each AI feature can now run on a
223
+ specific model (e.g. a cheap model for alt-text, a frontier model for content
224
+ writing) or inherit the default. Adds `resolveFeatureModel` /
225
+ `resolveAllFeatureModels` to cms-core as the authoritative feature→model
226
+ resolver (capability-aware, with capability-typed and fallback defaults), and
227
+ extends `PUT /ai/settings` to persist + validate per-feature `modelId`/
228
+ `providerId` overrides. The admin AI Features card gains a per-feature model
229
+ picker that filters to capability-compatible models and warns when a routed
230
+ model is missing a required capability (e.g. a text-only model on a vision
231
+ feature).
232
+
233
+ ## 0.34.0
234
+
235
+ ### Minor Changes
236
+
237
+ - 89eebe0: Add Settings > AI (Phases 1-2): DB-backed provider connections + dynamic model catalog.
238
+
239
+ **cms-core**
240
+ - 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).
241
+ - 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.
242
+ - SSRF-safe adapters: built-in providers use fixed hostnames; custom base URLs are validated and DNS-resolved via the existing webhook guards.
243
+ - 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.
244
+ - Monthly usage summary aggregated from existing audit-log token totals (real counts; 0 until per-run metering lands).
245
+ - 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`.
246
+
247
+ **cms-admin**
248
+ - 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.
249
+ - Feature enforcement across modules and per-run usage metering are deferred to later phases; toggles and budget persist now.
250
+
251
+ ## 0.33.0
252
+
253
+ ### Minor Changes
254
+
255
+ - 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).
256
+ - **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).
257
+ - **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.
258
+
259
+ ## 0.32.0
260
+
261
+ ### Minor Changes
262
+
263
+ - 7336b5f: Add content-ownership transfer to Settings > Users.
264
+
265
+ cms-core: new `transferContentOwnership` / `countOwnedContent` helpers reassign
266
+ a member's authored documents, uploaded media, and created templates to another
267
+ active user (immutable version/audit history is never touched). New admin-only,
268
+ audited `POST /users/:id/transfer-content` endpoint validates the recipient
269
+ (must exist and be active) and applies the same Owner/last-Owner guards as the
270
+ other per-user actions. `contentOwnedCount` in the user detail now reflects all
271
+ three content types, not just documents.
272
+
273
+ cms-admin: the user detail drawer Overview now offers "Transfer content" with an
274
+ active-member picker before revoking access. Auxiliary dialogs (transfer,
275
+ confirm, reauth) now render inside the Drawer's Radix focus scope so they are
276
+ keyboard-reachable and no longer marked `aria-hidden` behind the drawer.
277
+
278
+ ## 0.31.0
279
+
280
+ ### Minor Changes
281
+
282
+ - 951963a: Build the Settings > Users access-control system: real invites, roles, seats, and access review.
283
+
284
+ **@actuate-media/cms-core**
285
+ - 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.
286
+ - 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.
287
+ - 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.
288
+ - 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.
289
+ - 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.
290
+
291
+ **@actuate-media/cms-admin**
292
+ - 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.
293
+
294
+ **create-actuate-cms**
295
+ - 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.
296
+
297
+ - 951963a: Settings > Users phase 2: per-user detail, sessions, activity, and security actions.
298
+
299
+ **@actuate-media/cms-core**
300
+ - 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.
301
+ - 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.
302
+ - 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.
303
+
304
+ **@actuate-media/cms-admin**
305
+ - 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.
306
+
307
+ ## 0.30.0
308
+
309
+ ### Minor Changes
310
+
311
+ - f043e18: Fix day-one auth dead-ends and wire up transactional email + user invites.
312
+
313
+ **@actuate-media/cms-core**
314
+ - 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).
315
+ - Password reset links are now built from the configured admin path instead of a hardcoded `/admin`, so resets work on apps mounted elsewhere.
316
+ - 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.
317
+
318
+ **@actuate-media/cms-admin**
319
+ - `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.
320
+ - 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.
321
+
322
+ **create-actuate-cms**
323
+ - The scaffolded admin page now wires `onVerifyTotp` (so MFA users can complete the second-factor step) and `onLogout`, matching the dev app.
324
+
325
+ **@actuate-media/plugin-email**
326
+ - `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.
327
+
328
+ - 05537a8: Replace the decorative AI settings tab with an honest, read-only status panel.
329
+
330
+ 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.
331
+ - `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.
332
+ - `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.
333
+
334
+ ### Patch Changes
335
+
336
+ - 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.
337
+
338
+ 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.
339
+
340
+ ## 0.29.0
341
+
342
+ ### Minor Changes
343
+
344
+ - 008544f: Complete the MFA loop: account enrollment, login challenge, and backup-code recovery.
345
+
346
+ Previously the TOTP backend existed but had no user-facing plumbing — there was no
347
+ way to enroll from the admin, the login screen couldn't complete a second-factor
348
+ challenge, and backup codes were generated but never consumed anywhere (a lost
349
+ authenticator meant permanent lockout). The "Require MFA" policy was therefore
350
+ impossible to satisfy.
351
+
352
+ **cms-core**
353
+ - `POST /auth/totp/login` now accepts a one-time backup code as well as a TOTP code.
354
+ Matches are constant-time and single-use (the code is removed and remaining codes
355
+ re-encrypted); `login_success` audit records whether a backup code was used.
356
+ - New pure helpers `normalizeBackupCode` / `findBackupCodeMatch` in `auth/totp`.
357
+ - `GET /auth/me` now returns `totpEnabled` so the admin can show enrollment state.
358
+
359
+ **cms-admin**
360
+ - New `/profile` view (fixes the previously dead account-menu link) with an
361
+ **Account security** card: password reauth → QR code + manual key + downloadable
362
+ one-time backup codes → verify, plus a reauth-gated disable flow.
363
+ - The login screen now renders a **two-factor challenge** step (authenticator code
364
+ with a backup-code fallback). `AdminRoot`/`Login` gain an `onVerifyTotp` prop and
365
+ `onLogin` may now return `{ requiresTOTP, mfaPendingToken }`.
366
+ - The Security tab's "MFA not set up" warning now links to the enrollment flow.
367
+ - Adds `qrcode.react` for QR rendering.
368
+
369
+ ## 0.28.0
370
+
371
+ ### Minor Changes
372
+
373
+ - 06940a1: Add the Settings → Security tab as a real Security Center.
374
+
375
+ **cms-core**
376
+ - New `@actuate-media/cms-core/security-center` subpath export with pure, client-safe
377
+ helpers: `computeSecurityStatus`, `parseSecuritySettings`, `validateSecuritySettings`,
378
+ `validateIpRange`, `mfaEnableLockoutError`, `summarizeUsers`, `summarizeApiKeys`, and types.
379
+ - New API routes: `GET /security` (computed posture + cross-module summaries +
380
+ secret/encryption verdicts, never secret values), `PUT /security` (persists enforced
381
+ MFA + session policy with server-side lockout-safe MFA enabling, audited as
382
+ `security_settings_changed`), and `POST /security/sessions/revoke-all` (revokes the
383
+ caller's other sessions, audited).
384
+ - Concurrent-session enforcement now honors the stored `security.session.maxConcurrentSessions`
385
+ policy (config/default precedence preserved when unset; `0` = unlimited).
386
+ - Login responses include `mfaSetupRequired` when org-wide MFA is required but the account
387
+ has not enrolled past its grace window.
388
+ - `settings` global writes now audit nested `security` changes.
389
+
390
+ **cms-admin**
391
+ - New Security tab with a computed Security Status card, Authentication & MFA, Session
392
+ Policy (with revoke-other-sessions), Access & Request Protection, and Secrets/Encryption/Audit
393
+ cards. Wired to the real `/security` endpoints with source badges, dangerous-change
394
+ confirmations, lockout-safe MFA toggle, validation, and read-only states for non-admins.
395
+ Replaces the previous decorative security toggles.
396
+
397
+ ## 0.27.0
398
+
399
+ ### Minor Changes
400
+
401
+ - 3667ac2: Add the Settings → Appearance tab: a production-ready brand + admin-theme system.
402
+
403
+ **cms-core**
404
+ - New `@actuate-media/cms-core/appearance` subpath module with pure utilities:
405
+ logo-shape classification, WCAG contrast checks, admin accent-token
406
+ generation, and brand/favicon resolution (admin + public fallback chains).
407
+ - `GET /public/brand` (unauthenticated) resolves public logos + favicon
408
+ metadata for the public frontend; `GET /appearance` (authenticated) returns
409
+ stored appearance/brand settings plus resolved admin/public/favicon views and
410
+ per-asset URLs for editor previews.
411
+ - `PUT /globals/:slug` audit logging now records nested `appearance`/`brand`
412
+ changes.
413
+
414
+ **cms-admin**
415
+ - New Appearance tab (Branding, Admin Theme, Typography & Motion, live Brand
416
+ Preview) wired to the Media Center for logo/favicon uploads (assets referenced
417
+ by ID, never raw paths), with shape warnings, dark-logo warnings, accent
418
+ contrast validation, source badges, and read-only/permission states.
419
+ - `ThemeProvider` now applies color mode, accent color, sidebar density, content
420
+ font, and reduce-motion live, and feeds the resolved admin brand to the
421
+ sidebar logo (with site-initials fallback).
422
+
423
+ ## 0.26.0
424
+
425
+ ### Minor Changes
426
+
427
+ - 58eff9b: Build a production-ready General Settings tab with a computed Site Index Status card.
428
+
429
+ **cms-core**
430
+ - 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.
431
+ - Add `GET /seo/index-status` (ADMIN) returning the computed status, per-check rows, and actions.
432
+ - Audit `PUT /globals/settings` with a per-key before/after diff (`settings_changed`, including source + environment).
433
+ - 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.
434
+
435
+ **cms-admin**
436
+ - 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).
437
+ - 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.
438
+ - Use semantic theme tokens (purple `brand` accent, status colors) so the tab matches the design system in light and dark modes.
439
+
440
+ ## 0.25.0
441
+
442
+ ### Minor Changes
443
+
444
+ - 8e38837: Integrate the form block into the page builder (PR7).
445
+ - **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.
446
+ - **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.
447
+ - **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.
448
+
449
+ ## 0.24.0
450
+
451
+ ### Minor Changes
452
+
453
+ - b328ec8: Add the global **All Entries** inbox (`/forms/entries`) — a cross-form
454
+ submissions view with a Form column, per-form filter, the same search / status
455
+ pills (All / Unread / Starred / Archived), bulk actions, CSV export, pagination,
456
+ and detail drawer as the per-form Submissions view. The shared submissions
457
+ table is now extracted into `components/forms/EntriesTable.tsx` and reused by
458
+ both views.
459
+
460
+ Expand the **Forms sidebar item into a submenu**: an "All Entries" child plus
461
+ one child per active form, each carrying its own unread badge, with the total
462
+ unread surfaced on the parent (and as a dot on the collapsed rail / in the
463
+ hover flyout).
464
+
465
+ ## 0.23.0
466
+
467
+ ### Minor Changes
468
+
469
+ - 3a641fd: Rebuild the per-form Submissions view as a table (was a card list). Entries
470
+ now load from the real entries API via `forms-service.ts` instead of mock
471
+ attribution data, with summary chips (total / unread / starred / this week),
472
+ search, status pills (All / Unread / Starred / Archived), row selection with
473
+ bulk actions (mark read, star, archive, delete), per-row star toggle, and CSV
474
+ export. Clicking a row opens a token-only detail drawer that renders the
475
+ submission against the schema version it was captured with, surfaces sender
476
+ and attribution, marks the entry read on open, and supports star / archive /
477
+ mark-unread / delete. Adds entries client helpers (`fetchEntries`,
478
+ `fetchEntryCounts`, `fetchEntry`, `updateEntry`, `deleteEntry`,
479
+ `bulkUpdateEntries`) and replaces the previous raw-palette styling with theme
480
+ tokens throughout.
481
+
482
+ Add a reusable token-only `FormsPagination` footer (matching the Posts/Pages
483
+ list controls) and wire it into both Forms tables: the Submissions view now
484
+ paginates server-side (25 per page), and the Forms list paginates the filtered
485
+ results (10 per page). Both reset to page 1 when search or status filters
486
+ change.
487
+
488
+ The per-form notification toggles (Forms list "Notify" column and the schema
489
+ drawer Notifications tab) now read green when enabled and red when disabled
490
+ using the semantic success / destructive tokens.
491
+
492
+ ## 0.22.0
493
+
494
+ ### Minor Changes
495
+
496
+ - 9ed05b2: Forms admin — list view, schema drawer, and live sidebar badge (PR 3/9).
497
+ - Rebuilt the Forms list view to match the design: dynamic summary chips
498
+ (forms / active / total entries / unread) sourced from `/forms/stats`,
499
+ search across name/slug/description/field labels, Active/Draft/Archived
500
+ filter pills, and a compact table (Form / Fields / Status / Entries / Last
501
+ Entry / Notify / Schema) with field-type and status badges. The notify
502
+ toggle persists optimistically and rolls back on error.
503
+ - Added the right-side Form Schema drawer with Fields, Embed, and
504
+ Notifications tabs. The Fields tab is a keyboard-accessible drag-to-reorder
505
+ (dnd-kit) editor; saving creates a new schema version server-side so
506
+ historical submissions are never rewritten. Embed exposes copyable
507
+ public/schema endpoints, iframe/script snippets, and domain restrictions.
508
+ Notifications persists per-form recipients, templates, field inclusion, and
509
+ autoresponder settings (delivery wiring lands in a later phase).
510
+ - Added a typed `forms-service.ts` client (re-using `@actuate-media/cms-core`
511
+ domain types) and a tiny `forms-events` bus so the sidebar refreshes counts
512
+ after a mutation.
513
+ - The Forms sidebar item now shows a live unread badge (total unread across
514
+ active forms), rendered as a pill when expanded and a dot in the collapsed
515
+ rail. `NavItem` gained an optional `badge` field.
516
+
517
+ ## 0.21.1
518
+
519
+ ### Patch Changes
520
+
521
+ - 8891547: Scope the SEO surfaces to real front-end content and keep the admin out of search indexes.
522
+ - **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).
523
+ - **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.
524
+
525
+ - 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.
526
+
527
+ ## 0.21.0
528
+
529
+ ### Minor Changes
530
+
531
+ - aa9797d: Consolidate Users and API Keys into the Settings view as dedicated tabs. The
532
+ sidebar Settings group now shows only "Settings"; Users and API Keys are
533
+ reachable as tabs (`/settings?tab=users`, `/settings?tab=api-keys`). The legacy
534
+ `/users` and `/api-keys` routes still resolve and land on the matching tab, and
535
+ Settings tabs are now deep-linkable via the `?tab=` query param (with
536
+ browser back/forward support). The `Users` and `ApiKeys` views gain an optional
537
+ `embedded` prop that drops page padding and demotes the heading when rendered
538
+ inside the Settings shell.
539
+
540
+ ### Patch Changes
541
+
542
+ - a8976e2: SEO admin UI reliability and accessibility fixes:
543
+ - `useSeoResource` now ignores stale, out-of-order fetch responses (request-id
544
+ guard + unmount guard), so rapidly switching SEO views no longer flashes data
545
+ from a superseded request.
546
+ - `SeoEditorDrawer` Field wrapper auto-associates each `<label>` with its child
547
+ control via `useId`, fixing WCAG label/`for` associations for the SEO meta
548
+ inputs.
549
+
550
+ - 1bd4a49: SEO production-readiness Polish tier — post-launch nice-to-haves from the SEO review.
551
+
552
+ cms-core:
553
+ - **Markdown-aware heading analysis** — `analyzeContent` and `countH1` now detect
554
+ Markdown ATX (`#`/`##`) and Setext (`===`/`---`) headings in addition to HTML
555
+ tags, so markdown-bodied posts are no longer wrongly flagged "No H1 found" /
556
+ "no subheadings".
557
+ - **Correct default OG image URL** — the auto-generated `og:image` now points at
558
+ `/api/cms/og.png` (where the endpoint is actually mounted) instead of `/og.png`,
559
+ which 404'd for social crawlers.
560
+ - **Consistent environment resolution** — `resolveSeoEnvironment()` centralises the
561
+ production gate, preferring `VERCEL_ENV` over `NODE_ENV` everywhere (preview
562
+ deploys report `NODE_ENV=production`), so `noIndexNonProduction` reliably
563
+ noindexes preview deployments.
564
+ - **Admin route boundary** — the CMS middleware matches `/admin` and `/api/cms` on
565
+ a path-segment boundary, so sibling public routes like `/administrators` or
566
+ `/admin-news` are no longer treated as admin routes.
567
+ - **AI status semantics** — SEO AI endpoints now return `503` when the provider is
568
+ installed-but-unconfigured (vs `501` when the plugin is genuinely missing).
569
+ - Removed a duplicated `gradeForScoreBand` helper in favour of the canonical
570
+ `gradeForSeoScore`.
571
+
572
+ cms-admin:
573
+ - SEO filter chips use accessible toggle-button semantics (`aria-pressed`) instead
574
+ of misused `role="tab"`.
575
+ - Crawl-settings toggles use the Radix `Switch` primitive and are disabled while a
576
+ save is in flight.
577
+ - Score progress bars expose an accessible name and `aria-valuetext`; drawers
578
+ without a description no longer trigger Radix's missing-description warning.
579
+ - Redirect CSV export defers object-URL revocation (and mounts the anchor) so the
580
+ download isn't cancelled before it starts.
581
+ - The SEO AI service treats both `501` and `503` as the graceful "AI unavailable"
582
+ state.
583
+
584
+ - 90f53a6: Fix SEO Operations Center production-readiness blockers.
585
+ - **SSRF (`GET /seo/summary`)**: the link-health probe now uses `safeFetch`
586
+ (blocks private/loopback IPs, no redirect following), only probes same-origin
587
+ internal links, skips all probing when `NEXT_PUBLIC_SITE_URL` is unset, and is
588
+ gated to EDITOR+ — matching `/seo/link-health`.
589
+ - **`GET /llms.txt` leak**: now excludes soft-deleted documents and
590
+ internal/system collections (`__seo_config__`, `__*`, etc.) via the shared
591
+ `isInternalCollection` helper, so "deleted" and internal content can no longer
592
+ appear on a public surface.
593
+ - **`PUT /seo/content` data-layer bypass**: SEO field edits now route through the
594
+ new `updateDocumentSeoFields` action, which enforces the collection's
595
+ `access.update` rule, snapshots a `Version` row, recomputes the search index,
596
+ and fires the `afterUpdate`/cache-invalidate effects — while preserving the
597
+ undeclared SEO convention keys the generic field-access layer would strip.
598
+ Inbound SEO fields are now type- and length-validated.
599
+ - **cms-admin**: SEO read fetchers in `seo-service` now re-throw on `cmsApi`
600
+ errors so the error/retry UI works instead of rendering a misleading empty
601
+ fallback (e.g. a fake score of 0 / grade "Critical") on a backend failure.
602
+
603
+ ## 0.20.0
604
+
605
+ ### Minor Changes
606
+
607
+ - 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`).
608
+ - 5fa84d3: Media Library improvements:
609
+ - 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).
610
+ - 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).
611
+ - Removed the focal-point picker from the detail drawer to reduce clutter (the `focalPointX/Y` columns are retained in the schema).
612
+
613
+ - 5fa84d3: SEO Operations Center — a full detect → prioritize → explain → route → fix → audit → generate workflow.
614
+
615
+ **cms-core**
616
+ - 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.
617
+ - Pure, explainable scoring model (`seo/score.ts`): `calculatePageSeoScore` / `calculateSiteSeoScore` with documented weights and breakdowns.
618
+ - 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).
619
+ - 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.
620
+ - `/resolve` now returns Next.js-ready `metadata` plus validated JSON-LD; public `robots.txt` honors the admin-edited body.
621
+
622
+ **cms-admin**
623
+ - 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.
624
+
625
+ **mcp-server**
626
+ - 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`.
627
+
628
+ **create-actuate-cms**
629
+ - Scaffolded projects now ship a redirect-resolving `middleware.ts` backed by the database redirect table.
630
+
631
+ ## 0.19.0
632
+
633
+ ### Minor Changes
634
+
635
+ - 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.
636
+
637
+ 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.
638
+
639
+ ### Patch Changes
640
+
641
+ - d95c8e7: Post editor: edit the body as rich text, pick a featured image from the media library, and keep the body WYSIWYG.
642
+ - 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).
643
+ - 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.
644
+ - 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.
645
+
646
+ - 5567daf: Post templates: seed sensible defaults and route the admin through the dedicated endpoint.
647
+ - 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.
648
+ - 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.
649
+
650
+ - 1768a8a: Render the post type's template header on the public site so the live page matches the editor canvas and preview.
651
+ - `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.
652
+ - 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".
653
+
654
+ - 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.
655
+ - bf799ac: Posts editor polish: the post editor's permission gate now fails closed when no
656
+ role is present (the server already enforces access independently); typed the
657
+ `config` / `session` props on `PostSectionEditor` instead of `any`; removed a
658
+ dead category branch and an arbitrary brand color (now the `primary` token) in
659
+ the post header / empty-state; and corrected stale doc comments in
660
+ `NewPostPicker` and `PostTypesView`.
661
+ - d85e89e: Add a server-side aggregate `GET /posts` endpoint for the admin Posts area.
662
+ Filtering, sorting and pagination now run at the database level (one query plus
663
+ one count) and respect per-collection read scope, so the Posts list scales past
664
+ the old ~500-row-per-type client cap instead of pulling every document into the
665
+ browser. The admin `fetchPosts` service now calls this endpoint; its public
666
+ `Post` / `PostType` / `PostAuthor` shapes are unchanged.
667
+
668
+ ## 0.18.0
669
+
670
+ ### Minor Changes
671
+
672
+ - d649ba4: Pages production-readiness hardening:
673
+ - **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.
674
+ - **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.
675
+ - **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.
676
+ - **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.
677
+ - **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.
678
+ - **Preview:** the standalone preview "Close" button falls back to the editor when the tab wasn't script-opened.
679
+
680
+ Adds regression tests for the API mapping/write payloads, `fetchPages` counts/pagination, bulk concurrency, the schema guard, and the list view.
681
+
682
+ ## 0.17.0
683
+
684
+ ### Minor Changes
685
+
686
+ - 4fb0097: Add semantic theme tokens and migrate the Dashboard off raw palette colors.
687
+ - 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.
688
+ - `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.
689
+
690
+ No intended visual change to semantic/brand styling; the stat-card icon accents adopt the chart palette.
691
+
692
+ - 4fb0097: Dashboard performance: cache the expensive SEO compute and reduce refetch churn.
693
+ - **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.
694
+ - **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.
695
+
696
+ ## 0.16.1
697
+
698
+ ### Patch Changes
699
+
700
+ - 8a51783: Dashboard production-readiness fixes.
701
+ - **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`).
702
+ - **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.
703
+ - **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.
704
+ - A document opened from the dashboard via the bare `/pages/:id` route now receives the preview handler, matching the `/pages/:id/edit` route.
705
+
706
+ 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.
707
+
708
+ ## 0.16.0
709
+
710
+ ### Minor Changes
711
+
712
+ - 91885de: Add a section-based visual Page Editor for the Pages area.
713
+
714
+ `/pages/new` and `/pages/:id/edit` now open a flat, section-based editor (a
715
+ left **Sections** panel + scaled preview canvas + slide-over inspector)
716
+ instead of the tree-based page builder. Pages are an ordered list of
717
+ `PageSection`s persisted as JSON on the page document, so there is no schema
718
+ change.
719
+
720
+ Highlights:
721
+ - Sections panel with accessible drag-and-drop reorder (dnd-kit, keyboard
722
+ support), visibility toggle, duplicate, and delete-with-confirmation.
723
+ - Top bar with breadcrumb, live status indicator, real viewport presets
724
+ (4K / 1080p / Laptop / Tablet / Mobile) that scale the preview canvas,
725
+ Save Draft, and Publish.
726
+ - Shared `SectionRenderer` (Hero Banner, Text, Image + Text, By the Numbers,
727
+ CTA Banner) used by both the editor canvas and the chrome-free
728
+ `/pages/:id/preview` route, so the preview matches what ships.
729
+ - Schema-driven inspector for content/appearance/advanced settings, media
730
+ picking, draft/publish validation, unsaved-changes guards, permission
731
+ gating, and loading/empty/error states.
732
+
733
+ - 91885de: Add the Pages management area and restructure the sidebar.
734
+ - **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.
735
+ - **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.
736
+ - **Tag manager** (`/pages/tags`): create, rename, recolor, merge, and delete tags (detaching them from pages on delete).
737
+ - **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.
738
+ - Reserves the `/pages` and `/widgets` route namespaces and wires `/pages/new`, `/pages/:id/edit`, and `/pages/tags*`.
739
+
740
+ Note: page seed/mock data lives only in the private `apps/dev` package and is never published.
741
+
742
+ - 91885de: Add the Posts admin area (All Posts + Post Types).
743
+ - `/posts` aggregates every collection marked `type: 'post'` (or
744
+ `admin.group: 'Posts'`) into a single sortable, filterable table
745
+ with type tabs, search, status/author filters, bulk actions
746
+ (publish / move to draft / delete), and pagination. Counts come
747
+ from the data layer, not constants.
748
+ - `/posts/types` shows a card grid per post-type collection with
749
+ accent border, icon, description, post count, field count, and
750
+ URL base — plus a dashed "+ New Post Type" card linking to setup
751
+ instructions.
752
+ - `/posts/new` lands on a type picker before opening the existing
753
+ per-collection editor.
754
+ - `/posts/types/new` offers a **Quick Post Type** gallery (Case
755
+ Studies, News, Portfolio, Events, Team) that pre-fills a tailored
756
+ field set, icon, and accent color, plus a manual form — both feed a
757
+ paste-ready `actuate.config.ts` snippet generator. Collections
758
+ remain code-defined because the read-only serverless filesystem
759
+ rules out runtime config writes.
760
+ - Sidebar collapses every post-type collection under a single
761
+ "Posts" parent with fixed `All Posts` / `Post Types` children.
762
+ - `/api/cms/collections` now surfaces `admin.description`, `icon`,
763
+ `color`, and `group` so the admin client can render type cards
764
+ and tabs without a separate fetch.
765
+ - New `admin.color` field on `CollectionDefinition` (accent token).
766
+
767
+ V1 deliberately skips an in-app Post Type editor / field builder —
768
+ the cards click through to the existing per-collection list and
769
+ new Post Types are added via the snippet generator. A follow-up PR
770
+ introduces a DB-overlay for visual metadata (icon / color /
771
+ description) editable from the admin UI.
772
+
773
+ - 91885de: Add a design-driven Posts template system that mirrors the Pages section editor.
774
+ - **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`.
775
+ - **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.
776
+ - **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.
777
+
778
+ - 91885de: Page and Post previews now open in a new browser tab as standalone, chrome-free pages.
779
+ - 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.
780
+ - Preview routes render without the admin sidebar/header, so a previewed page looks like the real published page.
781
+ - The slim preview bar is now tab-scoped: "Close" (closes the tab) and "Open editor".
782
+ - `useAdminRouter` exposes `buildHref(path)` for constructing absolute, basePath-aware admin URLs without navigating.
783
+
784
+ - 91885de: Add a "View on site" editor action that previews a draft through the real public front-end.
785
+ - 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.
786
+ - New `lib/preview-link.ts` helpers: `createPreviewToken`, `buildPublicPreviewUrl`, and `resolveSiteUrl` (config `seo.siteUrl` → current origin).
787
+ - 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.
788
+
789
+ 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.
790
+
791
+ ### Patch Changes
792
+
793
+ - 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.
794
+ - **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.
795
+ - **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`.
796
+ - **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.
797
+
798
+ - 91885de: Sidebar: map the `navigation`, `menu`, and `list-tree` `admin.icon`
799
+ identifiers to Lucide icons so navigation/menu collections (e.g.
800
+ `@actuate-media/plugin-navigation`'s `navigations` collection) render
801
+ with a meaningful icon instead of the generic document fallback. Any
802
+ collection tagged `admin.group: 'Content'` already surfaces as its own
803
+ top-level Content nav item; this just gives "Menus"-style collections the
804
+ right glyph.
805
+
806
+ ## 0.15.0
807
+
808
+ ### Minor Changes
809
+
810
+ - 560d1de: Dashboard redesign + real-data backends.
811
+
812
+ `@actuate-media/cms-admin`:
813
+ - Sidebar now uses explicit Content and Settings group headers with nested
814
+ Pages children (Saved Sections, Templates). The collapse control moved
815
+ to the sidebar footer.
816
+ - Header hoists primary page actions (e.g. "+ New Post", "View Site")
817
+ into the top utility bar so the page hero is no longer the source of
818
+ truth for those affordances. The notification bell is wired to the
819
+ real `NotificationBell` component.
820
+ - Dashboard greeting picks the user's first name, and the "Delivery API"
821
+ - "Content Health" cards now read live data from new endpoints.
822
+
823
+ `@actuate-media/cms-core`:
824
+ - New `ApiRequestMetric` table (minute-bucketed counter + log-scale
825
+ latency histogram) and `recordApiRequest` instrumentation wired into
826
+ `handler-factory.ts`. Writes are fire-and-forget — request paths are
827
+ never blocked on the metric DB.
828
+ - New `ContentIssue` table + nightly `processContentHealthScan` cron
829
+ (`/api/cms/cron/content-health`) that runs a pure-logic scanner for
830
+ missing meta descriptions, missing alt text, outdated content
831
+ (>90 days), and broken internal links via HEAD probes.
832
+ - New endpoints `GET /api/cms/stats/api-delivery` and
833
+ `GET /api/cms/stats/content-health` return the dashboard's
834
+ aggregates with stable, never-undefined response shapes.
835
+ - Migration `0007_dashboard_metrics` adds both tables with the indexes
836
+ the new queries need. Safe to apply to populated DBs (no backfill).
837
+
838
+ `create-actuate-cms`:
839
+ - Scaffolded `vercel.json` now includes the nightly content-health
840
+ cron entry, and the deployment guide documents it.
841
+
842
+ ## 0.14.1
843
+
844
+ ### Patch Changes
845
+
846
+ - 40d48da: Two flake fixes from the OSS maintenance pass:
847
+ - **`NotificationBell` SSE/REST race.** The `seenIdsRef` lookup index
848
+ was reconciled with `items` via `useEffect([items])`, which runs
849
+ after React commits. An SSE push that arrived between a REST
850
+ refresh resolving and the effect running would read a stale ref,
851
+ fall through the de-dup gate, and append a duplicate row. The ref
852
+ is now maintained synchronously at every items-mutation call site,
853
+ and the SSE handler also re-checks the latest snapshot inside the
854
+ `setItems` updater as a final safety net.
855
+ - **`compareToDummyHash` eager-init test determinism.** Exported a
856
+ test-only `_waitForDummyHashReady()` so the timing-parity test can
857
+ measure verify-only latency (rather than racing the eager hash
858
+ precompute on a cold module). Added a paired test that asserts the
859
+ eager-init promise resolves with no work pending on the consumer
860
+ side — catches future refactors that accidentally drop the
861
+ top-level invocation.
862
+
863
+ ## 0.14.0
864
+
865
+ ### Minor Changes
866
+
867
+ - a0f6eba: Rich `@`-mention picker for comments (last Phase 7 carry-over).
868
+
869
+ **`cms-core`**: New `GET /api/cms/users/search?q=<query>` endpoint —
870
+ authenticated (any role), case-insensitive `contains` matching against
871
+ `name` + `email`, capped at 10 results, returns only `{ id, name,
872
+ email }` (no role / hash / audit metadata). Empty / whitespace
873
+ queries return an empty array so the endpoint can't be used as a
874
+ directory-enumeration tool.
875
+
876
+ **`cms-admin`**: New `<MentionableTextarea>` component plus pure
877
+ helpers in `lib/mention-picker.ts` (`detectMentionContext`,
878
+ `insertMentionToken`, `searchMentionUsers`). Wired into the three
879
+ comment composer surfaces in `CommentSidePanel`: the new-thread
880
+ textarea, the reply input, and the in-place edit textarea. The
881
+ picker is keyboard-driven (Arrow Up / Down, Enter / Tab to commit,
882
+ Escape to close) with a Radix-style listbox + ARIA roles, and
883
+ gracefully handles slow responses (drops stale requests when typing
884
+ quickly).
885
+
886
+ **`mcp-server`**: New `search_users(query)` tool exposes the same
887
+ lookup so AI agents can resolve teammates by name fragment before
888
+ drafting a comment. Wire format is unchanged — the picker writes
889
+ `@<userId>` tokens that the existing `extractMentionedUserIds`
890
+ parser already understands, so no notification schema migration is
891
+ required.
892
+
893
+ No breaking changes.
894
+
895
+ ## 0.13.0
896
+
897
+ ### Minor Changes
898
+
899
+ - 4732184: Per-user SSE push channel for the notification bell (Phase 7 carry-over).
900
+
901
+ `@actuate-media/cms-core` exposes a new authenticated SSE endpoint:
902
+
903
+ GET /api/cms/notifications/stream
904
+
905
+ The stream pushes each `comment_reply`, `comment_mention`, and
906
+ `comment_resolved` notification the instant it's persisted to
907
+ `DocumentNotification`. The wire format is `event: ready` (once on
908
+ connect, with the subscribed `userId`) followed by `event:
909
+ notification` frames carrying the full DTO, plus a 25 s heartbeat
910
+ keepalive and a 4 min server-initiated close so `EventSource`
911
+ reconnects before platform max-duration caps fire.
912
+
913
+ `@actuate-media/cms-admin` switches `NotificationBell` over to an
914
+ `EventSource`-backed subscription with the existing REST endpoints as
915
+ a convergence backstop (the in-process bus is single-node by design;
916
+ the poller now runs every 60 s instead of 30 s). The new
917
+ `subscribeNotificationStream` helper is exported for any consumer that
918
+ wants to drive its own UI off the same channel.
919
+
920
+ Both packages bump `minor`: the cms-core change adds a new public
921
+ route and a new realtime export surface; the cms-admin change adds a
922
+ new `NotificationsApi.subscribe` member and exports the
923
+ `NotificationStreamHandle` / `NotificationStreamOptions` /
924
+ `EventSourceLike` types. No breaking changes — callers who don't pass
925
+ a custom `api` continue to work unchanged.
926
+
927
+ ## 0.12.1
928
+
929
+ ### Patch Changes
930
+
931
+ - 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:
932
+ - `GHSA-37j7-fg3j-429f` (critical, fixed in 20.0.0): VM Context Escape → RCE through the script-execution sandbox.
933
+ - `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.
934
+ - `GHSA-6q6h-j7hj-3r64` (high, fixed in 20.8.8): `ECMAScriptModuleCompiler` interpolates unsanitised export names as executable code.
935
+
936
+ 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`.
937
+
938
+ ## 0.12.0
939
+
940
+ ### Minor Changes
941
+
942
+ - 02df678: Phase 4, PR #2 — admin form generator + cross-file type resolution.
943
+
944
+ `@actuate-media/cms-admin` adds a new `componentBlock` field renderer:
945
+ - `<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).
946
+ - Picking a new component seeds the props object from each prop's `defaultValue`, with structural defaults for required props that lack a literal default.
947
+ - Live structural validation mirrors `validateComponentBlockValue` from cms-core and surfaces per-prop errors under the correct input on every keystroke.
948
+ - The new field is wired into `FieldRenderer`'s `FIELD_MAP` under `componentBlock` and exported from the package's main entry.
949
+
950
+ `@actuate-media/component-blocks` extends the extractor:
951
+ - 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.
952
+ - TypeScript-ESM-style `.js` import specifiers resolve to their `.ts`/`.tsx` source siblings.
953
+ - 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.
954
+
955
+ Bug fixes from Bugbot review:
956
+ - `<EnumInput>` placeholder option now clears the field to `undefined` instead of silently selecting the first enum value (`Number("")` → `0` regression).
957
+ - `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.
958
+ - 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.
959
+ - `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).
960
+ - `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.
961
+ - `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.
962
+
963
+ - 12e042c: Phase 4, PR #4 — discriminated-union picker + marketing-grade docs.
964
+ Closes Phase 4 of the competitive gaps roadmap.
965
+
966
+ The headline UX win: a `union` PropType whose variants share a single
967
+ required literal field (the discriminator) no longer falls through
968
+ to the JSON textarea. The admin form renders a variant `<select>` and
969
+ a recursive object form for the chosen variant's remaining fields,
970
+ keyed automatically off the TypeScript shape — no manifest
971
+ annotation, no schema language, no configuration.
972
+
973
+ **New `@actuate-media/component-blocks` exports:**
974
+ - `detectDiscriminator(propType)` — pure function. Inspects a union
975
+ PropType; returns `{ field, kind, variants[] }` when every variant
976
+ is an object sharing a required-literal field with distinct values,
977
+ or `null` otherwise. The rule is strict by design — a half-
978
+ discriminated union would silently accept invalid states.
979
+ - `findVariant(value, detected)` — locate the variant whose
980
+ discriminator value matches a stored value. Used by the form to
981
+ keep the picker in sync, and by the validator + future migrator to
982
+ route checks into the correct variant's schema.
983
+ - New types: `DiscriminatedUnion`, `DiscriminatedVariant`,
984
+ `DiscriminatorValue`.
985
+
986
+ **`@actuate-media/cms-admin` updates:**
987
+ - `<PropInput>` `union` branch now calls `detectDiscriminator` first;
988
+ on a hit it renders the new `DiscriminatedUnionInput` (variant
989
+ picker + recursive object form for the chosen variant's remaining
990
+ fields). Non-discriminated unions still fall back to the JSON
991
+ textarea with a clearer hint.
992
+ - Variant switching preserves fields that exist on both variants
993
+ (so `{ kind: 'link', label: 'Go' }` → `{ kind: 'modal', label: 'Go',
994
+ modalId: '' }` keeps the label), drops variant-exclusive fields,
995
+ and seeds new required fields from their declared defaults.
996
+ - `defaultForType` now returns a structural default for discriminated
997
+ unions (first variant + its required field defaults). A required
998
+ discriminated-union prop arrives on screen as a fully-editable form
999
+ rather than a `Missing required prop` error.
1000
+ - `seedPropsForComponent` carries that forward — required
1001
+ discriminated-union props seed cleanly; non-discriminated unions
1002
+ / references / unknowns still stay absent so the validator's
1003
+ message and the data state agree honestly.
1004
+ - `clientShapeError` routes discriminated-union values into the
1005
+ matching variant's schema. New error messages: `cta.kind must be
1006
+ one of: "link", "modal"`, `cta.modalId is required but missing`,
1007
+ `cta must be an object with a 'kind' discriminator`. Same
1008
+ precision as plain object props.
1009
+
1010
+ **Example component upgraded:**
1011
+ - `examples/Hero.tsx` now ships a discriminated `cta` prop
1012
+ (`{ kind: 'link', href, label } | { kind: 'modal', modalId, label }`).
1013
+ The `/component-blocks-demo` live playground doubles as the
1014
+ marketing artifact and exercise the picker end-to-end.
1015
+
1016
+ **Docs:**
1017
+ - New `docs/component-blocks-intro.md` — marketing-grade landing page
1018
+ with the comparison matrix vs schema-first CMSs and a 60-second
1019
+ hands-on tour. The technical reference doc links to it from the top.
1020
+ - `docs/component-blocks.md` gains a dedicated "Discriminated-union
1021
+ picker" section covering the detection rule + programmatic API.
1022
+ Known limits refreshed; "variant picker ships in PR #4" notes
1023
+ removed.
1024
+ - `docs/roadmap-status.md` marks Phase 4 as feature-complete /
1025
+ in-review.
1026
+
1027
+ **Tests:** 18 new unit tests in `discriminated-union.test.ts`
1028
+ (positive + negative cases for the detector, `findVariant` matching);
1029
+ 7 new helper tests covering `defaultForType` / `seedPropsForComponent`
1030
+ / `clientShapeError` for discriminated unions; existing extractor
1031
+ test updated to assert the new Hero CTA shape via the public detector.
1032
+ Workspace test counts: component-blocks 141 → 159, cms-admin 82 → 89.
1033
+
1034
+ This PR closes Phase 4. The next session can start fresh on
1035
+ Phase 3 (real-time collaboration) or Phase 6 (importers).
1036
+
1037
+ ## 0.11.0
1038
+
1039
+ ### Minor Changes
1040
+
1041
+ - 72db484: Ship the Actuate Media brand mark as the default admin/login logo, and extend the existing `admin.branding` override to the login screen.
1042
+
1043
+ **Sidebar**
1044
+ - 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.
1045
+ - `config.admin.branding.logo` still overrides the default — unchanged behaviour for integrators who already configured a custom logo.
1046
+
1047
+ **Login**
1048
+ - 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.
1049
+ - New `branding?: LoginBrandingConfig` prop on `<Login />` exposes `logo`, `name`, and `tagline`. Pass `logo: null` to opt out of any logo on whitelabel deployments.
1050
+ - `AdminRoot` wires `config.admin.branding` through to `Login` automatically.
1051
+
1052
+ Docs updated in `docs/admin-guide.md` ("Admin Branding") with the override snippet.
1053
+
1054
+ - 8f2d3f8: Redesign the admin Dashboard and fix the recurring "content cut off on the left" layout bug.
1055
+
1056
+ **Dashboard (`@actuate-media/cms-admin`)**
1057
+ - 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).
1058
+ - 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.
1059
+ - 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.
1060
+ - Deterministic per-author avatar colors (hash-based) so the activity feed is stable across renders without state or extra fetches.
1061
+
1062
+ **Layout cutoff fix (`@actuate-media/cms-admin`)**
1063
+ - 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.
1064
+ - 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.
1065
+
1066
+ **Stats endpoint extension (`@actuate-media/cms-core`)**
1067
+ - `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.
1068
+
1069
+ - d3d06bf: Add layout primitives package (Phase 0.3 of the best-in-class roadmap).
1070
+
1071
+ Introduces a sanctioned set of layout components every admin view must
1072
+ compose against, so layout bugs (sidebar overlap, breakpoint mismatches,
1073
+ content blowout) cannot recur in new views:
1074
+ - `AdminShell` — the CSS-Grid based admin chrome. Owns the
1075
+ desktop/mobile decision, the sidebar overlay, and the scroll lanes.
1076
+ `<Layout>` now delegates to it.
1077
+ - `PageContainer` — the canonical per-page wrapper. Owns max-width,
1078
+ padding, and the title/description/actions header.
1079
+ - `Stack` — vertical stack with a single `space` token.
1080
+ - `Cluster` — horizontal wrap-friendly group with `align` / `justify`.
1081
+ - `Grid` — explicit-column or `autoFit` responsive grid.
1082
+ - `Split` — two-pane primary+aside layout that stacks on mobile.
1083
+ - `Box` — lowest-level forwardRef'd container (semantic `as` prop).
1084
+ - `tokens` — single source of truth for the spacing / radius /
1085
+ breakpoint scales (mirrors Tailwind).
1086
+
1087
+ All primitives are exported from `@actuate-media/cms-admin`. Existing
1088
+ views continue to render the same markup — the primitives package
1089
+ formalizes the patterns rather than reskinning the app.
1090
+
1091
+ - 5d18097: Wire per-type SEO defaults into the admin Settings panel — the last loose end from Slice A (SEO Completeness).
1092
+
1093
+ **`cms-core`**
1094
+ - 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.
1095
+ - 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).
1096
+ - `/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.
1097
+ - Empty strings in PUT payloads are treated as "unset" (fall back to static config) — the panel is purely additive.
1098
+
1099
+ **`cms-admin`**
1100
+ - 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).
1101
+ - Static `actuate.config.ts` values render as input placeholders so admins always see what they're overriding.
1102
+
1103
+ 12 new unit tests cover the merge semantics, unset behaviour, DB roundtrip, upsert (no duplicate rows), and DB-error fallback.
1104
+
1105
+ - e021937: Add typed component variants via a built-in `cv()` helper
1106
+ (class-variance-authority pattern, no runtime dependency).
1107
+
1108
+ Refactors `Button` and `Badge` to use typed variants, and introduces three
1109
+ new variant-typed primitives: `Card` (with `CardHeader` / `CardTitle` /
1110
+ `CardDescription` / `CardContent` / `CardFooter`), `Input`, and `Select`.
1111
+
1112
+ All existing props (Button: `variant`, `size`, `loading`; Badge: `status`)
1113
+ continue to work — the new API is purely additive. New options:
1114
+ - Button: `variant: 'outline' | 'link'`, `size: 'xs' | 'icon'`,
1115
+ `leftIcon`, `rightIcon`, `full`.
1116
+ - Badge: `tone`, `size`, `outline` for generic non-status badges.
1117
+ - Card / Input / Select: typed variants for size, padding, elevation,
1118
+ validation state.
1119
+
1120
+ Also exports each component's variant function (`buttonVariants`,
1121
+ `badgeVariants`, ...) so consumers can compose the same class strings
1122
+ outside the JSX (e.g. for headless components or third-party renderers).
1123
+
1124
+ ### Patch Changes
1125
+
1126
+ - 3115d90: Two follow-ups to the recent brand-logo addition:
1127
+
1128
+ **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.
1129
+
1130
+ **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.
1131
+
1132
+ - 848309f: Improve admin dev tooling (Phase 0.6).
1133
+ - `pnpm dev` in `packages/cms-admin` now runs `tsc --watch` and the
1134
+ Tailwind CLI `--watch` in parallel via `concurrently`, so JS _and_ CSS
1135
+ rebuild on save. Previously you had to remember to run `pnpm build:css`
1136
+ manually, which is what allowed the stale-precompiled-CSS class of
1137
+ layout bugs to slip in.
1138
+ - Adds `prettier-plugin-tailwindcss` so Tailwind class lists are
1139
+ auto-sorted in canonical order on save / `pnpm format`. The plugin is
1140
+ configured to recognize the in-house `cv()`, `cn()`, and `clsx()`
1141
+ helpers so classes inside variant maps are sorted too.
1142
+
1143
+ - a815027: Refactor admin shell layout to CSS Grid (with inline-styled grid template
1144
+ columns) and a JS-driven `matchMedia` breakpoint, replacing the brittle
1145
+ `fixed` ↔ `md:static` positioning toggle that repeatedly let dashboard
1146
+ content render under the sidebar when the precompiled CSS bundle was
1147
+ stale or browser-cached.
1148
+
1149
+ The new layout is bulletproof against missing utility classes: even if a
1150
+ single Tailwind class went missing, the grid column reserved by the
1151
+ parent guarantees the main content never overlaps the sidebar. Mobile
1152
+ overlay behavior is preserved.
1153
+
1154
+ Also adds a `dev:css` watch script for parallel CSS rebuilds during
1155
+ development.
1156
+
1157
+ - 8a87aac: Add Playwright visual regression infrastructure (Phase 0.5).
1158
+
1159
+ `e2e/visual-regression.spec.ts` snapshots four canonical admin screens
1160
+ (login, dashboard, posts list, settings) at four breakpoints
1161
+ (375 / 768 / 1024 / 1440 px), giving every PR a pixel-level safety net
1162
+ for the kinds of layout regressions that caused recurring "content under
1163
+ the sidebar" bugs.
1164
+
1165
+ New scripts:
1166
+ - `pnpm test:visual` — run the suite.
1167
+ - `pnpm test:visual:update` — refresh baselines after an intentional
1168
+ design change.
1169
+
1170
+ Docs: `docs/visual-regression.md`.
1171
+
1172
+ Baselines will be captured on the first CI run after this PR merges.
1173
+
1174
+ ## 0.10.0
1175
+
1176
+ ### Minor Changes
1177
+
1178
+ - 39f5941: Slice E — Preview & Publishing Polish.
1179
+
1180
+ **Draft preview links**
1181
+ - Preview tokens now accept a configurable `ttlSeconds` (clamped to
1182
+ `[60, 2,592,000]`, default 7 days). The old 5-minute hard-coded TTL
1183
+ was too short for sharing with clients.
1184
+ - `POST /preview/token` now verifies the document exists before
1185
+ issuing a token, includes the doc's `slug` in the response so the
1186
+ admin can build a friendly URL, and records the issuing user in the
1187
+ JWT `sub` claim for audit logs.
1188
+ - `GET /resolve?preview=<token>` drops the `status: PUBLISHED` filter
1189
+ when a valid token is presented, returning draft content at the
1190
+ document's real URL. The response includes a `preview: { active,
1191
+ expiresAt }` block so frontends can render a "preview mode" banner.
1192
+ Tokens are bound to a single `(collection, documentId)` — using one
1193
+ against a different URL returns `403`.
1194
+
1195
+ **Scheduled publishing**
1196
+ - New `POST /collections/:slug/:id/schedule` endpoint sets
1197
+ `scheduledAt` and/or `scheduledUnpublishAt` (validated as future
1198
+ ISO 8601 timestamps with `unpublish > publish`) and flips status to
1199
+ `SCHEDULED`. The existing scheduling cron picks the doc up at the
1200
+ scheduled time.
1201
+ - New `DELETE /collections/:slug/:id/schedule` cancels a pending
1202
+ schedule. `SCHEDULED` docs revert to `DRAFT`; `PUBLISHED` docs with
1203
+ a pending unpublish stay live.
1204
+
1205
+ **Admin UI**
1206
+ - New **Share preview** toolbar button on `DocumentEdit` opens a
1207
+ dialog to generate signed preview URLs with selectable TTL
1208
+ (1 hour / 1 day / 7 days / 30 days) and a copy-to-clipboard control.
1209
+ - New **Schedule** button in the Status panel opens a datetime picker
1210
+ for setting both publish and (optionally) unpublish times. Active
1211
+ schedules render an inline blue pill with the times. "Reschedule"
1212
+ and "Cancel schedule" supported.
1213
+ - New exported components: `SharePreviewLinkDialog`,
1214
+ `SchedulePublishDialog`.
1215
+
1216
+ **Tests & docs**
1217
+ - 20 new tests: preview adapter TTL clamping, JWT round-trip, secret
1218
+ isolation; `/preview/token` + `/resolve?preview=` happy/sad paths;
1219
+ schedule create/cancel/validation paths.
1220
+ - New `docs/preview-and-scheduling.md` walkthrough; `docs/api-reference.md`
1221
+ expanded with the new endpoints.
1222
+
1223
+ ## 0.9.0
1224
+
1225
+ ### Minor Changes
1226
+
1227
+ - 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.
1228
+
1229
+ ## 0.8.2
1230
+
1231
+ ### Patch Changes
1232
+
1233
+ - 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).
1234
+
1235
+ 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.
1236
+
1237
+ - 6d2ea6e: Two fixes that emerged while stabilizing the e2e suite:
1238
+ - **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.
1239
+ - **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 ?? []`.
1240
+
1241
+ ## 0.8.1
1242
+
1243
+ ### Patch Changes
1244
+
1245
+ - 22c314b: Fix React Rules of Hooks violation in `AdminShell` that caused
1246
+ "Rendered more hooks than during the previous render." after login.
1247
+
1248
+ The `useMemo` for the collection→type map (added in v0.8.0 to power
1249
+ PageBuilder routing) was placed _after_ the early-return guards for
1250
+ `setupRequired` and `!session`. The pre-login render therefore called
1251
+ N hooks; the post-login render called N+1 — which crashes React. The
1252
+ hook is now hoisted above the early returns so the count is stable
1253
+ across renders. Caught by the e2e Playwright suite (`auth.spec.ts →
1254
+ login redirects to dashboard`).
1255
+
1256
+ ## 0.8.0
1257
+
1258
+ ### Minor Changes
1259
+
1260
+ - 6db988e: Major engineering quality + integration upgrades.
1261
+
1262
+ **Security & reliability (cms-core)**
1263
+ - Audit log: fixed runtime crash from `orderBy: createdAt` → `timestamp`; never throws on insert; allows null `details`.
1264
+ - IPv4 + IPv6 + CIDR allowlist with canonical comparison; reject `unknown` IPs against non-empty allowlists.
1265
+ - Centralised trusted client IP extraction via `x-vercel-forwarded-for` / `x-real-ip` / `x-forwarded-for` (with explicit `trustProxy`).
1266
+ - Atomic Upstash `INCR` + `EXPIRE NX` rate limiter, fail-open with logging.
1267
+ - Encryption at rest for OAuth tokens, TOTP secrets, backup codes, and webhook secrets via `CMS_ENCRYPTION_KEY`.
1268
+ - Hardened TOTP login: `mfa_pending` JWT + per-IP/per-user rate limit + request fingerprint binding (no userId leak between steps).
1269
+ - Reauth required for TOTP setup/disable; revoke other sessions on TOTP changes.
1270
+ - OAuth state nonce bound to a cookie; `allowSelfSignup` flag; never silently link to password-protected accounts.
1271
+ - SSRF-safe `safeFetch()` for webhooks and `/seo/link-health`; concurrency cap + URL count cap; private IPs/loopback/link-local rejected.
1272
+ - Magic-byte + MIME validation + DOMPurify SVG sanitisation on `/media/upload`; tightened WebP/AVIF/GIF/SVG detectors.
1273
+ - Webhook + plugin hook dispatch moved outside the DB transaction so a rollback never fires events for a doc that no longer exists.
1274
+ - AI: per-user rate limit + token/cost budget + input length cap + balanced-brace JSON extractor + `validateTree` after every step + secret redaction in audit prompts.
1275
+ - CSRF exemption tightened to exact-prefix match on the rewritten path (no more `pathname.includes`).
1276
+ - `refreshSession`: validate session row exists & is active before re-issuing.
1277
+ - Public SEO endpoints require auth; `/llms.txt` filters PUBLISHED + non-deleted.
1278
+ - `/forms`, `/forms/:id/submissions`, `/search/global` users restricted to EDITOR+.
1279
+ - Open-redirect host allowlist for `/redirects`; folder routes validate scope on move/delete; password-reset email enumeration blocked.
1280
+ - `/script-tags/resolve` returns proper public cache headers.
1281
+
1282
+ **Integration fixes (consumer feedback)**
1283
+ - **`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.
1284
+ - **`createSiteClient()`** now auto-resolves `baseUrl` from `NEXT_PUBLIC_SITE_URL` or `VERCEL_URL` when not explicitly provided.
1285
+ - **`/public/globals/:slug`** defaults to public when no `access.read` is set (was a 403 footgun).
1286
+ - **`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.
1287
+ - **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.
1288
+ - **`BlockCatalog`** now reads custom blocks from `globalThis.__actuateConfig.pageBuilder.blocks` automatically. Also accepts an explicit `customBlocks` option and an `includeCore: false` flag.
1289
+ - **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.
1290
+ - **`/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.
1291
+ - **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.
1292
+ - **`pageBuilder.previewTheme`** now applied to the canvas via scoped CSS variables.
1293
+ - OpenAPI: documented `/public/globals/:slug` and `/resolve`.
1294
+
1295
+ **DX**
1296
+ - `.env.example` documents required formats for `CMS_ENCRYPTION_KEY` (64-hex), `BLOB_READ_WRITE_TOKEN`, and `RESEND_*`.
1297
+ - Tests: 266 passing (was 230); regression tests for IP allowlist, client-ip, redact, secret-storage, audit, upload-magic, server-site, custom block discovery.
1298
+
1299
+ ## 0.7.3
1300
+
1301
+ ### Patch Changes
1302
+
1303
+ - 58bf2e6: Add site-wide SEO robots defaults with per-page index/follow override controls.
1304
+
1305
+ ## 0.7.2
1306
+
1307
+ ### Patch Changes
1308
+
1309
+ - e2fabee: Add AI-friendly deployment diagnostics, align generated schemas with first-run admin features, and harden setup guidance for design-first deployments.
1310
+
1311
+ ## 0.7.1
1312
+
1313
+ ### Patch Changes
1314
+
1315
+ - b66800c: Align admin-facing API response shapes with cms-admin consumers, add Vercel Blob image configuration, and harden admin views against partial data.
1316
+
1317
+ ## 0.7.0
1318
+
1319
+ ### Minor Changes
1320
+
1321
+ - c3c55c7: Add AI-powered page generation pipeline with prompt-to-page workflow.
1322
+
1323
+ **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.
1324
+
1325
+ **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.
1326
+
1327
+ **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.
1328
+
1329
+ - 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).
1330
+ - 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.
1331
+ - 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.
1332
+
1333
+ ## 0.6.0
1334
+
1335
+ ### Minor Changes
1336
+
1337
+ - 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.
1338
+ - New PasswordResetToken Prisma model with migration 0005_password_reset_tokens.
1339
+ - POST /auth/forgot-password and POST /auth/reset-password API endpoints with rate limiting.
1340
+ - Core auth module (reset.ts) with token generation, email dispatch, and password update logic.
1341
+ - ForgotPassword and ResetPassword admin views with full form validation.
1342
+ - AdminRoot routes /forgot-password and /reset-password in the unauthenticated shell.
1343
+
1344
+ ### Patch Changes
1345
+
1346
+ - b5de8dd: Fix ESM module resolution and dependency duplication issues reported during consumer integration.
1347
+ - 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).
1348
+ - 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.
1349
+ - 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).
1350
+
1351
+ ## 0.5.0
1352
+
1353
+ ### Minor Changes
1354
+
1355
+ - 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.
1356
+
1357
+ ### Patch Changes
1358
+
1359
+ - Updated dependencies [54c7a3b]
1360
+ - @actuate-media/cms-core@0.6.0
1361
+
1362
+ ## 0.4.0
1363
+
1364
+ ### Minor Changes
1365
+
1366
+ - 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.
1367
+
1368
+ ### Patch Changes
1369
+
1370
+ - Updated dependencies [c08cf8b]
1371
+ - Updated dependencies [80aa939]
1372
+ - @actuate-media/cms-core@0.5.0
1373
+
1374
+ ## 0.3.0
1375
+
1376
+ ### Minor Changes
1377
+
1378
+ - 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.
1379
+
1380
+ ### Patch Changes
1381
+
1382
+ - Updated dependencies [f2cf7d7]
1383
+ - @actuate-media/cms-core@0.4.0
1384
+
1385
+ ## 0.2.1
1386
+
1387
+ ### Patch Changes
1388
+
1389
+ - 0621037: Make Dashboard collection-aware instead of hardcoding Posts/Pages labels.
1390
+
1391
+ **cms-core:**
1392
+ - `/stats` API now returns `collectionCounts` (per-collection document counts via groupBy)
1393
+ - Recent documents include `collection` field in response
1394
+
1395
+ **cms-admin:**
1396
+ - Dashboard reads `config.collections` to derive stat card labels, counts, routes, and quick actions
1397
+ - "Edit" links use `/{doc.collection}/{doc.id}` instead of hardcoded `/posts/{id}`
1398
+ - "New" quick actions are generated from configured collections
1399
+ - Falls back to generic "Documents" / "Pages" labels when no config is provided
1400
+
1401
+ - Updated dependencies [0621037]
1402
+ - @actuate-media/cms-core@0.3.1
1403
+
1404
+ ## 0.2.0
1405
+
1406
+ ### Minor Changes
1407
+
1408
+ - 4e3a5eb: RC2-RC4 and hardening release.
1409
+
1410
+ **cms-core:**
1411
+ - Replace isomorphic-dompurify with sanitize-html (eliminates Vercel ESM crash)
1412
+ - Remove cms-core from serverExternalPackages, move to transpilePackages
1413
+ - Add GET /api/cms/health endpoint with model probing and diagnostics
1414
+ - Harden db() Proxy with try/catch on property access and rawDb()
1415
+ - Harden safeCount/safeFindMany against Proxy throws
1416
+ - Add startup validation for missing CMS_SECRET (503 instead of 500)
1417
+ - Bypass auth on /stats when secret is missing (returns zero-data)
1418
+ - Loosen NextConfigLike types (experimental/images/webpack as unknown)
1419
+ - Implement login anomaly detection (new IP, brute force, unusual hours)
1420
+ - Implement webhook DNS resolution with private range checking
1421
+ - Implement TOTP reauth verification
1422
+ - Add Zod validation for secret in defineConfig (min 32 chars)
1423
+ - Read CMS_CORE_VERSION from package.json at runtime instead of hardcoding '0.1.0'
1424
+
1425
+ **cms-admin:**
1426
+ - Ship pre-compiled CSS (dist/actuate-admin.css) with all 48 responsive classes
1427
+ - New export: @actuate-media/cms-admin/styles/precompiled.css
1428
+ - Add CSS containment (`contain: layout style`) on .actuate-admin
1429
+ - Fix ShortcutHelp modal using React state instead of DOM manipulation
1430
+ - Add fixed-position layout isolation wrapper to AdminRoot
1431
+ - Add useApiData retry limits (maxRetries=3) with exponential backoff
1432
+ - Dashboard shows health-aware "Database Setup Required" banner
1433
+
1434
+ **cli:**
1435
+ - Add `actuate db:init` command (injects CMS models into Prisma schema)
1436
+ - Add `actuate db:status` command (checks model presence and DB connectivity)
1437
+
1438
+ **create-actuate-cms:**
1439
+ - Embed full CMS schema (9 models + enum) in generated schema.prisma
1440
+ - Add @actuate-media/cli to scaffolded devDependencies
1441
+ - Use pre-compiled CSS import in scaffolded admin layout
1442
+
1443
+ ### Patch Changes
1444
+
1445
+ - Updated dependencies [4e3a5eb]
1446
+ - @actuate-media/cms-core@0.3.0
1447
+
1448
+ ## 0.1.4
1449
+
1450
+ ### Patch Changes
1451
+
1452
+ - 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.
1453
+ - Updated dependencies
1454
+ - @actuate-media/cms-core@0.2.3
1455
+
1456
+ ## 0.1.3
1457
+
1458
+ ### Patch Changes
1459
+
1460
+ - Updated dependencies
1461
+ - @actuate-media/cms-core@0.2.2
1462
+
1463
+ ## 0.1.2
1464
+
1465
+ ### Patch Changes
1466
+
1467
+ - Updated dependencies
1468
+ - @actuate-media/cms-core@0.2.1
1469
+
1470
+ ## 0.1.1
1471
+
1472
+ ### Patch Changes
1473
+
1474
+ - 682dac7: Fix consumer integration issues discovered during MaidPro.co deployment
1475
+ - **cms-core**: API handlers gracefully degrade when CMS Prisma models are missing (returns 501 instead of 500)
1476
+ - **cms-core**: Accept CMS secret via `actuate.config.ts` (`secret` field) in addition to `CMS_SECRET` env var
1477
+ - **cms-core**: `withActuateCMS` now accepts `Partial<ActuateCMSConfig>` (no more forced type casts)
1478
+ - **cms-core**: `withActuateCMS` auto-injects all `@actuate-media/*` packages into `transpilePackages`
1479
+ - **cms-core**: Expanded Prisma schema fragment with all CMS models for consumer merge
1480
+ - **cms-admin**: Removed duplicate Tailwind import from admin styles (consumers already load Tailwind)
1481
+ - **platform-vercel**: Email adapter accepts both `defaultFrom` and `from` for sender address
1482
+ - **plugin-email**: Email plugin accepts both `defaultFrom` and `from` for consistency
1483
+
1484
+ - 3fd38a2: Fix all 13 integration issues discovered during MaidPro.co deployment
1485
+
1486
+ **cms-core:**
1487
+ - Add .js extensions to all ESM relative imports (58 files)
1488
+ - Replace workspace:\* with versioned deps in all packages
1489
+ - Add sideEffects:false for proper tree-shaking
1490
+ - Subpath exports (/config, /api, /middleware, /next) avoid barrel side effects
1491
+ - Stats/search routes use safeCount/safeFindMany for missing Prisma models
1492
+ - Ship prisma/cms-schema.prisma with all required CMS models
1493
+ - Accept both CMS_SECRET and CMS_SESSION_SECRET with clear error messages
1494
+ - Fix hardcoded /admin paths in OAuth flow (now uses getAdminPath())
1495
+ - Loosen withActuateCMS and createCMSMiddleware parameter types
1496
+ - Auto-inject transpilePackages and serverExternalPackages in withActuateCMS
1497
+ - Unify PluginDefinition with ActuatePlugin interface
1498
+
1499
+ **cms-admin:**
1500
+ - Scope all 40+ CSS theme variables to .actuate-admin instead of :root
1501
+ - ThemeProvider applies dark class to .actuate-admin, not document.documentElement
1502
+ - Scaffold uses (admin)/(site) route groups for full CSS/script isolation
1503
+ - Sidebar branding supports custom logo via config.admin.branding
1504
+
1505
+ **platform-vercel:**
1506
+ - Email adapter accepts both `from` and `defaultFrom`
1507
+
1508
+ **All plugin/platform packages:**
1509
+ - Fix ESM .js extensions in compiled output
1510
+ - Replace workspace:\* with ^0.1.0 for publishable deps
1511
+
1512
+ - Updated dependencies [682dac7]
1513
+ - Updated dependencies [3fd38a2]
1514
+ - @actuate-media/cms-core@0.2.0