Commit Graph

307 Commits

Author SHA1 Message Date
Arnas Donauskas
0181169a92
Merge pull request #419 from opastorello/fix/i18n-ko-parity
fix(i18n): sync ko.json with en.json and add a locale key parity test
2026-07-29 10:07:13 +03:00
Arnas Donauskas
ff2fb06181
Merge pull request #417 from Andeveling/fix/i18n-settings-overview-roles
fix(i18n): settings-overview role namespace, scopesHint rich-tag, and role SelectValue from #375
2026-07-29 10:06:42 +03:00
Arnas Donauskas
27abb92c45
Merge pull request #443 from ArnasDon/fix/420-delete-pipeline-button-contrast
fix(pipelines): make the Delete Pipeline button text visible
2026-07-29 09:34:34 +03:00
Arnas Donauskas
fc1f19b72a
Merge pull request #444 from ArnasDon/fix/409-automation-dispatch-reliability
fix(automations): stop inbound runs being dropped and mislogged
2026-07-29 09:33:49 +03:00
Arnas Donauskas
59dbf27f43 fix(automations): stop inbound runs being dropped and mislogged
Two defects that together produce the reported symptom — automation logs
that read `status: 'success'` with `steps_executed: []`.

1. The dispatch loop in the inbound webhook was fire-and-forget inside the
   route's `after()` block. `after()` only keeps the function alive for
   promises it can see, so the run could be frozen after the log row was
   inserted but before any step executed. This is the same failure mode as
   issue #301, recurring one level down — the comments on the AI auto-reply
   and `message.received` dispatches immediately below already spell out
   the invariant this violated. Now awaited; the per-iteration `.catch` is
   kept so one trigger type can't skip the rest of the loop.

2. `executeAutomation` seeded the log row with `status: 'success'` before
   running anything. A run that died mid-flight therefore left a permanent
   success with no steps, indistinguishable from an automation that
   genuinely had nothing to do. Seeded as `'failed'` instead, so the status
   only becomes success if execution actually reaches a terminal path. No
   migration needed — 'failed' is already in the column's CHECK constraint,
   and every terminal path overwrites it.

Two more points from the issue are deliberately not addressed here, as
both change existing behaviour and want a maintainer decision: switching
`contains` keyword matching to word boundaries (would alter which
automations fire for current users) and re-opening a closed conversation
on an inbound reply.

Refs #409.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 15:21:28 +03:00
Arnas Donauskas
7b7c5b1d96 fix(pipelines): make the Delete Pipeline button text visible
The button combined `variant="destructive"` — which is the soft treatment,
`bg-destructive/10 text-destructive` — with a `bg-red-600` override. The
override replaced the tinted background but not the red text, leaving red
on red and no readable label.

Drop the variant and add `text-white`, which is how every other solid-red
button in the app is written, including the delete-confirm button 100
lines up in this same dialog.

Closes #420.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 15:19:16 +03:00
Arnas Donauskas
44183bc750 fix(whatsapp): guard the template image-header fetch against SSRF
`ensureImageHeaderHandle` fetched the caller-supplied `header_media_url`
server-side with no validation, so a template submit — an action any
authenticated member can perform — could reach loopback, RFC1918 and
cloud-metadata addresses, with the response status and content-type
echoed back through the thrown error.

Wire in the project's existing `isDeliverableUrl` guard, which the two
other outbound-fetch call sites (webhook delivery, automation
`send_webhook`) already use, and reuse their `redirect: 'manual'` +
timeout so a public URL can't 3xx-bounce to an internal one. The
rejection reuses the unreachable-host message verbatim so the failure
doesn't distinguish "blocked" from "down".

Addresses GHSA-6fr5-pxw7-r3xw.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 13:07:09 +03:00
Nicolas Pastorello
bc3d0ccdcd test(i18n): guard message catalogue key parity across locales 2026-07-20 12:58:41 -03:00
Fabian Andres Parra Sanchez
999a080a29 fix(i18n): render translated label inside role SelectValue
Base UI's <SelectValue> with no children does not reliably extract
text from the selected <SelectItem> when the item's children is a
function call (e.g. tRoles(r.value)) — the trigger falls back to
displaying the raw enum string ('admin', 'agent', 'viewer').

Render the translated label explicitly as children of <SelectValue>
so the trigger always shows the human-readable role name.

Scope: only the two role selectors in Settings (matches this PR's
theme). The same pattern affects ~5 other sites (template-manager
header format, button type; ai-config provider and handoff target;
flows/broadcasts) — those are left as follow-up and tracked in #375.

