Commit Graph

1147 Commits

Author SHA1 Message Date
Rodribm10
cf7338a6e2 chore: sync fazer-ai chatwoot 4.13.0 2026-05-06 06:24:46 -03:00
Codex CLI
d670c5644b fix(captain/reservations): prioritize confirmed bookings 2026-05-03 11:03:38 +00:00
Rodribm10
c954f0fab4 feat(profile): UI pra ligar/desligar alerta de conversa parada
Nova seção em Configurações → Perfil "Alerta de conversa parada" com:
- Checkbox principal "Ativar alerta de conversa parada" (OFF salva
  ui_settings.aggressive_alert_inbox_ids = []).
- Sub-checkbox "Aplicar em todas as caixas" (ON salva null = todas).
- Lista de inboxes (visível quando não é "todas") pra selecionar caso
  por caso (salva [id, id, ...]).
- Persiste a cada change via updateUISettings (sem botão "salvar").

Antes só dava pra mexer via Rails runner. Cada admin agora controla
sozinho sem mexer em DB.

i18n: PROFILE_SETTINGS.FORM.AGGRESSIVE_ALERT_SECTION.* em pt_BR e en.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 22:04:14 -03:00
Rodribm10
28e880d7b6 feat(captain/hermes-builder): aba Verificação com 22+ checks + reparo automático
UI nova dentro do Construtor (Hermes) — TabBar com Chat e Verificação.
Verificação roda HermesBuilder::Validator (DB+runtime) e exibe resultado
agrupado por categoria, com botão Refazer inline em FAIL/WARN reparáveis.

Backend (porta dos checks DB do CLI bin/hermes-validate):
- HermesBuilder::Validator com 22+ checks: engine, profile, port,
  secret, parent, unit, Brand, CaptainInbox sync (o bug que travou
  Juliana), pricing dry-run, Inter creds, typing/response_delay,
  registry MCP completo.
- HermesBuilder::Repairer com 4 handlers automáticos: set_engine_hermes,
  sync_captain_inbox_unit, set_default_typing_delay,
  set_default_response_delay.
- Endpoints novos: GET assistants, GET validate?slug=, POST repair.

Frontend:
- builder/Index.vue: wrapper com TabBar.
- builder/BuilderChat.vue: extraído do Index original.
- builder/BuilderVerification.vue: dropdown + Conferir agora + lista
  agrupada por categoria com badges + botão Refazer inline.

i18n: keys em pt_BR e en sob CAPTAIN_HERMES_BUILDER.VERIFY.*.

Filesystem/systemd checks ficam pro CLI hermes-validate (Rails container
não enxerga /root/.hermes/profiles do host).

Validado HTTP: GET /validate?slug=juliana_qnn1 → 28 PASS / 0 FAIL / 1 WARN.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 15:27:40 -03:00
Rodribm10
3182002bd9 feat(captain): engine column + DB-driven Hermes routing + Express pricing
Marca cada Captain::Assistant com engine ('captain_interno' | 'hermes')
e move o roteamento Hermes do env var pro banco — admin troca engine
re-apontando a inbox no painel, sem deploy. Mantém fallback pras env
vars antigas (CAPTAIN_HERMES_INBOX_IDS etc) durante a migração gradual,
pra não quebrar Valentina antes da re-associação.

Frontend: badge "Hermes" (âmbar) ou "Interno" (cinza) ao lado de cada
assistant no dropdown switcher e no card da listagem, com chaves i18n
em en + pt_BR.

Tabela de preço (pricing_tables.rb): adiciona unit Express (id=5) e
estende a estrutura pra aceitar preço por dia da semana
(mon_wed/thu_sun) — necessário pro Express, retrocompatível com Dolce
Amore (preço único).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 07:54:01 -03:00
Rodribm10
041766b427 fix(captain/hermes-builder): namespace controller correto + botao iniciar
Bug: controller estava em enterprise/api/v1/accounts/captain/ com namespace
Enterprise:: — convencao Chatwoot eh enterprise/app/controllers/api/v1/...
direto, classe Api::V1::Accounts::Captain::HermesBuilderController. Sem
namespace Enterprise::. 404 acontecia porque rotas registravam Captain::
sem prefixo Enterprise::.

Move controller pro path correto. Remove diretorios vazios criados.

UX: adiciona endpoint POST /start que envia comando-gatilho oculto pro
Construtor comecar fluxo socratico — admin nao precisa digitar primeira
mensagem. Vue mostra empty state com botao "Iniciar criacao" em vez de
exigir mensagem inicial.

i18n keys novas: START + EMPTY_STATE atualizado em pt_BR + en.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 22:06:04 -03:00
Rodribm10
35358a42b1 fix(captain/hermes-builder): i18n keys no caminho correto
Convencao Chatwoot pra features Captain novas: keys no root level
(CAPTAIN_HERMES_BUILDER, igual a CAPTAIN_ROLETA, CAPTAIN_FUNNEL etc) e
nao como sub-key de CAPTAIN.

Move CAPTAIN.HERMES_BUILDER -> CAPTAIN_HERMES_BUILDER em pt_BR e en.
Atualiza referencias t() no Vue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 21:17:53 -03:00
Rodribm10
40fd0c8f50 feat(captain/hermes-builder): UI Vue + endpoints pra chat com Construtor
Tela "Construtor" no painel Captain (acessivel em sidebar pra admins) que
permite criar novo agente Hermes via chat guiado com agente Construtor
(profile Hermes separado).

Backend (admin scope):
- POST /api/v1/accounts/:id/captain/hermes_builder — manda mensagem do
  admin pro gateway do Construtor (Hermes na porta 8646)
- GET — retorna historico da sessao (Rails.cache, TTL 4h)
- DELETE /reset — limpa sessao
- POST /webhooks/captain/builder_callback — recebe respostas async do
  Construtor via plugin captain-http-callback do Hermes
- HermesBuilder::Storage (Rails.cache) — persiste msgs por session_key
  (account_id + user_id) com role/content/created_at
- HermesBuilder::Dispatcher — encaminha pro webhook do Construtor com
  HMAC opcional via ENV HERMES_BUILDER_WEBHOOK_SECRET

Frontend:
- Pagina Vue HermesBuilder/Index.vue — chat simples com:
  * Lista de mensagens com bubbles user/construtor
  * Indicador "digitando..." enquanto aguarda resposta
  * Input com Enter pra enviar / Shift+Enter pra nova linha
  * Polling 2s pra novas msgs
  * Botao Limpar conversa
- API client em api/captain/hermesBuilder.js
- Rota captain_hermes_builder_index (admin only)
- Item no sidebar Captain "Construtor (Hermes)"
- i18n keys CAPTAIN.HERMES_BUILDER em pt_BR + en

UX flow:
  Admin abre tela → digita "olá" → Construtor pergunta nome → admin
  responde → marca, persona, tabela (com opcao copiar de existente),
  regras, FAQs, identidade → resumo → confirmar → Construtor chama
  save_agent_spec → JSON salvo em /tmp/agent-specs/<slug>.json pra
  revisao posterior. NAO cria filesystem do profile nem registros DB
  (etapa SEPARADA, prox sessao).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 21:00:41 -03:00
