# @getmunin/dashboard-pages

## 4.65.0

### Patch Changes

- 07f1d6e: analytics-tracker: expose a readiness signal. Once the tracker's public API is installed it sets `window.mn.ready = true` and dispatches a `munin:ready` CustomEvent on `document`, so consumers can run identify round trips (or any `window.mn.*` call) as soon as the async script is ready — no polling, no dependence on the loader's own readiness callback:

  ```js
  window.mn?.ready ? go() : document.addEventListener('munin:ready', go, { once: true });
  ```

  `skill://analytics/identify-visitors`, the frontend-integration playbook, and the dashboard embed snippet now show this pattern.
  - @getmunin/types@4.65.0
  - @getmunin/ui@4.65.0

## 4.64.0

### Minor Changes

- 1823364: Security hardening from a full audit.

  - **Voice tool bridges (Vapi, Threll):** enforce tenancy on every self-service tool call. The bridges previously disabled RLS without setting `app.org_id` and granted wildcard scope, allowing cross-tenant reads/writes; they now apply the standard tenancy GUCs and the restricted self-service scope set.
  - **OAuth JWT verification:** pin verification to the algorithm bound to the trusted JWKS key and reject symmetric algorithms, closing an algorithm-confusion gap.
  - **Analytics `identify` (BREAKING):** the identity hash now signs `${externalId}:${visitorId}` so a leaked hash can't link a different visitor. Compute `HMAC(secret, "<externalId>:<visitorId>")` where `visitorId` comes from the new `window.mn.getVisitorId()`. The server-rendered `data-external-id`/`data-user-hash` auto-identify is removed — do the read-visitor-id → sign → `window.mn.identify()` round trip instead.
  - **Webhook replay guidance:** documented that receivers should reject deliveries whose signed `createdAt` is outside a freshness window (in addition to the existing `x-munin-delivery-id` idempotency). No wire-format change — the signature scheme is unchanged.
  - **MCP scopes:** `webhooks_*`, `feedback_*`, and `system_alerts_*` tools now require real `webhooks:*` / `feedback:*` / `system_alerts:*` scopes instead of being gated by audience alone.
  - **Capability tokens:** view, unsubscribe, and email-open tokens now enforce a max age (and reject future-dated tokens), preventing indefinite replay of leaked links.
  - **Tool hints:** `conv_test_channel` and `conv_test_email_channel` are marked destructive (they open outbound vendor connections) so they prompt before running.
  - **Input validation:** a caller-supplied `endUserId` is validated against the caller's org in delegated-token minting and `crm_create_contact`.

### Patch Changes

- b4978ab: Format `date` and `datetime` fields in the read-only CMS entry drawer using the viewer's locale instead of printing the raw stored ISO string (e.g. `Jun 29, 2026, 12:00 PM` rather than `2026-06-29T12:00:00.000Z`), matching how the edit-mode date picker displays them.
- bd0cb38: fix(channels): stop the email channel dialog button label flashing on cancel

  The "Edit email channel" dialog derived its edit/create state live from the
  `editChannel` prop. Cancelling cleared that prop before the dialog's close
  animation finished, briefly re-rendering the still-mounted dialog in create
  mode (the footer button flashed from "Save changes" to "Create"). The edit
  state is now frozen while the dialog is open.
  - @getmunin/types@4.64.0
  - @getmunin/ui@4.64.0

## 4.63.1

### Patch Changes

- 0d56ab7: Fix two rendering gaps in the CMS draft-approval block editor. Array (string-list) and `multi_select` fields — and array-of-text block props such as a stat block's `items` — no longer fall through to a raw JSON dump: they render as a clean list / comma-separated values, and in edit mode get a proper list editor (add / remove / reorder) and a checkbox group. Markdown prose now renders GitHub-flavored markdown (tables, strikethrough) via `remark-gfm`, so a comparison table in a prose block shows as a table instead of raw pipe-delimited text. The GFM upgrade applies to all shared markdown rendering in the dashboard drawers (KB, outreach, conversation drafts, message bubbles).
- 9a87f0b: Fix dashboard overview row heights and pill text centering. Queue rows no longer grow taller on hover — the right-hand slot now always reserves the action buttons' height (`h-7`) whether it shows the timestamp or the hover-revealed approve/dismiss buttons. The "open conversations" rows now reserve the same height, so both sections line up at a consistent row height. Pills (e.g. the `CMS` badge) get `leading-none` so their uppercase text is vertically centered within the badge instead of floating high.
- Updated dependencies [9a87f0b]
  - @getmunin/ui@4.63.1
  - @getmunin/types@4.63.1

## 4.63.0

### Minor Changes

- 499cce8: Render and edit CMS blocks in the draft approval drawer. Block-typed fields (e.g. an article body) previously fell through to a raw JSON dump; they now render as labeled block cards — each prop shown through its own field viewer (markdown, assets, etc.). In edit mode you can change block prop values, replace inline assets, add blocks (by type), remove them, and reorder them. Saving converts expanded asset props back to ids and restores inline `asset://` references so block content round-trips without losing asset links.

  Also: the CMS draft drawer's `select` fields now use the shared `NativeSelect` (consistent chevron with the rest of the dashboard instead of the browser-default arrow), and the outreach draft drawer's "Edit" action is now disabled while an approve/dismiss is in flight, matching the other queue drawers.

- 5902396: Show who a conversation is with in the inbox drawer instead of a bare end-user id.

  `GET /v1/conversations/:id` (and the `ConversationDetail` it returns) now carries
  the resolved counterpart identity — `contactEmail`, `contactName`, `contactPhone`
  — preferring the linked `conv_contacts` row and falling back to the `end_users`
  row. Both the full and simplified conversation drawers render the email (then
  name) in the header rather than the raw end-user id.

  Also tightens the queue row layout so long titles truncate and the row actions
  swap in on hover without overlapping the timestamp.

### Patch Changes

- @getmunin/types@4.63.0
- @getmunin/ui@4.63.0

## 4.62.1

### Patch Changes

- @getmunin/types@4.62.1
- @getmunin/ui@4.62.1

## 4.62.0

### Patch Changes

- @getmunin/types@4.62.0
- @getmunin/ui@4.62.0

## 4.61.1

### Patch Changes

- 47eb749: Keep the dashboard's recent open conversations list in sync with realtime conversation events, so closing a conversation removes it from the list without a page reload.
- 308d78e: Share a single realtime WebSocket connection across all `useRealtime` callers instead of opening one socket per component. The dashboard previously held several concurrent connections (inbox, usage summary, recent conversations, system alerts, activity rail); they now multiplex over one connection, with each `event` frame routed to only the listeners subscribed to its channel. The `useRealtime` API is unchanged.
  - @getmunin/types@4.61.1
  - @getmunin/ui@4.61.1

## 4.61.0

### Patch Changes

- 7576050: fix(setup): show the managed provider name in the onboarding summary

  The "Lift-off" summary on the setup wizard always rendered the host of `providerBaseUrl`, so when the workspace was configured to use the managed AI provider it still displayed a stale bring-your-own-key host. The summary now renders the managed provider's name when no API key is set, falling back to the base-URL host for self-configured providers.
  - @getmunin/types@4.61.0
  - @getmunin/ui@4.61.0

## 4.60.0

### Patch Changes

- 84ee716: feat(access): show the authorizing member on each flock row

  The flock (Settings → Agents) groups OAuth connections by client _and_ the org member who authorized them, but only the client name was shown — so two members who each connected, say, Claude produced two visually identical rows with no way to tell whose access a revoke would cut off.

  `GET /v1/tokens` now joins the authorizing user and returns `user: { name, email }` per row. The Agents page shows that member inline after the client name ("Claude · Kjell Rune Monsø", with the email on hover and as the fallback when no name is set), replacing the "· N connections" count — which only reflected dynamic-client-registration reconnects and wasn't actionable, since a row already represents one member's access to one client and revoke cuts off that whole group.

- 6719043: Dashboard: replace the single "last conversation" widget with "Last open conversations" — the 20 most recently active open conversations, newest first, with closed/snoozed/spam filtered out.

  Conversations: add a snooze-wake worker that reopens snoozed conversations once their `snoozeUntil` elapses, flagging them as needing human attention so they resurface in the inbox. Previously `snoozeUntil` was stored but never honored, so timed snoozes never woke on their own.
  - @getmunin/types@4.60.0
  - @getmunin/ui@4.60.0

## 4.59.2

### Patch Changes

- 3546224: feat(dashboard-pages): export skeleton loading components

  `Skeleton`, `TableSkeleton`, `CardSkeleton`, `CardListSkeleton`, and the `SkeletonColumn` type are now re-exported from the package barrel so downstream consumers can reuse them. Previously they lived in `components/skeleton.tsx` and were only reachable via relative imports inside the package — the `exports` map exposes only `.` and `./server`, so deep imports were blocked too.

- 6f941e1: feat(access): OAuth-only flock with client identity; tidy end-user display

  **The flock (Settings → Agents)** now lists only OAuth-authorized agents. Delegated end-user tokens are no longer mixed in — they're managed on the End-users page. Each row leads with the OAuth client's name (e.g. "Claude · 3 connections") and a small client icon/glyph (matching the consent screen) instead of a generic "OAuth refresh token" label, the Origin column is dropped (its info moved into the primary label), and the table uses a fixed layout so the scopes list wraps inside the Token column instead of squeezing the other columns. `GET /v1/tokens` returns only OAuth agents (with `iconUrl`) and no longer merges the `tokens` table.

  **The End-users page** now shows a single identity line (name, else email, else phone, else "—") with an avatar of initials derived from the name ("Jens Pettersen" → "JP") or the email's first letter ("kjell@apps.no" → "K").
  - @getmunin/types@4.59.2
  - @getmunin/ui@4.59.2

## 4.59.1

### Patch Changes

- 7c3fa39: Refresh stale product copy: drop the hardcoded "~80 tools" count from the MCP server instructions (the surface has long since outgrown it) and replace the old "agent-native business apps" tagline with "the customer platform for the agentic era" in the dashboard metadata titles.
- 1940b63: fix(control): list OAuth agents from refresh tokens, not access tokens

  The previous fix read `oauth_access_token`, but MCP clients (Claude Code, Cursor, …) send a `resource` parameter per RFC 8707, so BetterAuth issues them **stateless JWT access tokens that are never persisted** — that table is empty in practice, so the flock still showed "Agents · 0".

  `GET /v1/tokens` now lists live (non-expired, non-revoked) **refresh tokens** — the durable record of a connected OAuth agent. Because dynamic client registration mints a fresh `client_id` on every connect, grants are collapsed into one row per (client name, user) with a connection count. Revoking a row soft-revokes (`revoked = now()`) every live refresh token in that group, so the agent can't refresh back in once its short-lived JWT expires.
  - @getmunin/types@4.59.1
  - @getmunin/ui@4.59.1

## 4.59.0

### Minor Changes

- 2e3b87a: feat(conv): per-channel default agent mode

  Add `defaultAgentMode` (`auto` | `draft_only` | `off`) to conversation channels. New conversations inherit the channel's mode when no explicit mode is passed — including inbound replies that fail threading and open a fresh conversation. Set an outreach-only inbox to `draft_only` so prospect replies are always drafted for human approval and never auto-sent, even when threading can't link the reply to its originating conversation. Configurable via `conv_setup_email_channel` and the email channel dialog.

### Patch Changes

- 0fb358d: fix(control): show OAuth-authorized agents in the flock

  The Settings → Agents page ("The flock") read only the `tokens` table, which is populated solely by delegated end-user tokens. OAuth-authorized MCP clients (Claude Code, Cursor, Claude Desktop, …) have their access/refresh tokens persisted by BetterAuth in the separate `oauth_*` tables, so a fully-connected agent always showed up as "Agents · 0 / No connected agents yet".

  `GET /v1/tokens` now also lists live (non-expired) OAuth access tokens — one row per (client, user), scoped to the calling org via `org_members` — with the OAuth client name as the origin. Revoking such a row (`DELETE /v1/tokens/:id` for an `oat_*` id) deletes both the access and refresh tokens so the agent can't silently refresh back in.

- ad62308: feat(settings): standardize settings page loading with content-shaped skeletons

  Replaces the inconsistent per-page "Loading…" text with content-shaped skeletons across every settings page (API keys, channels, trackers, team, end-users, activity, audit log, usage, agents, AI, account). Table pages render proportional column-width row skeletons, list pages render card placeholders, the usage page shows tile and by-agent placeholders, and the activity feed shows row placeholders with a vertically-centered empty state. The AI settings page now renders each section header with its own per-section loading instead of a single global loader.

- edabd57: fix(auth): move Turnstile widget below the footnote links on auth forms

  Repositions the Cloudflare Turnstile widget to render below the "Create an account · Forgot password?" (and equivalent) footnote links on the login, signup, and forgot-password forms, instead of between the password field and the submit button. Submit gating is unchanged — the button still stays disabled until a captcha token is present.

- Updated dependencies [2e3b87a]
  - @getmunin/types@4.59.0
  - @getmunin/ui@4.59.0

## 4.58.0

### Minor Changes

- cd6b338: feat(auth): optional Cloudflare Turnstile captcha on email auth endpoints

  Adds opt-in captcha protection to the BetterAuth email flows (`/sign-up/email`, `/sign-in/email`, `/request-password-reset`). It is disabled by default and turns on only when both `TURNSTILE_SITE_KEY` and `TURNSTILE_SECRET_KEY` are set — the server verifies the token via the captcha plugin, and the shared login / signup / forgot-password forms render the Turnstile widget using the public site key exposed through `/v1/auth/providers`. Requiring both keys avoids a lockout where the server enforces a captcha the client cannot produce. Self-hosters who set neither key see no change.

### Patch Changes

- 238d889: fix(auth): carry the pending OAuth authorize request through Google/GitHub social login

  When a client (e.g. an MCP server in Claude) starts the OAuth 2.1 authorization flow and the user signs in with a social provider, the social `callbackURL` now resumes the original `/auth/oauth2/authorize` request instead of dropping to the dashboard. Previously only the email/password path preserved the pending authorize request, so social logins skipped the consent screen and never completed authorization.

- Updated dependencies [3d91858]
  - @getmunin/types@4.58.0
  - @getmunin/ui@4.58.0

## 4.57.1

### Patch Changes

- @getmunin/types@4.57.1
- @getmunin/ui@4.57.1

## 4.57.0

### Patch Changes

- bbfbdba: Fix infinite /setup ↔ /oauth/consent redirect loop for managed-provider orgs.

  The server-side onboarding gate (`redirectIfSetupIncomplete`) still keyed off `providerApiKeySet`, while the client setup/dashboard gates were migrated to `providerConfigured` (= own key OR a usable host-supplied default provider) in #513. For orgs running on a managed default provider — which never set their own key — the two gates permanently disagreed: the setup page considered onboarding complete and forwarded to OAuth consent, while the consent page considered it incomplete and bounced back to setup. `redirectIfSetupIncomplete` now reads `providerConfigured`, matching the client gates.
  - @getmunin/types@4.57.0
  - @getmunin/ui@4.57.0

## 4.56.1

### Patch Changes

- 2a7498b: Show a "Saved" confirmation after selecting the managed provider in AI settings. Previously the confirmation only appeared for BYOK providers, because the managed save path never set the message.
- ccafe38: Tidy up the origin-allowlist create forms. The "Leave empty to allow any origin" hint is gone from both the widget channel and analytics tracker forms — it's misleading when the deployment requires an allowlist. When an allowlist is required — `NEXT_PUBLIC_WIDGET_REQUIRE_ALLOWLIST=1` for the widget form (mirroring backend `MUNIN_WIDGET_REQUIRE_ALLOWLIST`), `NEXT_PUBLIC_TRACKER_REQUIRE_ALLOWLIST=1` for the tracker form (mirroring `MUNIN_TRACKER_REQUIRE_ALLOWLIST`) — the origin field is marked `required`, so an empty submit is blocked client-side with the browser's native "Please fill in this field" prompt instead of a round trip to the server. The bespoke "at least one origin required" message has been removed in favour of the native one.

  Also fix the full-page loading spinner background. In light mode it rendered `bg-bone` (`#E8E4DC`, the warm outer chrome) while the body and page content are `bg-background` (`#FBFAF7`), so the spinner was a visibly different shade than the page it was loading. All full-page spinners now use `bg-background`, matching the body exactly in both light and dark mode.

  And fix the agent-recipe links on the dashboard Get Started screen, which pointed at `/guides/recipe-*` (a 404) when `NEXT_PUBLIC_DOCS_URL` was unset. They now use the same resolved docs host as the MCP setup snippets (`DEFAULT_DOCS_HOST`, which includes the `/docs` base), so they correctly land on `/docs/guides/recipe-*`.
  - @getmunin/types@4.56.1
  - @getmunin/ui@4.56.1

## 4.56.0

### Patch Changes

- ccbc3a4: Update UI runtime dependencies within range: lucide-react 1.21, @base-ui/react 1.6, tailwind-merge 3.6, next-intl 4.13, and @react-email/render 2.0.9.
- Updated dependencies [ccbc3a4]
  - @getmunin/ui@4.56.0
  - @getmunin/types@4.56.0

## 4.55.0

### Minor Changes