Part of #375
2026-07-20 07:33:59 -05:00
Fabian Andres Parra Sanchez
18b1b2943b fix(i18n): use Settings.roles namespace in settings-overview
The identity card at the top of the Settings overview was calling
useTranslations('roles') (root-level) instead of useTranslations
('Settings.roles'). The root 'roles' namespace only exists in the
es-CO translation as a workaround added to mask this bug — in English
the chip rendered the raw keypath (e.g. 'roles.admin').

Aligns with invite-member-dialog.tsx:78 and members-tab.tsx:129,
which already use the 'Settings.roles' namespace.

Part of #375
2026-07-20 07:33:50 -05:00
Arnas Donauskas
8bbbe0a471
fix(settings): wrap useSearchParams pages in Suspense so nav works in prod
The settings page derived its active panel from `useSearchParams()` but
wasn't wrapped in a Suspense boundary. In Next 16 that fails the
production build ("missing Suspense with CSR bailout") and forces the
page to bail to full client-side rendering — shipping a settings screen
whose rail click handlers never wire up. Because the account-menu
Settings link targets `?tab=whatsapp`, users land on the WhatsApp panel
and can't navigate to any other section ("locked on WhatsApp").

Wrap the `useSearchParams` consumer in `<Suspense>` on the three
affected dashboard pages — settings, inbox, automations/new — mirroring
the split login/signup already use. Also add the missing
`Settings.sections.quick-replies` label, which was rendering as a raw
translation key in the settings rail.