Gabriel Jablonski
72c9821270
feat(whatsapp): add emoji reactions UI (#276)
* feat(whatsapp): add emoji reactions UI

Adds end-to-end agent UI for emoji reactions on WhatsApp inboxes
(Cloud API, Baileys, Z-API). Reactions arrive as Messages with
is_reaction=true; this PR exposes them in the bubble UI and lets
agents react with toggle/replace/remove semantics.

- Add POST /reactions endpoint with toggle/replace logic that handles
  multi-device echoes from the same connected number
- Add Channel::Whatsapp#supports_reactions? capability
- Add Message.hide_removed_reactions scope and use it in conversation
  card preview / last_non_activity_message
- Enrich last_non_activity_message with in_reply_to_snippet for
  reaction previews in chat list
- Frontend: hover EmojiReactionPicker (8 quick + full picker) with
  alignment-aware positioning, single ReactionDisplay chip aggregating
  emojis with total count, conversation card preview shows "Você
  reagiu" for own/multi-device echoes

* fix: address CodeRabbit review feedback

- MessagePreview: render "Você" for outgoing reaction echoes that have no
  sender (multi-device echoes from the connected number)
- MessagesView#findCurrentUserReaction: prefer active reactions over
  deleted rows so a stale deleted echo cannot hijack the toggle target
- conversationHelper: drop removed reactions up-front so the activity
  fallback never returns null when older non-removed messages exist
- imap_import rake: wrap IMAP work in begin/ensure so the session is
  closed even when uid_search/scan_new_email_uids raises
- ReactionDisplay: include reaction.id in the user row so v-for keys
  stay stable across re-renders

* fix: address CodeRabbit round 2 feedback

- enterprise Message override of mark_pending_conversation_as_open_for_human_response
  now early-returns on reaction? so reactions can no longer auto-open Captain-pending
  conversations (matches the OSS guard)
- whatsapp incoming reaction-removal handlers (Cloud/Baileys/Z-API) look up the
  reaction Message globally by sender instead of through the inbound conversation
  scope, then operate on existing.conversation; otherwise an old/resolved thread
  could be silently no-op'd while the inbound flow created a stray empty thread
- EmojiReactionPicker: localize quick-emoji tooltip labels via i18n keys
- Message.vue: track pendingTimeouts and clear them on unmount so the cooldown
  setTimeout no longer touches state after teardown
- toggleMessageReaction action returns the API promise so callers can reconcile
  if the cable echo is delayed

* fix: address CodeRabbit round 3 feedback

- MessageFinder#page_window: pluck the 20-row window IDs before taking .min
  so the latest page honors PAGE_LIMIT (ActiveRecord's .minimum(:id) ignores
  .limit and aggregates over the whole relation)
- ReactionsController#current_user_reaction: rank active reactions ahead of
  deleted rows (same invariant as the frontend lookup) so a stale deleted
  echo can no longer hijack the toggle target and resurrect itself
- Whatsapp incoming handlers (Cloud, Baileys individual & group, Z-API) now
  branch on reaction_removal? BEFORE set_conversation / find_or_create_group_
  conversation, so a blank reaction-removal webhook can never open or create
  a stray thread just to no-op
- Message#reaction?: strict-boolean cast (via ActiveModel::Type::Boolean)
  so a stored string "false" no longer leaks through .present? as truthy

* fix: address CodeRabbit round 4 feedback

- MessageList: anchor unread divider on the filtered visibleMessages
  (firstUnreadId can land on a reaction that's filtered out, otherwise
  the separator silently disappears)
- ReactionDisplay: render the removable user row as a real <button> when
  it's the current user's reaction so keyboard users can focus/activate it
- MessagesView#findCurrentUserReaction: read sender_type from m.sender_type
  OR m.sender?.type so REST-loaded messages match the same row instead of
  spawning a duplicate optimistic reaction
- Whatsapp incoming reaction-removal lookup (Cloud, Baileys, Z-API): pick
  the newest active row first and only fall back to the newest deleted row
  when no active reaction exists, mirroring the controller invariant
- CardMessagePreview: use MESSAGE_TYPE.OUTGOING constant in place of the
  literal 1 for the multi-device reaction echo check

* fix: address CodeRabbit round 5 feedback

- ReactionsController#ensure_target_is_reactable: reject activity,
  template, failed, is_unsupported and missing-source_id targets so the
  API mirrors the client toolbar gate and refuses reactions that could
  never land on WhatsApp
- MessageList reaction aggregator: treat "agent reacted via Chatwoot"
  and "agent reacted via the connected phone" as the same self bucket
  so the chip no longer double-counts the current user when both shapes
  coexist for one target
- internalChat ReactionDisplay: render the removable user row as a real
  <button> so keyboard users can focus and trigger removal (mirrors the
  fix already applied to components-next/message/ReactionDisplay)
- EventDataPresenter#push_last_non_activity_message: reorder
  created_at: :desc before .first so the cable snapshot publishes the
  latest preview instead of the oldest row
- Z-API mark_existing_reaction_as_removed: drop the blanket
  `return unless incoming_message?` and route the lookup by direction
  (contact sender for incoming removals, senderless outgoing row for
  multi-device removals from the connected phone). Chatwoot-originated
  echoes stay idempotent because the active-first guard finds nothing
  once the controller has flipped deleted=true locally
- spec: assert reaction removal does not change messages.count on the
  in-place Cloud path

* fix: address CodeRabbit round 6 feedback

- ReactionsController: validate the emoji payload is a single grapheme
  cluster containing a Unicode Emoji codepoint (not just <=32 bytes), so
  arbitrary short strings like "ok" or "123" can no longer be persisted
  as a reaction or enqueued as a WhatsApp reaction send
- target_unreactable_error: add the content_attributes['deleted'] guard
  to mirror the frontend picker gate on deleted messages
- IncomingMessageBaseService: move contact_processable? AFTER the
  reaction_removal? early-return so a blocked contact's removal webhook
  can still reconcile an existing reaction row instead of leaving a
  stale chip/preview
- imap_import rake: add safe_close_imap(imap) that falls back to
  disconnect when logout raises Net::IMAP::Error, mirroring
  terminate_imap_connection in BaseFetchEmailService, and replace the
  three ensure-block imap&.logout sites with it

* fix: address CodeRabbit round 7 feedback

- CardMessagePreview: resolve `lastNonActivityMessage` against the live
  `messages` array by id before rendering, so the chat-card preview
  picks up the freshest copy instead of the (possibly stale) snapshot
  that was mutated in place by a reaction toggle / multi-device echo
- Message + ReactionDisplay: thread an `inboxSupportsReactions` →
  `read-only` prop into the chip so non-supported channels (eg.
  360Dialog) render historical reactions without a clickable
  toggle/remove path that would only hit a 422
- conversations/index.js: replace the truthiness `&&` guard around the
  out-of-order MESSAGE_UPDATED check with `Number.isFinite` parsing so
  a malformed/missing `updated_at` is treated as stale instead of
  silently overwriting a fresher local row
- Baileys mark_existing_reaction_as_removed: drop the blanket
  `return unless incoming?` and split the lookup by direction
  (sender for incoming, sender-less outgoing for multi-device removals)
  to mirror the Z-API/Cloud handlers
- Whatsapp reaction-removal lookup (Cloud, Baileys, Z-API): drop the
  fallback to the newest deleted row so a Chatwoot-originated removal
  echo no-ops cleanly instead of bumping `updated_at` and dispatching
  another `conversation.updated`
- conversation jbuilder: explicit `reorder(created_at: :desc)` on
  `last_non_activity_message` so REST and cable both serialize the
  same most-recent preview

* fix: address CodeRabbit round 8 feedback

- ReactionsController#current_user_reaction: also match on
  content_attributes.in_reply_to_external_id = @target_message.source_id
  (via OR with the existing in_reply_to check), so WhatsApp-echoed
  reactions persisted by the incoming handlers — where in_reply_to could
  be blanked if the target wasn't resolvable at save time — are found and
  toggled instead of stacking a duplicate self-reaction
- Mirror the same defensive OR check in the frontend
  MessagesView#findCurrentUserReaction, and thread the target's
  source_id through the toggleReaction event from Message.vue so the
  lookup sees it

* fix: address CodeRabbit round 9 feedback

- emoji_payload_valid?: tighten the final property check from \p{Emoji}
  to \p{Extended_Pictographic} so plain "1", "#", "*" (which Unicode
  tags as Emoji because they're valid keycap bases) are rejected as
  reaction payloads
- EmojiReactionPicker: mirror the translated `title` into `aria-label`
  on the icon-only smile-plus / plus buttons so screen readers announce
  a meaningful action name
- internalChat ReactionDisplay: close the popover when the post-removal
  state would leave ≤1 reactions, so a singleton-user popover never
  lingers after removing one of a pair
- EventDataPresenter + conversation jbuilder: strip HTML before
  truncating `in_reply_to_snippet` so reactions to email/HTML bubbles
  don't surface literal "<p>..." markup in the chat-list preview

* fix: address CodeRabbit round 10 feedback

- MessageList#reactionsByMessageId: break createdAt ties with `<=` so a
  later iteration wins on second-resolution tie; two toggles in the same
  second no longer leave the chip pointing at a stale row
- MessagePreview: require a non-empty `message.attachments` array (via
  `?.length`) before taking the attachment preview branch, so a removed
  reaction with `[]` no longer renders the attachment placeholder
- MessagesView#findCurrentUserReaction: replace the sort-based pick with
  a reduce that deterministically takes the last element on tie, so a
  fast toggle can't hit a stale/deleted row with the same created_at
- Baileys group handler: guard against `@sender_contact.blank?` before
  dispatching mark_existing_reaction_as_removed, otherwise a nil sender
  would fall into the senderless-outgoing branch and match the wrong row
- WhatsApp reaction-removal lookups (Cloud, Baileys, Z-API): scope the
  base query to `inbox_id: inbox.id` so a colliding WhatsApp message id
  across inboxes can never mutate a reaction row from another inbox

* fix: address CodeRabbit round 11 feedback

- ReactionsController#emoji_payload_valid?: broaden the final property
  check to accept flag and keycap emoji. `\p{Extended_Pictographic}` by
  itself is per-codepoint, so 🇧🇷 (two Regional Indicators) and 1️⃣
  (digit + VS16 + U+20E3) failed validation. Allow any grapheme cluster
  that contains at least one pictographic codepoint, a Regional
  Indicator, or the combining keycap, while still rejecting plain
  ASCII like "ok", "1", "#"
- Message.vue#canShowReactionToolbar: hide the picker when the target
  has no provider source_id, mirroring the server guard in
  ReactionsController#target_unreactable_error instead of letting the
  click fall through to a 422
- MessageList#reactionsByMessageId: fall back to a sourceId → id
  lookup when a reaction only carries `inReplyToExternalId` (WhatsApp
  echo / phone-originated), so its chip still renders against the
  target bubble after reload
- getLastMessage: merge the fresher store fields onto the API
  snapshot instead of replacing it, so jbuilder-only enrichments like
  `in_reply_to_snippet` survive the store refresh

* fix(reactions): preserve API fields on card preview and expose a11y state on quick picker

* fix(reactions): consistent originalId resolution, natural PT-BR snippet phrasing, accurate outgoing-echo spec

* fix(reactions): reject requests missing emoji param and align zapi outgoing-echo spec fixture

* fix(reactions): activity preview fallback, camelCase event listener, EN snippet quoting, fromMe group removals, REST chat-only preview

* fix(reactions): reject non-string emoji, scope page reactions to window, exempt reactions from human_response, add cloud multi-device removal

* test(message): isolate hide_removed_reactions deleted-branch from blank-content branch

* fix(reactions): coerce in_reply_to_snippet to plain String

strip_tags returns an ActiveSupport::SafeBuffer; truncate preserves the
class. When this snippet flowed into ActionCableBroadcastJob via the
CONVERSATION_UPDATED dispatch, Sidekiq's strict-args check rejected the
non-native JSON type, raising synchronously through the dispatcher and
turning the reactions controller response into a 500 even though the row
had already persisted. The UI then surfaced the generic 'failed to update
reaction' toast despite the chip rendering correctly.

Wrap with String.new so the broadcast payload contains plain Strings.

* fix(reactions): don't auto-scroll to bottom on reaction add

ADD_MESSAGE emits SCROLL_TO_MESSAGE for every new push, which makes
sense for regular outgoing messages (the user just hit send and wants
to see it). Reactions render as chips on the parent bubble, so the
auto-scroll yanked the agent away from whichever older message they
were reacting to. Skip the emit when the incoming message is flagged
as a reaction.

* fix(reactions): skip scroll on conversation-only updates triggered by reactions

The reactions controller dispatches CONVERSATION_UPDATED so the chat list
preview can refresh in place. UPDATE_CONVERSATION mutation always emitted
SCROLL_TO_MESSAGE for the open conversation, so every toggle yanked the
viewport back to the bottom even after the previous fix in ADD_MESSAGE.
When the refreshed preview row is itself a reaction the update is
preview-only and the scroll is unwanted; for a regular incoming message
the latest non-activity row is the message itself, which still triggers
the scroll as before.

* fix(reactions): anchor compact picker to button side instead of centering

The compact picker was centered on the smile button, so half its width
always extended toward the bubble side and overflowed past the chat edge
on short messages. Anchor it to the button's outer side and nudge 4px
toward the bubble so it lines up with the trigger.

* test(reactions): regression coverage for safebuffer + scroll skip

The previous CodeRabbit rounds shipped three bugs the existing specs
didn't catch: a SafeBuffer return from `strip_tags` that 500'd the
reactions controller via Sidekiq strict-args, and two SCROLL_TO_MESSAGE
emits (one per mutation) that yanked the open conversation to the
bottom on every emoji toggle. Lock all three behaviors.

Also tighten the spec policy in AGENTS.md so new features default to
having specs instead of skipping them.

* test(baileys): align send_message_body helper with id:updated_at format

The reactions PR switched chatwootMessageId to "<id>:<updated_at>" so
toggle/replace cycles get a fresh idempotency key against baileys-api,
but the shared spec helper still merged the bare integer id. 18 baileys
provider specs were silently broken on CI as a result.

* fix(reactions): skip set_contact for unknown reaction-removal webhooks

Reaction-removal cloud webhooks were unconditionally creating a contact
even when the sender was unknown and there was nothing to remove,
because set_contact ran before the reaction_removal? short-circuit
(needed earlier so blocked-contact reconciliation works). Add a
sender-agnostic existence check on the inbox/in_reply_to scope and bail
out before set_contact when no candidate row exists.

Also realign two specs that were not updated when the chatwootMessageId
format gained an `:updated_at` suffix and when zapi reaction-removal
short-circuited instead of creating a Message.

* test(conversation): include last_non_activity_message in push_data fixture

Reactions PR added last_non_activity_message to the push_data payload
but conversation_spec's exact-match expectation wasn't updated, so the
sharded CI shard that landed on this file flipped red.
2026-04-30 21:09:12 -03:00
Gabriel Jablonski
b5757eea5d
fix(branding): add SuperAdmin-only notice on upgrade gates (#278)
* fix(branding): add SuperAdmin-only notice on upgrade gates

Some upgrade prompts (Kanban paywall, group creation form, group-disabled
banner in conversation view) are rendered only to SuperAdmins and link to
fazer.ai. Admins viewing those screens were worried that the fazer.ai
link was also being shown to their agents, even though it is not.

Add a discreet "Only system administrators can see this message" line
under each SuperAdmin-only block to make the audience explicit.

* fix(branding): inline SuperAdmin notice into Banner component

The notice was being rendered as a standalone <p> below the conversation
banner, which made it easy to miss. Add an optional noticeMessage prop
to the Banner component and render it inside the banner with italic +
reduced opacity styling, then pass it from the groups-disabled branch
of the MessagesView banner.
2026-04-28 13:20:38 -03:00
Rodribm10
d831ee4d33 feat(reports): Painel Diretoria — Onda 1A (leitura)
Primeira onda do roadmap de indicadores executivos do Grupo Nova. Mede
ADOÇÃO DO CANAL DIGITAL, não a operação total — banner explícito alerta
que reservas fechadas manualmente na recepção ainda não estão capturadas
(Onda 1B vai adicionar marcação manual via botão na conversa).

Backend:
- V2::Reports::ConversionFunnelBuilder — leads (novo/retorno/total),
  reservas (criadas != draft, pagas in active/completed/confirmed),
  taxas de conversão. Filtro opcional por inbox.
- V2::Reports::InboxBenchmarkingBuilder — uma linha por inbox com
  brand_name (via Captain::UnitInbox -> Unit -> Brand)
- Endpoints GET /reports/conversion_funnel e /reports/inbox_benchmarking
- RSpec do ConversionFunnelBuilder

Frontend:
- Rota top-level Reports → Painel Diretoria
- DirectoryDashboard.vue: banner de adoção + filtros + cards + funil + tabela
  benchmarking agrupada por marca com variação vs média
- API client getConversionFunnel + getInboxBenchmarking
- i18n EN + PT

Memórias suporte: feedback_metricas_adocao_canal.md + project_painel_diretoria_roadmap.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 12:44:59 -03:00
Rodribm10
617eadbfe4 feat(reports): breakdown auto/manual no BotReports + nomes corretos
Engrenagem fechada e nomes que casam com o que a métrica mede:

Cards superiores (taxas):
- "Tempo de resolução" -> "Resolvidas pelo bot %"
  Tooltip: bot resolveu sozinho (sem humano via Chatwoot ou WhatsApp) / total
- "Taxa de entrega" -> "Transferidas pra humano %"
  Tooltip: agora soma auto (Jasmine chamou) + manual (humano respondeu sem
  handoff explícito). Junto com a resolução, fecha ~100%.

Cards de detalhe (segunda linha, contagem absoluta):
- "Resolvidas pelo bot" — quantas o bot fechou sozinho
- "Transferência automática (Jasmine)" — bot_handoff explícito (loop, timeout,
  max turns, intent)
- "Tomada manual (agente)" — humano respondeu (UI ou WhatsApp echo) SEM a
  Jasmine ter chamado bot_handoff. Era o "bucket fantasma" antes.

Backend:
- BotMetricsBuilder.metrics inclui bot_resolutions_count, auto_handoffs_count,
  manual_takeovers_count
- handoff_rate agora é (auto + manual) / total — daí a engrenagem fechar
- manual_takeovers_count: conversas com mensagem outgoing humana
  (sender_type='User' OR NULL) MENOS as que tiveram conversation_bot_handoff

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 12:01:50 -03:00
Rodribm10
429567495f fix(reports): inbox_id reativo + nome da inbox visível na tab Novas × Retorno
Bugs originais (tabela Reports → Inbox → unidade específica → Novas × Retorno):
1. Backend recebia sempre inbox_id=1 — useFunctionGetter('inboxes/getInboxById', route.params.id)
   passava string crua, não Ref reativa, então o getter ficava travado no ID inicial
2. UX: tab não mostrava qual unidade estava sendo filtrada

Correções:
- InboxReportsShow.vue: passa inboxIdParam computed pra useFunctionGetter (reativo agora)
- Passa inbox.name como prop pro InboxLeadsReport
- InboxLeadsReport.vue: header com título + label "Caixa de entrada: <nome>" no topo

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 09:14:44 -03:00
Rodribm10
3897db325e feat(reports): aba "Novas × Retorno" no Inbox Report
Mede por inbox/período: leads novos (1ª conversa do contato em qualquer
inbox da rede), retorno (conversa anterior resolved há >24h) e outras
(conversa anterior open ou resolved <24h). Categorias somadas batem com
o conversations_count nativo do report — bucket "outras" garante o
fechamento.

- Novo builder V2::Reports::InboxLeadsSummaryBuilder com CTE única
- Endpoint GET /api/v2/accounts/:id/reports/inbox_leads_summary
- Tabs no InboxReportsShow (Visão Geral | Novas × Retorno)
- Componente InboxLeadsReport com 3 metric cards + barras empilhadas
- API client + Pinia (state/getters/actions/mutations)
- i18n en + pt_BR
- RSpec do builder cobrindo classificação e isolamento por inbox

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:22:43 -03:00
Rodribm10
b69fa21e53 feat(aggressive-alert): filtro por inbox configurável por admin
Admin configura em Account Settings → Agents → Editar quais inboxes
disparam o banner agressivo (reopened + inatividade) pra cada agente.

- user.ui_settings.aggressive_alert_inbox_ids: null (todas) | [] (nenhuma) | [1,2,3]
- Filtro aplicado no actionCable.feedInactivityTracker, maybeTriggerAggressiveAlert
  e no hydrate do AggressiveConversationBanner
- Backend aceita ui_settings no agents#update e serializa em _agent.json.jbuilder
- UI no EditAgent com toggle "todas inboxes" + multiselect

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 11:51:00 -03:00
Rodribm10
4b0e8c314e feat(aggressive-alert): escalada amarelo/laranja/vermelho + toggles
Banner agressivo passa de uma notificação só ("status→open") pra
um sistema de escalada baseado em inatividade quando o cliente é
o último a falar.

Níveis:
- 5 min sem resposta  → AMARELO, sem som
- 15 min sem resposta → LARANJA, beep 1x + notificação do SO
- 28 min sem resposta → VERMELHO pulsante + som em loop infinito
- status→open (reabertura) → VERMELHO imediato

Por conversa, o banner mostra um item com nome do contato, inbox
e contexto ("reabriu agora" / "15 min sem resposta"). Headline
grande e explicação clara sobre como limpa.

Comportamento do × dismiss:
- Antes: apagava o alerta de vez. Agente podia "fingir que viu".
- Agora: esconde temporariamente. Volta quando escalar (próximo
  threshold) ou nova mensagem. A única forma de LIMPAR de vez é
  responder o cliente (tracker detecta msg outgoing do User ou
  AgentBot e chama dismissForReply).

Permissões:
- account.settings.aggressive_alert_enabled (master switch admin)
- user.ui_settings.aggressive_alert_enabled (toggle do próprio agente)
- Default true pros dois; um false em qualquer bloqueia alertas.

Settings UI:
- Conta → General: novo card "Alerta agressivo (master switch)"
- Perfil do usuário: novo card "Receber alertas agressivos"

Arquivos:
- helper/aggressiveAlert.js: multi-level state, hide vs dismiss-for-reply
- helper/inactivityAlertTracker.js: timer único, thresholds declarativos
- helper/actionCable.js: hook em onMessageCreated (feed tracker) +
  isAggressiveAlertEnabled() + limpa tracker em status_changed != open
- components/app/AggressiveConversationBanner.vue: variantes de cor,
  headline grande, explanation, × temp-hide
- account.rb + accounts_controller.rb: store_accessor + permitted
- settings UI components (account + profile): switches auto-persist

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 20:49:24 -03:00
Rodribm10
2b9ada259e feat(dashboard): aggressive alert on conversation reopening
Banner persistente + som em loop + OS notification + title flash
+ vibração mobile quando conversa transiciona pra 'open' vindo de
pending/snoozed/resolved. Exige interação pra dismissar — atendente
não perde evento de reabertura.

- AggressiveConversationBanner.vue: banner full-width no topo,
  dismissable, mostra nome do contato + inbox + status anterior
- aggressiveAlert.js: manager do som (loop infinito), title flash
  (intervalo 1s), Notification API (requireInteraction: true),
  navigator.vibrate (padrão 500-200-500-200-500)
- actionCable.onStatusChange: detecta transição pra 'open' e dispara
  trigger via BUS_EVENTS (só se status anterior ≠ open, pra não
  alertar conversa nova criada já em open)
- i18n pt_BR + en: chaves de notificação (title/body/dismiss)
- busEvents: AGGRESSIVE_ALERT_TRIGGER + AGGRESSIVE_ALERT_DISMISS

Camada 1 da feature. Camada 2 (escalation SMS/WhatsApp se não
dismissar em X segundos) fica pra outro PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 18:03:26 -03:00
Rodribm10
34d42dfbbd feat: remove Notificações Automáticas + ajusta assinatura WhatsApp
1. Captain — remove feature Notificações Automáticas (captain_notification_templates)
   Feature órfã: 0 registros em prod, substituída pela Jornada do Cliente
   (Lifecycle). Sem dependências fora dela própria. Removido:
   - Vue: routes/settings/captain/notifications/ + entry no captain.routes.js
   - Sidebar: item "Notifications" do Captain menu
   - Store: modules captainNotificationTemplates + import no store/index
   - API: api/captain/notificationTemplates.js
   - Controller: api/v1/accounts/captain/notification_templates_controller
   - Model: Captain::NotificationTemplate
   - Job: enterprise/app/jobs/captain/notifications/
   - Routes: resources :notification_templates no config/routes.rb
   - i18n: chaves CAPTAIN_SETTINGS.NOTIFICATIONS + SIDEBAR.CAPTAIN_NOTIFICATIONS
     em pt_BR e en (captain.json + settings.json)

   Tabela captain_notification_templates mantida (0 rows, sem consumidores).
   Se quiser drop, criar migration separada depois.

2. WhatsApp — tira colchetes do prefixo de assinatura
   Era: *[ Jasmine(PrimeAL) ]*\ncontent
   Agora: *Jasmine(PrimeAL)*\ncontent
   Afeta wuzapi_service (outgoing) + incoming_message_wuzapi_service (echo)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 15:34:45 -03:00
Rodribm10
6fa2f621fa feat(retention): UI layer — badge, filters, cohort matrix, KPI dashboard
- RetentionSummaryBadge in the "Previous conversations" sidebar:
  tiered status (First contact / Active / Recurring / Sleeping /
  At risk / Inactive) + counts of interactions, one-shots, Pix.

- Retention tab in Captain Reports: KpiCards, FlowCard, CohortMatrix
  (12x13 heatmap with CSV export).

- Five new filters on the contacts list: recurring, last interaction,
  days since, interactions count, reservations paid.

- Full pt_BR + en i18n under CAPTAIN_REPORTS.RETENTION.*

- Spec for InteractionCalculatorService covering gap behavior,
  one-shot classification, internal-label exclusion, multi-conversation
  grouping across the 30h window.

- Docs: docs/captain-retention-indicators.md with business rules,
  column reference, endpoint shape, and backup SQL queries.
2026-04-22 10:30:19 -03:00
Rodribm10
cfffea9c16 feat(captain): semantic memory fixes + roleta + reclamações + analytics
Consolida o trabalho desta branch de abril/2026 em um bloco pronto pra
testar em staging antes do merge pra main.

## Correções de memória semântica
- ExtractionService: Princípio Zero + Regra de Ouro (ação consumada vs intenção).
- Cenário Daniela_Reservas: Passo 0 de classificação (consulta/intenção/fora).

## Roleta da Sorte (end-to-end)
- Schema Supabase + 7 RPCs atômicas (server-side, idempotentes).
- Services: Offer, Redeem, WeeklyReport.
- Jobs: OfferRouletteJob (hook em ConfirmationService após Pix pago),
  NotifyRevealed + Scheduler de fallback.
- Tool manual GenerateRoletaLinkTool + endpoint público /roleta/notify.
- Dashboard /captain/roleta com Resgate + Relatório + anomaly detection.

## Cenário Reclamacoes_Ouvidoria
- Triagem P1-P4, framework LAST, Three-level listening, Self-check.
- Sem compensação material, detecção de cliente frustrado eleva prioridade.

## Analytics
- Funil de conversão /captain/funnel: 5 etapas via regex, zero LLM.
- Detector de churn via ChurnOutreach* (cron dias úteis 10h-17h BRT).

## Trabalho pré-existente incluído
- Captain Executive Reports (ceo_digest, mattermost_delivery).
- get_reserva_preco_tool, Lifecycle ajustes, Reservations UI polimentos.

## Outros
- .gitignore: patterns pra credenciais.
- Migrations de scenarios idempotentes.
- i18n completa pt_BR+en pra roleta/funnel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 15:36:25 -03:00
Rodribm10
6ecafd30c6 feat(captain-memory): redesign Contact Memories UI with type badges + relative time + fix i18n keys 2026-04-19 07:38:50 -03:00
Rodribm10
b07486c430 feat(captain-memory): wire Contact Memories section into conversation sidebar 2026-04-19 07:30:30 -03:00
Rodribm10
2f7d8edd92 feat(captain-memory): add Contact Memory UI component + API client + i18n
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 01:47:56 -03:00
Gabriel Jablonski
e032fc7774
feat(whatsapp): convert inbox between WhatsApp providers (#268)
* feat(whatsapp): allow converting inbox between WhatsApp providers

Adds a Convert flow to switch a WhatsApp inbox between the four
supported providers (default/360dialog, whatsapp_cloud, baileys, zapi)
without losing conversations, agents, or history.

- Channel::Whatsapp#convert_provider! runs inside a transaction:
  disconnects the old provider, clears provider_connection and
  message_templates, assigns the new provider/config, and triggers
  webhook setup plus template resync on the new service.
- New POST /api/v1/accounts/:id/inboxes/:id/convert_provider endpoint
  guarded by InboxPolicy#convert_provider? (admin only).
- UI adds a Convert button on the inbox Settings page with a
  type-to-confirm ConvertInboxModal that lists the effects before
  redirecting to a dedicated route reusing the WhatsApp provider
  wizard in convert mode (phone number locked, current provider
  hidden from the picker).

* chore(whatsapp): polish convert UI colors and expand specs

- Settings: use slate for the Convert trigger and ruby for the modal
  confirm to mirror the delete gate instead of the less conventional
  amber variant.
- Drop the redundant "current provider is hidden from the list"
  sentence from the convert wizard description.
- Add specs for the post-conversion webhook setup path (triggered and
  skipped branches) and the sync_templates error-rescue behaviour.

* fix: address CodeRabbit review on convert-provider flow

- Whitelist provider_config keys in the convert endpoint via permit
  rather than permit!, and default to an empty hash when omitted so
  the request no longer crashes.
- Pre-validate the new provider config before disconnecting the old
  session so a bad target config no longer terminates the existing
  provider; also keep the disconnect bound to the old provider_url.
- Guard ConvertInboxModal's submit handler so pressing Enter cannot
  bypass the type-to-confirm gate, and migrate it to <script setup>.
- Reject invalid ?provider= query values in convert mode so hidden
  providers (Twilio, the current provider) cannot be reached via URL.
- Await the inbox fetch in InboxConvert before running the route guard
  so directly opening the route for a non-WhatsApp inbox redirects.
- Remove the unreachable second CloudWhatsapp branch in Whatsapp.vue.

* fix: address second CodeRabbit round on convert-provider flow

- Unify provider picker validation so create mode also rejects
  unknown ?provider= values, with a single helper that accepts
  available providers plus the whatsapp_manual fallback.
- Simplify the pre-validation rollback in convert_provider!: the
  errors snapshot/merge dance was redundant because assign_attributes
  does not clear errors.
- Follow the repo convention of asserting on error.class.name so the
  rollback spec stays stable under reloading/parallel environments.
- Strengthen the controller success spec with provider_connection and
  message_templates cleanup invariants, and set Content-Type on the
  templates stub so HTTParty parses the empty data array correctly.

* fix: address third CodeRabbit round on convert-provider flow

- Add 360Dialog entry to the Whatsapp provider catalog, keep it hidden
  from the create picker (preserving the existing fork behavior) but
  expose it in the convert picker where it is a valid target. Restore
  URL reachability for ?provider=360dialog in create mode.
- Scope the WHATSAPP_MANUAL allowance to create mode only: the manual
  fallback flow is not reachable in convert mode.
- Redirect to the inboxes list in InboxConvert when the inbox is still
  absent after the store fetch, so the page no longer stays blank.
- Use an explicit allowlist of WhatsApp providers to gate the Convert
  button instead of negating Twilio, so adding a new WhatsApp channel
  type will not silently expose the flow.
- Bind the disabled provider display field with :value instead of
  v-model, since the underlying computed is getter-only.
- Add Content-Type: application/json to the templates stub in the
  model spec so HTTParty parses the empty data array.

* fix: address fourth CodeRabbit round on convert-provider flow

- Reject no-op conversions that target the same provider as the one
  already configured, so the endpoint no longer wipes provider
  connection and message templates on a request that changes nothing.
- Call the provider service's disconnect directly so failures abort
  the conversion instead of being silently swallowed; otherwise the
  old external session could remain live while the inbox flips to
  the new provider.
- Cover both behaviors with specs.

* fix: address fifth CodeRabbit round on convert-provider flow

- Reset the Vuelidate state when closing ConvertInboxModal so reopening
  the gate does not surface stale validation errors.
- Call teardown_webhooks before converting away from whatsapp_cloud so
  the Meta webhook subscription is removed for embedded_signup channels,
  mirroring the destroy-time cleanup (manual-setup channels keep the
  existing no-op behavior). Swallow teardown failures so a flaky Meta
  call does not abort the swap.
- Switch the rollback specs to compare message_templates counts instead
  of the boolean be_present matcher so they remain meaningful if the
  fixture happens to have an empty templates list.

* fix: address sixth CodeRabbit round on convert-provider flow

- Derive the convert header's current-provider label from the shared
  PROVIDER_CATALOG so the picker and header stay in sync.
- Assert the full Cloud provider_config payload and the absence of the
  Baileys-only provider_url key on both the controller success spec
  and the model atomic-swap spec.
- In the sync-error spec, reload and assert that the record was
  actually flipped to the new provider before the sync rescue fires,
  so the test can't pass on a pre-save failure.

* test: pin 422 error payload on convert_provider negative paths

The unsupported-conversion and invalid-config specs only checked the
status code, so they would have stayed green if the 422 started coming
from a different branch. Pin the response body so each example actually
covers the failure case it names.

* fix(baileys): save custom host as provider_url, not url

The Baileys form was writing the custom endpoint to
provider_config['url'] while the backend reads
provider_config['provider_url']. That silently broke the custom-host
feature for newly created or converted Baileys inboxes: they always
fell back to BAILEYS_PROVIDER_DEFAULT_URL. Align the key on both ends.

* fix(whatsapp): skip second validation pass in convert_provider!

The transaction's save! was re-running validate_provider_config after
the old provider's session had already been disconnected, so a transient
Graph API failure on the second check could roll back the swap while
leaving the external session terminated — the exact inconsistency the
pre-flight valid? was meant to rule out.

Capture the validated provider_config snapshot after valid? (so fields
populated by before_validation callbacks like webhook_verify_token are
preserved) and switch the final persist to save!(validate: false) so the
earlier check stays authoritative.

* fix: normalize provider-conversion failures and pass accountId

- The convert_provider action only rescued ActiveRecord::RecordInvalid,
  so disconnect/teardown failures bubbled up as 500 with no stable
  payload. Catch StandardError, log the class + message, and return a
  422 with a generic user-facing message so the dashboard can surface
  the error consistently.
- Nested settings routes live under /accounts/:accountId, so the
  router push from Settings.vue must include accountId alongside
  inboxId. Mirrors how sibling pages navigate to settings_inbox_show.

* fix: report missing :provider as 400 and sync modal v-model

- The generic rescue StandardError on convert_provider was masking
  ActionController::ParameterMissing behind a misleading
  provider-conversion error message. Catch it explicitly before the
  generic rescue and return 400 with the parameter-missing message.
- ConvertInboxModal's closeModal now drives localShow to false so
  parents using v-model:show stay in sync on every close path,
  not only when the explicit onClose listener flips the flag.

* fix(whatsapp): serialize concurrent convert_provider calls with_lock

Without a per-record lock, two admin requests against the same inbox
could both pass the pre-flight validation, race the disconnect/save,
and then run setup_webhooks/sync_templates in arbitrary order, leaving
the persisted provider out of sync with the external configuration.

Wrap the whole convert flow in with_lock so the loser blocks until the
winner commits; the subsequent no-op guard then rejects a second
conversion request targeting the provider the first one just set.

* test: harden convert_provider policy + controller failure specs

- Pass accountId explicitly in InboxConvert redirects so the route
  navigation mirrors how Settings.vue reaches settings_inbox_convert.
- Add a spec that assigns the agent to the inbox and still expects 401,
  so a future regression in InboxPolicy#convert_provider? can no longer
  slip past on the show policy alone.
- Add a spec that stubs convert_provider! to raise StandardError and
  asserts the controller's generic-failure 422 payload, pinning the
  dashboard contract for provider-side failures.

* test: pin convert_provider success response payload

Parse the rendered body and assert provider + provider_config so the
spec catches regressions where the DB is updated correctly but the
serialized response drifts (dashboard store commits response.data).

* fix(whatsapp): reset teardown guard after pre-conversion webhook cleanup

teardown_webhooks memoizes @webhook_teardown_initiated = true to prevent
double execution during destroy. Calling it from convert_provider!
leaves that flag set, so a subsequent destroy! or follow-up conversion
on the same instance would skip webhook removal silently. Reset the
flag in an ensure block so the destroy-time guard stays scoped to
destroy only.

* fix: include accountId in post-conversion redirect params

* test: pin same-provider convert returns 422

* fix(whatsapp): reset template columns when post-conversion sync fails

* fix(convert): enforce provider allowlist in InboxConvert route guard

* test: broaden Cloud templates stub to match account-scoped path

* test(whatsapp): cover cloud to baileys conversion branch
2026-04-18 20:57:27 -03:00
Gabriel Jablonski
adc0d892e0
feat(internal-chat): support paste and drag-drop for attachments (#269) 2026-04-18 10:48:50 -03:00
gabrieljablonski
112385fd9e Merge branch 'main' into chore/merge-4.13.0
Resolves 26 conflicts via manual review. Key decisions:

- signature: kept fork's send-time architecture (PR #79), discarded upstream's
  editor-manipulation functions
- WhatsApp incoming: combined fork's two-layer locking (source_id + contact
  phone) with upstream's blocked-contact drop. Fixed pre-existing regression
  where echoes were silently dropped
- InstallationConfig: upstream's simplified coder (validated against legacy
  YAML-in-jsonb data)
- schema.rb: regenerated, stripped kanban tables from other branches,
  restored f_unaccent SQL function
2026-04-17 16:23:47 -03:00
Sojan Jose
135be52431
feat: Introduce last responding agent option to automation assign agent (#12326)
Introduce a `Last Responding Agent` options to assign_agents action in
automations to cover the following use cases.

- Assign conversations to first responding agent : ( automation message
created at , if assignee is nil, assign last responding agent )
- Ensure conversations are not resolved with out an assignee : (
automation conversation resolved at : if assignee is nil, assign last
responding agent )

and potential other cases.

fixes: #1592
2026-04-16 18:54:35 +05:30
Captain
03c10ba147
chore: Update translations (#14080)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-04-16 18:12:33 +05:30
Sojan Jose
aee979ee0b
fix: add explicit remove assignment actions to macros and automations (#12172)
This updates macros and automations so agents can explicitly remove
assigned agents or teams, while keeping the existing `Assign -> None`
flow working for backward compatibility.

Fixes: #7551
Closes: #7551

## Why
The original macro change exposed unassignment only through `Assign ->
None`, which made macros behave differently from automations and left
the explicit remove actions inconsistent across the product. This keeps
the lower-risk compatibility path and adds the explicit remove actions
requested in review.

## What this change does
- Adds `Remove Assigned Agent` and `Remove Assigned Team` as explicit
actions in macros.
- Adds the same explicit remove actions in automations.
- Keeps `Assign Agent -> None` and `Assign Team -> None` working for
existing behavior and stored payloads.
- Preserves backward compatibility for existing macro and automation
execution payloads.
- Downmerges the latest `develop` and resolves the conflicts while
keeping both the new remove actions and current `develop` behavior.

## Validation
- Verified both remove actions are available and selectable in the macro
editor.
- Verified both remove actions are available and selectable in the
automation builder.
- Applied a disposable macro with `Remove Assigned Agent` and `Remove
Assigned Team` on a real conversation and confirmed both fields were
cleared.
- Applied a disposable macro with `Assign Agent -> None` and `Assign
Team -> None` on a real conversation and confirmed both fields were
still cleared.
2026-04-16 15:57:41 +05:30
Sivin Varghese
5eee331da3
feat: add slash command menu to article editor (#14035) 2026-04-16 11:27:59 +05:30
Rodribm10
2e9551a0f3 feat(lifecycle): rules tab with templates, wizard and variable autocomplete
Implements Task 15: Rules.vue (template grid + rules table), RuleWizardDialog.vue
(4-step wizard: Quando/Pra quem/O quê/Revisão) and MessageEditor.vue (textarea with
{{ variable }} autocomplete). Adds WIZARD.CANCEL, OFFSET_UNIT_LABEL, STEP_LABELS and
REVIEW i18n keys in en and pt_BR.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 11:15:00 -03:00
Rodribm10
94fdb5c318 feat(lifecycle): settings tab with guards form and concierge per unit
Replaces stub Settings.vue with full implementation: anti-spam guard
form (quiet hours, interval, pause-on-reply, opt-out label) and a
collapsible ConciergeUnitCard per unit (inbox selector, persona name,
knowledge base, key-value variables). Adds CONCIERGE_CONFIGURED /
CONCIERGE_NOT_CONFIGURED i18n keys to en + pt_BR.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 11:05:40 -03:00
Rodribm10
ae4647d1c2 feat(lifecycle): history tab with paginated list and preview modal
Implements Task 13 — replaces the stub History.vue with a real paginated
table filtered by status, and adds DeliveryPreviewModal to show rendered_body.
Also extends i18n keys (TOTAL, PAGINATION, MODAL labels) in en + pt_BR.
2026-04-15 10:57:56 -03:00
Rodribm10
1459655243 feat(lifecycle): i18n keys for Jornada do Cliente UI
Adds CAPTAIN_LIFECYCLE block (en + pt_BR) to captain.json with full
key set for Rules, Wizard, Settings, History and sidebar entry.

Also stages pre-existing uncommitted additions to captain.json from
prior work (KPI, PILLS, QUICK_DATE, CARD, ACTIONS extras for
CAPTAIN_RESERVATIONS) — those were already in the working tree and
belong to the same feature branch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 10:39:50 -03:00
Sivin Varghese
64f6bfc811
feat: Inline edit support for contact info (#13976)
# Pull Request Template

## Description

This PR adds inline editing support for contact name, phone number,
email, and company fields in the conversation contact sidebar

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

**Screencast**


https://github.com/user-attachments/assets/e9f8e37d-145b-4736-b27a-eb9ea66847bd



## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Pranav <pranav@chatwoot.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-04-14 16:53:40 +04:00
Gabriel Jablonski
11e9932e9b
feat(whatsapp): show contact typing and recording indicators via baileys presence (#264)
* feat(whatsapp): show contact typing and recording indicators via baileys presence

Subscribe to WhatsApp presence updates via the baileys-api provider to
display real-time typing and recording indicators in the dashboard.

- Handle presence.update webhook events (composing, recording, paused,
  available) and broadcast via ActionCable
- Add conversation.recording event to ActionCable, webhook, and channel
  listeners for parity with typing_on/typing_off
- Show "typing..." / "recording..." in green text on the chat list,
  replacing the message preview
- Show "X is typing" / "X is recording audio" in the conversation view
- Add presence_subscribe provider config option (default off) to gate
  all subscription calls to the baileys-api
- Subscribe to presence on conversation open and periodically (1 min)
  for the top 10 chat list conversations
- Consolidate contact LID from presence.update jidAlt payload
- Prevent echo-back of contact typing events to the channel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address review feedback

- Filter chat list typing indicator to contact-only events
- Add dedupe to presence subscribe bulk calls
- Use strong parameters for conversation_ids
- Remove redundant YAML quotes in swagger webhook enum

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address review feedback

- Extract phone from data[:id] when JID is @s.whatsapp.net (fallback
  when jidAlt is absent)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address review feedback

- Filter recording users in getTypingUsersText to show correct names
- Add 10s timeout to presence_subscribe HTTP request

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: scope typing timer per user instead of per conversation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: make presence subscribe best-effort with rescue per channel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address review feedback

- Add messagePreviewClass to typing preview for consistent padding
- Fix specs to use WebMock assertions instead of instance spying

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 11:38:11 -03:00
Sojan Jose
45b6ea6b3f
feat: add automation condition to filter private notes (#12102)
## Summary

Adds a new automation condition to filter private notes.

This allows automation rules to explicitly include or exclude private
notes instead of relying on implicit behavior.

Fixes: #11208 

## Preview



https://github.com/user-attachments/assets/c40f6910-7bbf-4e59-aae5-ad408602927a
2026-04-13 10:40:46 +05:30
Gabriel Jablonski
3aca86aa43
feat(internal-chat): implement internal chat system for agents (#247)
* feat(internal-chat): implement internal chat system for agents (Phase 1+2 MVP)

Add a Slack/Discord-style internal messaging system for Chatwoot agents with
text channels (public/private), direct messages, reactions, typing indicators,
and real-time updates via ActionCable.

Backend:
- 6 database migrations (categories, channels, members, messages, attachments, reactions)
- 6 models under InternalChat:: namespace with validations and associations
- API controllers for categories, channels, messages, members, and reactions
- Pundit policies for authorization (public/private/DM access control)
- MessageCreateService, TypingStatusManager, DefaultChannelSetupService
- InternalChatListener for real-time broadcasting to channel members
- Event types for internal chat events
- Default category/channel setup for new and existing accounts

Frontend:
- Vuex store modules for channels, messages, and typing status
- API clients for channels and messages
- Vue 3 components: InternalChatLayout, ChannelSidebar, ChannelView,
  ChannelHeader, MessageList, MessageBubble, MessageEditor,
  EmojiReactionPicker, ReactionDisplay, TypingIndicator
- Sidebar integration with "Internal Chat" menu item
- ActionCable handlers for real-time message/reaction/typing events
- Route definitions and i18n translations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test(internal-chat): add comprehensive specs for models, controllers, policies, services, and listener

- 6 model specs (74 examples) covering associations, validations, scopes, methods
- 5 request specs for all API controllers (categories, channels, messages, members, reactions)
- 4 policy specs testing authorization rules for all actions
- 3 service specs (DefaultChannelSetupService, MessageCreateService, TypingStatusManager)
- 1 listener spec testing real-time broadcasting for all event types
- 6 FactoryBot factories with traits for all InternalChat models

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): fix dispatcher mock in service specs and cursor pagination test

- Allow dispatcher.dispatch in service specs to handle Account.created
  callbacks from factory setup before asserting specific event dispatch
- Fix after-cursor pagination test by adding 1 second offset to avoid
  timestamp precision issues with iso8601 rounding

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): address CodeRabbit review — 7 critical security/correctness fixes

- Scope member creation through Current.account.users to prevent cross-account membership
- Scope member_ids in DM creation through Current.account to prevent cross-account invites
- Scope reaction message lookup through channel account to prevent cross-account access
- Fix Vuex store to commit messages array instead of response envelope
- Add UUID generation callback on Channel model (before_validation)
- Add channel access check to reaction deletion policy
- Validate parent_id belongs to same channel in MessageCreateService

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): address CodeRabbit round 2 + fix ChannelSidebar runtime error

- Re-throw error in fetchMessages instead of swallowing with empty array
- Wrap message + attachment creation in transaction for atomicity
- Fix factory to derive account from message (prevent cross-account fixtures)
- Guard listener against cross-account mismatch (not just missing records)
- Add cross-account regression tests to listener spec
- Fix ChannelSidebar computed properties to default to empty arrays

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(internal-chat): auto-setup default channels on account creation and migration

- Add after_create_commit :setup_internal_chat callback on Account model
- Add data migration to create default channels for existing accounts
- Make DefaultChannelSetupService convergent (find_or_create) instead of
  bail-on-exists, so it can sync new members on subsequent runs
- Fix specs to handle default category/channel created by callback

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): avoid Vuex state mutation in sort + align muted styling in fallback section

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): fix store module name mismatch — register as 'internalChat' not 'internalChatChannels'

Components dispatch to 'internalChat/get' but the module was registered
as 'internalChatChannels'. Also fix ActionCable handlers to use
'internalChat/messages/' nested module path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* i18n(internal-chat): add pt-BR translations for internal chat feature

Backend: default_category_name ('Canais') and default_channel_name ('Geral')
Frontend: all 40+ keys translated to Brazilian Portuguese

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): handle ISO 8601 timestamps in MessageBubble and MessageList

The API returns created_at as ISO strings but messageTimestamp() expects
Unix seconds and MessageList used `* 1000`. Now handles both formats.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(internal-chat): build swagger output for internal chat API endpoints

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(internal-chat): register internal chat tags and paths in swagger index

Add tag definitions and path entries for all 5 internal chat resource
groups in swagger/index.yml and swagger/paths/index.yml. Rebuild output.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* i18n(internal-chat): add SIDEBAR.INTERNAL_CHAT key to pt-BR settings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): comprehensive review fixes — backend and frontend

Backend:
- Add attachments to message API responses in both controllers
- Add internal_chat_channel_updated listener handler
- Include reactions in message event broadcast data

Frontend:
- Fix ActionCable dispatch paths to use correct action names
  (addMessageFromCable, updateMessageFromCable, deleteMessageFromCable)
- Connect typingUsers to internalChatTypingStatus store getter
- Fix message field references (edited → content_attributes.edited_at)
- Fix channel type comparisons (use 'private_channel'/'dm' strings)
- Add parent 'internal_chat' to sidebar activeOn array
- Increment unread_count on ActionCable message receive
- Add loadMore handler for cursor-based pagination
- Remove unused is-direct-message prop from InternalChatLayout

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(internal-chat): implement phases 3-5 — threads, mentions, notifications, polls, drafts

Phase 3 — Threads, Mentions, Notifications:
- MentionService: parse @user mentions, @all (admin only), generate notifications
- NotificationService: create notifications for channel messages (respects mute)
- Add internal_chat_message/mention notification types to Notification model
- ThreadPanel.vue: slide-out panel for threaded replies
- Integrate mentions + notifications into MessageCreateService

Phase 4 — Polls:
- 3 new migrations: polls, poll_options, poll_votes tables
- 3 new models: Poll, PollOption, PollVote with validations
- PollsController: create poll, vote, unvote with routes
- PollService: voting logic with multiple choice + revote support
- PollCreator.vue: modal for creating polls with options
- PollDisplay.vue: vote UI with progress bars and results
- Polls Vuex store module
- INTERNAL_CHAT_POLL_VOTED event type

Phase 5 — Drafts:
- 1 new migration: drafts table
- Draft model with validations
- DraftsController: full CRUD (replace stub)
- DraftsList.vue: list all user drafts with navigation
- Drafts Vuex store module with auto-save
- Draft route and sidebar integration

Phase 6 — Feature Flag:
- Add INTERNAL_CHAT feature flag to features.yml and featureFlags.js

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): fix API routing for drafts and polls, add poll voting ActionCable handler

- Fix draft API client to use channel-scoped PATCH/DELETE endpoints
- Create dedicated polls API client with correct poll-based endpoints
- Update polls store to use InternalChatPollsAPI with pollId-based voting
- Add ActionCable handler for internal_chat.poll.voted events
- Add thread and drafts routes to sidebar activeOn array
- Fix drafts store to pass channelId to API calls

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): fix poll response format and API client routing (review round 2)

- Return message with embedded poll data instead of raw poll response
- Add poll data to message_response in messages controller
- Create dedicated InternalChatPollsAPI client with correct endpoints
- Update PollDisplay.vue to read from message.poll or content_attributes.poll
- Use option.voted flag instead of checking voters array
- Add missing PERCENTAGE i18n key to pt-BR
- Remove unused currentUserId prop from PollDisplay

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): fix poll voting and draft lookup bugs (review round 3)

- Fix draft getter to use internal_chat_channel_id field name
- Split poll set_poll into vote/unvote variants — unvote doesn't need option_id
- Unvote finds user's vote by user_id across all poll options
- Fix ChannelView to extract pollId from message.poll before dispatching
- Fix unvote handler to not require optionId

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review feedback (round 3)

- Expose is_dm, favorited, muted on channel API responses
- Normalize poll cable updates into message-shaped patch
- Add file presence validation to MessageAttachment
- Remove duplicate mention notifications from MentionService
- Make data migration rollback safe (IrreversibleMigration)
- Update factory to include file by default

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: return 201 for channel creation and optimize DM lookup

- Return :created (201) instead of :ok (200) for channel creation
- Replace O(n) Ruby scan with SQL-based DM lookup using ARRAY_AGG

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review feedback (round 5)

- Broadcast channel event after create for real-time notifications
- Separate create/update strong params to prevent channel_type transitions
- Use strong params for typing_status input
- Replace find_by with detect on preloaded collections to fix N+1
- Preload attachments with blobs in show response

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review feedback (round 6)

- Serialize DM creation with advisory lock to prevent duplicates
- Broadcast channel deletion event for real-time UI updates
- Use strong params for mark_unread message_id
- Batch unread count computation to eliminate N+1 in index

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: eliminate N+1 in compute_unread_counts with single JOIN query

Replace per-membership COUNT loop with a single JOIN + GROUP BY query
that returns all unread counts in one database call.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): quality fixes, missing tests, and Playwright E2E setup