- e64b320: The setup/onboarding gate now treats an org as configured when the agent has a usable provider — not only when an org-level API key is set. `/v1/agent-config` exposes `providerConfigured` (`providerApiKeySet` OR a host-supplied `defaultProviderAvailable`), and `AgentHostModule.forRoot`/`forRootAsync` accept a `defaultProviderAvailable` flag. Hosts that supply a default provider can let key-less orgs finish onboarding and reach the dashboard; self-hosted setups (no flag) are unchanged.

### Patch Changes

- @getmunin/types@4.55.0
- @getmunin/ui@4.55.0

## 4.54.0

### Patch Changes

- @getmunin/types@4.54.0
- @getmunin/ui@4.54.0

## 4.53.0

### Minor Changes

- c3a62e1: Add host extensibility hooks for the agent runner and provider configuration:
  - Rate-limit counters can be incremented by an arbitrary amount (`record(bucket, amount)`); add monthly `ai_tokens` and per-minute `ai_generates` buckets.
  - The usage summary (`/v1/usage/summary`) reports monthly AI token usage, surfaced as a tile on the usage and overview pages.
  - Agent passes can report a `quota_exceeded` skip outcome.
  - The agent host accepts an optional provider factory, credential resolver, and pre-generate gate via `runnerOptions`. The gate is consulted for both live chat and scheduled background work (distinguished by a `trigger` argument), so a host can supply its own provider implementation and meter or limit usage per org without forking the runner.
  - The provider picker accepts host-supplied presets — including a credential-less "managed" preset that renders host content and clears the org key on selection — plus a default selection. The AI settings and usage pages accept an optional content slot.

- 82fef68: Redesign the onboarding "Lift-off" summary's website-import section into three real states — importing, failed, and succeeded — driven by live crawl progress.

  The web crawler now emits incremental progress (`{ total, done, recentPaths }`) as it reads pages; the runner persists it to a new nullable `curator_jobs.progress` column (throttled, best-effort), and the curator-job DTO surfaces it via `GET /v1/curator/jobs/:id`. The summary screen polls that to show a live `done / total` counter, a progress bar, and the paths being read while importing; the imported page count and duration on success; and the failure reason plus an inline **Retry import** on failure. A new internal `POST /v1/curator/jobs/:id/progress` endpoint backs the out-of-process runner path.

  Also align the full-screen loading screens with the page background: `AuthLoading` (and the root route loader) now paint `bg-bone` so the loader no longer flashes the lighter paper surface before the bone-backed page resolves.

### Patch Changes

- 1e51d24: Give the settings shell's full-screen loading spinner the same `bg-bone dark:bg-background` backdrop as the dashboard and onboarding spinners, so initial loads no longer flash a mismatched background.
- a8f1469: Rename the "Page views" nav label and section eyebrow to "Trackers" (nb: "Målepunkter") to match the section header and actions, and reword the subtitle to lead with the action.
- Updated dependencies [95f2983]
- Updated dependencies [82fef68]
  - @getmunin/types@4.53.0
  - @getmunin/ui@4.53.0

## 4.52.1

### Patch Changes

- @getmunin/types@4.52.1
- @getmunin/ui@4.52.1

## 4.52.0

### Minor Changes

- e0a87c0: Replace the one-way data export with bidirectional per-module import/export.

  Removes the dashboard "Data export" page and `GET /v1/export`. Adds symmetric
  `*_export` / `*_import` MCP tools and `/v1/<module>/export|import` REST endpoints
  for KB, CRM, CMS, Conversations, Outreach, and Analytics so an agent can move an org's data
  between a self-hosted server and the cloud in either direction. Imports upsert by
  natural key where one exists and return an `idMap` for foreign-key remapping;
  embeddings are regenerated on import; secrets are redacted and re-entered on the
  target; CMS asset bytes are copied to the target's storage. Adds
  `skill://playbooks/data-migration`.

### Patch Changes

- @getmunin/types@4.52.0
- @getmunin/ui@4.52.0

## 4.51.4

### Patch Changes

- @getmunin/types@4.51.4
- @getmunin/ui@4.51.4

## 4.51.3

### Patch Changes

- be9e0b3: Rename the analytics section label to be more concrete and natural. The nav item and section eyebrow change from "Analysesporing" to "Sidevisninger" (Norwegian) and from "Analytics trackers" to "Page views" (English), matching the existing "page-view analytics" subtitle. The individual-item term ("Målepunkter" / "Trackers") is unchanged.
- 139d00e: feat(channels): pick voice options from a dropdown, discover them over MCP, and dedup the Threll webhook

  Setting up a voice channel no longer makes you hand-type opaque ids. For Threll you now enter just the API key and press Continue — the account is resolved from the key (via `GET /v1/accounts/current`, since a key maps 1:1 to an account) and the dialog fetches that account's workers into a dropdown; nothing is persisted until you pick a worker and confirm, so cancelling leaves no channel and no webhook subscription behind. Vapi follows the same two-step shape: enter the API key (and optional public key / phone number id), press Continue, then pick the assistant from a dropdown — no more hand-typed assistant id. Edit dialogs load the same dropdowns from the channel's stored credentials.

  The Threll account ID is no longer required input anywhere (MCP `conv_configure_channel` / control-plane / dashboard) — it's derived from the key when omitted (still accepted as an optional override, and re-derived if the API key is rotated on edit). It's still persisted and shown as a chip on the channel row.

  Option discovery is exposed generically so agents get parity with the dashboard: a new `conv_list_channel_options` MCP tool returns a vendor's selectable options (Threll `workers`, Vapi `assistants`) as `{ value, label, hint }` groups — pass `vendor` + credentials before the channel exists, or `channelId` for an existing one. Adding discovery for a new vendor is just a `listOptions` method on its `ChannelAdminProvider`. The control plane exposes the same via `POST /v1/conversations/channels/options` and `POST /v1/conversations/channels/:id/options`.

  Threll webhook auto-setup now lists the account's existing subscriptions and reuses a matching one's signing secret instead of blindly creating another. The post-setup "webhook URL" screen is gone — Munin registers the webhook with Threll automatically.

  Vapi now auto-configures its webhook too: on create, Munin points the chosen assistant's `server` at the channel's webhook URL (with the shared-secret header) — but only when that server is unset or already a Munin URL, so it never clobbers an assistant you've wired elsewhere (in which case it falls back to the manual connection screen). The prior server config is stashed and restored when the channel is archived, via a new best-effort `onArchive` provider hook.

  When auto-setup would collide with an existing webhook, Munin now asks instead of failing. Threll rejects a second account-wide `*` subscription, and Vapi's server URL may already point elsewhere — in both cases setup now returns a `409 webhook_conflict` and the dashboard shows a "Replace existing webhook?" confirm. Confirming retries with `replaceWebhook: true` (Threll deletes the conflicting subscription and registers its own; Vapi overwrites the assistant's server URL); cancelling goes back with nothing changed. The flag is exposed on `conv_configure_channel` too, so agents can resolve the conflict the same way.

  Internal: the Threll and Vapi HTTP clients now route every call through one `request` helper that centralizes auth headers, timeouts, and status→error mapping; the dashboard `ApiError` now surfaces the response `code` so callers can branch on `webhook_conflict`.

- Updated dependencies [139d00e]
  - @getmunin/types@4.51.3
  - @getmunin/ui@4.51.3

## 4.51.2

### Patch Changes

- @getmunin/types@4.51.2
- @getmunin/ui@4.51.2

## 4.51.1

### Patch Changes

- 7744261: Render markdown in the dashboard conversation drawer. Message bubbles, internal notes, and the agent draft preview now render bold, italics, lists, links, code, and rules instead of showing raw markdown source — matching how the chat widget displays the same content.
- 1ee9d7f: Split the 1.7k-line `inbox-sections.tsx` into focused modules (`inbox-types`, `inbox-helpers`, `inbox-data` hook, `inbox-message-bubble`, `inbox-activity-rail`, `inbox-conv-drawers`), with `inbox-sections` retained as the section/list components plus a re-export of the public API. Pure refactor — no behavior change.
  - @getmunin/types@4.51.1
  - @getmunin/ui@4.51.1

## 4.51.0

### Patch Changes

- @getmunin/types@4.51.0
- @getmunin/ui@4.51.0

## 4.50.1

### Patch Changes

- @getmunin/types@4.50.1
- @getmunin/ui@4.50.1

## 4.50.0

### Minor Changes

- 3f034de: Auto-provision the Threll webhook subscription when creating a Threll voice channel.

  Munin now uses the Threll API key to register the webhook subscription with Threll (`POST /accounts/{accountId}/webhook-subscriptions`, `eventType: "*"`) and stores the signing secret Threll returns — the admin no longer generates a secret and pastes it into Threll. Provisioning happens atomically during channel create: the channel id is minted up front and the Threll call runs before the row is inserted, so if provisioning fails nothing is persisted and the dashboard shows a retry-only error. The webhook URL is built from the canonical server-side API base (`readApiBaseUrl()` / `MUNIN_API_URL`). The webhook signing secret is now Threll-owned and immutable, so the manual webhook-secret field is removed from the Threll create and edit dialogs. `ConfigureThrellBody` and the Threll MCP configure tool no longer accept `webhookSecret` on create. The Vapi flow is unchanged.

### Patch Changes

- 3e3c76a: Polish channel and analytics UI in the dashboard.
  - Unify the SMS/voice vendor picker into a gapless segmented control matching the MCP client selector (no gaps between buttons).
  - Make Threll the first voice provider and the default selection when the add-voice dialog opens; display it as "Threll.ai".
  - Improve the Norwegian analytics-tracker terminology: "Analytikk-sporere"/"Sporere" become "Analysesporing"/"Målepunkter", and "Sporenøkkel" becomes "Sporingsnøkkel".

- Updated dependencies [3f034de]
  - @getmunin/types@4.50.0
  - @getmunin/ui@4.50.0

## 4.49.0

### Patch Changes

- 5c7ef9c: Fix the CMS and KB queue drawers hanging on "Loading…" when their detail fetch fails. Previously a failed `/v1/cms/drafts/:id` or `/v1/kb/curation/candidates/:id` request (e.g. a 404) was swallowed, leaving the drawer stuck on the loading text indefinitely with the approve/dismiss actions still clickable.
  - Surface detail-fetch errors instead of swallowing them, keyed per queue item.
  - Replace the inline "Loading…" text box with a centered spinner in the middle of the drawer.
  - Show a centered error message with a retry button when the detail fails to load.
  - Disable the approve/dismiss/edit/schedule actions (and the ⌘↵ shortcut) while the detail is loading or errored.

- d5d03e9: Fix Norwegian (nb) localization terminology. "credentials" was translated as "legitimasjon" (an ID document) across the dashboard — now uses "påloggingsinformasjon". Also corrected two related strings: the audit-log subtitle's "Vedheft-bare" ("attach-only") → "kun-tillegg" for "append-only", and the tracker origins hints' "skjema" → "scheme" for URL scheme, matching the channels module.
  - @getmunin/types@4.49.0
  - @getmunin/ui@4.49.0

## 4.48.0

### Patch Changes

- fdf2e9c: Fix misaligned usage-KPI sparklines on the dashboard. Three causes were making the four cards' sparklines sit at inconsistent vertical positions:
  - **Stroke clipping** — the polyline mapped values to the full `0–22` viewBox height, so the 1.2px stroke was sliced in half at peaks and troughs. Values now inset into a `2–20` band so the line never touches the edges.
  - **Floating flat lines** — a constant series (`max === min`) no longer floats at an arbitrary height; it rests on the baseline like every other card's minimum.
  - **Collapsing delta row** — a tile with no comparison (e.g. API calls, whose previous period is 0) rendered an empty space that HTML collapsed to zero height, shifting its sparkline up by one line. The placeholder is now a non-breaking space so the row keeps its height.

- Updated dependencies [dc70c67]
  - @getmunin/types@4.48.0
  - @getmunin/ui@4.48.0

## 4.47.0

### Patch Changes

- 461a7fc: Align the dashboard "agent recipes" starter list with the recipe guides shipped in `@getmunin/docs-pages`. The panel had drifted: it still listed **Content Marketer** (renamed to Conversation Distiller), **CRM Deduper**, and **KB Curator** (both dropped — the built-in curator now runs `skill://crm/clean-contact-data` and `skill://kb/review-content` automatically), and their "view prompt" links pointed at guide slugs that no longer exist (404).

  Replaces those with current recipes (Lead Research, Lead Scoring, Conversation Distiller) alongside the still-valid Bug Triage, Renewal Watch, and SDR — keeping the list at 6, every `id` now resolving to a real `recipe-*` guide, and tool chips using current MCP tool names.

- 4b889cf: Rename MCP tools for naming consistency. The dominant convention is `<module>_<verb>_<object>`; these tools deviated and have been renamed:
  - `crm_propose_merge_candidate` → `crm_propose_merge` (the other merge tools all say "proposal", not "candidate")
  - conv channel admin (verb/object order): `conv_channel_configure` → `conv_configure_channel`, `conv_channel_test` → `conv_test_channel`, `conv_channel_send_test` → `conv_send_channel_test`
  - conv email: `conv_email_setup_channel` → `conv_setup_email_channel`, `conv_email_test_channel` → `conv_test_email_channel`, `conv_email_send_test` → `conv_send_email_test`
  - voice ("call", not voice/phone split): `conv_voice_call` → `conv_call_channel`, `conv_voice_call_contact` → `conv_call_contact`
  - end-user self-service (drop awkward possessive/suffix): `crm_log_activity_self` → `crm_log_my_activity`, `conv_request_handover_in_my_conversation` → `conv_request_human`, `conv_request_phone_call_for_my_conversation` → `conv_request_callback`
  - analytics report tools (add the verb the rest of the surface uses): `analytics_top_subjects` → `analytics_list_top_subjects`, `analytics_top_countries` → `analytics_list_top_countries`, `analytics_traffic_by_source` → `analytics_get_traffic_by_source`, `analytics_referrer_hosts` → `analytics_list_referrer_hosts`, `analytics_views_over_time` → `analytics_get_views_over_time`, `analytics_subject_engagement` → `analytics_get_subject_engagement`, `analytics_contact_journey` → `analytics_get_contact_journey`, `analytics_zero_result_searches` → `analytics_list_zero_result_searches`

  Breaking for MCP clients pinned to the old tool names.

- 448953f: Rename REST control-plane routes for naming consistency, following the same
  `<module>/<resource>` + spelled-out-verb conventions used across the rest of the `/v1` surface:
  - `v1/cms-drafts/*` → `v1/cms/drafts/*` (nest under the module like `crm/segments`, `kb/spaces`)
  - `v1/curation/jobs/*` → `v1/curator/jobs/*` (match the module name; frees "curation" to mean only the KB-nested qualifier)
  - `v1/curator/jobs/:id/ack` → `:id/acknowledge` (match `system/alerts/:id/acknowledge`; no more clipped verb)
  - `v1/admin/audit-logs` → `v1/audit-logs` (drop the lone `admin/` tier — every other admin resource sits directly under `v1/`)
  - feedback "reject" → "dismiss" to match the proposal-queue convention (`dismiss` everywhere else): REST `v1/feedback/:id/reject` → `:id/dismiss`, **and** the MCP tool `feedback_reject` → `feedback_dismiss`.

  The two controllers that both mounted `v1/usage` are merged into a single `UsageController`
  (routes unchanged — non-breaking).

  Breaking for REST clients pinned to the old paths and MCP clients pinned to `feedback_reject`.
  No deprecation aliases.
  - @getmunin/types@4.47.0
  - @getmunin/ui@4.47.0

## 4.46.0

### Patch Changes

- 32c883e: Log previously swallowed errors in widget realtime, dashboard, and voice session paths. Empty `catch {}` blocks now emit `console.warn` for socket lifecycle/fetch failures and `console.debug` for listener-loop exceptions so issues surface during debugging instead of disappearing.
  - @getmunin/types@4.46.0
  - @getmunin/ui@4.46.0

## 4.45.1

### Patch Changes

- f1b4446: Surface the backend's 4xx error message in the dashboard's `SaveErrorStage` instead of always rendering the generic "couldn't reach the server" copy. The shared dialog was discarding `ApiError.message` even when the server returned an actionable validation error. `SaveErrorDetail` now carries an optional `message`, and the channel and tracker save flows populate it from the parsed server body for 4xx responses (5xx and network errors keep the original "try again" copy).

  Also stops the widget channel create form from POSTing with an empty origin allowlist when the deployment requires one. Set `NEXT_PUBLIC_WIDGET_REQUIRE_ALLOWLIST=1` on the dashboard to mirror the existing backend `MUNIN_WIDGET_REQUIRE_ALLOWLIST` and the form will show an inline "at least one origin required" error before submit.
  - @getmunin/types@4.45.1
  - @getmunin/ui@4.45.1

## 4.45.0

### Patch Changes

- @getmunin/types@4.45.0
- @getmunin/ui@4.45.0

## 4.44.1

### Patch Changes

- @getmunin/types@4.44.1
- @getmunin/ui@4.44.1

## 4.44.0

### Minor Changes