Verified: production build now succeeds (previously errored out during
static generation); every settings rail section navigates correctly in a
production build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 15:08:25 +03:00
Arnas Donauskas
8bbda771ce
Merge pull request #386 from Andeveling/contrib/feat-colombian-peso-currency
feat: add Colombian Peso (COP) currency option
2026-07-17 13:47:55 +03:00
Arnas Donauskas
5413dd634a
Merge pull request #393 from mooouuu/feat/tag-added-trigger
feat(automations): dispatch tag added triggers
2026-07-17 12:48:18 +03:00
Arnas Donauskas
96e60adecf
Merge pull request #396 from opastorello/fix/cron-timing-safe
fix: use timing-safe compare for cron secret
2026-07-17 12:48:10 +03:00
Arnas Donauskas
1110f1fb45
fix: full-page navigation after login so the session is applied (#365)
Login signed in client-side then soft-navigated with router.push into
/dashboard, a route the middleware gates on the Supabase session cookie.
The soft navigation can reach the protected route before the server
observes the freshly written cookies, so the middleware bounces it back
to /login -- which looks like the page "just refreshing" instead of
signing in. It surfaced over ngrok/HTTPS tunnels (added latency widens
the window) while staying hidden on localhost.

- login: navigate with window.location.href after signInWithPassword,
  mirroring the deliberate full reload the invite-accept flow already
  uses in join/[token]/page.tsx; drops the now-unused useRouter.
- next.config: allow-list common dev tunnel origins (ngrok, cloudflared,
  localtunnel) so Next 16 stops 403-ing dev-only resources over the
  tunnel, extendable via ALLOWED_DEV_ORIGINS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 12:29:01 +03:00
Nicolas Pastorello
ed4ce6b52e fix: use timing-safe compare for cron secret
Match flows cron implementation (timingSafeEqual).
2026-07-16 13:03:21 -03:00
Mauricio Martinez
292f52f903 feat(automations): dispatch tag added triggers 2026-07-16 00:26:21 -06:00
Fabian Andres Parra Sanchez
3b48b6e3f4 feat: add Colombian Peso (COP) currency option 2026-07-14 06:55:31 -05:00
Arnas Donauskas
faf0f740a4
fix: stop inbound chats fragmenting into duplicate conversations (#363)
An inbound message could create a second conversation for a contact
under a race (Meta retries a delivery, or a batch fans out to concurrent
after() runs). There was no UNIQUE index and no unique-violation
backstop on conversations, unlike contacts (migration 022). Worse, once
two conversations existed the `.single()` lookup errored on every later
message and the webhook created yet another conversation each time,
snowballing into a wall of duplicate chats.

- findOrCreateConversation now resolves oldest-first with .limit(1)
  instead of .single(), so pre-existing duplicates converge, and catches
  23505 on insert to re-resolve the winning row.
- Same hardening applied to the public-API resolver (resolve-conversation).
- Migration 036 merges existing duplicate conversations into the oldest
  thread (re-pointing messages, reactions, deals, flow_runs,
  notifications, ai_usage_log — no data loss) then adds a
  UNIQUE (account_id, contact_id) index as the authoritative guarantee.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 12:27:46 +03:00
Arnas Donauskas
78b42dfba9 feat: WhatsApp interactive messages (reply buttons + lists)
Add full support for WhatsApp Cloud API interactive messages across the
inbox, automations, and a new quick-replies feature. Extends the
interactive plumbing that previously existed only for the Flows engine.

Sending
- send-message core handles `interactive` (reply buttons ≤3, list ≤10
  rows) via the existing Meta senders; validated server-side before send.
- Inbox composer gains a "+" menu → builder dialog with live WhatsApp-style
  preview + Meta char limits (button 20, list row 24, body 1024).
- Automation "Send Buttons" / "Send List" steps in the builder.
- Public v1 /messages accepts type=interactive.

Storage / round-trip
- migration 035: messages.interactive_payload JSONB + quick_replies table.
- Outbound interactive persists the structured payload (composer,
  automations, and Flows) so the thread re-renders the buttons/rows.

Receiving
- Bubble renders outbound interactive via the shared preview; inbound taps
  (button_reply/list_reply) keep the "chosen option" affordance.

Automations
- New `interactive_reply` trigger matches on tapped button/list IDs
  (exact), enabling chained multi-step menus.

Quick replies
- New account-scoped quick_replies (text or interactive snippets), managed
  in Settings and insertable from the composer; "Save as quick reply" in
  the builder.

Shared core in src/lib/whatsapp/interactive.ts + src/components/interactive/.
Includes unit tests for the validator, send-param validation, the
interactive_reply trigger match, and the new automation step/trigger
validation. typecheck + lint clean; full suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 19:03:15 +03:00
Arnas Donauskas
2311b7ea35 Merge origin/main; internationalize new AI auto-reply UI
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>
2026-07-08 16:35:27 +03:00
Arnas Donauskas
077d203980
Merge pull request #348 from ivanmercedes/feat/i18n-dashboard-shell
feat(i18n): finalize localization for Dashboard, Inbox, Settings, and Automations
2026-07-08 15:39:18 +03:00
Arnas Donauskas
e132cc7a15 fix(ai): address code-review findings on auto-reply polish
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>
2026-07-08 15:35:41 +03:00
Ivan Mercedes
4690c4d929 refactor: address PR feedback for security and cleanups
- refactor: migrate dangerouslySetInnerHTML(t.raw) to t.rich() to prevent HTML injection vectors
- fix: restore per-status wording for flow activation toasts ("Flow activated.", "Archived.", "Saved as draft.")
- chore: remove unused `tComposer` in message-thread.tsx
- chore: remove unused `ROLE_DESCRIPTIONS` in invite-member-dialog.tsx
2026-07-08 07:58:33 -04:00
Arnas Donauskas
c736e7a43f feat(ai): add token-usage dashboard (AI Agents → Usage)
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>
2026-07-08 14:31:36 +03:00
Arnas Donauskas
b0194dfc27 feat(ai): polish auto-reply — inbox controls, real handoff, usage logging
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>
2026-07-08 14:03:33 +03:00
Arnas Donauskas
23838a9959
Merge pull request #352 from ArnasDon/fix/ghsa-8jqh-598v-rfxc-webhook-ssrf
fix(security): SSRF guard on automation send_webhook action (GHSA-8jqh-598v-rfxc)
2026-07-08 10:09:23 +03:00
Arnas Donauskas
7d1ddbfdb8 fix(security): SSRF guard on automation send_webhook action (GHSA-8jqh-598v-rfxc)
The send_webhook automation step fetched an account-controlled URL with
attacker-controlled headers and no SSRF protection, following redirects
by default — letting an agent+ reach cloud metadata (169.254.169.254),
loopback, and RFC1918 hosts from the server's network (status code
surfaces in automation logs). The separate webhook_endpoints delivery
path already guards this; the automation path did not.

Route send_webhook through the existing isDeliverableUrl guard before the
fetch, and mirror the deliver.ts hardening: redirect: 'manual' (a public
URL can't 3xx-bounce to an internal one) and a 10s timeout. Add an engine
regression test asserting a link-local target never reaches fetch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 10:04:11 +03:00
Arnas Donauskas
03e851bea5
Merge pull request #351 from ArnasDon/fix/ghsa-34q7-fv77-625j-role-checks
fix(security): enforce agent role on service-role write routes (GHSA-34q7-fv77-625j)
2026-07-08 09:39:41 +03:00
Arnas Donauskas
1d7829e68a fix(security): enforce agent role on service-role write routes (GHSA-34q7-fv77-625j)
Flows and automations write routes verify only authentication/membership
(via the RLS client, whose *_select policies have no minimum role) and
then mutate with the service-role client, which bypasses the agent-gated
*_insert/update/delete RLS policies. A read-only `viewer` therefore had
full write power.

H3 (High): add requireRole('agent') to the flows write handlers —
PUT/DELETE (flows/[id]), activate (flows/[id]/activate), and create
(flows POST). GET/list stay open to viewers.

M2 (Medium): same gate on automations POST, the engine trigger
(automations/engine, which sends outbound WhatsApp), and the [id]
mutation handlers (PATCH/DELETE/duplicate). The cron route is unchanged —
it is gated by the x-cron-secret shared secret, not user auth.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 09:35:20 +03:00
Ivan Mercedes
de75715cff feat(i18n): localize Inbox components and update english translations 2026-07-07 21:24:54 -04:00
Ivan Mercedes
801969a361 feat(i18n): localize settings ai and meta sections
- 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.
2026-07-07 21:17:41 -04:00
Ivan Mercedes
9ee79638da feat(i18n): localize settings configuration panels
- 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.
2026-07-07 21:09:50 -04:00
Ivan Mercedes
c30e117552 refactor: update translation key types and simplify delete aria-label implementation 2026-07-07 21:00:33 -04:00
Ivan Mercedes
a6596de2c9 refactor: improve type safety for translations by replacing any types with specific parameters and node types 2026-07-07 20:55:21 -04:00
Ivan Mercedes
eeea478315 feat(i18n): localize settings templates and profile modul 2026-07-07 20:50:57 -04:00
Ivan Mercedes
a91b8a7ba7 feat(i18n): complete flows module internationalization 2026-07-07 20:12:38 -04:00
Ivan Mercedes
7cb019081b feat(i18n): translate settings page titles 2026-07-07 19:57:36 -04:00
Ivan Mercedes
9357fd9ec9 feat(i18n): complete settings module internationalization 2026-07-07 19:56:55 -04:00
Ivan Mercedes
caf0f3660e feat(i18n): complete internationalization of flow builder components 2026-07-07 19:43:26 -04:00
Ivan Mercedes
d518d54520 i18n: Localize AutomationBuilder component and fix typing 2026-07-07 17:51:09 -04:00
Ivan Mercedes
92e5789837 feat(i18n): localize Broadcasts module and wizard
- 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
2026-07-07 17:29:45 -04:00
Ivan Mercedes
8b1d75456f feat(i18n): localize pipelines module and components
- Added Pipelines namespace to messages/en.json encompassing page, board, card, form, settings, and analytics.
- Migrated PipelinesPage empty states, dropdowns, and pipeline creation dialogues.
- Localized pipeline-board and deal-card UI strings and badges (Won/Lost).
- Translated deal-form inputs, dynamic tooltips, select options, and error toasts.
- Localized pipeline-settings including the sortable stage management rows and delete confirmations.
- Migrated pipeline-analytics metrics and detailed statistical tooltips.
- Resolved typecheck dependencies to ensure the t function is correctly passed to nested sortable components.
2026-07-07 17:14:48 -04:00
Ivan Mercedes
dcfd66792b feat: internationalize contact 2026-07-07 17:02:02 -04:00
Ivan Mercedes
57a914d547 feat(i18n): translate inbox module and components
- Add Inbox namespace to en.json dictionary
- Localize InboxPage and WhatsApp connection banners
- Translate ConversationList, search placeholders, and filters
- Localize MessageThread, session timers, and agent assignment dropdowns
- Translate MessageComposer, attachment menus, and AI draft hints
- Update MessageBubble, MessageActions, ReplyQuote, and TemplatePicker
- Verify strict typing for next-intl hooks across all inbox files
2026-07-07 15:09:24 -04:00
Ivan Mercedes
afa136245b feat(i18n): translate dashboard overview components
- Added `Dashboard` namespace to English dictionary.

- Translated metric cards, quick actions, and activity feed.

- Localized charts (conversations, pipeline donut, response time) and empty states.
2026-07-07 14:31:01 -04:00
Ivan Mercedes
738b0dde1f refactor: update translation key type assertions from any to string in header and sidebar components 2026-07-07 13:49:56 -04:00
Ivan Mercedes
07b20f42d4 feat(i18n): translate dashboard layout 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.
2026-07-07 13:43:06 -04:00
Arnas Donauskas
79565c1fbe
Merge pull request #328 from ivanmercedes/feat/i18n-support
feat: integrate next-intl for i18n support
2026-07-07 12:57:04 +03:00
Arnas Donauskas
4e2192d84f
Merge pull request #347 from ArnasDon/fix/flows-add-node-crash
fix(flows): wrap add-node menu groups in DropdownMenuGroup (#336)
2026-07-07 11:49:36 +03:00