Addresses quality issues found during review and fills test coverage gaps
for the internal chat feature.

Backend fixes:
- Return 201 for all create endpoints (messages, categories, polls, reactions, members)
- Fix N+1 queries: replies.size, poll votes, category channels.count, votes.exists?
- Fix pagination has_more logic to check page size instead of total count
- Scope poll vote/unvote to current account (security fix)
- Add internal_chat.messages.deleted i18n key
- Use find_by! in mark_unread for proper 404 on non-members
- Guard time param parsing with rescue ArgumentError
- Align message response format between channels and messages controllers
- Switch notification service to ActionCable-only (avoid push/email crashes)

Frontend fixes:
- Fix pinned message detection (content_attributes.pinned, not message.pinned)
- Fix thread reply count key (replies_count, not thread_replies_count)
- Fix markUnread to pass message_id parameter
- Fix pagination: PREPEND_MESSAGES mutation instead of overwriting
- Fix typing status to read Vuex reactive state, not stale closure
- Fix deleteDraft argument shape (pass { channelId, draftId })
- Fix DM channel filtering (check both is_dm and channel_type)
- Fix DraftsList navigation to use correct channel ID key
- Wire PollCreator to poll button in MessageEditor
- Wire settings event handler on ChannelHeader
- Reset PollCreator isSubmitting on timeout