- bb38781: Route incomplete-setup users through onboarding before the OAuth consent page.

  If an owner/admin with an unconfigured org (no provider API key, or empty org name) hit the OAuth authorize flow — e.g. adding the Munin MCP to an AI agent — they landed directly on `/dashboard/oauth/consent` and could grant access before completing onboarding. New accounts created during the OAuth flow already get routed through `/setup?<oauth_params>` from the signup form, then back to consent once `useSetupGate` clears; existing accounts with incomplete orgs skipped that step entirely because the consent page is exempted from `useDashboardGate`.

  Adds a server-side gate (`redirectIfSetupIncomplete`) used by `apps/web/app/[locale]/dashboard/oauth/consent/page.tsx`. The Server Component forwards the session cookie to `/v1/agent-config` and `/v1/me/memberships`, and when setup is incomplete for an owner/admin it `redirect()`s to `/setup?<oauth_params>` before any consent HTML is sent. Once setup completes, `useSetupGate` already routes back to `/dashboard/oauth/consent?<oauth_params>`, so the consent UI shows on the next pass.

- 70d50ed: Add tracker key rotation for analytics trackers.

  Settings → Channels has long exposed a "Rotate key" action that revokes the active `mn_widget_*` key and mints a fresh one. Settings → Analytics trackers had no equivalent — only the identity-verification secret could be rotated, leaving operators stuck with `analytics_revoke_tracker` + `analytics_create_tracker` (which loses the tracker's name and config) if a `mn_track_*` key leaked.

  Adds the missing symmetric action:
  - New `analytics_rotate_tracker_key` MCP tool that revokes the tracker's active `mn_track_*` keys and mints a fresh one.
  - New `POST /v1/analytics/trackers/:id/rotate-key` endpoint.
  - Dashboard now shows "Rotate tracker key" above "Rotate identity secret" on each tracker row, with a one-time copy dialog matching the channels flow.

### Patch Changes

- @getmunin/types@4.44.0
- @getmunin/ui@4.44.0

## 4.43.2

### Patch Changes

- ff825e8: Fix prerender failure on `/setup` (round 2).

  `useSetupGate` called `useSearchParams()` at the top level to detect a resumed OAuth-authorize flow. Even though consumers render the gate via a one-line `'use client'` page, that hook still triggers Next.js's SSG bailout — the wrapped `<Suspense>` around the wizard's `ReadyCard` (from 4.43.1) doesn't help because the gate runs before the wizard mounts.

  Since the params are only read inside a `useEffect` (for navigation side effects), there's no need to subscribe to a reactive hook. Read `window.location.search` lazily inside the effect instead, removing the SSG bailout from the gate entirely.
  - @getmunin/types@4.43.2
  - @getmunin/ui@4.43.2

## 4.43.1

### Patch Changes

- 85245b2: Fix prerender failure in consumer apps that statically render `/setup`.

  `AgentSetupWizard`'s `ReadyCard` calls `useSearchParams()` to detect a resumed OAuth-authorize flow. In Next.js 16, that hook bails out of SSG and requires a `<Suspense>` boundary; without one, consumers that prerender the wizard page fail their build with `useSearchParams() should be wrapped in a suspense boundary`.

  The OSS test app (`apps/web`) hides this by setting `export const dynamic = 'force-dynamic'` in `[locale]/layout.tsx`, so it never exercises the SSG path. Consumer apps that don't opt out of static rendering hit the failure as soon as they upgrade to the version where `useSearchParams` was introduced.

  Wrap the `ReadyCard` instance in a `<Suspense fallback={null}>` so the wizard works regardless of the consumer's static/dynamic configuration.
  - @getmunin/types@4.43.1
  - @getmunin/ui@4.43.1

## 4.43.0

### Minor Changes

- 3858d3e: Link analytics tracking to CRM contacts and chat conversations through a shared `end_users` identity.

  Until now the analytics tracker, the chat widget, and the CRM lived in three separate identity silos: `analytics_view_events` carried only an opaque `visitor_id`, while the widget and CRM both spoke `end_users.id`. A visitor's page-view history stayed orphaned even when they later identified themselves in chat or signed in.

  This change introduces an `analytics_visitor_identities` bridge table mapping `(org_id, visitor_id) → end_user_id`, and a denormalised `end_user_id` column on both event tables that the analytics service stamps at ingest time. Two write paths populate the bridge:
  - **Widget**: `findOrCreateEndUser` in `widget-ingest.service.ts` now upserts the bridge whenever a chat session carries a `visitorId`. The chat widget and the analytics tracker now share the same `localStorage` key (`mn.vid`), so a visitor who first opens the widget retroactively links their already-stored tracker visitor id.
  - **Tracker**: new `POST /v1/a/identify` endpoint plus a `window.mn.identify(externalId, userHash)` method on the tracker bundle. Identity is verified by HMAC against a per-tracker secret; mint one via `analytics_create_tracker` (returned once) or rotate with the new `analytics_rotate_tracker_identity_secret` tool. Tampered hashes are rejected silently.

  Query tools now accept an optional `endUserId` / `contactId` filter (`analytics_views_over_time`, `analytics_subject_engagement`, `analytics_top_subjects`), and a new `analytics_contact_journey` tool returns the chronological page-view + search timeline for a known visitor. Past anonymous rows stay orphaned — there is no retroactive backfill.

  The dashboard gains a **Settings → Analytics trackers** page that lists trackers, mints new ones (with the public key + identity secret revealed once), shows whether identity verification is configured, and lets admins rotate the identity secret or revoke the tracker without dropping to MCP tools.

  The tracker bundle gains a script-tag identity path (`data-external-id` + `data-user-hash`), matching the chat widget's embed shape. The runtime `window.mn.identify()` call remains as the SPA escape hatch.

  The chat widget gets a matching runtime identity path: `window.munin.identify(externalId, userHash)` posts to a new `POST /v1/widget/identify` endpoint. When an anonymous chat session identifies mid-flight, the backend migrates the conversation: the verified `end_users` row replaces the `anon:…` one, the contact's `metadata.externalId` is updated, and the analytics bridge is rewritten — so the same browser's prior page-views attach to the now-known visitor without losing the chat history.

### Patch Changes

- f470743: Send the email verification callback to the web app's `/verify-email` page instead of leaving it relative to the API host (where it 404'd).
- de6865f: Fix the default OpenRouter provider base URL — was `https://openrouter.ai/v1`, should be `https://openrouter.ai/api/v1`.

  `PerOrgConfigRepository` materialized new `agent_config` rows with the wrong host, so hitting `/models` returned OpenRouter's marketing HTML page and `AgentModelsService` choked when parsing it. Same typo in the dashboard's `PROVIDER_PRESETS` and in two `shouldEnablePromptCache` test fixtures.

  Existing rows already persisted with the wrong URL are backfilled by an idempotent `UPDATE` inside `AGENT_HOST_MULTI_TENANT_DDL` (multi-tenant only — the OSS singleton DDL defaults to Anthropic).

- b8119f3: Onboarding wizard: drop the "Customize chatbot" / "Tweak settings" buttons from the lift-off card; keep only "Go to dashboard" + "Back". When the user arrived through an OAuth authorize flow (e.g. signing up via an MCP client), preserve OAuth params through the signup → setup chain and replace "Go to dashboard" with a "Continue" button that lands on the OAuth consent page.
- Updated dependencies [3858d3e]
  - @getmunin/types@4.43.0
  - @getmunin/ui@4.43.0

## 4.42.0

### Patch Changes

- @getmunin/types@4.42.0
- @getmunin/ui@4.42.0

## 4.41.1

### Patch Changes

- @getmunin/types@4.41.1
- @getmunin/ui@4.41.1

## 4.41.0

### Patch Changes

- @getmunin/types@4.41.0
- @getmunin/ui@4.41.0

## 4.40.4

### Patch Changes

- @getmunin/types@4.40.4
- @getmunin/ui@4.40.4

## 4.40.3

### Patch Changes

- @getmunin/types@4.40.3
- @getmunin/ui@4.40.3

## 4.40.2

### Patch Changes

- cdb5793: OAuth consent + auth-shell mobile polish:
  - OAuth consent page now uses the dashboard's 0.5px hairline convention (`border-[0.5px]` + `dark:border-rule-on-dark`) on the section card, identity row, avatar tile, trust timeline, permission rows, scope pills, reassurance block, buttons, and result-pane status circle.
  - Long client IDs no longer overflow: the H1, lede paragraphs, identity-card display name, trust-timeline body, reassurance block, and result-pane panel get `[overflow-wrap:anywhere]` (and `min-w-0 [word-break:break-word]` on the 72px H1 so single unbreakable client IDs wrap inside the 720px column).
  - Auth-shell submit buttons (`AuthSubmit` + the inline `Link` CTAs on verify-email and reset-password success states) render their hairline via `shadow-[inset_0_0_0_0.5px_…]` instead of `border-[0.5px]`, matching the shared `@getmunin/ui` Button pattern — fixes iOS Safari dropping border edges on the ghost "Resend link" button.
  - @getmunin/types@4.40.2
  - @getmunin/ui@4.40.2

## 4.40.1

### Patch Changes

- 328cae6: OAuth consent identity card polish: the icon container now has a white background with the favicon inset (h-9 w-9 inside the 50×50 frame) so vendor logos sit on a neutral surface with breathing room, instead of bleeding to the dark edge. Tightened the gap between the client name and the "Registered …" line. Reassurance footer now links to **Settings → Agents** (`/dashboard/settings/agents`) — the actual revocation surface — instead of the stale "Connected Apps" label and path.
  - @getmunin/types@4.40.1
  - @getmunin/ui@4.40.1

## 4.40.0

### Minor Changes

- f8e82f2: OAuth consent page redesigned end-to-end. Three concrete changes:
  1. **Backend — enriched client lookup.** `GET /v1/oauth/clients/:id` now returns `{ client_id, name, uri, icon_url, redirect_uri_host, created_at }`. `name` falls back to a host-derived label when the client's DCR didn't include `client_name` (well-known hosts like `claude.ai`/`chatgpt.com`/`cursor.sh` get a branded label; anything else falls back to the bare host). `redirect_uri_host` is the host portion of the first registered redirect URI — the full URI stays off the wire.
  2. **Backend — favicon proxy.** New `GET /v1/oauth/clients/:id/icon` route. Server-side fetches `oauth_client.icon` if present, otherwise `https://<redirect_uri_host>/favicon.ico` using `safeFetch` (SSRF-guarded). Validates MIME (`image/*` only), caps response size, falls back to a generic SVG on any failure. Served from our origin with a 24h browser cache — keeps the user's IP off third-party hosts pre-authorization.
  3. **Frontend — SSR refactor + new layout.** The page is now an async server component (`apps/web/.../consent/page.tsx`) that fetches the enriched client info before render. The fixed CORS bug along the way: cookies are no longer sent on the lookup (closes the `Access-Control-Allow-Credentials` failure path that was leaving the page stuck on the raw `client_id`). New three-state machine (`new` / `granted` / `denied`) with intermediate result panes — instead of redirecting immediately on Authorize/Deny, the page shows a brief "Access granted/denied · Returning to claude.ai…" panel with spinner, then redirects. Layout matches the editorial design: serif headline that shifts copy per state, identity card with app icon, trust-timeline strip, grouped per-module permissions with `Read`/`Write` pills, reassurance block, and an actions footer.

  Also adds an `anonymous: true` opt-out on the `api()` helper for callers of `@PublicController` endpoints that shouldn't send the BetterAuth session cookie.

  i18n strings in `en.json` and `nb.json` updated to match the new copy; the keys are different from before (`title`, `lede`, `scopesLabel`, etc. reshaped — see the keys under `dashboard.oauthConsent`).

### Patch Changes

- @getmunin/types@4.40.0
- @getmunin/ui@4.40.0

## 4.39.0

### Patch Changes

- @getmunin/types@4.39.0
- @getmunin/ui@4.39.0

## 4.38.0

### Patch Changes

- @getmunin/types@4.38.0
- @getmunin/ui@4.38.0

## 4.37.0

### Patch Changes

- @getmunin/types@4.37.0
- @getmunin/ui@4.37.0

## 4.36.0

### Patch Changes

- @getmunin/types@4.36.0
- @getmunin/ui@4.36.0

## 4.35.0

### Patch Changes

- @getmunin/types@4.35.0
- @getmunin/ui@4.35.0

## 4.34.0

### Patch Changes

- @getmunin/types@4.34.0
- @getmunin/ui@4.34.0

## 4.33.0

### Minor Changes

