db9b451eeb
900 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cf7338a6e2 | chore: sync fazer-ai chatwoot 4.13.0 | ||
|
|
b561aa8451 |
revert(hermes): remove camadas 2/3/4 + reabilita memória
A causa raiz dos bugs de "info repetida em turns anteriores" era o
default_scope ASC do Message conflitando com .order(desc) no debounce
(ver commit
|
||
|
|
ebf98c90f4 |
feat(captain/hermes): camada 4 — topic gating contra info não pedida
Camada 3 (strip de linhas repetidas) não cobre quando LLM mistura tópico antigo + atual numa LINHA SÓ (caso real: cliente pergunta sobre pet, Hermes responde "A senha é Prime2025 e pode levar animais sim" — overlap baixo, strip não dispara). Camada 4 detecta tópicos factuais (Wi-Fi/senha/pet/estacionamento/preço/ cancelamento) presentes NA RESPOSTA mas AUSENTES da última pergunta do cliente. Quando detectado, bloqueia entrega + dispara [SISTEMA: force_topic_focus] no Hermes mandando responder EXCLUSIVAMENTE sobre o tópico atual. 1 retry; persistindo, entrega. Validado: cliente "Posso levar animais?" + resposta "Senha + pet" → detecta [:wifi] como off-topic. Cliente "Qual senha wifi?" + resposta "Senha é X" → vazio (passa normal). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
cc58805722 |
feat(captain/hermes): camada 3 — strip de linhas repetidas + check FAQs
LLM tende a "resumir" info de turns anteriores em toda nova resposta.
Camada 3 strip linhas onde >=70% das palavras significativas já apareceram
nas últimas 3 outgoings (filtra reactions). Saudações curtas preservadas.
Caso real Juliana 2026-05-02 (turn 3 ela ia repetir "Senha Prime2025 +
pet" mesmo cliente só dizendo "valeu"). Após strip: vira só "Imagina,
Rodrigo 😊".
Validator UI: novo check "FAQs próprias aprovadas > 0" — alerta quando
zero (faq_lookup cai no parent, risco de info desatualizada igual ao
bug do X-Captain-Assistant-Id que vazou Wi-Fi do parent hoje cedo).
Filtro SQL `content_attributes ->> 'external_source'` não casava (coluna
json, não jsonb); migrado pra filtro Ruby.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
ed99f67525 |
feat(captain/hermes): camada 2 — gating de saída factual sem tool call
Detecta alucinação de memória: se resposta do Hermes contém info factual (preço/senha/horário/regra/política) E o LLM NÃO chamou nenhuma tool MCP entre dispatch e callback, bloqueia entrega + dispara system_message forçando consulta a tool. 1 retry; persistindo, escala. Implementação: - McpController: incrementa Rails.cache hermes_tool_calls:<conv_id> em cada tools/call. - OutgoingJob: snapshot do contador como hermes_tool_calls_baseline ANTES de despachar pro Hermes. - HermesCallbackController.gate_factual_no_tool!: compara baseline vs current; se igual + FACTUAL_PATTERNS bate, intercepta. Patterns cobrem R$, %, "senha", check-in/out + horário, política de cancelamento, "permitido", "pode levar pet/animal". Caso real: cliente pede senha do Wi-Fi → Hermes responde de cabeça "é passada presencialmente" sem chamar faq_lookup → callback intercepta, não entrega pro cliente, manda [SISTEMA: force_factual_tool] pro Hermes com instrução de chamar faq_lookup. Se faq_lookup vier vazio → frase- âncora handoff. Auto-react ambient: removido filtro de "?" que barrava em prod. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
c960dc7e1e |
feat(captain/hermes): handoff por loop + label-guard em outgoing
Substitui o interceptor agressivo de "Um momento — vou verificar"
(que bloqueava silenciosamente) por handoff explícito + loop detection:
- HANDOFF_PATTERNS: detecta a frase-âncora ("Um momento — vou
verificar", "Aguarde um instante") e ENTREGA pro cliente,
marcando conv com label triagem_humana.
- looped_response?: detecta 2 outgoing similares (Jaccard >= 0.50)
OU pergunta reformulada sobre mesmo tópico (>= 3 keywords em comum
+ ambas inquisitivas via "?" / "me confirma" / "qual prefere"
etc). 1ª resposta passa, 2ª escala. Cobre o caso "endereço ou
link?" → "apenas link ou link + endereço?".
- OutgoingJob: guard que pula dispatch se conv tem label
triagem_humana ou hermes_placeholder. Hermes não responde mais →
não gasta token + não gera loop.
Cobre 2 casos do Rodrigo:
1. Bot da Claro insistindo em menu → 2ª resposta similar escala.
2. Hermes pedindo confirmação 2x sem entregar → escala.
Tokenize normaliza acentos (transliterate) pra stopwords baterem
"voce/você", "endereco/endereço", etc.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
c8785b999c |
fix(captain/hermes): intercepta placeholder e força tool call
Quando o LLM emite "⏳ Um momento — vou verificar." (ou variantes:
"deixa eu verificar", "aguarde um instante") sem chamar nenhuma tool,
o callback agora:
1. NÃO entrega a msg pro cliente (UX terrível ver "vou verificar" e
ficar esperando indefinidamente).
2. Dispara notify_event pro Hermes com [SISTEMA: force_tool_call]
instruindo a retomar e chamar a tool relevante (generate_pix,
send_suite_images, faq_lookup) com base na última msg do cliente.
3. Limita 2 retries por conversation via Rails.cache (TTL 5min). Após
esgotar, marca labels hermes_placeholder + triagem_humana e descarta.
Caso real do Rodrigo: cliente confirmou reserva ("Para hoje 23h por 4h")
e o LLM respondeu apenas o placeholder (api_calls=1 no daemon, sem
tool). Cliente ficava esperando sem resposta. Agora Captain força o
LLM a chamar a tool, ou cai pra triagem humana após 2 tentativas.
PLACEHOLDER_PATTERNS cobre as variações observadas. SKILL.md já proibia
"Um momento", mas o LLM ignorava — defesa em camadas.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
ed21722dc4 |
fix(captain/hermes): callback aceita ?slug= além de ?inbox_id=
Hermes daemon faz POST /webhooks/captain/hermes_callback?slug=<profile> mas controller só conhecia ?inbox_id. 404 → resposta do LLM nunca chegava ao Captain. Cliente via só auto-react. Fix: fetch_inbox resolve via Captain::Assistant.find_by(hermes_profile_name) quando slug está presente. Inbox é a primeira CaptainInbox associada a esse assistant. Suporta o pattern admin de re-apontar uma inbox de teste (ex: Angelina) entre vários agentes Hermes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5f6aed05c9 |
fix(captain/hermes-builder): callback resolve session via last_session
Bug: Hermes nao propaga chat_id no metadata do callback. Callback controller nao conseguia resolver qual session armazenar a resposta. WARN "no session_key resolvable — ignorando" descartava todas as respostas do Construtor. Fix: HermesBuilder::Storage.remember_last_session() grava ultima session por account quando admin chama /start ou /create. Callback le essa key via last_session_for(account_id). MVP-safe pra 1 admin por conta. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
48fad2977b |
feat(captain/hermes): payload enriquecido + humanizadores + notif Pix proativa
Captain::Hermes::Client (enterprise/app/services/captain/hermes/client.rb): - text_for_hermes: transcreve audio via Whisper antes de enviar pro Hermes (reusa Captain::OpenAiMessageBuilderService) - image_urls_for_hermes: URLs publicas de imagens da message; plugin captain-webhook do Hermes baixa em /tmp/ e popula event.media_urls pra vision multimodal (gpt-4o-mini auxiliary) - contact_history_snapshot: dados eager pro [ctx] (last_reservation_*, total_conversations, ultima_suite, etc) — memoria do contato direto no prompt sem precisar tool call - notify_event + build_event_payload: dispara webhook sintetico pro Hermes pra eventos do sistema (Pix pago etc) — Valentina manda mensagem espontanea sem cliente perguntar Captain::Payments::ConfirmationService: - Hook notify_hermes_proactively! enfileira NotifyPaymentConfirmedJob apos confirmacao de Pix, somente se inbox estiver no fluxo Hermes (Captain interno continua igual sem mudanca) Captain::Hermes::NotifyPaymentConfirmedJob (NOVO): - Monta system_message "[SISTEMA: pagamento_confirmado]\n..." e dispara webhook pro Hermes Valentina - Valentina (via SOUL.md) interpreta como evento do Captain e manda mensagem celebrativa pro cliente Captain::Hermes::DelayedReplyJob (NOVO) — humanizadores: - Liga indicador "digitando..." (composing) via wuzapi - Aguarda delay configuravel via Captain::Assistant.config['response_delay'] (modos: none, fixed, typing_simulation com chars_per_second + min/max) - Posta msg outgoing - Desliga typing - Fallback no HermesCallbackController posta direto se class nao carregada Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
9ed3491d55 |
feat(captain/mcp): suite de 9 tools MCP + pricing tables Dolce Amore
Tools novas em enterprise/app/services/captain/mcp/tools/: - generate_pix: Pix Inter via PricingTable + fallback link reserva-1001 - check_pix_payment: consulta Inter + dispara ConfirmationService - send_suite_images: fotos da galeria (Captain::GalleryItem) via wuzapi - reschedule_reservation: remarca reserva (regra 3h antecedência Dolce) - update_contact: persiste nome/CPF/email/notas no Contact - get_contact_history: markdown do histórico do cliente on-demand - react_to_message: reage com emoji via wuzapi (is_reaction=true) Captain::Mcp::PricingTables: tabela hardcoded Dolce Amore (8 categorias x 4 periodos + regras de pessoa extra). Backend e fonte de verdade — LLM nao inventa preco. add_label_tool: cria Label oficial automaticamente se nao existir, aceita conversation_id em arguments. mcp_controller: aceita X-Captain-Account-Id/Assistant-Id/Inbox-Id como fallback de contexto. tool_registry: 9 tools ativas. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
713bb16012 |
feat(captain): MCP controller aceita Bearer token além de HMAC
Hermes Agent (cliente MCP) usa `--auth header` que envia `Authorization: Bearer <token>` — padrão MCP. Antes o Captain MCP server só aceitava HMAC-SHA256 (X-Hub-Signature-256), incompatível com o que Hermes manda nativamente. Agora aceita qualquer um dos 2 modos: - Bearer token (recomendado, padrão MCP) — Hermes envia automaticamente - HMAC-SHA256 do body — pra clientes que preferem assinar payload Ambos validados com ActiveSupport::SecurityUtils.secure_compare contra o mesmo CAPTAIN_MCP_SECRET. Sem secret = validação desabilitada (PoC/dev). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
23911ea878 |
feat(captain): MCP server (HTTP) expondo tools pro Hermes Agent
Implementa servidor MCP (Model Context Protocol) HTTP no Captain pra o Hermes Agent invocar tools do Captain via `hermes mcp add`. Substrato pra integração de Nível 2 onde o agente consulta tools quando precisa executar ações reais (buscar FAQ, adicionar label, futuramente Pix etc). Arquivos: - app/controllers/webhooks/captain/mcp_controller.rb Endpoint POST /webhooks/captain/mcp. Valida HMAC (CAPTAIN_MCP_SECRET), parseia JSON-RPC, despacha pro Server. Extrai params._captain_context com multi-tenant ids (conversation_id, inbox_id, account_id, etc). - enterprise/app/services/captain/mcp/server.rb Subset MCP suficiente: initialize, tools/list, tools/call, ping, notifications/initialized. JSON-RPC síncrono (sem SSE). - enterprise/app/services/captain/mcp/tool_registry.rb Lista centralizada de tools. - enterprise/app/services/captain/mcp/tools/base_tool.rb Interface base pras tools (.name, .description, .input_schema, #call). - enterprise/app/services/captain/mcp/tools/add_label_tool.rb Tool 1: aplica label na conversation atual. - enterprise/app/services/captain/mcp/tools/faq_lookup_tool.rb Tool 2: busca semântica em FAQs (Captain::AssistantResponse via pgvector cosine). Reaproveita SearchReplyDocumentationService pra paridade com o caminho legado do Captain. - config/routes.rb Rota POST /webhooks/captain/mcp. Conexão pelo Hermes: hermes mcp add captain-tools --url http://CAPTAIN_HOST/webhooks/captain/mcp Auth: HMAC X-Hub-Signature-256 quando CAPTAIN_MCP_SECRET setado. TODO próxima sprint: generate_pix_tool, send_suite_images_tool. Handoff fica via automation hoje (UI Chatwoot). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
cd519a73c4 |
fix(captain): converte markdown bold pra formato WhatsApp no callback Hermes
Hermes (e LLMs default em geral) emitem **negrito** no formato markdown padrão. WhatsApp usa formato próprio: *negrito* (single asterisk). Sem conversão, o cliente vê asteriscos literais no WhatsApp, parecendo bug. Defesa em camadas: 1. SOUL.md da Valentina foi atualizado com regra explícita de formato WhatsApp (single asterisk pra bold, underscore pra itálico, etc). 2. Este controller faz normalização defensiva no callback recebido do Hermes: regex `**texto**` -> `*texto*` antes de criar a mensagem outgoing. Não afeta o resto do conteúdo. normalize_for_whatsapp() é trivialmente reversível e idempotente (executar 2x é igual a 1x). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
35de8b7fde |
feat(captain): cliente Captain ↔ Hermes (outgoing job + callback endpoint)
Implementa o lado Captain da integração Nível 2 (Hermes como cérebro). Ativação por inbox via env var CAPTAIN_HERMES_INBOX_IDS — inboxes não listadas seguem usando o orquestrador interno do Captain (Daniela_Reservas etc) sem mudança alguma. Princípio "só adiciona, não retira". Componentes: - enterprise/app/services/captain/hermes.rb Módulo helper de config (env vars, URLs, secrets per-inbox). - enterprise/app/services/captain/hermes/client.rb Service que monta payload (msg + contexto da conversa/inbox/contato) e faz POST autenticado via HMAC-SHA256 (X-Hub-Signature-256) no webhook do Hermes Agent (porta 8644). DispatchError em falha de rede/HTTP. - enterprise/app/jobs/captain/hermes/outgoing_job.rb Wrapper Sidekiq do Client. Retry 3x em DispatchError. - app/controllers/webhooks/captain/hermes_callback_controller.rb Recebe callback do plugin captain-http-callback do Hermes. Valida HMAC se CAPTAIN_HERMES_CALLBACK_SECRET setado, identifica conversation pela última pending da inbox (janela 5min) e cria mensagem outgoing. - config/routes.rb Rota POST /webhooks/captain/hermes_callback (fora de /api/v1/accounts). - enterprise/app/services/enterprise/message_templates/hook_execution_service.rb Branch novo no schedule_captain_response: se Hermes habilitado pra inbox, dispara HermesOutgoingJob; senão, fluxo Captain interno como antes. Env vars (todas opcionais; sem set = Hermes desabilitado em todas inboxes): - CAPTAIN_HERMES_INBOX_IDS (CSV de inbox.id) - CAPTAIN_HERMES_WEBHOOK_BASE_URL (default http://172.17.0.1:8644) - CAPTAIN_HERMES_CALLBACK_SECRET (HMAC validar callbacks de entrada) - CAPTAIN_HERMES_SUBSCRIPTION_SECRET_INBOX_<id> (HMAC assinar saídas) Limitação: identificação da conversation no callback usa última pending da inbox dentro de 5min. OK pra PoC com 1 conversa de teste por vez. Em produção, melhorar mapeando delivery_id ↔ conversation_id em Redis. Próximo passo manual (admin VPS): criar subscription no Hermes: hermes webhook subscribe captain-inbox-1 \\ --prompt 'Cliente disse: {message}. Responda como Daniela ...' \\ --deliver http_callback \\ --deliver-chat-id 'http://CAPTAIN_HOST/webhooks/captain/hermes_callback?inbox_id=1' Depois set CAPTAIN_HERMES_INBOX_IDS=1 + CAPTAIN_HERMES_SUBSCRIPTION_SECRET_INBOX_1 no stack do Captain e testar pela inbox Angelina. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
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.
|
||
|
|
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> |
||
|
|
e9b8b6e587 |
feat(reports): filtro por inbox no Relatório do Bot
Hoje as métricas e séries do BotReports agregam toda a conta — não dá pra ver "a Jasmine da PrimeAL está errando mais que a do Qnn01". Cada unidade tem prompt próprio, então um sintoma localizado precisa de medição localizada. Backend: - Inbox#has_many :reporting_events (relação inversa que faltava) - BotMetricsBuilder aceita inbox_id e filtra bot_conversations + base_reporting_events - bot_metrics endpoint passa inbox_id pelos params permitidos - count_report_builder já suporta scope=inbox; agora funciona pra bot_resolutions_count e bot_handoffs_count graças à relação acima Frontend: - BotReports.vue: ReportFilters com filter-type='inboxes' e dropdown ativo - Quando uma inbox é escolhida, requestPayload inclui inboxId/type/id e os fetches (BotMetrics + ReportContainer) passam o filtro - API client getBotMetrics aceita inboxId; getBotSummary aceita type+id - Sem inbox selecionada: comportamento antigo (agregação da conta) Bonus na rake task de retroativo: - rebuild_bot_resolved.rake: Message.unscope(:order) pra evitar conflito PG::InvalidColumnReference (DISTINCT + ORDER BY default scope) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
74aea5e2d5 |
fix(agents): converte Parameters pra Hash antes de chamar any?
ActionController::Parameters não tem .any?; chamar sem to_h quebra
com NoMethodError (500) ao salvar agente. Bug introduzido em
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
928b1ec6b9 |
feat(captain): Codex OAuth auth module + proxy controller
Implementa Fases 1+2 do plano Captain Codex OAuth.
Fase 1 — Auth módulo:
- Migration captain_codex_credentials (tokens AR-encrypted)
- Model Captain::CodexCredential (singleton-ish com .current)
- Captain::Codex::AuthService com device flow completo:
start_device_login, poll_once, exchange_for_credential,
valid_access_token (auto-refresh), refresh!
- Rake task captain:codex:{login,status,refresh}
- Sidekiq job Captain::Codex::RefreshTokensJob rodando a cada 30min
Fase 2 — Proxy Chat Completions → Responses:
- Captain::Codex::Translator (chat ↔ responses, tools, tool_calls)
- Captain::Codex::Client (streaming SSE → agregado)
- Api::Internal::CodexProxyController expondo
POST /codex/v1/chat/completions
- 10 specs do Translator passando
Próximo: Fase 3 (feature flag + fallback) e reconfiguração dos
clientes RubyLLM/Agents/ruby-openai pra apontarem pro proxy quando
CAPTAIN_LLM_PROVIDER=openai_codex_oauth.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
55c7c435bc
|
fix(pwa): decouple installability from DISPLAY_MANIFEST branding flag (#272)
DISPLAY_MANIFEST previously gated the entire PWA manifest link and theme-color meta, so white-label installs that set the flag to false to hide Chatwoot branding also lost the mobile "Install app" prompt. Serve /manifest.json from a new dynamic controller that reflects INSTALLATION_NAME, LOGO_THUMBNAIL and a new BRAND_COLOR config, and keep the manifest link, theme-color and apple-mobile-web-app meta emitted regardless of DISPLAY_MANIFEST. The flag now gates only the Chatwoot-branded raster assets (favicons, apple-icon PNGs, ms-icon, marketing meta description). |
||
|
|
aed6d62640 |
feat(retention): summary KPIs + cohort endpoints
Exposes two JSON endpoints under /api/v1/accounts/:id/captain/reports: - GET /retention — aggregate KPIs (active/recurring/sleeping/at-risk/ churned, new vs returned in period, Pix generated/paid/conversion, retention rates at 30d and 90d) - GET /retention/cohort — monthly cohort matrix, 12 months lookback, 12 months of offset. Each cell is % of the cohort that interacted in month M+N. SQL-aggregated with DATE_TRUNC + DISTINCT so it is a single query even on large histories. |
||
|
|
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> |
||
|
|
bd61458720
|
fix(internal-chat): use internal_chat_channel_id in delete payloads (#270)
Backend broadcast payloads for internal_chat.message.deleted and internal_chat.reaction.deleted used channel_id as the key, but the frontend ActionCable handlers (and all other internal-chat events) expect internal_chat_channel_id. This caused deleted messages and removed reactions to stay visible on screen until a manual refresh. Rename the key on the backend so the payloads match the convention shared with message.created/updated and reaction.created, and drop the defensive fallback on the frontend reaction-deleted handler. |
||
|
|
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 |
||
|
|
4d155e4c01 |
fix(merge): CI offenses missed by pre-commit hook
- Editor.vue: consolidate duplicate defineExpose() calls introduced when removing signature functions (broke Vite build, cascaded into super_admin request specs via ActionView::Template::Error) - omniauth_callbacks_controller + backfill migration: Rails/SaveBang autocorrect (offenses live in unchanged upstream files, so pre-commit hook skipped them; CI runs rubocop project-wide) |
||
|
|
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 |
||
|
|
fa1dd8b6cb |
feat(lifecycle): expose concierge config update on UnitsController
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
cb67a1063d |
fix(lifecycle): move stub controllers from non-enterprise to enterprise path
Os stubs de lifecycle criados na task anterior estavam em app/controllers/ causando futura colisão de redefinição de classe quando os controllers reais forem implementados em enterprise/app/controllers/ (tasks 4-6). Move os 3 stubs para o enterprise path onde vivem todos os controllers Captain. Routing spec: 7 examples, 0 failures Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
fbc91e2fa8 |
feat(lifecycle): add REST routes for rules, config, deliveries, concierge
Wires 3 new captain namespace resources (lifecycle_rules, lifecycle_config, lifecycle_deliveries) and a member action `patch :concierge` on units. Includes stub controllers (to be expanded in Tasks 4-7) and passing routing spec. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
996704350b |
feat: Captain::Reservation callback cria nota interna automaticamente
Cobre ambos os caminhos (generate_pix_tool e PublicReservationsController): toda reserva criada recebe um after_create_commit que posta uma mensagem privada na conversa com os detalhes (suite, check-in, valores, ID). Remove a criacao duplicada do PublicReservationsController. |
||
|
|
4f33deb978
|
release v4.12.0-fazer-ai.54 (#265)
* fix(whatsapp): preserve green color on chat list typing indicator The messagePreviewClass computed includes text-n-slate-11/12, which overrode text-green-500 in the compiled Tailwind order. Split padding into a dedicated computed and apply only it on the typing preview. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(whatsapp): clear contact typing indicator when message is received Dispatch CONVERSATION_TYPING_OFF after a new incoming message is persisted from baileys messages.upsert, so the dashboard clears the typing/recording indicator without waiting for a paused/unavailable presence event. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(conversations): dispatch messages.read event when unread messages exist The throttling introduced in upstream #13355 returned early for the "has unread" branches, skipping dispatch_messages_read_event. That meant the MESSAGES_READ event only fired when there were no unread messages, so ChannelListener never called channel.read_messages on the baileys provider when an agent actually read a conversation. Consolidate the unread/throttle guard so the dispatch runs in all paths where update_last_seen_on_conversation runs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
7c9411a0b0 |
feat: persiste metadados do cliente em custom_attributes + tool blindada
- Controller grava cpf/ultima_suite/ultima_permanencia/ultima_reserva_em/total_reservas em contact.custom_attributes (aparece no painel lateral do Chatwoot) - GenerateReservationLinkTool exige marca/unidade/categoria/permanencia/checkin_at; retorna erro se Jasmine chamar sem esses dados |
||
|
|
6e1b80002e | feat: adiciona label aguardando_pagamento ao criar reserva (fecha fase 2+3) | ||
|
|
9dabaaa505 |
fix: usa phone digits como source_id (whatsapp inbox exige E.164 sem +)
Smoke test revelou que o inbox do tipo whatsapp valida source_id com
regex ^\d{1,15}\z. Trocar UUID por telefone em digitos (phone_digits)
e normalizar phone_number pra +phone_digits antes de criar o contato.
|
||
|
|
5ff3a70474 | feat: implementa POST create (contact + conversa + reserva + pix) e GET status | ||
|
|
e9a5e734ff | feat: rota + controller esqueleto PublicReservations com token auth | ||
|
|
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> |
||
|
|
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>
|
||
|
|
b0a8fa70d0
|
fix(signatures): allow admins to manage inbox signatures without explicit membership (#260)
Administrators have access to all inboxes in an account but the validate_inbox_access filter only checked InboxMember records, returning 401 for admins not explicitly added as inbox members. |
||
|
|
bd14e96ed9
|
chore: allow article to create without content (#14007) | ||
|
|
e5107604a0
|
feat: account enrichment using context.dev [UPM-27] (#13978)
## Account branding enrichment during signup This PR does the following ### Replace Firecrawl with Context.dev Switches the enterprise brand lookup from Firecrawl to Context.dev for better data quality, built-in caching, and automatic filtering of free/disposable email providers. The service interface changes from URL to email input to match Context.dev's email endpoint. OSS still falls back to basic HTML scraping with a normalized output shape across both paths. The enterprise path intentionally does not fall back to HTML scraping on failure — speed matters more than completeness. We want the user on the editable onboarding form fast, and a slow fallback scrape is worse than letting them fill it in. Requires `CONTEXT_DEV_API_KEY` in Super Admin → App Config. Without it, falls back to OSS HTML scraping. ### Add job to enrich account details After account creation, `Account::BrandingEnrichmentJob` looks up the signup email and pre-fills the account name, colors, logos, social links, and industry into `custom_attributes['brand_info']`. The job signals completion via a short-lived Redis key (30s TTL) + an ActionCable broadcast (`account.enrichment_completed`). The Redis key lets the frontend distinguish "still running" from "finished with no results." |
||
|
|
871f2f4d56
|
fix: harden fetching on upload endpoint (#14012) | ||
|
|
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> |
||
|
|
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> |
||
|
|
b3d0af84c4
|
fix(widget): Queue SDK-set conversation attributes and labels for first message (#13912)
### Description
When integrating the web widget via the JS SDK, customers call
setConversationCustomAttributes and setLabel on chatwoot:ready — before
any conversation exists. These API calls silently fail because the
backend endpoints require an existing conversation. When the visitor
sends their first message, the conversation is created without those
attributes/labels, so the message_created webhook payload is missing the
expected metadata.
This change queues SDK-set conversation custom attributes and labels in
the widget store when no conversation exists yet, and includes them in
the API request when the first message (or attachment) creates the
conversation. The backend now permits and applies these params during
conversation creation — before the message is saved and webhooks fire.
### How to test
1. Configure a web widget without a pre-chat form.
2. Open the widget on a test page and run the following in the browser
console after chatwoot:ready:
`window.$chatwoot.setConversationCustomAttributes({ plan: 'enterprise'
});`
`window.$chatwoot.setLabel('vip');` // must be a label that exists in
the account
3. Send the first message from the widget.
4. Verify in the Chatwoot dashboard that the conversation has plan:
enterprise in custom attributes and the vip label applied.
5. Set up a webhook subscriber for `message_created` confirm the first
payload includes the conversation metadata.
6. Verify that calling `setConversationCustomAttributes` / `setLabel` on
an existing conversation still works as before (direct API path, no
regression).
7. Verify the pre-chat form flow still works as expected.
|