New RSpec tests (67 examples):
- Factories: polls, poll_options, poll_votes, drafts
- Model specs: Poll, PollOption, PollVote, Draft
- Controller specs: PollsController, DraftsController
- Service specs: PollService, NotificationService, MentionService

Playwright E2E setup (37 tests):
- Install Playwright with Chromium
- Auth helper with Devise Token Auth login flow
- 8 test suites: navigation, channels, messaging, DMs, reactions, threads, polls, mark-read-unread

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review feedback (round 7)

Backend:
- Use lambda for UUID default in channels migration
- Wrap poll creation in transaction for atomicity
- Preload replies in thread action to avoid N+1
- Broadcast replies_count + attachments in listener (match REST shape)
- Scope draft listing through accessible channels
- Key draft upserts/deletes by parent_id for thread drafts

Frontend:
- Remove duplicate poll methods from internalChatMessages.js (use internalChatPolls.js)
- Persist toggleMute/toggleFavorite to backend via updateMember API
- Clear active channel on DELETE_CHANNEL mutation
- Skip unread increment for active channel in ActionCable handler
- Filter archived channels from sidebar getters
- Fix ChannelHeader isArchived to check status === 'archived'
- Prevent duplicate reactions in ADD_REACTION mutation
- Merge poll data into existing content_attributes on cable updates
- Guard infinite scroll against duplicate loads
- Add response.ok() check in E2E auth helper, remove hardcoded account ID

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review feedback (round 8)