- 9042f0e: Schema-driven CMS draft drawer + safeFetch streaming fix.

  **`@getmunin/core` — `safeFetch` body-stream lifecycle fix.** The undici agent was closed in a `finally` block as soon as `safeFetch` returned, so any response body larger than the initial socket receive buffer got cut off mid-stream and the body reader hung until the caller's `AbortSignal.timeout` fired. `safeFetch` now hands the agent's lifetime over to the response body via a `ReadableStream` wrapper that closes the agent on stream end, error, or cancel; small bodies and redirect/error paths still close immediately. New regression test exercises a 2 MB payload flushed in two halves with a 50 ms gap so this class of bug can't sneak back in. As part of the cleanup the same module dropped two silent `catch (() => {})` swallows in favour of `console.warn`, and the redirect/agent-cleanup logic was DRYed up.

  **`@getmunin/backend-core` — CMS draft + asset endpoints.**
  - `GET` and `PATCH /v1/cms-drafts/:id` now return `CmsDraftDetailDto extends EntryDto { fields: FieldDef[] }` so the dashboard always has the collection schema in hand.
  - New `POST /v1/cms-drafts/:id/assets` uploads an asset (`{ name, mime, base64Body, altText? }` JSON) and returns the `AssetDto`. It does not touch the entry — the dashboard stages the new asset locally and commits it on Save.
  - `CmsService.updateEntry` now runs `expandAssetsInDtos` before returning, so the PATCH response carries fully-expanded asset objects (previously the bare id string).
  - `CmsService.listDraftEntries` derives a fallback `title` (and exposes `titleFieldName`) via `title → name → headline → subject → first required text field → slug`, so collections without a hardcoded `title` field still surface a sensible header.
  - `validateEntryData` treats `""` / `[]` as "not present" for required-field purposes — previously a required text field with empty string passed validation.
  - `CmsInvalidError` carries structured `fieldErrors`, and the controller surfaces them as `{ message, fieldErrors: [{ field, message }] }` on 400 responses so the dashboard can highlight the offending field instead of dropping a toast.
  - `cms_create_collection` / `cms_update_collection` MCP descriptions now spell out that `fields` is an **ordered** array — order = render order in editor and public surfaces — and that `cms_update_collection` REPLACES the existing array.

  **`@getmunin/dashboard-pages` — schema-driven CMS draft drawer.**
  - Replaced the body-only editor with a per-field editor driven by `detail.fields`. Editors per type: `text` → input, `markdown` / `rich_text` → textarea (markdown is multi-row), `integer` / `number` → number input, `boolean` → checkbox, `select` → dropdown of `options.choices`, `date` / `datetime` → matching inputs, `asset` → drop-zone with click-to-pick, drag-and-drop, in-place replace, and uploading state.
  - Read-mode renders each field in a consistent `ValueBox` (matches body's existing border treatment); markdown via `ReactMarkdown`; assets as a 16:9 figure. Empty optional fields are hidden in read mode; the field whose name matches `titleFieldName` is also hidden (drawer header already shows it).
  - Save sends only the diffed fields as a single `PATCH /v1/cms-drafts/:id` with `{ data: ... }`. Asset fields serialize back to their id string.
  - Backend `fieldErrors` surface inline: red label + destructive border + `aria-invalid` + a `role="alert"` message under each editor (no more "validation failed: x" toast).
  - Asset drop-zone now reveals its "Replace cover image" label on hover with a paper-tinted overlay, instead of always overlaying text on the image.
  - Drawer header close button gets `shrink-0 whitespace-nowrap` so "close ×" stays inline next to long wrapping titles.
  - Inbox drawer reads its queue item from the live queue (by id) instead of holding a snapshot, so post-save header refreshes are visible.
  - New `ApiError.fieldErrors` carries structured field errors through the fetch helper. Unused i18n keys (`cmsBody`, `cmsBodyPlaceholder`, `cmsCoverImage`, `cmsCoverEmpty`) removed.

### Patch Changes

- 3c83ffe: Dashboard polish: four small bug fixes.
  - **`LoadFailed` red dot was invisible.** The eyebrow dot's class string was a concatenation typo (`bg-alert-bad-border-[0.5px]` — `bg-alert-bad-border` ran into a stray `border-[0.5px]`), so the dot rendered without a background colour. Fixed to `bg-alert-bad-border animate-pulse` to match the live-state dot pattern used elsewhere.
  - **`AuthForm` invalid border was invisible.** Same concatenation typo (`border-alert-bad-border-[0.5px]`) meant invalid inputs in the auth flow didn't actually get a red border. Now uses `border-alert-bad-border` (with the border-width already declared on the base class).
  - **Inbox `LoadFailed` no longer hugs the top-left.** When the overview can't load, the error card now sits in a `flex min-h-[70vh] items-center justify-center` wrapper so it's centred both horizontally and vertically.
  - **`SystemAlertsBanner` no longer flickers on transient reconnects.** A short WS blip used to flash the yellow "Connection lost. Reconnecting…" banner. The component now debounces the disconnected state by 1.5 s — if the socket reconnects within that window, the banner never shows; it still hides immediately on reconnect.
  - @getmunin/types@4.33.0
  - @getmunin/ui@4.33.0

## 4.32.0

### Minor Changes

- bd8cd79: Surface CMS draft entries in the dashboard approval queue. Adds `CmsService.listDraftEntries` + `archiveEntry`, a new `/v1/cms/drafts/*` control endpoint family for approve/schedule/dismiss/patch, and a dedicated CMS drawer with metadata grid, cover-image preview, inline body editor, and a schedule popover. The shared `QueueDrawer` is also split into per-kind files (`queue-drawers/{kb,crm,outreach,feedback,cms}.tsx`) backed by a small dispatcher so adding the next kind is a new file rather than another branch.

### Patch Changes

- f6cb178: Dashboard overview: the Live Now section's tinted background now spans the full page width instead of stopping at the page's `max-w-7xl` content cap. The section uses a full-bleed `w-screen` breakout and keeps inner content aligned via an inner `max-w-7xl` container. The dashboard shell `<main>` gets `overflow-x-clip` so the breakout can't introduce a horizontal scrollbar on browsers that reserve space for the vertical scrollbar.
- f6cb178: Inputs and dashboard reply/edit textareas now render at `text-base` (16px) on mobile and `md:text-sm` (14px) from the `md` breakpoint up. iOS Safari auto-zooms on focus whenever the focused field's effective font-size is below 16px; bumping mobile sizes avoids that without disabling viewport zoom (which is a WCAG 1.4.4 regression). Desktop density is unchanged.
- Updated dependencies [bd8cd79]
- Updated dependencies [f6cb178]
- Updated dependencies [03d62af]
  - @getmunin/ui@4.32.0
  - @getmunin/types@4.32.0

## 4.31.0

### Patch Changes

- @getmunin/types@4.31.0
- @getmunin/ui@4.31.0

## 4.30.0

### Minor Changes

- 56c588c: Auth pages now redirect already-signed-in users away from `/login` and `/signup` on the server, before any UI renders. Adds a new `@getmunin/dashboard-pages/server` subpath export with `getServerSession()` and `redirectIfAuthenticated({ locale, redirectParam })`, which forward the request cookies to the BetterAuth `/auth/get-session` endpoint and call the i18n-aware `redirect()` to `safeRedirect(redirectParam)` (defaults to `/dashboard`). The OSS `apps/web` login and signup pages adopt the helper. The server-only entry is kept off the main barrel export so client bundles aren't pulled into the `next/headers` graph.

### Patch Changes

- @getmunin/types@4.30.0
- @getmunin/ui@4.30.0

## 4.29.2

### Patch Changes

- 191a876: Auth-shell polish: nudged the logo to `left-8` on mobile (`md:left-14` from medium up) so it no longer clips into the left safe area on small screens, autoFocused the primary action on the forgot-password "sent", reset-password "done", and verify-email "done" success states so keyboard users land on the next step, restyled the reset/verify success CTAs to the ink/paper button treatment used elsewhere (dropping the legacy `auth-navy` token), and replaced the bordered info pill on the "email sent" state with a flat block that reads cleaner against the auth panel.
  - @getmunin/types@4.29.2
  - @getmunin/ui@4.29.2

## 4.29.1

### Patch Changes

- @getmunin/types@4.29.1
- @getmunin/ui@4.29.1

## 4.29.0

### Minor Changes

- bc0d601: Introduces `org_alerts`, a first-class operational alerts surface (new `system_alerts_*` MCP tools, `GET /v1/system/alerts`, `org_alert.opened|resolved|acknowledged` realtime events). LLM-provider and channel-inbound failure paths now write to alerts instead of dedicated `last_error` columns on `agent_health` / `conv_inbound_state`, which are dropped. The dashboard banner reads from the alerts feed and renders per-source CTAs.

  Auto-deactivates an inbound poll channel after 5 consecutive failures: `conv_channels.active` flips to `false` (so the worker stops hammering broken credentials), the existing alert metadata records `deactivatedAt` + `attemptCount`, and the channels settings page renders an `ACTIVATE` button. `POST /v1/conversations/channels/:id/activate` re-enables the channel and resolves the alert.

  Also fixes an `imapflow` crash loop in the email adapter: a late TLS socket error after `tick()` returned was emitted with no listener attached, terminating the Node process. The adapter now attaches an `error` listener at construction and tears down the client on `connect()` failure.

### Patch Changes

- @getmunin/types@4.29.0
- @getmunin/ui@4.29.0

## 4.28.0

### Patch Changes

- @getmunin/types@4.28.0
- @getmunin/ui@4.28.0

## 4.27.1

### Patch Changes

- cc3ac20: Fix two URL regressions in the dashboard.
  - **OAuth sign-in landed on the API host.** `LoginForm` and `SignupForm` were passing a path-only `callbackURL: '/dashboard'` to `authClient.signIn.social({ provider: ... })`. BetterAuth's backend resolves a relative callback against its own baseURL, so post-OAuth users were redirected to `<auth-host>/dashboard` (404) instead of the dashboard host they signed in from. Both forms now wrap `redirectTo` with `absoluteCallbackUrl(...)` so the value sent to the backend is fully qualified against `window.location.origin`.
  - **"View prompt" on the dashboard home pointed at the dashboard host.** The recipe links in `GetStarted` used a relative `href` of `/docs/guides/recipe-<id>`, which the browser resolved against the dashboard host instead of the docs host. They now prepend `process.env.NEXT_PUBLIC_DOCS_URL` (already read for the MCP-setup `docsHref`), matching how the MCP `docsHref` is constructed.

  Both regressions were latent until 4.25/4.26 (when the recipes moved to docs and the auth pages consolidated into this package); the first deploy of either path against a backend-on-a-different-host surfaced them.
  - @getmunin/types@4.27.1
  - @getmunin/ui@4.27.1

## 4.27.0

### Minor Changes

- b46a41c: Rename agent recipes to role/task-shaped names that match how teams already describe the work: Lead Enricher → **Lead Research**, Lead Scorer → **Lead Scoring**, Bug Spotter → **Bug Triage**, Renewal Watcher → **Renewal Watch**, Win-Back Agent → **Win-Back**, Outreach Drafter → **SDR**. Recipe slugs in `packages/docs-pages/src/guides/` follow (e.g. `recipe-bug-spotter` → `recipe-bug-triage`, `recipe-outreach-drafter` → `recipe-sdr`); `dashboard-pages` `RECIPES` data updated to match. Cloud-side dependants need a coordinated bump of `@getmunin/docs-pages` to pick up the new exports.

  Add two client guides: **Connect Hermes Agent** (Nous Research) and **Connect OpenClaw**, each with config snippets verified against the upstream MCP reference docs and the standard mint-key / verify / scope flow. Sort the Recipes and Clients categories alphabetically in `guidesByCategory()` so the sidebar and overview grid stay predictable as the library grows.

  Tighten cloud landing-page copy and tool chips to match the actual recipes: drop the non-existent `task://web/scrape-website` chip from Lead Research; fix Bug Triage's italic ("hiding in conversations", not "tickets") and body (filed as internal notes via `conv_send_message`, not "structured proposals"); soften Renewal Watch's body ("account signals" rather than a fabricated "usage + sentiment + open issues"); fill in tool chips that were omitted (Lead Scoring, Renewal Watch, Event Follow-up, SDR, Conversation Distiller).

  When the AI provider is unreachable on a brand-new conversation, the runtime now posts a generic hardcoded greeting (`"Hi, what can we do for you?"`) instead of escalating to a human — there is nothing for an operator to reply to before the visitor has said anything. The handover fallback path is unchanged for visitor replies: those still escalate with `"I'm having trouble responding right now. A teammate will follow up shortly."` (the trailing `"Thanks for your message —"` opener was dropped — the lead-in doesn't fit a turn where the visitor hasn't messaged us yet).

### Patch Changes

- @getmunin/types@4.27.0
- @getmunin/ui@4.27.0

## 4.26.0

### Patch Changes

- @getmunin/types@4.26.0
- @getmunin/ui@4.26.0

## 4.25.0

### Patch Changes

- @getmunin/types@4.25.0
- @getmunin/ui@4.25.0

## 4.24.3

### Patch Changes

- @getmunin/types@4.24.3
- @getmunin/ui@4.24.3

## 4.24.2

### Patch Changes

- 70a7388: fix(dashboard): keep agent-health banner at a stable 48px height across state changes, match topbar's mobile padding, and hide the CTA below `md` so the message text gets the freed width
  - @getmunin/types@4.24.2
  - @getmunin/ui@4.24.2

## 4.24.1

### Patch Changes

- @getmunin/types@4.24.1
- @getmunin/ui@4.24.1

## 4.24.0

### Minor Changes

- 20ab00c: Promote the auth pages — `LoginForm`, `SignupForm`, `ForgotPasswordPage`, `ResetPasswordPage`, `VerifyEmailPage`, and `AuthLoading` — into `@getmunin/dashboard-pages` so OSS and cloud can share one implementation. Each accepts the brand footer as a prop (`OSS_AUTH_FOOTER` or `CLOUD_AUTH_FOOTER`); the signup form keeps OSS's invite-token lookup and gains OAuth provider buttons; the login form keeps `redirectTo` handling and moves the forgot-password link into the footnote next to "Create an account" (shortened from "Forgot your password?" to "Forgot password?"). The shared `useTranslateError` (and the pure `translateError` / `getErrorCode` helpers) are now re-exported from the package root, replacing the per-app duplicates.

### Patch Changes

- cb596ae: Stop the agent-health banner from pushing the settings-page Sign out button below the viewport. The banner now sticks to viewport top alongside the topbar, has a fixed `h-12` (with `truncate` on the message so long provider-error reasons ellipsize instead of wrapping), and exposes its presence via a stable `.agent-banner` marker class that downstream layout reads with Tailwind's `group-has-[]:` modifier. The `DashboardShell` wrapper gets a `group` class; both topbar variants and the settings sidebar shift down by exactly `3rem` (the banner height) only when the banner is rendered — no JavaScript measurement, no `ResizeObserver`, no CSS custom properties. Pure CSS, so the layout shift happens in the same paint that mounts the banner.
  - @getmunin/types@4.24.0
  - @getmunin/ui@4.24.0

## 4.23.5

### Patch Changes

- @getmunin/types@4.23.5
- @getmunin/ui@4.23.5

## 4.23.4

### Patch Changes

- 6dfabd2: Introduce `@getmunin/emails`: a shared React Email package that owns every transactional template Munin sends.
  - New templates (en + nb where applicable, all returning `{ subject, html, text }`):
    `renderResetPasswordEmail`, `renderVerifyEmail`, `renderDeleteAccountEmail`,
    `renderOrgInviteEmail`, `renderChannelTestEmail`, `renderPartnerClaimEmail`.
  - Org invite + channel-test now ship HTML alongside plaintext, matching the design system (serif heading, mono eyebrow, accent CTA, fallback URL block, footer attribution).
  - Org invite is now localized (en + nb) — was English-only. The "inviter name" prefix is rendered when the controller can resolve the inviting user.
  - `apps/backend/src/auth/email-templates.ts` deleted; OSS auth flow now calls into `@getmunin/emails`.
  - `MUNIN_EMAIL_LOGO_URL` env (optional) overrides the raven asset URL — useful for self-hosters that don't want the request to leave their network.
  - Self-host setting: BetterAuth's `sendResetPassword` and `sendVerificationEmail` hooks now produce HTML mail in addition to text.
  - OSS dashboard gains `(auth)/forgot-password` and `(auth)/reset-password` pages (ported from cloud) plus a `(auth)/verify-email` landing page; "Forgot your password?" link added under the login password field. `auth.forgotPassword`, `auth.resetPassword`, and `auth.verifyEmail` i18n keys added to `dashboard-pages/src/messages/{en,nb}.json`.
  - @getmunin/types@4.23.4
  - @getmunin/ui@4.23.4

## 4.23.3

### Patch Changes

- @getmunin/types@4.23.3
- @getmunin/ui@4.23.3

## 4.23.2

### Patch Changes

- f0e5389: Security: close widget→admin escalation, SSRF in website-import, upload signing weaknesses, and control-plane authorization gaps.
  - Public `mn_widget_*` keys now resolve as a new `widget_agent` actor (not `admin_agent`), with audience forced to `self_service` and scopes narrowed to `conv:widget:write`. New `ControlPlaneGuard` rejects widget/end-user/partner actors and scoped admin keys (must have `*`) on `/v1/*` admin routes, so embedded widget keys can no longer mint, list, or revoke admin API keys, configure channels, or enqueue curator jobs.
  - Website-import enqueue and the underlying crawler validate URLs against private/loopback/link-local/cloud-metadata ranges. A new `safeFetch` helper enforces an undici dispatcher that re-validates the resolved IP at connect time (DNS-rebinding-safe) and walks redirects manually.
  - Local-storage upload signing switched from plain SHA-256 to HMAC-SHA256; `LocalFsStorage` throws on startup if `MUNIN_STORAGE_LOCAL_SECRET` is missing under `NODE_ENV=production`. Static asset serving sets `X-Content-Type-Options: nosniff`.
  - S3 uploads switched from presigned PUT to presigned POST with a `content-length-range` policy condition pinned to the declared size, so an oversized body is rejected by S3 itself. `cms_complete_asset_upload` HEADs the object and rejects (deleting the storage object) on size mismatch. `AssetStorage.presignedUpload` now returns `{ uploadUrl, uploadMethod, uploadFields, … }`; `AssetStorage.statBytes` is now required on the interface.

- Updated dependencies [f0e5389]
  - @getmunin/types@4.23.2
  - @getmunin/ui@4.23.2

## 4.23.1

### Patch Changes

- @getmunin/types@4.23.1
- @getmunin/ui@4.23.1

## 4.23.0

### Patch Changes

- @getmunin/types@4.23.0
- @getmunin/ui@4.23.0

## 4.22.0

### Patch Changes

- 6b4276d: Extend the feedback MCP surface with global roadmap search and voting.
  - `feedback_search` queries the public Munin roadmap (`GET /v1/public/feedback`) so agents can find an existing item to vote on before filing a duplicate. Supports `q`, `appScope`, `status`, `sort` (`votes`|`recent`), and `limit` (≤100).
  - `feedback_vote` casts the instance's vote on a published item via the HMAC-signed `POST /v1/public/feedback/:id/vote` endpoint. Idempotent on `(feedbackId, instanceId)`; surfaces 404 (item missing or not public) and 429 (per-instance quota) as typed errors.
  - `FeedbackForwarder` keeps a single HTTP entry point for submit/search/vote; reuses the existing `munin-feedback-intake-v1` HMAC derivation so both directions share one key and constant.
  - OSS landing page gains a "Read the docs →" link under the Get started / Sign in buttons (en + nb).
  - @getmunin/types@4.22.0
  - @getmunin/ui@4.22.0

## 4.21.0

### Patch Changes

- @getmunin/types@4.21.0
- @getmunin/ui@4.21.0

## 4.20.0

### Minor Changes

- cedba8d: Adds an opt-in feedback module: OSS instances can collect feedback locally and, with an org admin's explicit approval, forward each item to `feedback.getmunin.com`. Gated by `MUNIN_FEEDBACK_ENABLED` (default `false`) — when disabled, no controllers, no MCP tools, no outbound code path is loaded.
  - `db`: new `feedback_outbox` table (org-scoped, RLS) for pending items and `system_config` for the deployment-wide `instance_id`. Drizzle migration `0032_feedback_outbox.sql`.
  - `backend-core`: `@Global() FeedbackModule` exposing `feedback_{create,list,get,approve,reject}` MCP tools and `POST /v1/feedback` + `/:id/{approve,reject}` REST routes. `InboxController` takes `@Optional() FeedbackService` so pending items appear inline in `GET /v1/inbox`'s queue when the module is loaded. Approval signs the outbound payload with `HMAC(instance_id, "munin-feedback-intake-v1")` so cloud can verify by re-deriving. Also renames `assistants.controller`'s `getOrCreate()` → `findOrCreateAssistant()` to match the dominant `findOrCreate*` convention.
  - `dashboard-pages`: extends `QueueItem` / `useQueueBuilder` / `QueueRow` / `QueueDrawer` with a `feedback` kind so pending items render in the unified inbox queue, with attribution copy disclosing data flow to Munin developers.
  - `ui`: new `feedback` tone variant on `Pill`.

- 75ad065: Add GitHub OAuth sign-in alongside Google and expose a public `/v1/auth/providers` endpoint so the login UI can show only the providers the deployment has actually configured.
  - `backend-core`: new `readGithubProviderFromEnv()` reading `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET`, and a new anonymous `AuthProvidersController` at `GET /v1/auth/providers` returning `{ google, github }` booleans.
  - `dashboard-pages`: split `use-auth-providers.tsx` into a `'use client'` hook module and a server-safe `fetch-auth-providers.ts` so server components (e.g. the OSS login page in Next 16) can call `fetchAuthProviders()` without tripping the RSC client-boundary check. Adds `GoogleLogo` / `GithubLogo` exports, `or` + `googleButton` / `githubButton` i18n strings (en + nb), and uppercases the first OSS auth footer item.

### Patch Changes

- Updated dependencies [cedba8d]
  - @getmunin/ui@4.20.0
  - @getmunin/types@4.20.0

## 4.19.4

### Patch Changes

- @getmunin/types@4.19.4
- @getmunin/ui@4.19.4

## 4.19.3

### Patch Changes

- @getmunin/types@4.19.3
- @getmunin/ui@4.19.3

## 4.19.2

### Patch Changes

- @getmunin/types@4.19.2
- @getmunin/ui@4.19.2

## 4.19.1

### Patch Changes

- @getmunin/types@4.19.1
- @getmunin/ui@4.19.1

## 4.19.0

### Minor Changes

- 0501880: Rename the Partner-access settings nav label key and adjust the MCP
  tool-name guard test.
  - `dashboard-pages`: `nav.partnerAccess` → `nav.partner` (en + nb). The
    cloud overlay now uses `labelKey: 'partner'` and a shorter "Partner"
    label, moved from the Workspace group to Access & integrations.
  - `backend-core`: the OSS MCP integration test's negative assertion is
    updated to `feedback_create` to match the cloud-feedback module's
    renamed tools (`suggestion_*` → `feedback_*`). OSS behavior is
    unchanged — the guard still verifies cloud-only tools don't leak.

  No production users yet, so no backwards-compat aliasing.

### Patch Changes

- @getmunin/types@4.19.0
- @getmunin/ui@4.19.0

## 4.18.0

### Minor Changes

- a0d31d7: Collapse the public URL surface to three vars, drop the path rewriter,
  and split MCP from auth.
  - Rename Nest mount `/api/v1` → `/v1` everywhere (controllers, OpenAPI
    spec, frontend calls, docs, skills, tests, fixtures). External and
    internal paths are now identical, so the `MUNIN_API_URL` rewriter
    branch is gone.
  - Rename env var `MUNIN_MCP_URL` → `NEXT_PUBLIC_MCP_URL`. Node still
    reads it on the backend; the `NEXT_PUBLIC_` prefix lets the dashboard
    inline the canonical MCP URL into the bundle at build time.
  - New env var `NEXT_PUBLIC_AUTH_URL` carries the OAuth issuer / auth
    callback host. Backend uses it as BetterAuth `baseURL` (falling back
    to `NEXT_PUBLIC_MCP_URL` origin when unset). Cloud points this at
    `api.getmunin.com` so Google sign-in callbacks live on the
    user-facing host instead of `mcp.*`.
  - Drop `MUNIN_API_URL`, `MUNIN_AUTH_URL`, `MUNIN_BASE_URL` — all
    collapsed into the three `NEXT_PUBLIC_*` vars above.
  - `oauth.constants.ts`: `authorizationServerUrl()` now reads
    `NEXT_PUBLIC_AUTH_URL` (with the same fallback). Drop the unused
    `apiExternalUrl()` helper. Drop ornamental doc comments.
  - `bootstrap-app.ts` `publicUrlRewriteMiddleware` simplified to MCP-only.
  - `docs-pages`: new guides `connect-claude`, `connect-chatgpt`,
    `connect-gemini` under a new `clients` category. MCP overview links
    to them. Docs cURL examples derive `/v1` from `NEXT_PUBLIC_API_URL`.
    REST sidebar/section headings prettified via the new `prettifyTag()`
    helper. The hamburger menu and stale `MCP_SETUPS` cloud-host
    fallbacks are gone.
  - `dashboard-pages`: dashboard MCP-setup card uses runtime fetch +
    env-derived defaults so OSS dev sees localhost URLs (no cloud-host
    flash), and cloud sees the real subdomain.
  - Dark mode follows the OS via Tailwind `darkMode: 'media'` and
    `@media (prefers-color-scheme: dark)` blocks — no flip-the-class
    script, no FOUC.
  - Sticky docs header + sidebar use a `--docs-stuck-h` CSS var measured
    by a `ResizeObserver`, so the header height matches the sidebar's
    `top` offset regardless of viewport. Sidebar background extended to
    full body height via a `:has()` pseudo-element.

  No production users yet, so no migration shim — set the new env vars
  on first deploy.

### Patch Changes

- @getmunin/types@4.18.0
- @getmunin/ui@4.18.0

## 4.17.0

### Minor Changes

- f1cff47: Update Next.js to 16.2.6, React to 19.2.6, next-intl to 4.12.0, move
  `apps/web` fully to Turbopack, and clear the `pnpm audit` finding for `qs`
  via a workspace override.

  Notes on the Next 16 upgrade:
  - The root layout now lives at `app/[locale]/layout.tsx` (the empty
    `app/layout.tsx` shim is gone). The locale layout retains the standard
    `setRequestLocale` + `NextIntlClientProvider` setup; `force-dynamic` is
    set at the locale layout so every route SSRs at request time.
  - Cache Components / `experimental.rootParams` are **not** enabled. The
    Next 16 cacheComponents model interacts badly with next-intl's client
    hooks (open tracker amannn/next-intl#1493) — once next-intl supports it
    natively, the locale layout can switch to `await connection()` inside a
    `<Suspense>` boundary and recover Partial Prerender.
  - `middleware.ts` → `proxy.ts` (Next 16 rename).
  - `next.config.mjs` swaps the custom `webpack:` hook for a `turbopack:`
    block. Both `next dev` and `next build` run on Turbopack. The
    `resolveAlias` entry redirects `tw-animate-css` to its concrete CSS
    file because the package only declares the `style` export condition,
    which Turbopack does not honour.
  - TypeScript source across the workspace now uses `.ts`/`.tsx`
    extensions in relative imports (replacing the previous NodeNext
    `.js` convention). The `packages/tsconfig/base.json` enables
    `allowImportingTsExtensions` + `rewriteRelativeImportExtensions`, so
    tsc still emits `.js` extensions in compiled `dist/` output for Node
    ESM consumers. This closes the Turbopack gap from
    vercel/next.js#82945 without waiting on an upstream extensionAlias
    implementation.
  - `packages/dashboard-pages` and `packages/docs-pages` set
    `declaration: false` in their tsconfigs to silence TS2742 portability
    warnings from next-intl's destructured re-exports — these packages ship
    source (`"main": "./src/index.ts"`), so declarations were never emitted
    anyway.
  - Root `package.json` adds two `pnpm.overrides` entries:
    - `qs >= 6.15.2` — clears the moderate transitive vulnerability that
      reached the workspace through `supertest → superagent → qs`.
    - `next-intl ^4.12.0` — forces a single resolved version across the
      workspace. Without this, the loose peer-dep range (`^4.0.0`) on
      `@getmunin/dashboard-pages` and `@getmunin/docs-pages` let pnpm keep
      older copies of next-intl alongside the bumped one in `apps/web`,
      producing two distinct React contexts so `useTranslations` in
      dashboard/docs client components could not find the
      `NextIntlClientProvider` set up by the locale layout.

### Patch Changes

- Updated dependencies [f1cff47]
  - @getmunin/ui@4.17.0
  - @getmunin/types@4.17.0

## 4.16.0

### Patch Changes

- @getmunin/types@4.16.0
- @getmunin/ui@4.16.0

## 4.15.0

### Patch Changes

- @getmunin/types@4.15.0
- @getmunin/ui@4.15.0

## 4.14.0

### Minor Changes

- 1fe1031: Make public-facing URLs configurable instead of hardcoding `api.munin.eu` / `docs.getmunin.com`.
  - `packages/docs-pages/src/page.tsx` and `_components/rest-endpoint.tsx`: the example `curl` URL is built from `process.env.NEXT_PUBLIC_API_URL` (defaulting to `http://localhost:3001`), matching the existing pattern in `guides/chat-widget/page.tsx`.
  - `packages/backend-core/scripts/generate-openapi.ts`: the OpenAPI spec's `servers[0]` is built from `MUNIN_OPENAPI_SERVER_URL` / `MUNIN_OPENAPI_SERVER_DESCRIPTION` (defaulting to `http://localhost:3001` / `local dev`). Cloud deploys set these at build time to render docs against the right host.
  - `packages/dashboard-pages/src/data/mcp-setups.ts`: `buildMcpSetups` takes an optional second `docsHost` argument; `MCP_SETUPS` keeps using the cloud-prod default. `get-started.tsx` reads `process.env.NEXT_PUBLIC_DOCS_URL` so dev points at `docs.dev.getmunin.com` and prod at `docs.getmunin.com`.

  Brand-attribution links (`getmunin.com` in the chat-widget "Powered by" footer, the web-crawler User-Agent) stay hardcoded — they identify Munin itself, not the deployment.

### Patch Changes

- @getmunin/types@4.14.0
- @getmunin/ui@4.14.0

## 4.13.0

### Minor Changes

- 7977f92: Rename the env var `MUNIN_PUBLIC_URL` → `MUNIN_MCP_URL`.

  The old name didn't say what surface it pointed at; the new name is symmetric with `MUNIN_API_URL` and `MUNIN_WEB_URL` and reflects that the value is the canonical MCP resource URL (used by the JWT issuer, OAuth audience, bootstrap rewriter `→ /mcp`, RFC 9728 metadata, and the SMS/outreach webhook bases that piggyback on the backend's external host).

  **Breaking** — `process.env.MUNIN_PUBLIC_URL` is no longer read. Set `MUNIN_MCP_URL` instead. No backwards-compat alias (no production users yet). Internal constants `PUBLIC_URL_FALLBACK` and `DEFAULT_PUBLIC_URL` renamed to `MCP_URL_FALLBACK` / `DEFAULT_MCP_URL` for consistency.

  Cloud consumers should bump `@getmunin/*` and rename the env in their deployment config.

### Patch Changes

- @getmunin/types@4.13.0
- @getmunin/ui@4.13.0

## 4.12.0

### Patch Changes

- @getmunin/types@4.12.0
- @getmunin/ui@4.12.0

## 4.11.0

### Patch Changes

- @getmunin/types@4.11.0
- @getmunin/ui@4.11.0

## 4.10.0

### Patch Changes

- @getmunin/types@4.10.0
- @getmunin/ui@4.10.0

## 4.9.0

### Patch Changes

- 6cdc2fc: Expose `safeRedirect` and `resumeOauthAuthorizeUrl` from `@getmunin/dashboard-pages` so OSS and cloud auth pages can share the same post-sign-in redirect logic instead of each inlining a copy.

  `safeRedirect(raw, fallback?)` guards against open-redirects by only honoring same-origin paths (`/...`, not `//...`); defaults to `/dashboard`.

  `resumeOauthAuthorizeUrl(params)` returns the upstream `auth/oauth2/authorize` URL when the user landed on sign-in/sign-up while resuming an OAuth flow (i.e. `response_type=code` + `client_id` are present in the query), or `null` otherwise. Reads the API base from `NEXT_PUBLIC_API_URL`.
  - @getmunin/types@4.9.0
  - @getmunin/ui@4.9.0

## 4.8.0

### Patch Changes

- @getmunin/types@4.8.0
- @getmunin/ui@4.8.0

## 4.7.1

### Patch Changes

- @getmunin/types@4.7.1
- @getmunin/ui@4.7.1

## 4.7.0

### Patch Changes

- 5108510: `MUNIN_PUBLIC_URL` is now the **canonical MCP resource URL** verbatim — no implicit `/mcp` appending. Adds an optional `MUNIN_API_URL` for a canonical REST URL.

  **Backend (`@getmunin/backend-core`)**
  - `mcpResourceUrl()` returns `MUNIN_PUBLIC_URL` exactly. `authorizationServerUrl()` (and `readPublicBaseUrl()`) return its origin.
  - New `publicUrlRewriteMiddleware` maps the canonical external URLs onto the internal Nest mount points — `/mcp` for MCP, `/api/v1` for REST. So a deploy can advertise `https://mcp.example.com` (no path) and `https://api.example.com/v1` while every controller stays mounted at its original internal path. Pass-through when the env vars name the same internal path (OSS default).
  - Adds `MCP_INTERNAL_PATH` (`'/mcp'`) and re-exports the old `MCP_RESOURCE_PATH` for back-compat.

  **Default change** — OSS default `MUNIN_PUBLIC_URL` is now `http://localhost:3001/mcp` (path included). Existing self-hosters who set `MUNIN_PUBLIC_URL=http://localhost:3001` (no path) will see their OAuth resource URL change from `…/mcp` to bare host — every active token will need refreshing. To keep the old behavior verbatim, set `MUNIN_PUBLIC_URL=http://localhost:3001/mcp`.

  **Dashboard (`@getmunin/dashboard-pages`)**
  - `GetStarted` fetches the canonical MCP URL from `/.well-known/oauth-protected-resource` and renders it in the Claude / ChatGPT / Gemini config snippets. OSS self-host now shows `http://localhost:3001/mcp` (or whatever the local backend advertises); cloud shows `mcp.getmunin.com`.
  - `mcp-setups.ts` ships a `buildMcpSetups(host)` helper alongside the static fallback.
  - @getmunin/types@4.7.0
  - @getmunin/ui@4.7.0

## 4.6.1

### Patch Changes

- @getmunin/types@4.6.1
- @getmunin/ui@4.6.1

## 4.6.0

### Minor Changes

- b770bce: OAuth consent UX rework and bootstrap MCP removal.

  **Backend**
  - New `GET /api/v1/oauth/clients/:clientId` endpoint (anonymous, on `OAuthModule`) returns the disclosure-safe fields `{ client_id, name, uri, icon }` from the `oauth_client` table. Lets the consent page render the registered client name + URL + logo instead of the random RFC 7591 `client_id`.
  - `SUPPORTED_SCOPES` gains `outreach:read` / `outreach:write`. Outreach MCP tools are retagged from `crm:*` to `outreach:*` so an external connector can be granted outreach access without inheriting CRM access.

  **Dashboard pages**
  - `OAuthConsentPage` rewritten:
    - Fetches the new client-info endpoint on mount, falls back to `client_id` if missing.
    - Hides scopes that aren't user-tunable on the consent screen — `openid`, `profile`, `email`, `offline_access` (OIDC/OAuth standards required by any connector), and `mcp:tools` / `mcp:admin` / `mcp:self_service` (the MCP umbrella + audience-decided-by-user, not by-scope).
    - Groups remaining scopes by user-facing app: Knowledge Base, Conversations, Contacts, Content, Outreach. Internal modules (`curator`, `playbooks`, `web`) are not surfaced — they remain reachable via the `mcp:tools` umbrella.
    - Disclosure footer: "Sign-in identity and session refresh are also granted."

  Scope-narrowing checkboxes at consent time are still deferred — needs upstream `@better-auth/oauth-provider` support or a wrap-and-mutate layer in the consumer.

  **Bootstrap MCP removal**
  - Removes the `bootstrap_status` / `bootstrap_answer` MCP tools, the `@getmunin/bootstrap` package, the per-app `*.bootstrap.ts` runners (kb / conv / crm / cms), and the `bootstrap_state` table (migration 0028). The conversational first-run wizard was redundant with the dashboard's UI onboarding and never picked up real callers. Direct admin tools (`kb_create_space`, `crm_create_pipeline`, `cms_create_locale`, `cms_create_collection`, `conv_*_setup_channel`) now cover everything bootstrap did.
  - Skill markdown for `kb-onboarding` and `conv/bulk-channel-setup` rewritten to call the direct tools.

### Patch Changes

- @getmunin/types@4.6.0
- @getmunin/ui@4.6.0

## 4.5.1

### Patch Changes

- @getmunin/types@4.5.1
- @getmunin/ui@4.5.1

## 4.5.0

### Patch Changes

- @getmunin/types@4.5.0
- @getmunin/ui@4.5.0

## 4.4.1

### Patch Changes

- 71a6c84: Fix sign-in error alert. Two bugs:
  - `auth.signIn.invalid.hintWithReset` used `{resetLink}` placeholder syntax, but the consumer (cloud login) calls `t.rich(...)` with a React-function value, which requires `<resetLink>...</resetLink>` tag syntax. The mismatch silently rendered nothing for the link, producing user-visible text like `"Check the address, or ."`. Switched the message to tag syntax (`<resetLink>reset your password</resetLink>`); the dead `resetLinkLabel` key is removed.
  - Added `auth.signIn.unreachable.{title,hint}` so consumers can distinguish "wrong credentials" from "backend unreachable" instead of showing the same alert title for both. The OSS login page now picks the right title/body based on whether `authClient.signIn.email` returned a structured error or the request threw.
  - @getmunin/types@4.4.1
  - @getmunin/ui@4.4.1

## 4.4.0

### Minor Changes

- ac20d4b: Mobile responsive pass across the dashboard:
  - **Overflow**: responsive `px-4 md:px-10` on the overview container, and `min-w-0` on the Get-Started grid cells so the long `Authorization: Bearer mn_live_…` snippet no longer widens the body and bleeds the recipes column past the viewport.
  - **Tables**: api-keys, team, agents, audit-log, and end-users tables now hide low-priority columns on mobile (`hidden md:table-cell`) and wrap in an `-mx-6 overflow-x-auto px-6` scroll container so anything still overflowing scrolls within the content area instead of widening the body.
  - **Hover-on-touch**: enable Tailwind's `future.hoverOnlyWhenSupported` so `hover:` and `group-hover:` only fire on devices with `@media (hover: hover)`, eliminating sticky-hover on tap.
  - **Truncation**: `RecentConversations` rows now truncate as a single line (move `truncate` from the inline preview span to the parent block).
  - **Topbar (mobile)**: org/brand name now appears centered in the topbar on mobile (was desktop-only). Settings menu button is now a `<Button variant="outline" size="icon">` instead of an inline `<button>`.
  - **Dashboard hero**: eyebrow shows the date only; org name moved to the topbar.
  - **Section dividers**: get-started's top hairline removed; recent-conversations and queue rows keep their soft-gray bottom border on the last item so the section self-closes.

  ### `@getmunin/ui`
  - **Button primitive**: all variants except `link` now render their hairline frame via `shadow-[inset_0_0_0_0.5px_…]` instead of `border-[0.5px]`. Shadows are rasterized through a different paint path and don't collide with adjacent hairlines (table-row bottom borders, header bottom borders), which on iOS Safari Retina was dropping the button's bottom edge.
  - **Pill primitive**: same shadow-inset hairline using `currentColor`, so the frame inherits whatever text color the variant sets without a separate `border-current` declaration.

  The `border-[0.5px]` convention is unchanged everywhere else (Hairline primitive, card / dialog / input / table-row dividers, etc.); only the elements that sit flush against another hairline switched to the shadow rendering path.

### Patch Changes

- Updated dependencies [ac20d4b]
  - @getmunin/ui@4.4.0
  - @getmunin/types@4.4.0

## 4.3.0

### Minor Changes

- 21a8189: Add a "Last conversations" section to the dashboard home, below the Usage KPIs. Lists up to 10 conversations from the past 7 days, sorted by most recent message. Each row shows the subject (or `Conversation #displayId` fallback), the last inbound (end-user) message as a muted preview, status badge for non-open states, and a relative timestamp. Click opens the conversation drawer. Hidden when there's nothing in the 7-day window.

  Backend: `ConversationSummary` gains an optional `lastInboundPreview` field. `GET /api/v1/conversations` populates it via a correlated subquery over `conv_messages` (latest non-internal `author_type='end_user'` body, collapsed and truncated to 200 chars). Other code paths that build a summary leave the field undefined.

  Dashboard: matching styling pass — Queue and Last-conversations headers now use the same ink-black eyebrow + ink underline pattern as Usage; the trailing row border is dropped via `last:border-b-0`. Extracts `useRelative` to `lib/use-relative.ts` so the new section and the existing inbox rows share one source of truth.

### Patch Changes

- 21a8189: Introduce `@getmunin/docs-pages`: lifts the developer-portal routes (`/docs`, `/docs/rest`, `/docs/mcp`, `/docs/skills`, `/docs/guides`) out of `apps/web` into a shared package so munin-cloud can mount the same docs under its own auth/header chrome. The OSS `apps/web/app/[locale]/docs/*` routes are now thin one-liner shells that re-export from the package.

  `@getmunin/backend-core` now publishes the OpenAPI spec and docs fixtures (mcp-tools.json, skills.json) via package subpath exports (`@getmunin/backend-core/openapi.json`, `@getmunin/backend-core/docs-fixtures/*`) so downstream consumers can read them at build time.

  Dashboard: removes the CONV pill from the Last conversations rows — the conversation rows in that section are conversations by definition; the pill was redundant.
  - @getmunin/types@4.3.0
  - @getmunin/ui@4.3.0

## 4.2.0

### Minor Changes

- 0040252: Add a "Last conversations" section to the dashboard home, below the Usage KPIs. Lists up to 10 conversations from the past 7 days, sorted by most recent message. Each row shows the subject (or `Conversation #displayId` fallback), the last inbound (end-user) message as a muted preview, status badge for non-open states, and a relative timestamp. Click opens the conversation drawer. Hidden when there's nothing in the 7-day window.

  Backend: `ConversationSummary` gains an optional `lastInboundPreview` field. `GET /api/v1/conversations` populates it via a correlated subquery over `conv_messages` (latest non-internal `author_type='end_user'` body, collapsed and truncated to 200 chars). Other code paths that build a summary leave the field undefined.

  Dashboard: matching styling pass — Queue and Last-conversations headers now use the same ink-black eyebrow + ink underline pattern as Usage; the trailing row border is dropped via `last:border-b-0`. Extracts `useRelative` to `lib/use-relative.ts` so the new section and the existing inbox rows share one source of truth.

### Patch Changes

- @getmunin/types@4.2.0
- @getmunin/ui@4.2.0

## 4.1.1

### Patch Changes

- 8c11b50: Rename the Account settings page title from "Your workspace." to "Your perch." (en) / "Din grein." (nb), aligning with the raven/flock metaphor used on the rest of the dashboard pages.
- 1f7ea3d: Two polish fixes:
  - Settings sidebar (nav + sign-out) is now `sticky` under the topbar so scrolling the main content area no longer hides the nav or the sign-out button.
  - Account page's save button label and confirmation message now match the rest of the dashboard: `Save` (not `Save changes`) and a muted-gray `Saved` toast (matching `identity-card`/`models-card`) instead of the previous cobalt-blue confirmation.
  - @getmunin/types@4.1.1
  - @getmunin/ui@4.1.1

## 4.1.0

### Minor Changes

- cf3fd9d: Update auth-page styling: primary action (Sign in / Continue) is now black (`bg-ink`) with cobalt-deep on hover, matching the rest of the dashboard's primary buttons. Inputs and buttons are now square (12px corner radius removed) on auth and invite acceptance pages. The `variant="navy"` prop name on `AuthSubmit` is kept for backwards compatibility but no longer uses the navy color token.

### Patch Changes

- @getmunin/types@4.1.0
- @getmunin/ui@4.1.0

## 4.0.0

### Major Changes

- b5dce5d: Remove `OrgSwitcher` from `@getmunin/dashboard-pages`. OSS is single-tenant and never used it; cloud should ship its own switcher into the existing `leftSlot` on `DashboardShell` / `DashboardTopbar`. Also: when `leftSlot` is provided, it now replaces the brand text in the topbar instead of rendering alongside it.

### Patch Changes

- @getmunin/types@4.0.0
- @getmunin/ui@4.0.0

## 3.9.1

### Patch Changes

- 90ffd9c: Fix the org switcher dropdown throwing `Base UI error #31` (MenuGroupRootContext missing) when opened. Wrap the label, separator and items in a `<DropdownMenuGroup>` so `Menu.GroupLabel` has the group context it now requires under base-ui 1.4.
  - @getmunin/types@3.9.1
  - @getmunin/ui@3.9.1

## 3.9.0

### Minor Changes

- ed2bb6b: Add generic `SmtpMailer` provider to `@getmunin/core`.

  Covers any SMTP-speaking transactional email service (Scaleway TEM, Postmark,
  Mailgun, Postmark, etc.) via a single implementation. Activated by setting
  `MUNIN_MAIL_PROVIDER=smtp` along with `MUNIN_SMTP_HOST`, `MUNIN_SMTP_PORT`,
  `MUNIN_SMTP_USER`, `MUNIN_SMTP_PASSWORD` (optional `MUNIN_SMTP_SECURE=1` for
  implicit-TLS on port 465). `nodemailer` is the underlying transport.

### Patch Changes

- Updated dependencies [ed2bb6b]
  - @getmunin/types@3.9.0
  - @getmunin/ui@3.9.0

## 3.8.0

### Minor Changes

- a3f532e: Onboarding cleanup, agent-config hot-reload, provider auth validation.
  - Dropped the chatbot-name field from the onboarding form; new orgs seed with an empty name so step 1 is shown until the user names their bot.
  - Removed the unused `orgs.slug` column (migration 0027); CMS delivery routes (`/api/v1/cms/:orgId/...`) and the matching SDK clients now key on `orgId` rather than the slug.
  - `AgentConfigService` validates provider credentials _before_ persisting — OpenRouter is probed via `/auth/key` (since its `/models` endpoint is public), Anthropic/OpenAI rely on `/models` 401. Bad keys no longer silently overwrite a working config.
  - Saving agent config emits `agent.config.updated` via the WebhookDispatcher; the realtime gateway broadcasts it and `AgentHostRunner` respawns the affected runner — model/provider changes apply without a backend restart.
  - Models picker reconciles a stale stored model slug against the fetched model list at render time, so the dropdown can't round-trip an unknown id back to the server.
  - Chat widget no longer filters the current session's conversation out of the past-conversation list — going back from a fresh conversation shows it.

### Patch Changes

- Updated dependencies [a3f532e]
  - @getmunin/types@3.8.0
  - @getmunin/ui@3.8.0

## 3.7.0

### Minor Changes

- 1cec7ea: Make `@getmunin/dashboard-pages` the canonical home for OSS messages so downstream apps don't have to copy the shared keys.

  **New exports:**
  - `loadBaseMessages(locale)` — dynamic-imports the bundled `en.json` / `nb.json`. Returns a `MessagesTree`.
  - `mergeMessages(base, overrides)` — recursive deep merge for spreading host-app overrides on top of the base messages.
  - `BASE_LOCALES` / `BaseLocale` — the locale set the package ships translations for.

  The OSS web app's `apps/web/messages/{en,nb}.json` are gone — their content moved to `packages/dashboard-pages/src/messages/`. `apps/web/i18n/request.ts` now calls `loadBaseMessages(locale)` directly.

  Downstream apps (e.g. munin-cloud) can adopt the same loader and pass only their cloud-specific overrides:

  ```ts
  const base = await loadBaseMessages(locale);
  const overrides = (await import(`../messages/${locale}.json`)).default;
  return { locale, messages: mergeMessages(base, overrides) };
  ```

  This is additive — no existing exports removed.

### Patch Changes

- Updated dependencies [1cec7ea]
  - @getmunin/types@3.7.0
  - @getmunin/ui@3.7.0

## 3.6.0

### Minor Changes

- bbd1d03: Extract dashboard + settings shells from `@getmunin/web` into `@getmunin/dashboard-pages` so downstream consumers can compose the same dashboard structure instead of redeclaring it.

  **New exports from `@getmunin/dashboard-pages`:**
  - `DashboardShell` — wraps `useDashboardGate`, session check, topbar render, and the `inSettings` pathname toggle. Props: `brand`, `logoSrc?`, `leftSlot?`, `withConfirmDialog?`.
  - `SettingsShell` — wraps the settings layout: role gate, `SettingsTopbar`, `RailNav` sidebar built from a `groups` prop, and the mobile `Sheet`. Consumers pass a `SettingsSubNavGroup[]`.
  - `OSS_SETTINGS_GROUPS` — the canonical OSS settings nav config (moved from `apps/web/.../nav-config.ts`).
  - `extendSettingsGroups(base, extensions)` — merges items into existing groups (or appends a new group). Supports `insertAfter`, `insertBefore` (by slug or labelKey), and `position: 'start' | 'end'` for ordering.
  - `createSettingsIndexRedirect({ defaultLocale, target? })` — factory for the `settings/page.tsx` default redirect.

  **Convention:** any `labelKey` you put in a settings group must have a matching `nav.*` entry in the host app's `messages/*.json`. Group keys map to `dashboard.settings.groups.*`.

  This is purely additive — no public API removed. The web app's own `dashboard/{layout,settings/layout,settings/page}.tsx` files were collapsed onto the new shells in the same PR (#166).

### Patch Changes

- Updated dependencies [bbd1d03]
  - @getmunin/types@3.6.0
  - @getmunin/ui@3.6.0

## 3.5.0

### Minor Changes

- be32cb4: Email channel polish, read tracking, and agent-model tier rename.

  **Email channel (#136, #140)**
  - New "Send test email" action in the channel dropdown — opens a dialog
    prefilled with the logged-in user's email, sends via the channel's real
    outbound transport.
  - SMTP/IMAP networking: force IPv4 DNS resolution at backend startup
    (fixes `EHOSTUNREACH` on hosts with broken IPv6 routing); auto-pick TLS
    mode by port (465 implicit, 587/25/2525 STARTTLS).
  - SMTP error surfacing: readable messages for `EAUTH` / `ECONNECTION` /
    `EENVELOPE` plus the server's response text, replacing generic
    "Internal error".
  - Inbound mail now creates an `end_users` row keyed
    `external_id = email:<addr>` and links the contact; agent runtime no
    longer skips conversations with "no end-user bound".
  - Inbound dedupe on RFC-5322 `Message-ID` — defense-in-depth against
    cursor failures, UIDVALIDITY changes, restored backups.
  - IMAP poll fixes: cursor read/write use `app.bypass_rls=on`; fetch by
    UID range instead of sequence numbers; per-tick logging.
  - Strip quoted reply blocks (multi-language) AND signatures (RFC 3676 +
    mobile-client openers + common separators) before persisting inbound
    bodies. Nested-quote prior 3 messages in outbound replies; add `Re:`
    prefix when missing.

  **Read tracking (#137, #139)**
  - New `conv_message_reads` table; chat widget reports agent messages as
    read when they enter the viewport (`IntersectionObserver` + 200 ms
    coalesce window). Backend gateway handles the `read` WS frame,
    inserts with `ON CONFLICT DO NOTHING`, emits
    `conversation.message.read` webhook per new row.
  - Email open pixel: opt-in per channel (`trackOpens` flag), HMAC-signed
    token, `GET /api/v1/c/o/:token.gif` endpoint returns a transparent
    GIF and bumps `first_opened_at` / `last_opened_at` / `open_count` on
    `conv_message_deliveries`. Emits `conversation.message.opened` on
    first open.
  - Operator-side "Seen HH:MM" badge under outbound messages in the
    dashboard conversation drawer. Live-updates through the existing
    realtime hook on `conversation.message.read` events.

  **Model tier rename (#141)**
  - `chatModel` → `fastModel`, `curatorModel` → `smartModel` across
    `agent_config` schema, types, controllers, dashboard form, and i18n
    strings. Capability tiers instead of use-cases — every code path
    picks the right tier without adding a new column per feature.
  - Idempotent `ALTER COLUMN RENAME` in both DDL strings handles
    existing databases.
  - Dashboard form now shows example use-cases under each field.

  **Schema migrations**
  - `0020_conv_read_and_open_tracking.sql` — `conv_message_reads` table
    - `first_opened_at` / `last_opened_at` / `open_count` columns on
      `conv_message_deliveries`.
  - `agent_config` `chat_model` → `fast_model`, `curator_model` →
    `smart_model` (idempotent rename inside the agent-host DDL).

### Patch Changes

- Updated dependencies [be32cb4]
  - @getmunin/types@3.5.0
  - @getmunin/ui@3.5.0

## 3.4.1

### Patch Changes

- 1b3b959: Include `src/` in the published tarballs for every package that declares a `development` export condition (`@getmunin/types`, `core`, `db`, `sdk`, `mcp-toolkit`, `bootstrap`, `backend-core`, `agent-runtime`, `agent-host`).

  The `development` condition resolves to `./src/index.ts`, which is the right path in the OSS workspace (pnpm-linked) but didn't exist in the published tarball — `files: ["dist"]` excluded it. Downstream consumers whose toolchain activates the `development` condition (e.g. vitest 2.x in cloud) hit `Cannot find module '.../src/index.ts'` errors at runtime. Shipping `src/` alongside `dist/` makes the condition resolve in both environments.
  - @getmunin/types@3.4.1
  - @getmunin/ui@3.4.1

## 3.4.0

### Minor Changes

- 6a6e9f7: Dashboard navigation overhaul, action feedback via toasts, widget fixes, and onboarding polish.

  **Navigation**
  - New `DashboardTopbar` (cog → Settings, rotate-on-hover) replaces the multi-item nav. Settings page gets its own `SettingsTopbar` (back arrow → /dashboard, mobile hamburger). Settings page uses `bg-paper` to match the topbar; sidebar keeps `bg-bone`.
  - Sign-out moves to the bottom of the settings sidebar (and the mobile drawer). `UserMenu` removed.
  - Account moved into the settings sidebar (first item under Workspace). New `AccountPage` (org-name field, `GET`/`PATCH /api/v1/orgs/me`) accepts `extraSections` so cloud can compose its destructive Delete-account UI on top.

  **Onboarding wizard**
  - New step 1 collects the org name (`OrgNameCard`); existing steps renumbered to 2–4. `useDashboardGate` and `useSetupGate` redirect to /setup when the org name is empty, not just when the agent is unconfigured.
  - `invalidateActiveMembershipCache()` exported so the topbar brand refreshes immediately after a rename.

  **Team page**
  - Row-level Edit per member opens a dialog to rename. Owner/admin can edit anyone; members can edit only themselves. Self-rename also calls `authClient.updateUser({ name })` to sync the Better Auth session.

  **Action feedback**
  - New `Button` `pending` prop renders a spinning Loader2 and disables the button.
  - New `notify` helper wraps `sonner` (`notify.success` / `notify.error` / `notify.info`). Inline `<Card><CardContent text-destructive>` patterns swept across team, channels, agents, end-users, export, api-keys, audit-log, agent-setup-wizard, inbox, suggestions. The InboxErrorBanner export is gone; inbox actions now toast directly.
  - Revoke flows (agents, api-keys, end-users) wire `pending` per row and toast success/failure. End-users "no tokens to revoke" is now an `info` toast, not an error.

  **Backend**
  - `PATCH /api/v1/orgs/me/members/:userId` accepts `{ name? }`. Name edits allowed for owner/admin or self-edit; role edits still owner-only.
  - `POST /api/v1/conversations/:id/messages` accepts `claim?: boolean` (default true). Quick-reply flow passes `false` so approving the AI's draft no longer claims the conversation.
  - `POST /api/v1/conversations/:id/status` releases the human claim when transitioning to `closed`.
  - `/api/v1/inbox` `loadLive` filters closed/spam at SQL via a new `excludeStatuses` option on `listConversations` and `listConversationsByIds`.
  - Widget ingest accepts `visitorId` (stable per-browser token); anon end-users key on `anon:<visitorId>` when present, falling back to `anon:<sessionId>` for legacy clients. One end-user per visitor instead of one per session.
  - Members controller `PatchMemberDto` accepts `name`; users.name + updatedAt written when editing.

  **Chat widget**
  - `getVisitorId(channelId)` mints a long-lived browser token, sent on every payload.
  - Saved-email confirmation stays inline at its original position (no longer pushed down or pinned).
  - Less padding on the saved-state card. Top bar's "Online now" line removed; subtitle renamed from "Chat · instant" to "Online now".
  - Header title: "New conversation" when starting fresh, "Conversation" when opening an existing one (subject still wins).

  **Agent runtime**
  - System prompt forbids placeholders (`[Name]`, `[Phone Number]`, …) — every message must be deliverable verbatim.

  **Visual polish**
  - All 1px / 2px borders swept to `border-[0.5px]` for hairline rendering on retina (49 files, ~115 occurrences). Topbar bottom border + section dividers + KPI tile outlines all hairline now.
  - "Delegated end-user token" → "End-user token" in the Agents settings table.
  - "TAKEN OVER" pill swaps the shield icon for a person icon and drops the leading blue dot.
  - Conversation drawer's "Close" button now reads "Close conversation".
  - Agents table row vertically centers single-line cells against the two-line "End-user token + scopes" cell.

  **Settings layout**
  - Account redirect target unchanged (`/dashboard/settings/team`); Account is the new first item in the workspace nav group.

### Patch Changes

- @getmunin/types@3.4.0
- @getmunin/ui@3.4.0

## 3.2.1

### Patch Changes

- @getmunin/ui@3.2.1

## 3.2.0

### Minor Changes

- 9d84e3c: Drop the unused `displayName` field from chat-widget channels. The field was required at create time but was never read by the chat-widget itself — only echoed in the dashboard's channel list. Removed from the MCP tool inputs (`conv_widget_create_channel`, `conv_widget_update_channel`), the `WidgetChannelConfig` zod schema, the REST body schemas in `ConvChannelsController`, the dashboard's "Add chat widget" form and channel-row display, and the widget-onboarding / bulk-channel-setup skill docs. Existing rows keep `displayName` in their `conv_channels.config` jsonb but it gets silently stripped on next parse — no migration required.

  Also fixes a NestJS route-ordering bug where `ConversationsController @Get(':id')` shadowed `ConvChannelsController @Get()`, causing `/api/v1/conversations/channels` to return `conv_not_found: conversation channels` instead of the channel list. `ConvChannelsController` is now registered before `ConversationsController` in `ControlModule`.

### Patch Changes

- @getmunin/ui@3.2.0

## 3.1.0

### Minor Changes

- 23a22f8: Add shared auth-shell components for the redesigned auth pages: `AuthShell`, `AuthEpigraph`, `AuthHeading`, `AuthSubheading`, `AuthFootnote`, `AuthDivider`, `AuthField`, `AuthLabel`, `AuthInput`, `AuthSubmit`, `AuthOAuthButton`, `AuthFieldHint`, `ErrorAlert`, `AuthInviteCard`, plus the `OSS_AUTH_FOOTER` / `CLOUD_AUTH_FOOTER` constants and `AuthState` type. Also adds `--munin-auth-navy`, `--munin-alert-bad-*`, and `--munin-invite-{good,bad}-*` design tokens to `@getmunin/ui` and exposes them as Tailwind utilities (`bg-auth-navy`, `bg-alert-bad`, `bg-invite-good`, etc.).

### Patch Changes

- Updated dependencies [23a22f8]
  - @getmunin/ui@3.1.0

## 3.0.0

### Major Changes

- e5a5450: Migrate from the deprecated `oidcProvider` (in-tree better-auth plugin) to the published `@better-auth/oauth-provider`. The OAuth schema changes from 3 tables to 4 (`oauth_client`, `oauth_access_token`, `oauth_refresh_token`, `oauth_consent`) plus a `jwks` table for the JWT plugin. RFC 8707 resource indicators are now native via `validAudiences`, JWT access tokens replace opaque tokens for resource-bound flows, and the consent page contract switches from `consent_code` to a signed `oauth_query`. The dashboard consent page is fully localized (en + nb).

  Breaking: any deployment with rows in the old `oauth_applications` / `oauth_access_tokens` / `oauth_consents` tables will lose them — Munin OAuth has not been deployed anywhere yet, so this is a no-op in practice.

### Patch Changes

- @getmunin/ui@3.0.0

## 2.5.1

### Patch Changes

- @getmunin/ui@2.5.1

## 2.5.0

### Minor Changes

- e962f04: feat(oauth): branded consent UI at /dashboard/oauth/consent (Phase 4)

  Custom consent page for the OAuth 2.1 authorization flow. Replaces Better-Auth's default `getConsentHTML` fallback with a Munin-styled card showing the client name, requested scopes, and Allow/Deny actions. Submission posts to `/auth/oauth2/consent` with `accept: true|false` and the `consent_code` from the query string; on success the user is redirected back to the OAuth client.

  The page is added to `useDashboardGate`'s exempt list so a user can authorize an external app even before completing the built-in-AI setup wizard.

  A wrapper at `apps/web/app/dashboard/oauth/consent/page.tsx` re-exports the component; cloud picks it up automatically when it bumps the package.

### Patch Changes

- @getmunin/ui@2.5.0

## 2.4.0

### Patch Changes

- @getmunin/ui@2.4.0

## 2.3.0

### Patch Changes

- @getmunin/ui@2.3.0

## 2.2.0

### Patch Changes

- @getmunin/ui@2.2.0

## 2.1.0

### Minor Changes

- f9ecaa9: feat(widget): in-tree chat widget — drop-in `<script>` for self-hosted Munin

  Self-hosted Munin now serves a first-party browser chat widget directly
  at `https://<host>/widget.js`. Operators don't need a token-mint proxy,
  a separate hosting target, or the old `chat-widget-vanilla` example —
  they create a chat-widget channel in the dashboard, copy the embed
  snippet from **Settings → Channels → Embed snippet**, and paste it on
  their site.

  **`@getmunin/backend-core`**
  - Per-channel `identityVerificationSecret` + `requireVerifiedIdentity`
    flag on `WidgetChannelConfig`. The secret is generated at channel
    creation, surfaced once via `conv_widget_create_channel`, and rotatable
    via the new `conv_widget_rotate_identity_secret` MCP tool.
  - `verifyIdentity()` runs on every widget request: timing-safe HMAC check
    on the `(verifiedExternalId, userHash)` pair against the channel's
    secret. Failures collapse to a single `403 identity_verification_failed`
    so callers can't distinguish failure modes by status or timing.
  - `originAllowlist` is now enforced on `POST /api/v1/widget/messages` —
    browser callers must declare an `Origin` on the channel's allowlist;
    server-to-server callers (no `Origin`) pass through unchanged.
  - New `GET /api/v1/widget/messages?since=` endpoint for WS-reconnect
    backfill. Capped at 100, returns `hasMore`. Verified mode binds the
    result set to the requester's externalId (mismatch returns empty
    rather than 403 to avoid leaking session existence).
  - `RealtimeGateway` learns a `widget` subscription type. Widget keys
    authenticate at upgrade with origin-allowlist + HMAC identity gates;
    subscriptions are scoped to `widget:<channelId>:<sessionId>`.
    Operator-side replies fan out via a per-connection conversation-meta
    cache, no upstream emit-site changes needed.
  - Bidirectional `typing` events: visitor ↔ operator, server-side throttle
    of 1 broadcast per 1.5 s per (sender, conversation), 5 s auto-clear if
    the sender goes silent. `requireVerifiedIdentity` is honored for both
    sides.
  - Inbound WS frames capped at 64 KB.
  - Backend serves the bundle: `GET /widget/<sha>.js` is immutable
    (`max-age=31536000, immutable`); `GET /widget.js` is a 302 redirect to
    the current sha with `max-age=300, must-revalidate`. The redirect
    target is read from `manifest.json` and refreshed on file mtime change
    so deploy-time swaps propagate without restart. Path traversal is
    blocked; missing manifest yields 503 `no-store`.
  - Visitor-message body capped at 1000 chars (`role: end_user`); operator
    / agent / system messages keep the prior 50K cap.
  - New REST surface for the dashboard: `requireVerifiedIdentity` on the
    create/update bodies and `POST .../widget/:id/rotate-identity-secret`.

  **`@getmunin/dashboard-pages`**
  - The Channels page now surfaces the identity-verification secret on
    channel creation alongside the widget API key (one combined callout,
    shown once).
  - New per-chat-channel actions: **Embed snippet** (a dialog with a
    copyable `<script>` tag pre-filled with the dashboard origin and
    channel id, plus tabbed Node / Ruby / PHP / Python snippets for
    computing `data-user-hash` server-side) and **Rotate identity secret**.

  **Companion changes**
  - A new `@getmunin/chat-widget` workspace package (private, deployable
    artifact like `apps/backend` and `apps/web`; not published to npm)
    hosts the widget source. Built as a single content-hashed IIFE bundle
    via Vite, copied into `apps/backend/public/widget/` by a `prebuild`
    step.
  - The standalone `chat-widget-vanilla` example in the `munin-examples`
    repo is removed — the dashboard's embed snippet replaces it.

### Patch Changes

- @getmunin/ui@2.1.0

## 2.0.0

### Major Changes

- d4f7a27: refactor!: route alignment + ai-agent → builtin-ai rename + setup gate

  Frontend route alignment, the second pass after the API rename. Three things in one diff:

  **1. Rename `/dashboard/settings/ai-agent` → `/dashboard/settings/builtin-ai`** in OSS and updates the wizard's hardcoded internal link. The package export `AgentSettingsPage` is renamed to `BuiltinAiSettingsPage` to match the URL.

  **2. New gate hooks** for use in dashboard layouts and the setup page:
  - `useDashboardGate()` — returns `{ ready, role }`. When the active org's built-in AI is not configured (`providerApiKeySet === false`) and the user is owner/admin, redirects to `/setup`. Members are allowed through (they see the dashboard's per-page empty states). `/dashboard/account` is exempt — escape hatch if onboarding goes sideways.
  - `useSetupGate()` — returns `{ ready }`. Inverse: redirects to `/dashboard` when configuration is already complete.
  - `useAgentConfigStatus()` — small primitive used by both gate hooks.

  **3. OSS app wired up.** `apps/web/app/dashboard/layout.tsx` now uses `useDashboardGate`; `apps/web/app/setup/page.tsx` now uses `useSetupGate`.

  Companion frontend changes ship in `munin-cloud` once a release of this package is published.

### Patch Changes

- @getmunin/ui@2.0.0

## 1.0.0

### Patch Changes

- @getmunin/ui@1.0.0

## 0.25.0

### Patch Changes

- @getmunin/ui@0.25.0

## 0.24.1

### Patch Changes

- @getmunin/ui@0.24.1

## 0.24.0

### Minor Changes

- 950694e: feat(agent-host): bundled in-process agent runner

  New `@getmunin/agent-host` package — a hosting layer that runs the
  agent (chat replies + curator queue) in-process inside the backend,
  replacing the separate `apps/agent-sidecar` topology.

  What's in the package:
  - `agent_config` table with both singleton (single-tenant) and
    multi-tenant DDL variants. Adds a `chat_model`/`curator_model`
    split so curation can use a stronger model than chat.
  - `AgentConfigRepository` (singleton + per-org impls) and
    `AgentConfigService` for CRUD over the config row.
  - `AdminKeyProvider` (no-op + auto-mint impls) for hosts that want
    rotated per-config admin credentials.
  - `AgentHostRunner` — reconcile loop that spawns per-config
    `ConversationHandler` + curator worker. Multi-replica safe via a
    `ReplicaLockManager` that pins a postgres-js `sql.reserve()`
    client and uses `pg_try_advisory_lock` to elect a chat-loop owner
    per config; curator drains on every replica via existing SKIP
    LOCKED. Two-tier model dispatch: `chatModel` for chat,
    `curatorModel ?? chatModel` for `runSkillPass`.
  - `AgentModelsService` — proxies the provider's `/v1/models`
    endpoint. Returns objective fields (id, contextLength, prompt /
    completion price per million) when the provider includes them
    (OpenRouter, Anthropic). 10-min in-memory cache.
  - `AgentConfigController` — `GET/PUT /api/agent-config` and
    `GET /api/agent-config/models`, user-actor only.
  - `AgentHostModule.forRoot({ configRepository, adminKeyProvider,
runnerOptions })` for DI wiring; uses `useExisting: DB` against
    `@getmunin/backend-core`'s global `DbModule`.

  `@getmunin/dashboard-pages`: new `AgentSetupPage` export — single-
  form `/setup` wizard for first-run agent configuration.

  `@getmunin/agent-runtime`: default `clientName` in
  `mcp-client.ts` changed from `'munin-agent-sidecar'` to
  `'munin-agent'` after the sidecar app was removed.

### Patch Changes

- @getmunin/ui@0.24.0

## 0.23.3

### Patch Changes

- @getmunin/ui@0.23.3

## 0.23.2

### Patch Changes

- @getmunin/ui@0.23.2

## 0.23.1

### Patch Changes

- 4ff9c11: Remove dashboard outreach campaigns config page. Campaign CRUD now lives only via the admin MCP tools (`outreach_create_campaign`, `outreach_update_campaign`, `outreach_list_campaigns`, `outreach_get_campaign`) — agent-native setup, dashboard-native review. Drops the `/dashboard/settings/outreach` route, the `OutreachCampaignsPage` export, and the `/api/outreach/campaigns` REST controller. The Review tab (`OutreachDraftsTab`) and `/api/outreach/proposals` are unaffected.
  - @getmunin/ui@0.23.1

## 0.23.0

### Minor Changes

- 88b1bc3: Outreach feature, PR3 of 3 — `agentMode` + draft-on-reply loop. Closes the outreach loop: every reply on an outreach-originated conversation gets drafted by an admin agent and waits for human approval. The AI conversational runner never auto-replies on these conversations, even when the prospect responds.

  **`agentMode` on conversations.** New enum column `agent_mode` on `conv_conversations` with values `auto | draft_only | off`, default `auto`. Orthogonal to claims (claims are _who's working it now, with TTL_; agentMode is _what posture the agent takes, durable_). Reusable beyond outreach — a customer can flip a single conversation or a whole channel into `draft_only` for trust-building, moderation, or VIP review.
  - `ConvService.setAgentMode(id, mode)` + REST `POST /api/conversations/:id/agent-mode`.
  - `ConvService.createConversation` accepts `agentMode` (default `'auto'`).
  - `ConversationSummary`/`Detail` DTOs now expose `agentMode` and `outreachCampaignId`.
  - `agent-runtime`'s `ConversationHandler.shouldRespond` defers when `agentMode !== 'auto'` (logged as `skip <id>: agentMode=draft_only`). Two new unit tests cover both `draft_only` and `off`.
  - `MuninRestClient.ConversationDetail` adds `agentMode` and `outreachCampaignId`.

  **Outreach reply-curator skill.** New `skill://outreach/draft-reply`. Triggered event-driven: when an inbound message lands on a conversation that has both `outreachCampaignId` set and `agentMode='draft_only'`, `ConvService.sendMessage` enqueues a curator job (dedupe-keyed by message id). The skill reads the thread, identifies the prospect's intent (question / decline / ask-for-human / off-topic / hostile), grounds factual claims via `kb_search`, drafts a 30–120-word reply, and files it via `outreach_propose_reply` for human approval. Strict rules: no unsubscribe footer (initials carry it; replies thread inside), no auto-send.

  **Outreach service.**
  - `OutreachService.proposeReply({ conversationId, draftBody, evidence })` — files a `kind='reply'` proposal. Rejects when the conversation is not outreach-originated. Resolves CRM contact via the conversation's `conv_contacts.email`.
  - `OutreachService.approveProposal` now branches on kind. `kind='initial'` flips the new conversation to `agentMode='draft_only'` (so the AI runner defers on subsequent inbound messages). `kind='reply'` sends the draft body verbatim via `conv.sendMessage` on the existing conversation — no unsubscribe footer.
  - New MCP tool `outreach_propose_reply` (admin audience). The reply skill calls it.

  **Sidecar `toolPrefixesFor`** adds `'skill://outreach/draft-reply'` → `['conv_', 'kb_', 'crm_', 'outreach_']`. Cloud `AgentRunnerService.toolPrefixesFor` needs the same one-line addition (separate cloud PR after this OSS release).

  **Dashboard.** `OutreachDraftsTab` differentiates kind with a coloured badge (`Reply` filled, `Initial` outline). Reply cards link to `/dashboard/conversations?id=<id>` so the operator can see thread context before approving. i18n string `viewThread` added in en + nb.

  **Schema migration** `0013_conv_agent_mode.sql` — single column add; default `'auto'` so all existing conversations are unaffected. Outreach conversations created via `approveProposal` going forward land in `'draft_only'`.

  **Tests.** 6 new (2 in agent-runtime for the defer; 2 in conv.service for the inbound-on-outreach enqueue path; 4 in outreach.service for proposeReply, approveReply send + no-footer assertion, agentMode=draft_only on initial approve, and the not-outreach-conversation rejection). All 321 backend-core tests pass; 67 agent-runtime tests pass.

  **End-to-end:** an operator can now run a campaign where the entire loop — first send and every reply — is human-approved. Combined with PR1's suppression+consent floor and the unsubscribe infrastructure, this is the GDPR-compliant, never-auto-sends outbound channel the plan promised.

### Patch Changes

- @getmunin/ui@0.23.0

## 0.22.0

### Minor Changes

- ebda56e: Outreach feature, PR2 of 3 — campaigns + initial drafts + send-approve loop.

  The first user-visible piece of outbound: an operator defines a campaign (name + brief + CRM segment + email channel + cadence + CTA), the new `skill://outreach/draft-initial` curator drafts a personalised first-touch email per consenting contact in the segment, the operator reviews each draft on `/dashboard/review` (third tab), and approving sends via the existing email-channel outbound pipeline. Replies thread into normal conversations via the existing RFC 5322 thread-resolution.

  **Schema:**
  - `outreach_campaigns` — operator-defined campaigns (segment_id → `crm_segments`, channel_id → `conv_channels` (must be email), brief, cadence_rules JSONB, cta_url, enabled, unsubscribe_required). Unique `(org_id, name)`. RLS admin-only.
  - `outreach_proposals` — drafted email queue with `kind` (`initial` in PR2; `reply` in PR3), nullable `conversation_id` (set when sent), `status` lifecycle (pending → sent / dismissed / failed), evidence JSONB, audit fields. **Unique pending index on (campaign_id, contact_id, kind)** to prevent dup drafts. RLS admin-only.
  - `conv_conversations` gains `outreach_campaign_id` (nullable FK + index) — sticky once set, used for reply attribution and (in PR3) `agentMode` defaulting.
  - New `packages/db/src/sql/outreach.sql` with RLS policies, wired into `runMigrations`.

  **Service / MCP / REST** (all in new `@getmunin/backend-core/src/modules/outreach/`):
  - `OutreachService` — `listCampaigns`/`getCampaign`/`createCampaign`/`updateCampaign`/`listProposals`/`getProposal`/`proposeInitial`/`approveProposal`/`dismissProposal`. `approveProposal` re-checks suppression+consent at decision-time (the contact may have unsubscribed between draft and approval), creates a conversation with `outreach_campaign_id` set, sends via the existing email outbound pipeline, and appends a signed unsubscribe footer to the body server-side so it can't be tampered with at draft-time.
  - MCP tools (admin audience): `outreach_create_campaign`, `outreach_update_campaign`, `outreach_list_campaigns`, `outreach_get_campaign`, `outreach_list_proposals`, `outreach_propose_initial`.
  - REST: `GET/POST /api/outreach/campaigns`, `GET/POST /api/outreach/campaigns/:id`, `GET /api/outreach/proposals?status=pending&kind=initial&campaignId=…`, `GET /api/outreach/proposals/:id`, `POST /api/outreach/proposals/:id/approve`, `POST /api/outreach/proposals/:id/dismiss`. The proposals list/get embeds `contact` and `campaign` summaries so the dashboard doesn't need parallel fetches.
  - Realtime events: `outreach.proposal.created`, `outreach.proposal.sent`, `outreach.proposal.dismissed` (rides existing WebhookDispatcher).

  **Conv-side:** `ConvService.createConversation` now accepts `outreachCampaignId` and enqueues outbound delivery for non-end_user authors on email channels (it previously only did this from `sendMessage`, which broke first-touch sends). All existing flows are unaffected — they don't pass `outreachCampaignId` and their authorType doesn't trigger outbound enqueue.

  **Skill:** `skill://outreach/draft-initial` (markdown, copied into dist by the existing `copy-skills.mjs`). Procedure: list enabled campaigns → materialise audience via `crm_list_contacts_in_segment` (which already enforces the suppression+consent floor) → dedupe via `outreach_list_proposals` → ground in `kb_search` → draft 80–200 word personalised email → file via `outreach_propose_initial`. Strict formatting: no headings, plain prose, no JSON-escaping; the unsubscribe footer is appended at approve-time, not draft-time.

  **Curator scheduling:**
  - New sweep `curator-outreach-draft-initial` (default cron `'0 0 * * 0'` weekly, env `MUNIN_CURATOR_OUTREACH_INITIAL_CRON`).
  - Sidecar `toolPrefixesFor` adds `'skill://outreach/draft-initial'` → `['conv_', 'kb_', 'crm_', 'outreach_']`. Cloud `AgentRunnerService.toolPrefixesFor` needs the same one-line addition (separate cloud PR after this OSS release).

  **Dashboard:**
  - Third tab on `/dashboard/review`: `OutreachDraftsTab` lists pending proposals with markdown body (heading-flatten components shared with KB), Approve / Edit (placeholder; inline editing ships next) / Dismiss buttons. Realtime updates on `outreach.proposal.*` events.
  - New `/dashboard/settings/outreach` (under Monitoring → Workspace group) — list campaigns, create dialog with name + brief + segment dropdown + channel dropdown + CTA URL, enable/disable toggle. Empty-state nudges the operator if they have no email channels or segments yet.
  - i18n: `dashboard.outreach.*`, `dashboard.outreachDrafts.*`, `nav.outreach`, `dashboard.review.tabs.outreach` in en + nb.

  **Tests:** 9 new integration tests covering campaign CRUD (including non-email-channel rejection and duplicate-name conflict), `proposeInitial` (dedupe + consent floor), `approveProposal` (success path stamps conv id + delivery row, suppression-race refuses, disabled-campaign refuses), and `dismissProposal`. Existing 306 backend-core tests unchanged. `curator-scheduler.test.ts` updated to expect the new fourth cron job.

  **Out of PR2 scope (lands in PR3):** `agentMode` column + reply-curator skill + draft-on-reply loop. Operators currently get a one-way send; replies land in normal conversations and the AI agent will reply auto-mode by default until PR3 wires `agentMode = 'draft_only'` on outreach-originated conversations.

### Patch Changes

- @getmunin/ui@0.22.0

## 0.21.0

### Minor Changes

- 914477f: Channels can now be created and managed from the dashboard.

  **Backend** — new REST controller at `/api/conv/channels`:
  - `GET /` — list widget + email channels for the org.
  - `POST /widget` — create a chat-widget channel; mints and returns a one-shot `mn_widget_*` API key bound to the channel and origin allowlist.
  - `POST /widget/:id` — update name / origin allowlist / display name.
  - `POST /widget/:id/rotate-key` — revoke prior keys and mint a new one (one-shot return).
  - `POST /email` — create an email channel with operator-supplied SMTP credentials and optional IMAP for inbound. Passwords are encrypted at rest.
  - `POST /email/:id/test` — verify SMTP/IMAP credentials before enabling.

  Munin doesn't ship a built-in mailer; email channels require operator-provided SMTP, matching the OSS posture for outbound on every other surface.

  **Dashboard** — new "Channels" entry under Settings with an "Add channel" dropdown (chat widget / email). Each option opens a dedicated dialog. Widget cards expose the bound key on creation and rotation; email cards expose a "Test" button. Norwegian (`nb`) translations included.

- 914477f: Unified Review surface for KB suggestions and CRM merges, with structured-field-driven curation candidates.

  **Dashboard** — replaces the standalone `/dashboard/crm-merge-proposals` page (now redirects) with `/dashboard/review`, a tabbed page combining KB suggestions and CRM merges. Tab counts update live from `kb.*` and `crm.merge_proposal.*` realtime events; the home overview backlog rows for both queues now link into Review. The KB tab renders each candidate's body as markdown (via `react-markdown`, peer dep) inside a `prose` block; `h1`–`h6` are flattened to bold paragraphs so the body never visually competes with the candidate title. Each card has its own "Publish to:" picker pre-selected to the candidate's proposed target space, with a per-card override.

  **Backend — KB candidate DTO** — new structured fields on the curation candidate response:
  - `proposedTargetSpaceSlug: string | null` — extracted from the candidate's `target:<slug>` tag.
  - `sourceConversationId: string | null` — extracted from the `source:<id>` tag.

  Two new service methods (`KbService.listCurationCandidates`, `KbService.getCurationCandidate`) return these fields directly so the dashboard never has to regex over body prose. New REST routes at `/api/kb/curation/candidates` (list/get/publish/dismiss) and `/api/kb/spaces` (list) back the new UI. The "Source conversation / Proposed target space" footer that `proposeCurationCandidate` used to splice into the body is gone — the tags carry the same data and the structured fields surface it.

  **KB curation skill prompt** — Step 4 now sets explicit formatting rules for candidate bodies: subject is the title, body is plain prose with bold/italic/inline-code/short bullets allowed, **no `#`/`##`/`###` headings**, no JSON-escaping the body string, no tables/HTML/images. The "Drafted from conversation …" footer example is gone (now redundant with structured fields). This makes review-UI rendering predictable and prevents big duplicate-of-title H1s in the body.

  **UI fix** — `TabsTrigger` previously used `data-[selected]:` for the active-tab styling, but `@base-ui/react` Tabs emit `data-active`. The selected pill never highlighted. Fixed.

### Patch Changes

- Updated dependencies [914477f]
  - @getmunin/ui@0.21.0

## 0.20.0

### Patch Changes

- @getmunin/ui@0.20.0

## 0.19.0

### Patch Changes

- @getmunin/ui@0.19.0

## 0.18.0

### Patch Changes

- @getmunin/ui@0.18.0

## 0.17.0

### Minor Changes

- db26079: Adds a self-service-agent availability indicator to the dashboard. The realtime gateway now tracks live subscribers whose audiences include `self_service` (excluding end-user widgets) per org. New endpoint `GET /api/overview/agent-status` returns `{ selfServiceAgentSubscriberCount, lastInboundEndUserMessageAt, lastAgentMessageAt }`. Overview page renders a card showing connected/not-connected, and surfaces a warning state when there's no agent connected and end-user messages are unanswered. Solves the OSS bootstrapping confusion where a self-hoster's chat widget delivers messages into the void with no UI signal that nothing is listening on the agent side.

  Adds an `audiences` jsonb column on `api_keys` (default `['admin']`) and the credential resolver now reads it instead of hardcoding the audience set. This lets a key be minted with `audiences: ['admin', 'self_service']` so its realtime subscriptions are recognised as self-service-agent connections. Backwards compatible — existing rows default to admin-only.

### Patch Changes

- @getmunin/ui@0.17.0

## 0.16.1

### Patch Changes

- @getmunin/ui@0.16.1

## 0.16.0

### Minor Changes

- 109e723: Adds a CRM merge proposals review page to the dashboard. New REST controller exposes `GET /api/crm/merge-proposals`, `GET /api/crm/merge-proposals/:id`, `POST /api/crm/merge-proposals/:id/apply`, `POST /api/crm/merge-proposals/:id/dismiss` so the dashboard can list pending proposals and resolve them with one click. The page subscribes to the new `crm.merge_proposal.*` realtime events so the queue updates without polling, and falls back to a 60s poll. The "Needs attention" backlog tile gets a CRM merge counter that links to the page; nav adds a top-level "CRM merges" entry. en + nb i18n strings included.

### Patch Changes

- @getmunin/ui@0.16.0

## 0.15.0

### Patch Changes

- @getmunin/ui@0.15.0

## 0.14.0

### Patch Changes

- @getmunin/ui@0.14.0

## 0.13.0

### Minor Changes

- a61dd11: Add a "Needs attention" backlog card to the dashboard overview, plus a
  small `/api/overview/backlog` aggregator that returns counts of items
  across modules waiting on human or admin-agent attention.

  The card is a _signal_, not a CRUD surface — it tells the operator
  what to attend to (open conversations needing handover, KB curation
  candidates pending review) but the actual work still happens through
  the connected admin agent. This keeps the dashboard on-thesis ("the
  agent is the UI") while still giving operators a single place to see
  the backlog grow and shrink.

  Today the card surfaces:
  - conversations with `needsHumanAttention = true`
  - KB documents in the `kb-curation-inbox` space tagged `candidate`

  Future modules (CRM dirty-data, CMS stale-content, …) can extend the
  endpoint shape without controller refactoring — it returns a flat
  `{ key: count }` object.

### Patch Changes

- @getmunin/ui@0.13.0

## 0.12.0

### Patch Changes

- @getmunin/ui@0.12.0

## 0.11.0

### Patch Changes

- @getmunin/ui@0.11.0

## 0.10.0

### Patch Changes

- @getmunin/ui@0.10.0

## 0.9.1

### Patch Changes

- @getmunin/ui@0.9.1

## 0.9.0

### Minor Changes

- 19466a0: Localize all dashboard pages and UI components with [next-intl](https://next-intl.dev). Ships English (`en`) and Norwegian Bokmål (`nb`) message catalogs that consumers extend in their own `messages/{locale}.json`.

  **Breaking-ish (pre-1.0 minor):**
  - `next-intl` is now a required peer dependency of `@getmunin/dashboard-pages`. Consumers must wrap their app in `<NextIntlClientProvider>` and configure `next-intl/plugin` in `next.config.mjs`.
  - `GoogleButton.label` (in `@getmunin/ui`) is now required. Pass a translated label rather than relying on the previous English default.

  **What's translated:** all `dashboard-pages` exports (`AgentsPage`, `ApiKeysPage`, `TeamPage`, `AuditLogPage`, `UsagePage`, `EndUsersPage`, `ExportPage`, `DashboardPage`, `AcceptInvitePage`, `OrgSwitcher`) plus error messages mapped from stable backend codes (e.g. `SIGNUP_DOMAIN_NOT_ALLOWED`, `SIGNUP_INVITE_ONLY`).

  **Backend changes (`@getmunin/backend`):** `auth.config.ts` now emits two distinct codes (`SIGNUP_DOMAIN_NOT_ALLOWED` and `SIGNUP_INVITE_ONLY`) instead of a single `SIGNUP_NOT_ALLOWED`. Email templates (password reset, verification) move into `email-templates.ts` keyed by locale, with a default driven by `MUNIN_DEFAULT_LOCALE` (`en` | `nb`).

### Patch Changes

- Updated dependencies [19466a0]
  - @getmunin/ui@0.9.0

## 0.8.0

### Patch Changes

- @getmunin/ui@0.8.0

## 0.7.0

### Patch Changes

- @getmunin/ui@0.7.0

## 0.6.0

### Minor Changes

- 1aaaa24: Move suggestions feature out of OSS to a private feature board.

  The `suggestions` feature was structured as a Canny-clone but its `appScope`
  enum (`kb | conv | crm | core`) was hardcoded to Munin's own modules — the
  real intent was a vendor roadmap, not per-org product feedback.

  **Breaking changes (pre-1.0; consumers must update at the same minor):**
  - Removed `SuggestionsModule` from `@getmunin/backend-core`.
  - Removed `suggestions` and `votes` tables from `@getmunin/db`'s published
    schema. New OSS migration `0002_drop_suggestions.sql` drops the tables on
    fresh and existing installs (idempotent).
  - Removed RLS policies for `suggestions` / `votes` from `rls.sql`.
  - Removed `SuggestionsPage`, `CommunityBoardPage`, and the
    `publicSuggestionsMetadata` / `publicSuggestionsRevalidate` exports from
    `@getmunin/dashboard-pages`.
  - Removed `/api/suggestions` and `/api/public/suggestions` REST routes.
  - Removed five MCP tools (`suggestion_*`) from the OSS surface.
  - Removed `suggestions` from the data-export bundle.

  The replacement lives in a downstream package. Voting is now per-org instead of
  per-actor — one vote per `(suggestion_id, org_id)` so multiple
  users/agents in the same customer org collectively contribute one vote.
  The five MCP tool names are unchanged; admins/agents keep calling
  `suggestion_search`, `suggestion_create`, etc., but they hit the cloud
  schema.

  **OSS users who relied on the per-org board:** the feature is gone. Build
  your own roadmap using the existing CRM/CMS primitives or a third-party
  tool. (No public OSS deployment uses it pre-this release.)

### Patch Changes

- @getmunin/ui@0.6.0

## 0.5.0

### Patch Changes

- @getmunin/ui@0.5.0

## 0.4.0

### Patch Changes

- @getmunin/ui@0.4.0

## 0.3.1

### Patch Changes

- fe8fd21: TenancyInterceptor: bypass RLS for `actor.type === 'partner'`.

  Partner actors (in a downstream package) operate across multiple orgs they
  provisioned. Their controllers filter manually by `partner_id`. OSS
  never produces `'partner'` actors, so this branch is dead code there.

- Updated dependencies [fe8fd21]
  - @getmunin/ui@0.3.1

## 0.3.0

### Minor Changes

- 5c140d5: Add credential-resolver extension point to AuthGuard.

  `AuthGuard` now accepts an optional injected `AdditionalCredentialResolver[]`
  via the `ADDITIONAL_CREDENTIAL_RESOLVERS` token. When OSS's `resolveApiKey`
  returns null, each additional resolver gets a shot at the raw key.
  Downstream packages plug in via this token to recognize their own key
  kinds without touching OSS code.

  `looksLikeApiKey` regex broadened from `mn_(admin|dlg)_*` to `mn_[a-z]+_*`
  so additional kinds reach the resolver chain.

### Patch Changes

- Updated dependencies [5c140d5]
  - @getmunin/ui@0.3.0

## 0.2.0

### Minor Changes

- f3abef4: Add cross-org switcher endpoint + UI.
  - New `GET /api/orgs/me/memberships` — list every org the caller is a member of (id, name, slug, role, isDefault).
  - New `PATCH /api/orgs/me/memberships/active` — flip `is_default` so the next session-cookie request resolves to the chosen org.
  - New `<OrgSwitcher />` component in `@getmunin/dashboard-pages` that wraps both endpoints. Cloud's dashboard layout renders it in the header.

  OSS (single-tenant) installs see exactly one membership and don't render a switcher.

### Patch Changes

- Updated dependencies [f3abef4]
  - @getmunin/ui@0.2.0
