Brings in the i18n PR (#348) that landed on main and localizes the new
strings this branch added so nothing regresses the new next-intl
convention:
- ai-config.tsx: resolved import conflict; handoff-target picker strings
moved to Settings.aiConfig (handoffTo / handoffToDesc / handoffQueue).
- message-bubble.tsx: "AI" badge + tooltip → Inbox.bubble
(aiBadge / aiBadgeTitle).
- ai-thread-banner.tsx: take-over/resume banner + toasts → new
Inbox.aiBanner namespace.
The AI Agents section (/agents playground, setup tabs, usage card) is
still English on main, so those strings are left as-is for a future
section-wide i18n pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Correctness:
- Resume AI now releases ANY assignment (not just the caller's own).
A handoff-assigned thread previously stayed assigned after resume, so
the eligibility gate silently kept the bot muted — Resume was a no-op.
- GET /api/ai/usage: guard days >= 1 (Number(null)===0 is finite, so the
30-day default was unreachable and the window collapsed to 1 day).
- GET /api/ai/usage: align the query cutoff to the start of the oldest
charted local day so rows in the oldest partial day no longer count in
totals while missing from the daily series (totals now match the bars).
- ai-thread-banner: cache account AI-status per accountId and never cache
a transient fetch failure — fixes cross-account leak and the banner
staying hidden all session after one failed /api/ai/config call.
- /api/ai/draft: construct the service-role client inside a guard and
fire-and-forget the usage log, so a missing service-role key (or any
log failure) can't 500 an already-generated, already-billed draft.
Efficiency:
- Don't await usage logging on the reply hot path (auto-reply + draft);
logAiUsage swallows its own errors, so fire-and-forget is safe and
removes a DB round-trip from the customer-facing send.
Reuse / consistency:
- usage route reuses date-utils (daysAgoStart/lastNDayKeys/localDayKey)
and now buckets by LOCAL day like every other dashboard chart (was UTC).
- extract formatCompactNumber (shared with formatCurrencyShort); drop the
hand-rolled month table in favour of date-fns; extract
fetchAccountMembers/memberLabel and use them in the AI settings picker.
Resume still resets the per-conversation reply count (a deliberate,
manual, rate-limited re-engagement — documented in the handler).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Surfaces the ai_usage_log data added in the previous commit:
- GET /api/ai/usage — admin-only aggregation (totals, per-mode,
per-model, zero-filled daily series) over a 7/30/90-day window,
mirroring the ai_usage_log SELECT policy
- AiUsageCard — stat tiles + daily token bar chart + by-model list,
rendered under a new admin-only "Usage" tab on the AI Agents page
Counts only; no message content is stored or shown.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Makes the AI auto-reply bot visible and controllable from the inbox,
completes the handoff, and records token spend. Builds on the existing
lib/ai engine rather than replacing it.
Track A — user-facing
- messages.ai_generated flag → small "AI" badge on bot-generated bubbles
- "Take over" / "Resume AI" banner per conversation
(POST /api/ai/autoreply/[id]) — take-over pauses + assigns to the agent
- handoff now assigns to a configurable target (agent or unassigned
queue) and writes an internal note summarizing the exchange; the
existing on_conversation_assigned trigger notifies the assignee
- Settings → AI: "Hand off to" target picker
Track B — observability + safety
- providers return normalized token usage; logged per run to ai_usage_log
(admin-readable) from both auto-reply and draft
- account-wide auto-reply rate limit (30/min) on top of the
per-conversation cap
Migration: 033_ai_reply_polish.sql (messages.ai_generated,
ai_configs.handoff_agent_id, conversations.ai_handoff_summary,
ai_usage_log). Tests + typecheck + lint pass (607 tests).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Integrated next-intl into ai-config.tsx and ai-knowledge.tsx panels.
- Defined translation schemas for Settings.aiConfig and Settings.aiKnowledge in en.json.
- Migrated hardcoded role labels from role-meta.ts to use translation keys dynamically in settings-overview.tsx using the 'roles' namespace.
- All Typescript and Linter checks successfully passed without regressions.
- Implemented next-intl across Appearance, Security, API Keys, and Deals settings panels.
- Defined translation schemas for Settings.appearance, Settings.security, Settings.apiKeys, and Settings.deals in en.json.
- Removed explicit 'any' types in components by utilizing local useTranslations hooks directly inside sub-components (ModeCard, ThemeCard, CreateKeyDialog).
- Eliminated all new typescript and eslint errors related to the localization migrations.
- Updated progress trackers to reflect completion.
- Add 'Broadcasts' namespace to en.json for all messaging flows
- Localize broadcast list and detail views (stats, recipient tables, funnel)
- Localize 4-step Broadcast creation wizard (Choose Template, Select Audience, Personalize, Schedule)
- Refactor status configuration to use translation keys instead of hardcoded labels
- Pass full typechecking across all wizard components
- Added `Sidebar`, `Header`, and `ModeToggle` namespaces to English dictionary.
- Refactored `sidebar.tsx` and `header.tsx` to use dynamic translations.
- Updated accessibility labels for navigation and theme switcher.
Clicking "Add node" in the flow builder crashed the whole page to a
"This page couldn't load" error boundary, in both the list and canvas
views.
The add-node menus render a DropdownMenuLabel per category. That is
base-ui's Menu.GroupLabel, which calls useMenuGroupRootContext() during
render and THROWS ("MenuGroupContext is missing") when it has no
Menu.Group ancestor. Both menus wrapped each category in a plain <div>
instead of a DropdownMenuGroup, so the label had no group context. The
menu content only mounts when the menu opens, which is why initial
render was fine and the crash fired exactly on click.
Wrap each category's label + items in DropdownMenuGroup (base-ui
Menu.Group), which provides the context Menu.GroupLabel needs. The
between-group separator moves into a keyed Fragment so it stays a
sibling of the groups.
Adds a regression test pinning the contract: a bare DropdownMenuLabel
throws, a group-wrapped one does not.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Promotes the AI assistant out of Settings into a first-class "AI Agents"
sidebar area.
- Sidebar: new "AI Agents" item → /agents.
- /agents page with two tabs:
- Playground — a test chat (POST /api/ai/playground) that runs the
real auto-reply path (KB retrieval + provider, auto_reply prompt,
multi-turn, shows handoff). loadAiConfig gains requireActive so you
can test before enabling.
- Setup — the existing provider/key + knowledge-base config, relocated.
- Removed the AI section from Settings (settings-sections + page); same
account config, new home. No migration.
Tests +6 (query, loadAiConfig requireActive). 593 pass; typecheck, lint,
build green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Grounds the AI assistant (0.5.0) in the account's own content so it can
answer instead of handing off.
- Hybrid retrieval: Postgres full-text search for every account; optional
semantic search (pgvector + OpenAI text-embedding-3-small) when an
embeddings key is set. Semantic-primary, topped up with FTS.
- src/lib/ai: chunk, embeddings, knowledge (ingest + retrieve), query;
buildSystemPrompt gains a knowledge block; config carries an
(optional) embeddings key.
- Wired into /api/ai/draft and the auto-reply bot (best-effort — never
blocks a reply).
- KB API (/api/ai/knowledge CRUD + /reindex) and a Settings → AI
Assistant knowledge-base manager + embeddings-key field.
- Migration 030: pgvector, ai_knowledge_documents, ai_knowledge_chunks
(fts + embedding), match RPCs, ai_configs.embeddings_api_key.
Migration required: supabase/migrations/030_ai_knowledge.sql.
Stacked on feat/ai-reply-assistant (#329).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds an AI reply assistant powered by the account's own OpenAI or
Anthropic key (stored AES-256-GCM-encrypted; no per-seat AI fee).
- AI-drafted replies in the inbox composer (✨ button, agent+), via
read-only POST /api/ai/draft.
- Optional auto-reply bot on inbound messages that no Flow consumed and
no agent owns, with a per-conversation cap and clean [[HANDOFF]] to a
human. Stands down when a message-level automation would also reply.
- Settings → AI Assistant (admin+): provider/model, key, business
context, toggles, and a Test-key button that verifies before saving.
- Provider layer (src/lib/ai) with OpenAI + Anthropic adapters behind
one interface; migration 029 (ai_configs + per-conversation columns +
atomic claim_ai_reply_slot RPC).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds contact-based filters to the Inbox conversation list (issue #272):
- Multi-select tag filter with OR logic (a conversation matches if its
contact has any selected tag), consistent with Broadcast audiences.
- Company filter (exact match) over companies present in the inbox.
- Filters stack with the existing status filter and search; active
selections show as removable chips with a clear-all control.
Tags are hydrated via a shared CONVERSATION_SELECT embed and normalized
onto contact.tags, so the realtime hydrate path keeps new conversations
matching an active filter. Filtering logic lives in a pure, unit-tested
matchesContactFilters helper.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The WhatsApp settings load effect re-ran fetchConfig() whenever `user`
or `profileLoading` changed. Supabase's onAuthStateChange fires a token
refresh (new `user` object, profileLoading toggles) when the browser
tab regains focus, so returning to the tab re-hydrated the form and
overwrote any unsaved credential input.
Guard the effect with a ref tracking the account id already loaded, so
it only fetches once per account and ignores auth-state churn that
doesn't actually change the account. Explicit reloads after save,
registration verification, and reset still call fetchConfig directly.
Closes#307
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace `user` object with `user?.id` in dependency arrays to prevent unnecessary re-renders. Add `lastFetchedUserIdRef` to track the user ID for successfully initiated profile fetches, preventing redundant API calls when the window refocuses or the auth token is refreshed.
The inbox/contact TemplatePicker fetched approved templates filtered by
`user_id = current user`, so in a shared account a teammate only saw
templates they personally created — others' approved templates were
hidden, leaving them unable to send those templates.
message_templates is account-owned and its RLS SELECT policy already
scopes rows to account members (is_account_member, migration 017), so
the user_id filter was both redundant and over-restrictive. Drop it and
rely on RLS, matching the broadcast template picker.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a "Send template" action to the contact detail view so the
business can initiate (or re-open) a WhatsApp conversation with one
known contact by sending an approved template — without the awkward
single-recipient broadcast workaround.
The /api/whatsapp/send route now accepts `contact_id` as an
alternative to `conversation_id`: it verifies the contact is in the
caller's account, find-or-creates the contact's conversation
server-side, then runs the existing send path (phone-variant retry,
template send-builder, message + conversation persistence, flow
pause). The contact view reuses the existing inbox TemplatePicker for
template selection + variable/header/button input, keeping Meta
compliance unchanged (approved templates only).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The canvas and list add-node menus both listed all 10 node types in
one flat dropdown. As the type list grows — especially for forks
adding their own node types (see #312) — a flat list gets hard to
scan. Bucket the types into Messaging / Logic & data / Flow control,
driven by a new `category` field on NODE_META so a new type can't go
uncategorized, and a `groupNodeTypesByCategory` helper shared by both
menus so they stay in lockstep.
No triggers/events category: in wacrm a flow is triggered by
flow-level config (trigger_type), not a canvas node, so `start` lives
under Flow control.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reworks the flow editor (/flows/[id]) to the Flow Builder design
handoff, adapted to wacrm's existing tokens, components, and
light+dark theming. Keeps all behaviour — React Flow canvas,
drag-to-connect, typed config forms, validation, save/activate/delete,
per-browser view persistence, mobile-forces-list.
- shared.tsx: per-node-type color system (nodeColors) + one-line
blurbs, and a shared NodeIconChip component. Label color blends
toward --foreground via color-mix so it stays readable in both modes.
- header.tsx: compact single-row toolbar (back / icon / inline name /
status chip / edited dot · Runs / Delete / Activate / Save) plus a
subtle inline description note.
- flow-editor-shell.tsx: app-like full-height layout (toolbar → mode
row → stage → validation bar) with a segmented Canvas/List control
and a complete node-type legend derived from NODE_META.
- flow-canvas.tsx: per-type colored node cards, branch ports, dot-grid
background, rounded controls/minimap, top-left primary Add-node menu,
colored inspector header.
- flow-builder.tsx: list rows tied to the same per-type color language.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Templates with an IMAGE/VIDEO/DOCUMENT header could not be broadcast:
the wizard never offered a place to provide the header media URL, so
the send either shipped the template's stale stored URL or — when none
was stored — threw "image header requires a media link or id at send
time", failing the whole broadcast. The send path already supported
`messageParams.headerMediaUrl`; only the UI/hook never collected it.
- step3-personalize: show a required "Header media" URL field (with
http(s) validation and an image preview) when the selected template
has a media header; seed it from the template's stored sample URL and
block Next until a valid URL is provided.
- new-broadcast page: thread headerMediaUrl through wizard state.
- use-broadcast-sending: pass it as messageParams.headerMediaUrl to the
broadcast API for media-header templates.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the machine-to-machine auth layer the public REST API needs so
external scripts and automations can drive wacrm (issue #245). This is
the groundwork release; data endpoints land one at a time on top of it.
- Migration 026: account-scoped `api_keys` table. SHA-256 hash at rest
(plaintext shown once), display `key_prefix`, `scopes[]`, RLS via
`is_account_member` (members read, admin+ manage).
- `src/lib/api-keys/`: key gen/hash (wacrm_live_ prefix, timing-safe),
scope vocabulary, service-role auth-path store.
- `requireApiKey(request, scope?)` (src/lib/auth/api-context.ts):
bearer parse -> hash lookup -> liveness -> per-key rate limit ->
scopes-only authz. Returns a service-role-scoped account context.
- `/api/v1` envelope ({data} / {error:{code,message}}) + GET /api/v1/me
reference endpoint that exercises the whole stack.
- Dashboard: admin+ key management routes + Settings -> API keys tab.
- Docs (docs/public-api.md), CHANGELOG, README, .env note.
Checks: typecheck clean, lint 0 errors, full suite green (+27 tests).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a live presence layer so the Settings → Team members roster and the
inbox Assign dropdown show who is actively using the dashboard, idle, or
gone. Implements #269.
How it works
- New `member_presence` table + `touch_presence` SECURITY DEFINER RPC
(migration 024). Account-scoped SELECT RLS mirrors the read-only
roster; the RPC derives account_id server-side, never client-supplied.
- The active client heartbeats 'online'/'away' every ~30s; 'offline' is
derived by viewers from staleness, so a closed tab resolves to offline
without an unreliable unload write.
- Pure, unit-tested derivation/formatting in src/lib/presence.ts.
- `usePresence` reads the table, subscribes to Realtime (subscribe-then-
merge so events landing mid-fetch aren't clobbered), and re-derives
offline on a 15s tick.
- Roster: avatar badge + tooltip + "N online · N away · N offline"
summary, with an aria-label so AT/keyboard users get presence too.
Inbox Assign dropdown: matching dot before each teammate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Creating a keyword_match automation with Match type left on the default
"Contains" failed activation with `match type must be "exact" or
"contains"` (path trigger.match_type). The dropdown showed "Contains"
via a display-only fallback, but the value was never written to the
config, so the saved payload omitted match_type entirely.
Both engines already treat a missing match_type as "contains"
(automations/engine.ts, flows/engine.ts), so the activation validator
was stricter than runtime. Two coordinated fixes:
- validate.ts: accept a missing match_type (defaults to "contains");
only an explicit, unrecognised value is rejected. Aligns the
validator with the engines and any non-UI client.
- automation-builder.tsx: seed match_type: "contains" on mount of
KeywordMatchConfig so the displayed default is actually persisted,
keeping the saved record explicit and self-describing.
Adds a regression test that a keyword_match config with no match_type
passes activation validation.
Fixes#271
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a desktop-only toggle in the message-thread header to show/hide the
contact sidebar (tags, deals, notes). On smaller laptops the always-on
panel crowds the message thread; agents who only want to read and reply
can now reclaim that horizontal space.
The page owns the open/closed state and conditionally renders the
sidebar; the thread renders the toggle button and reflects the state.
The choice is persisted to localStorage (device-scoped, like the theme
prefs) so it survives reloads. To avoid a hydration mismatch we default
to open on the server and reconcile from storage in an effect after
mount. Mobile is unchanged — the sidebar is never a permanent panel
there and the toggle is hidden below lg.
Closes#258
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Settings page was the only dashboard route capped at
max-w-[1040px] mx-auto, so it floated centered with large empty
gutters on wide monitors while every other page is full-bleed.
Remove the cap so the page fills the available width like the rest of
the app, and spend the space where it helps readability:
- Overview status tiles go 3-up on xl (from 2-up).
- Templates list goes 2-up on xl.
- Form-heavy panels (Profile, Login & security, Deals) keep a readable
max-w-2xl; Fields & tags and Appearance use max-w-3xl — so inputs
don't stretch edge-to-edge on large displays.
- Grid uses minmax(0,1fr) to avoid blowout from long content.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the flat 8-tab Settings page with a cleaner information
architecture: a grouped left rail (Account / Workspace) plus a new
Overview landing with live status tiles. Related concerns are merged —
"Login & security" (password + sessions) and "Fields & tags" (tags +
custom fields).
- New shell: settings-sections (section model + legacy ?tab= mapping,
tags/custom-fields -> fields), settings-rail, settings-overview
(live counts + identity), security-panel, fields-and-tags-panel,
settings-panel-head, settings-chip, role-meta (shared role chip
metadata, de-duped from members-tab).
- Restyle the light panels to the mockup; reframe the heavy panels
(WhatsApp, Templates) with the shared panel header, internals intact.
- Tokens: add --card-2 (light+dark) and @theme mappings for
primary-hover/soft/soft-2 so the redesign stays fully tokenized.
- ?tab= remains the URL source of truth, keeping existing sidebar/header
deep-links working.
Overview decouples the slow WhatsApp Meta health-ping from the cheap
count queries so a flaky ping never blanks the whole landing; template
counts use head-only count queries.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>