- Remove unused nested typingStatus module from internalChat store
- Add parent_id to draft uniqueness scope and migration index
- Exclude reaction creator from reaction_created broadcast
- Preload attachments and poll associations in thread/messages queries
- Handle `after` fetches with APPEND_MESSAGES mutation
- Wrap channel creation payloads under `channel` key in E2E helpers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: rewrite Playwright E2E tests to use actual UI interactions

Completely rewrote all 8 E2E test suites to work with the live app:
- Test through actual UI interactions, not API bypass
- Use correct Portuguese (pt_BR) locale strings
- Use structural selectors matching real Vue component DOM
- Dynamic account ID from login response (no hardcoded values)
- 3 parallel workers, increased timeouts for reliability
- API calls only for preconditions (seeding test data)

29 tests passing across navigation, channels, messaging, DMs,
reactions, threads, polls, and mark-read-unread suites.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use partial unique indexes for draft uniqueness with NULL parent_id

PostgreSQL treats NULL as distinct in unique constraints, so a composite
index on (user_id, channel_id, parent_id) allows duplicate root drafts.
Split into two partial indexes: one for root drafts (WHERE parent_id IS
NULL) and one for thread drafts (WHERE parent_id IS NOT NULL).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review feedback (round 9)

- Remove duplicate index on internal_chat_polls.internal_chat_message_id
  (keep only unique index)
- Add options validation in polls create (return 400 instead of 500)
- Add expiration check to unvote action (match vote behavior)
- Use strong params in messages update action

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review feedback (round 10)

- Change channel associations from destroy_async to destroy (FK
  constraints are ON DELETE RESTRICT, blocking async deletion)
- Remove unused internal_chat notification types and PRIMARY_ACTORS
  entry (notification service uses ActionCable only, no DB records)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review feedback (round 11)

- Scope category_id to current account in channels controller (security)
- Defer message-created event in poll creation until after transaction
- Change message associations from destroy_async to destroy (FK compat)
- Validate option belongs to poll in poll_service
- Use strong params for emoji in reactions controller

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review feedback (round 12)

Backend (9 fixes):
- Gate message update/destroy by channel accessibility in policy
- Guard content_attributes nil before merge in polls controller
- Fix after-cursor pagination to use limit() instead of last()
- Wrap revote in transaction for atomicity in poll service
- Make unvote option-specific for multi-choice polls
- Exclude own messages from unread count
- Make channel activity update monotonic (only write if newer)
- Include actor in message/reaction broadcasts (multi-tab support)
- Return 400 for empty member create instead of 201

Frontend (8 fixes):
- Show uncategorized channels even when categories exist
- Clear editor on channel switch when no draft exists
- Soft-delete messages in store (update in place, don't remove)
- Guard ThreadPanel against out-of-order fetch responses
- Replace hardcoded channel label with i18n key in DraftsList
- Add accessible name to settings button in ChannelHeader
- Add aria-label to search field in ChannelSidebar
- Make MessageBubble actions keyboard-accessible via focus-within

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review feedback (round 13)

- Fix keyword argument mismatch in reactions dispatch_reaction_event
- Add user_id to reaction cable broadcast for shape consistency with REST

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): quality fixes, expanded RSpec + Playwright E2E tests

Fix isArchived computed (checked .archived instead of .status), fix
ReactionDisplay user identification (.user?.id vs .user_id), update
17 spec assertions from :success to :created on create endpoints,
add 32 new RSpec examples (polls, drafts, services), and rewrite
8 Playwright E2E test files with correct selectors, proper test
isolation, and dynamic user ID discovery.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review feedback (round 14)

Prevent duplicate votes on same option in multi-choice polls with
explicit BadRequest guard. Add internal_chat webhook events to
ALLOWED_WEBHOOK_EVENTS so users can subscribe to them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: include poll data in ActionCable broadcast for poll messages

Extract base_message_data helper and enrich message_event_data with
poll options when the message has an associated poll, ensuring
realtime subscribers receive the same poll data as REST API clients.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review feedback (round 15)

Backend: wrap single-choice revotes in transaction, capture member
tokens before channel destroy, exclude own messages from unread count,
strip attachments from deleted messages, enrich poll broadcast payload.

Frontend: use getCurrentRole getter, fix public-results poll display,
sync thread replies via store, add close button a11y, pass option_id
to unvote API, pass parent_id to deleteDraft API.

Models: handle nil last_read_at for new members, skip content
validation for attachment-only messages, align PollService guards
with controller, change category dependent to nullify.

Swagger: add attachments to message schema, fix create status to 201.
E2E: remove fragile waitForTimeout.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): fix 21 UX/functional issues

Address 21 UX gaps discovered during product testing:

Sidebar & Navigation:
- Fix search icon overlap, extend search to description + DM members
- Add create channel/DM/category buttons and modals
- Show DM member names instead of null
- Include members data in channel index API for DMs

Message Interactions:
- Add delete confirmation dialog
- Implement inline message editing with cancel support
- Toggle emoji reactions (add/remove)
- Support multiple pinned messages with click-to-scroll
- Prevent thread replies from appearing in main chat
- Fix reply count live updates
- Hide pin button on thread messages
- Improve deleted message styling with greyed-out card
- Replace spinner with skeleton loading
- Add markdown toolbar (bold/italic/code)
- Fix thread editing and add vote/unvote handlers

Features & Polish:
- Implement channel settings slide-over panel
- Fix thread loading not affecting main channel spinner
- Fix poll creation field name mismatch with backend API
- Fix drafts: show channel names, handle DM navigation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): use Dialog modal for delete confirmation

Replace window.confirm with the project's Dialog component for
message delete confirmation, providing a consistent UI experience.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review feedback (round 16)

- Require content field in message update OpenAPI schema

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review feedback (round 17)

- Sanitize advisory lock SQL with sanitize_sql_array
- Use semantic button for pinned message banner
- Add aria-label to ChannelSettings close button
- Add type="button" to all ChannelSettings buttons
- Gate channel/DM/category creation to admins
- Replace hardcoded 'Direct Message' with i18n key

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review feedback (round 18)

- Wrap DM creation payload in channel key for consistency
- Replace raw text in category select with i18n key
- Add IME composition guard to prevent premature send

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(internal-chat): UX round 2, rich editor, team members, drag-and-drop

- Reduce sidebar spacing between search bar and drafts
- Fix search icon overlapping placeholder text
- Replace inline category form with Dialog modal
- Add collapsible sidebar sections with localStorage persistence
- Add drag-and-drop channels across categories (admin-only, vuedraggable)
- Replace textarea editor with WootWriter ProseMirror rich text editor
- Replace regex markdown rendering with shared MessageFormatter
- Wire draft auto-save pipeline with WootWriter (3s debounce watcher)
- Add team + agent selection when creating private channels
- Auto-add all agents when creating public channels
- Sync team members to linked channels via TeamMember callback
- Fix member list not loading on first settings panel open
- Complete PT-BR translations for all internal chat strings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(internal-chat): UX round 3, Enter-to-send, mentions, copy link, poll modal

- Send with Enter (not Cmd+Enter), Shift+Enter for newlines
- Enable @mentions via WootWriter suggestions plugin
- Refocus editor after sending a message
- Copy link to message button in hover toolbar
- Poll creator refactored to Dialog with confirm-discard on close
- Channel type uses Switch instead of dropdown
- Category uses components-next Select instead of native select
- Skeleton loading: only on initial load, spinner for pagination
- Scroll position preserved when loading older messages
- Mute/Favorite buttons fixed (store members updated after fetch)
- Add/remove channel members after creation (admin-only)
- Save draft immediately when switching channels
- Settings sidebar remembers open/closed state via localStorage
- Search icon overlap fixed (increased padding)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): DM settings, copy updates, input refocus, member UX

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): member edit for private only, emoji overflow, reaction tooltips

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): thread count sync, scroll loading, copy link, thread/settings exclusivity

- Fix thread reply count doubling (remove duplicate INCREMENT_REPLY_COUNT from sendThreadReply, cable handles it)
- Fix copy link button (use window.location.origin + pathname as fallback)
- Hide poll button in thread editor
- Add "Also send in #channel" checkbox for thread replies
- Increase scroll threshold for loading older messages (100px instead of 0)
- Track and stop loading when oldest message reached
- Thread and settings panels are mutually exclusive
- Refocus editor after send with delayed focus

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): scroll to linked message via ?messageId= query param

Read messageId from route query on mount, scroll to and highlight the
target message after messages load, then clean the query param.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): prevent editor from becoming unfocusable after send

Root cause: passing disabled prop to WootWriter applies pointer-events-none
and ProseMirror does not re-enable contenteditable when disabled returns to
false. Fix: never disable the WootWriter, use a local isSending guard to
prevent double-sends. Refocus 300ms after send for ProseMirror state reload.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): simplify send guard, no artificial timeout

Content is cleared immediately before emit, so canSend naturally
returns false (empty content). No isSending guard needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): align poll option remove buttons vertically

Increase padding to p-1.5 and add flex-shrink-0 for consistent sizing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): close poll modal after creating poll

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): poll option X button alignment, discard modal on submit

- Button uses explicit 34px height matching input, no items-center
- Reset form before closing dialog so hasUnsavedChanges is false

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): close settings sidebar when clicking reply

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): update poll UI after voting, fix re-vote error

Vote/unvote actions now dispatch updateMessageFromCable with the API
response to update poll state locally. Pass channelId to enable this.
Clicking an already-voted option correctly triggers unvote.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): include poll data in message response, add timer and voters

Backend: message_response now includes poll data (options, votes, voted
status, voters for public polls) via eager-loaded poll association.
This fixes polls not rendering after page reload.

Frontend PollDisplay:
- Countdown timer showing time remaining until poll closes
- Read-only state when expired (div instead of button, no hover)
- Voter names shown below each option (public polls or admin)
- Prefer content_attributes.poll over message.poll for fresh data

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): include channel_id in poll voted cable broadcast

The poll_event_data was missing internal_chat_channel_id, so the
frontend cable handler could not route the update to the correct
channel's message store.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): poll vote highlight, typing off, reaction broadcast, translations

- Preserve per-user voted flags when merging cable poll broadcast
- Send typing_off after 3s of no typing activity
- Include internal_chat_channel_id in reaction event broadcasts
- Fix reaction deleted handler to also check channel_id field
- Simplify "also send in" copy (works for both channels and DMs)
- Add PT-BR translation for ALSO_SEND_IN_CHANNEL

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(internal-chat): unified reaction popover with all emoji groups

Clicking any reaction badge opens a single popover showing all reactions
grouped by emoji with user names. Current user can remove their own
reaction via X button. Replaces per-reaction popover with unified view.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): wire close DM button to archive and navigate home

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(internal-chat): close DM via hidden flag on channel membership

Add hidden boolean to channel_members table. Close DM sets hidden=true
via member update API. Sidebar filters out hidden DMs. New messages on
a DM channel automatically unhide all members via listener callback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): reaction popover, user names, file upload support

- Include user name in reaction API response (was missing)
- Redesign reaction popover: flat list with emoji + name per row,
  aligned X button for removing own reactions
- Add file upload: paperclip button opens file picker, attached files
  shown as chips with remove, sent via FormData with message
- Store action and API client support files parameter

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): reaction user names, unreact button, attachment rendering

- Include user name in reactions across all endpoints (messages_controller,
  listener base_message_data)
- Make unreact X button always visible (bg-n-alpha-2 background)
- Render message attachments as downloadable links with paperclip icon

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(internal-chat): image preview for attachments in messages and editor

Messages: images render inline with max-h-60, non-images as download links.
Editor: image files show thumbnail preview, non-images show file icon + name.
Remove button as floating circle on top-right corner of each attachment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): attachment preview matches conversation pattern

- File previews show name + size (e.g. "2 MB") in a horizontal card
- Image thumbnails as 32px squares, non-images show document emoji
- Remove button is a visible X icon (not a floating circle)
- Layout matches AttachmentsPreview from conversation ReplyBox

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): auto-detect image file type from MIME on upload

MessageCreateService now detects file type from content_type instead of
defaulting to :file. Images are correctly tagged as :image so they
render inline in message bubbles.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): include file_url in cable broadcast, fix filename display

Listener attachment_event_data now includes file_url so attachments
render correctly on real-time messages without page refresh.
MessageBubble extracts filename from URL or falls back to file_type+ext.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): pin attachments, edit members modal, settings persistence

- Skip content validation when pinning/unpinning (fixes pin on file-only messages)
- Add EditMembersModal with search, add, and remove members for private channels
- Fix settings sidebar always opening: @close now calls handleToggleSettings
  which updates localStorage, not just sets ref to false

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): fix X buttons for attachment remove and reaction unreact

Replace Icon component with inline SVG cross for reliable rendering.
Both attachment remove and reaction unreact buttons now show a visible
X icon at all times with proper vertical alignment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): allow any user to pin messages, not just sender/admin

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): restore Icon component for X buttons (size-4 in size-6 container)

SVG inline approach didn't render. Reverted to Icon i-lucide-x with
larger sizes (size-4 icon in size-6 button) which renders reliably.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): use p-1 + size-4 pattern for X buttons (matches message toolbar)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(internal-chat): thread indicator on messages from threads, allow pinning all

- Show "Thread" badge with icon on messages that have parent_id,
  clicking it opens the parent thread
- Remove parent_id restriction from canPin, any non-deleted message
  can be pinned

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): thread indicator, poll close loop, thread navigation

- Hide thread indicator inside thread panel (inThread prop)
- Open parent thread when clicking thread badge on messages with parent_id
- Fix PollCreator infinite close loop (handleClose no longer calls
  dialogRef.close, since Dialog already triggered the close)
- Look up parent message in store when opening thread from indicator

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): poll duration translations and clickable switch labels

- Duration options use i18n keys (EN + PT-BR: 24 horas, 7 dias, etc.)
- Multiple choice and Public results switches toggle by clicking label

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(internal_chat): enhance message handling and search functionality

- Added broadcasting of typing off events in InternalChatListener.
- Included member user IDs in channel data for better context.
- Updated message model to allow optional sender association.
- Implemented team mention expansion in MentionService to include team members.
- Enhanced message creation service to store mentioned user IDs in content attributes.
- Introduced a new SearchService for searching channels, DMs, and messages.
- Updated API responses to include has_unread_mention flag for channels.
- Added tests for user deletion behavior in internal chat, ensuring message preservation and reaction handling.
- Improved draft model to allow coexistence of root and thread drafts.
- Added unique indexes for drafts to prevent duplicate entries.
- Implemented foreign key constraints with appropriate delete behaviors for internal chat models.

* feat(internal-chat): swagger docs, webhook events, search UX improvements

Add Swagger documentation for drafts, polls, and search endpoints.
Wire internal_chat_message_deleted and internal_chat_channel_updated
webhook events to the UI and listener. Improve search empty state with
min-chars hint and friendly no-results message. Update CLAUDE.md to
include pt_BR translations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(internal-chat): add draft count display in channel sidebar

* chore: remove playwright config and dependencies

* feat(internal-chat): polish UX, swagger updates, and migration consolidation

- Editor toolbar shortcuts (@ and #) with instant popover trigger,
  including accent-insensitive matching and wider conversation popover
- Localized last activity time and inbox name on conversation preview cards
- Thread + main interplay: also-send-in-channel mirror, parent_id filter,
  per-message conversation link, hidden buttons inside thread view,
  reactions update across both lists, scroll-to-message behavior
- Search service uses f_unaccent for messages, channel names and user names
  via dedicated GIN trigram functional indexes
- Renamed InternalChat::ProGating to InternalChat::Limits with neutral
  semantics
- Consolidated 17 internal chat migrations into 3 (tables, default channels,
  unaccent search) and added a rake task to ensure the f_unaccent function
  exists before db:schema:load
- Swagger paths and definitions updated to match the current state of the
  feature (also_send_in_channel, status codes, pro-required responses,
  hidden member flag, search meta fields, etc.)

* fix(internal-chat): use Rake task augmentation for db:schema:load hook

The previous `Rake::Task['db:schema:load'].enhance(...)` guarded by
`task_defined?` silently no-op'd in CI when the rake file loaded before
ActiveRecord's rake_tasks block ran. Re-opening `db:schema:load` via
Rake's `task name => deps` DSL augments the existing task regardless of
load order, ensuring the f_unaccent function is created before schema.rb
references it.

* fix(internal-chat): enhance db:schema:load from Rakefile after load_tasks

Adding the prereq inside lib/tasks/internal_chat_search.rake (via either
`Rake::Task#enhance` or task DSL augmentation) was being silently dropped
in CI, presumably due to load order between application rake files and
ActiveRecord's `rake_tasks` block. Moving the `enhance` to the Rakefile
itself, after `Rails.application.load_tasks`, guarantees both
`db:schema:load` and `db:internal_chat:ensure_search_functions` are
defined before the prereq is added.

Also leaves a debug `puts` in the task body so future regressions are
visible from CI logs.

* chore(internal-chat): add diagnostic logging to f_unaccent hook

* fix(internal-chat): install f_unaccent on all envs iterated by db:schema:load

Rails' `db:schema:load` in development env iterates over BOTH the
development and test databases (see
`ActiveRecord::Tasks::DatabaseTasks.each_current_environment`), but our
hook was only installing the function on the currently-connected
database. CI defaults to development env (no `RAILS_ENV` set), so the
function landed on `chatwoot_dev` while `chatwoot_test` remained
without it, causing the schema load to fail when creating the functional
indexes against the test DB.

The hook now mirrors the same iteration logic and installs the function
on every relevant config, restoring the original AR connection
afterwards.

* fix(internal-chat): align listener spec with current broadcast payload

- internal_chat_message_created now emits two broadcasts (the message
  itself plus an automatic typing_off), so the spec switches to
  `allow`/`have_received` to assert the message broadcast without caring
  about the additional typing_off call.
- internal_chat_reaction_created payload uses `message_id`, not
  `internal_chat_message_id`. Update the spec expectation to match.

* chore(internal-chat): remove redundant DSL augmentation in rake task

* fix(internal-chat): harden gates, kill N+1s and reduce race risk

Closes review findings raised on the internal chat PR:

- Restrict role mass-assignment in ChannelMembersController so only
  account administrators can promote new members to channel admin.
- Wrap private-channel create/unarchive in a Postgres advisory lock per
  account so concurrent requests can no longer bypass the freemium limit.
- Replace `replies.size` and `votes.size` (per-broadcast queries) with
  `replies_count` / `votes_count` counter caches.
- Make `update_channel_activity` an atomic compare-and-set update so
  concurrent message creates can never regress `last_activity_at`.
- Optimize `Poll#total_votes_count` to use the cached column and eager-
  loaded options instead of a per-poll `votes.count` query.
- Add `internal_chat_messages.account_id` foreign key (`ON DELETE
  CASCADE`) to prevent orphan rows.
- Escape HTML in `ChannelSidebar.highlightMatch` to close a v-html XSS
  via incomplete tags in message search snippets.
- Cleanup `typingOffTimer` on `ChannelView` unmount.
- Add stable sort to `getChannelsByCategory` (alphabetical) and
  `getDMChannels` (last activity) to prevent UI reorder thrash.
- Localize `PollDisplay` time-remaining strings (en + pt-BR).
- Add specs covering the 90-day search history filter and the search
  controller endpoint, plus regenerate the consolidated migration
  with the new columns and FK.

* docs(swagger): note role mass-assignment restriction on channel members

Document that the `role` field on the channel member create payload is
silently coerced to `member` for callers that are not account
administrators, matching the controller behavior introduced in the
previous commit.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 13:50:15 -03:00
YJack0000
45124c3b41
fix(i18n): improve zh-TW translation coverage and quality (#14004)
Comprehensive update to Traditional Chinese (Taiwan) translations. As a
native zh-TW speaker and active user based in Taiwan, I found the
existing translations were quite incomplete (~54% overall) with many
strings still in English. Some existing translations also used
Simplified Chinese terms or unnatural phrasing.

I chose to submit this as a direct PR rather than going through Crowdin
because working through all the files at once is much faster and lets me
ensure consistent terminology across the entire locale.

Closes #14003

## What changed

**Backend (`config/locales/zh_TW.yml`)**
- Translated all ~259 previously untranslated strings (was ~19%
complete, now 100%)
- Covers: error messages, notifications, activity logs, integration
descriptions, Captain AI, public portal, reports

**Frontend (42 JSON files under `dashboard/i18n/locale/zh_TW/`)**
- Translated ~2,627 previously untranslated strings (was ~50% complete,
now ~100%)
- Most impacted files: `inboxMgmt.json`, `integrations.json`,
`settings.json`, `conversation.json`, `contact.json`, `report.json`

**Quality fixes across all files**
- Replaced Simplified Chinese terms mixed into zh-TW: 账→帳, 获→取得, 模板→範本,
收件箱→收件匣, 重置→重設, 自定義→自訂
- Standardized terminology for consistency: 客服人員 (agent), 延後 (snooze),
稽核 (audit), 巨集 (macro)
- Fixed incorrect translations (e.g., audit log table headers were
swapped, availability label was wrong)

## How to test

1. Set account/user language to 中文(台灣)
2. Navigate through the dashboard — settings, inbox management,
integrations, reports, conversations
3. Verify strings display in natural Traditional Chinese with no
remaining English gaps
4. Check that all placeholders (names, counts, dates) render correctly
2026-04-08 13:42:20 +05:30
Sivin Varghese
699b12b1d3
fix: Block inline images in message signatures (#13772)
# Pull Request Template

## Description

This PR includes, block inline images in message signatures and prevent
auto signature insertion when editor is disabled.

- Strip inline base64 images from signature on save and show warning
message
- Add `INLINE_IMAGE_WARNING` translation key for signature inline image
removal notification
- Add disabled check to `addSignature()` to prevent signature insertion
when editor is disabled
- Add `isEditorDisabled` checks to signature toggle logic in
`toggleSignatureForDraft()`, `replaceText()`, and `clearMessage()`
- Remove unused `replaceText` from the codebase, which belongs to old
`textarea` editor

Fixes
https://linear.app/chatwoot/issue/CW-6588/the-browser-hangs-when-the-message-signature-contains-inline-image

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

### Loom video
https://www.loom.com/share/fb556b46a12a4308a737eed732d5ed73


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-04-08 12:17:19 +05:30
Shivam Mishra
4f94ad4a75
feat: ensure signup verification [UPM-14] (#13858)
Previously, signing up gave immediate access to the app. Now,
unconfirmed users are redirected to a verification page where they can
resend the confirmation email.

- After signup, the user is routed to `/auth/verify-email` instead of
the dashboard
- After login, unconfirmed users are redirected to the verification page
- The dashboard route guard catches unconfirmed users and redirects them
- `active_for_authentication?` is removed from the sessions controller
so unconfirmed users can authenticate — the frontend gates access
instead
- If the user visits the verification page after already confirming,
they're automatically redirected to the dashboard
- No session is issued until the user is verified

<details><summary>Demo</summary>
<p>

#### Fresh Signup


https://github.com/user-attachments/assets/abb735e5-7c8e-44a2-801c-96d9e4823e51

#### Google Fresh Signup


https://github.com/user-attachments/assets/ab9e389a-a604-4a9d-b492-219e6d94ee3f


#### Create new account from Dashboard


https://github.com/user-attachments/assets/c456690d-1946-4e0b-834b-ad8efcea8369



</p>
</details>

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-04-07 13:45:17 +05:30
Aakash Bakhle
fbe3560b7a
feat(captain): Add paywall and expose Custom Tools (#13977)
# Pull Request Template

## Description

Custom tools is now discoverable on all plans

## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

Before:
<img width="390" height="446" alt="CleanShot 2026-04-02 at 13 40 11@2x"
src="https://github.com/user-attachments/assets/0a751954-f3ad-47d6-85b8-1e2f1476a646"
/>


After:

<img width="392" height="522" alt="CleanShot 2026-04-02 at 13 40 47@2x"
src="https://github.com/user-attachments/assets/62a252f6-2551-47a9-b50c-be949f08c456"
/>

<img width="1826" height="638" alt="CleanShot 2026-04-02 at 13 37 39@2x"
src="https://github.com/user-attachments/assets/77dc2a75-3d76-44cf-8579-8d3457879bd0"
/>


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2026-04-07 10:58:29 +05:30
Captain
8c0c0fd32c
chore: Update translations (#13990)
Co-authored-by: Sojan Jose <sojan.official@gmail.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-04-06 15:35:59 +05:30
Shivam Mishra
95463230cb
feat: sign webhooks for API channel and agentbots (#13892)
Account webhooks sign outgoing payloads with HMAC-SHA256, but agent bot
and API inbox webhooks were delivered unsigned. This PR adds the same
signing to both.

Each model gets a dedicated `secret` column rather than reusing the
agent bot's `access_token` (for API auth back into Chatwoot) or the API
inbox's `hmac_token` (for inbound contact identity verification). These
serve different trust boundaries and shouldn't be coupled — rotating a
signing secret shouldn't invalidate API access or contact verification.

The existing `Webhooks::Trigger` already signs when a secret is present,
so the backend change is just passing `secret:` through to the jobs.
Shared token logic is extracted into a `WebhookSecretable` concern
included by `Webhook`, `AgentBot`, and `Channel::Api`. The frontend
reuses the existing `AccessToken` component for secret display. Secrets
are admin-only and excluded from enterprise audit logs.

### How to test

Point an agent bot or API inbox webhook URL at a request inspector. Send
a message and verify `X-Chatwoot-Signature` and `X-Chatwoot-Timestamp`
headers are present. Reset the secret from settings and confirm
subsequent deliveries use the new value.

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-04-06 15:28:25 +05:30
Aakash Bakhle
8daf6cf6cb
feat: captain custom tools v1 (#13890)
# Pull Request Template

## Description

Adds custom tool support to v1

## Type of change
- [x] New feature (non-breaking change which adds functionality)


## How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.

<img width="1816" height="958" alt="CleanShot 2026-03-24 at 11 37 33@2x"
src="https://github.com/user-attachments/assets/2777a953-8b65-4a2d-88ec-39f395b3fb47"
/>

<img width="378" height="488" alt="CleanShot 2026-03-24 at 11 38 18@2x"
src="https://github.com/user-attachments/assets/f6973c99-efd0-40e4-90fe-4472a2f63cea"
/>

<img width="1884" height="1452" alt="CleanShot 2026-03-24 at 11 38
32@2x"
src="https://github.com/user-attachments/assets/9fba4fc4-0c33-46da-888a-52ec6bad6130"
/>



## Checklist:

- [x] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2026-04-02 12:40:11 +05:30
Sivin Varghese
4517c50227
feat: support bulk select and delete for documents (#13907) 2026-03-26 19:48:12 +05:30
salmonumbrella
6ff643b045
fix(i18n): add zh_TW snooze parser locale (#13822) 2026-03-25 16:54:18 +05:30
Captain
4b315bc2ec
chore: Update translations (#13884)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-23 20:06:17 -07:00
Gabriel Jablonski
d120c25917
fix(groups): restrict enable CTA to superadmin users (#248)
Non-superadmin users now see a "contact your administrator" message
instead of the "how to enable" action button on group disabled banners.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 13:01:02 -03:00
Captain
b974993886
chore: Update translations (#13845)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-21 02:20:27 -07:00