- RetentionSummaryBadge in the "Previous conversations" sidebar:
tiered status (First contact / Active / Recurring / Sleeping /
At risk / Inactive) + counts of interactions, one-shots, Pix.
- Retention tab in Captain Reports: KpiCards, FlowCard, CohortMatrix
(12x13 heatmap with CSV export).
- Five new filters on the contacts list: recurring, last interaction,
days since, interactions count, reservations paid.
- Full pt_BR + en i18n under CAPTAIN_REPORTS.RETENTION.*
- Spec for InteractionCalculatorService covering gap behavior,
one-shot classification, internal-label exclusion, multi-conversation
grouping across the 30h window.
- Docs: docs/captain-retention-indicators.md with business rules,
column reference, endpoint shape, and backup SQL queries.
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.
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>
Inboxes without portal_id were crashing with NoMethodError on save,
blocking landing host creation via UI for any inbox without a portal.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces stub Settings.vue with full implementation: anti-spam guard
form (quiet hours, interval, pause-on-reply, opt-out label) and a
collapsible ConciergeUnitCard per unit (inbox selector, persona name,
knowledge base, key-value variables). Adds CONCIERGE_CONFIGURED /
CONCIERGE_NOT_CONFIGURED i18n keys to en + pt_BR.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements Task 13 — replaces the stub History.vue with a real paginated
table filtered by status, and adds DeliveryPreviewModal to show rendered_body.
Also extends i18n keys (TOTAL, PAGINATION, MODAL labels) in en + pt_BR.
Adds CAPTAIN_LIFECYCLE block (en + pt_BR) to captain.json with full
key set for Rules, Wizard, Settings, History and sidebar entry.
Also stages pre-existing uncommitted additions to captain.json from
prior work (KPI, PILLS, QUICK_DATE, CARD, ACTIONS extras for
CAPTAIN_RESERVATIONS) — those were already in the working tree and
belong to the same feature branch.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
Existem 3 mecanismos de vinculo inbox-unit no fork:
1. captain_inboxes.captain_unit_id (fluxo Captain)
2. captain_unit_inboxes (tabela join pura, usada pela UI nova)
3. captain_units.inbox_id (legado direto)
O serializer lia so o #1 e retornava null nas outras, fazendo a UI
mostrar 'Nenhuma unidade vinculada' mesmo quando havia vinculo via
#2 ou #3. Agora o jbuilder cai em cascata nas 3 fontes.
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.
- 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
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.
- attribution_matcher_service: window 10min → 30min (mais realista para jornada do lead)
- LandingHostsConfig.vue: strip automático de https://, www e trailing slash antes de salvar
- Cria modelo LeadClick para registrar cliques das landing pages
- Cria modelo LandingHost para mapear hostname → inbox_id
- Endpoint público POST /track/click para receber eventos de clique
- Leads::AttributionMatcherService para correlacionar clique com conversa
- Integração com IncomingMessageWuzapiService para atribuição automática
- API REST para gerenciar LandingHosts por inbox (index/create/destroy)
- UI: nova aba 'Landing Pages' nas configurações da caixa de entrada
- Dashboard API client dedicado (landingHosts.js)
- RuboCop: refatora shift_signature_name, TrackingController, AttributionMatcherService e WuzapiService
- Remove filtro por captain_assistant_id (campo não existe no payload)
- Mostra todas as inboxes da conta para o usuário escolher em qual
caixa de entrada configurar os templates de notificação
- Arquitetura corrigida: templates agora pertencem à inbox (WhatsApp),
não à unidade PIX (que é uma config financeira, não de mensagens)
- Migration: troca FK captain_unit_id -> inbox_id (up/down explícito)
- Model: belongs_to :inbox; scope for_inbox
- Controller: escopo via account.inboxes.find(inbox_id)
- Rotas: move de captain/units/:id → inboxes/:id/notification_templates
- Scanner job: joins(:conversation).where(conversations: {inbox_id:})
- UI: página /captain/notifications com seletor de inbox no topo
(chips clicáveis, templates carregam por watch no selectedInboxId)
- i18n PT/EN: novas keys INBOX_LABEL, SELECT_INBOX_HINT, EMPTY
Sem o botão na tela de Units, não havia como chegar até a página
de templates de notificação (captain/units/:unitId/notifications).
Adiciona ícone de sino com rota correta + chave i18n PT/EN.
- Adiciona check_in_at/duration_hours ao schema do tool CreateReservationIntent
para que a IA capture o horário EXATO de chegada informado pelo cliente
- Cria captain_notification_templates: label, content, timing_minutes,
timing_direction (before/after), active, position
- Implementa SendNotificationService com interpolação de variáveis
(guest_name, check_in_time, check_out_time, suite_name, unit_name)
- Implementa NotificationScannerJob (Sidekiq-cron a cada 5min) com
janela de tolerância de ±5min e idempotência via metadata JSONB
- API REST: /captain/units/:unit_id/notification_templates (CRUD)
- Store Vuex captainNotificationTemplates + API client
- UI: página de gestão de templates com editor inline e botão '+'
- Configura rota captain_settings_notifications
- i18n PT/EN para todas as strings novas
- Rubocop e ESLint: zero offenses
Substitui rails_storage_proxy_url (URL absoluta com host ngrok) por
rails_storage_proxy_path (URL relativa) em file_url e thumb_url.
Problema: ngrok mostra página de interstitial HTML para sub-recursos
carregados pelo browser (img/audio) sem cookie ngrok válido.
O browser recebia HTML em vez da mídia → imagem 'não disponível' e
áudio '00:00/00:00'.
Solução: URL relativa (/rails/active_storage/blobs/proxy/...) resolve
para o servidor atual sem passar pelo ngrok, eliminando o interstitial.
Funciona tanto em localhost:3000 quanto acessando via ngrok no browser.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- MediaHandler: adiciona sanitize_content_type que normaliza audio/opus → audio/ogg
- MediaHandler: detect_extension retorna .ogg (não .mp3) para áudios WhatsApp
- MediaHandler: final_filename força extensão .ogg em áudios que chegam com .mp3
- Attachment: normalize_opus_blob_content_type! agora verifica apenas content_type
(remove checagem de extensão de filename que impedia normalização de blobs .mp3)
- Attachment: audio_metadata chama normalize_opus_blob_content_type! para corrigir
blobs existentes na primeira vez que são acessados (lazy fix)
WhatsApp envia áudio como container OGG/Opus (bytes OggS = 4f 67 67 53),
mas declarava mimetype audio/opus. Browsers não conseguem reproduzir container
OGG via MIME audio/opus — precisam de audio/ogg.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
O refactoring c48047ba5 removeu attachment_params acidentalmente sem mover
para outro lugar, quebrando o download de áudio, imagem, vídeo e documento.
O método é chamado por incoming_message_wuzapi_service.rb#attach_files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Corrige NoMethodError que impedia todas as mensagens de texto de chegarem
ao front. O método text_content era chamado mas não existia na classe.
Também adiciona UndecryptableMessage à lista de eventos ignorados para
evitar tentativa de processar mensagens sem conteúdo descriptografável.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace unit+inbox combined dropdown with inbox-only select.
Add ALL_INBOXES i18n key in pt_BR and en. Units (Pix) are unrelated
to conversation reports.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implementa a página Relatórios IA com geração de análises semanais
por IA baseadas nas conversas de cada unidade/caixa de entrada.
Funcionalidades:
- Página /settings/captain/reports com dois tabs (Insights IA / Operacional)
- Botão "Gerar Análise" que enfileira job Sidekiq
- Filtro por unidade ou caixa de entrada
- Exibe insights com status (pendente/processando/concluído/falhou)
- Mostra top_topics, ai_failures e period_summary
- Estado vazio com CTA para gerar primeiro relatório
Backend:
- InsightsController com endpoints index/show/generate
- GenerateInsightsJob que processa conversas com LLM
- ConversationInsightService com chunking e merge inteligente
- Migração para adicionar inbox_id à tabela captain_conversation_insights
- Link sidebar "Relatórios IA" em /settings/captain/reports
Frontend:
- Vuex store captainReports com actions/mutations/getters
- API client CaptainReportsAPI (getInsights, generateInsight)
- i18n en e pt_BR para CAPTAIN_REPORTS.*
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Melhorias na ferramenta send_suite_images para resolver confusão entre
categoria e número de suíte:
1. **Descrições de parâmetros mais claras**
- suite_category: exemplos específicos (Hidromassagem, ALEXA, STILO)
- suite_number: apenas números (101, 102, 103) - remove exemplos confusos
2. **Instruções explícitas no system prompt**
- Seção [Galeria de Fotos] com regras claras
- Prioriza suite_category quando ambíguo
- Evita confirmações desnecessárias com cliente
3. **Mensagens de erro melhoradas**
- Sugere buscar por categoria quando busca por número falha
- Feedback mais útil para a IA
Resultado esperado:
- Cliente: "Me manda foto da suite Alexa"
- IA: busca por suite_category="Alexa" ✓ (sem pedir confirmação)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* feat: add audio transcoding support for WhatsApp Cloud API
- Introduced `Audio::TranscodeService` to handle audio transcoding to OGG/Opus format.
- Updated `Messages::MessageBuilder` to transcode audio attachments based on `transcode_audio` parameter.
- Enhanced `WhatsappCloudService` to normalize audio content types and send voice flag for recorded audio in OGG format.
- Added utility functions for audio conversion in JavaScript.
- Updated Dockerfile to include FFmpeg for audio processing.
- Added tests for audio transcoding and WhatsApp Cloud service interactions.
* feat: enhance audio handling with transcoding support and error management
* feat: improve audio transcoding error handling and enhance audio recording features
* feat: enhance audio transcoding process and error handling for better reliability
* feat: update recorded audio handling to support boolean and array formats
* feat: Implement existing template linking for CSAT surveys
- Added functionality to link existing CSAT templates for WhatsApp channels.
- Introduced a new component for selecting existing templates.
- Updated the dashboard settings page to support template mode switching between creating new and using existing templates.
- Enhanced the CSAT template management service to handle linking existing templates and fetching available templates.
- Updated API routes to include linking and fetching available templates.
- Added tests for the new linking functionality and template availability checks.
* feat: Enhance CSAT template handling and validation across services and components
* feat: Refactor body variable extraction for CSAT templates and update related validations
* feat: Add linked_at field to CSAT template responses and update related handling
* feat: Add tests for ConversationDrop date formatting and CSAT template body variable handling
Fixes
https://linear.app/chatwoot/issue/CW-6494/add-shopify-mandatory-compliance-webhooks-for-app-store-listing
Shopify requires all public apps to handle three GDPR compliance
webhooks before they can be listed on the App Store. Their automated
review checks for these endpoints and verifies that apps validate HMAC
signatures on incoming requests. We were failing both checks.
This PR adds a single webhook endpoint at `POST /webhooks/shopify` that
receives all three compliance events. When Shopify sends a webhook, it
signs the payload with our app's client secret and includes the
signature in the `X-Shopify-Hmac-SHA256` header. Our controller reads
the raw body, computes the expected HMAC-SHA256 digest, and rejects
mismatched requests with a 401.
Shopify identifies the event type through the `X-Shopify-Topic` header.
For `customers/data_request` and `customers/redact`, we simply
acknowledge with a 200—Chatwoot doesn't persist any Shopify customer
data. All order lookups happen as live API calls at query time. For
`shop/redact`, which Shopify sends after a merchant uninstalls the app,
we delete the integration hook for that shop domain and remove the
stored access token and configuration.
### How to test via Rails console
```
secret = GlobalConfigService.load('SHOPIFY_CLIENT_SECRET', nil)
body = '{"shop_domain":"test.myshopify.com"}'
valid_hmac = Base64.strict_encode64(OpenSSL::HMAC.digest('SHA256', secret, body))
```
#### Test 1: No HMAC → 401
```
app.post '/webhooks/shopify', params: body, headers: { 'Content-Type' => 'application/json', 'X-Shopify-Topic' => 'customers/data_request' }
app.response.code # => "401"
```
#### Test 2: Invalid HMAC → 401
```
app.post '/webhooks/shopify', params: body, headers: { 'Content-Type' => 'application/json', 'X-Shopify-Hmac-SHA256' => 'invalid', 'X-Shopify-Topic' => 'customers/data_request' }
app.response.code # => "401"
```
#### Test 3: Valid HMAC, customers/data_request → 200
```
app.post '/webhooks/shopify', params: body, headers: { 'Content-Type' => 'application/json', 'X-Shopify-Hmac-SHA256' => valid_hmac, 'X-Shopify-Topic' => 'customers/data_request' }
app.response.code # => "200"
```
#### Test 4: Valid HMAC, customers/redact → 200
```
app.post '/webhooks/shopify', params: body, headers: { 'Content-Type' => 'application/json', 'X-Shopify-Hmac-SHA256' => valid_hmac, 'X-Shopify-Topic' => 'customers/redact' }
app.response.code # => "200"
```
#### Test 5: Valid HMAC, shop/redact → 200 (deletes hook)
```
# First check if a hook exists for this domain:
Integrations::Hook.where(app_id: 'shopify', reference_id: 'test.myshopify.com').count
app.post '/webhooks/shopify', params: body, headers: { 'Content-Type' => 'application/json', 'X-Shopify-Hmac-SHA256' => valid_hmac, 'X-Shopify-Topic' => 'shop/redact' }
app.response.code # => "200"
```
---------
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Some customers using WhatsApp inboxes with account-level webhooks were
reporting receiving duplicate `message_created` webhook deliveries for
every incoming message. Upon inspection, here's what we found
- Both payloads are identical.
- No errors appear in the application logs
- Webhook URL is only configured in one place.
This meant, the system was sending the webhooks twice. For some context,
there's a know related issue... Meta's WhatsApp Business API can deliver
the same webhook notification multiple times for a single message. The
codebase already acknowledges this — there's a comment in
`IncomingMessageBaseService#process_messages` noting that "multiple
webhook events can be received against the same message due to
misconfigurations in the Meta business manager account." A deduplication
guard exists, but it doesn't actually work under concurrency.
### Rationale
The existing dedup was a three-step sequence: check Redis (`GET`), check
the database, then set a Redis flag (`SETEX`). Two Sidekiq workers
processing duplicate Meta webhooks simultaneously would both complete
the `GET` before either executed the `SETEX`, so both would proceed to
create a message. The `source_id` column has a non-unique index, so the
database wouldn't catch the duplicate either. Each message then
independently fires `after_create_commit`, dispatching two
`message_created` webhook events to the customer.
```
Worker A Worker B
│ │
▼ ▼
Redis GET key ──► nil Redis GET key ──► nil
│ │
│ ◄── both pass guard ──► │
│ │
▼ ▼
Redis SETEX key Redis SETEX key
│ │
▼ ▼
BEGIN transaction BEGIN transaction
INSERT message INSERT message
DELETE Redis key ◄─┐ │
COMMIT │ DELETE Redis key
│ COMMIT
│ │
└── key gone before ───┘
B's commit lands
▼ ▼
after_create_commit after_create_commit
dispatch MESSAGE_CREATED dispatch MESSAGE_CREATED
│ │
▼ ▼
WebhookJob ──► n8n WebhookJob ──► n8n
(duplicate!)
```
There was a second, subtler problem visible in the diagram: the Redis
key was cleared *inside* the database transaction, before the
transaction committed. This opened a window where neither the Redis
check nor the database check would see the in-flight message.
The fix collapses the check-and-set into a single `SET NX EX` call,
which is atomic in Redis. The key is no longer eagerly cleared — it
expires naturally after 24 hours. The database lookup
(`find_message_by_source_id`) remains as a fallback for messages that
were created before the lock expired.
```
Worker A Worker B
│ │
▼ ▼
Redis SET NX ──► OK Redis SET NX ──► nil
│ │
▼ ▼
proceeds to create returns early
message normally (lock already held)
```
### Implementation Notes
The lock logic is extracted into `Whatsapp::MessageDedupLock`, a small
class that wraps a single `Redis SET NX EX` call. This makes the
concurrency guarantee testable in isolation — the spec uses a
`CyclicBarrier` to race two threads against the same key and asserts
exactly one wins, without needing database writes,
`use_transactional_tests = false`, or monkey-patching.
Because the Redis lock now persists (instead of being cleared
mid-transaction), existing WhatsApp specs needed an `after` hook to
clean up `MESSAGE_SOURCE_KEY::*` keys between examples. Transactional
fixtures only roll back the database, not Redis.
# Pull Request Template
## Description
This PR adds support for removing labels from the conversation card
context menu. Assigned labels now show a checkmark, and clicking an
already-selected label will remove it.
Fixes
https://linear.app/chatwoot/issue/CW-6400/allow-removing-labels-directly-from-the-right-click-menuhttps://github.com/chatwoot/chatwoot/issues/13367
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## How Has This Been Tested?
**Screencast**
https://github.com/user-attachments/assets/4e3a6080-a67d-4851-9d10-d8dbf3ceeb04
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
## Description
Adds missing analytics instrumentation for the editor AI funnel so we
can measure end-to-end usage and outcome quality.
### What was added
- Captain: Editor AI menu opened
- Captain: Generation failed
- Captain: AI-assisted message sent
### Behavior covered
- Tracks AI button click + menu open from both entry points:
- top panel sparkle button
- inline editor copilot button
- Tracks generation failures (initial + follow-up stages).
- Tracks whether accepted AI content was sent as-is or edited before
send.
### Notes
- Applies to editor Captain accept/send flow
(rewrite/summarize/reply_suggestion + follow-ups).
- Does not change Copilot sidebar flow instrumentation.
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update
## How Has This Been Tested?
### Manual verification steps
<img width="1906" height="832" alt="image"
src="https://github.com/user-attachments/assets/f0ade43b-aa8d-41be-8ca2-20a091a81f60"
/>
<img width="828" height="280" alt="image"
src="https://github.com/user-attachments/assets/be76219e-fb61-4a6e-bff5-dc085b0a3cc9"
/>
<img width="415" height="147" alt="image"
src="https://github.com/user-attachments/assets/36802c5c-33a7-49ed-bf7e-f0b02d86dccc"
/>
<img width="2040" height="516" alt="image"
src="https://github.com/user-attachments/assets/74b95288-bc86-4312-a282-14211ae8f25c"
/>
1. Open a conversation with Captain tasks enabled.
2. Click AI button in top panel and inline editor.
3. Confirm analytics events fire for:
- AI menu opened
4. Run an AI action and force a failure scenario (or empty response
path) and confirm generation-failed event.
5. Accept AI output, then:
- send without changes -> editedBeforeSend: false
- edit then send -> editedBeforeSend: true
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
## Description
Handle messages with null content properly in UI and email notifications
## Type of change
- [ ] Bug fix (non-breaking change which fixes an issue)
## Relevant Screenshots:
<img width="688" height="765" alt="Screenshot 2026-01-21 at 4 43 00 PM"
src="https://github.com/user-attachments/assets/6a27c22e-2ae6-4377-a05d-cfa44bf181fe"
/>
## Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Touches notification email templates and message rendering conditions;
mistakes could lead to missing content/attachments in emails or
incorrect UI visibility, but changes are localized and non-auth/security
related.
>
> **Overview**
> Agent notification emails for *assigned* and *participating* new
messages now include the actual message details (sender name, rendered
text when present, and attachment links) and gracefully fall back when
content is unavailable.
>
> To support this, the mailer now passes `@message` into Liquid via
`MessageDrop` (adding `attachments` URLs), and the dashboard message UI
now renders failed/external-error messages even when `content` is `null`
while tightening retry eligibility to require content or attachments
(and still within 1 day).
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
475c8cedda54eb5e806990f977faf8098d0b27d8. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
## Description
Fixes a critical bug where conversations assigned to a team could be
auto-assigned to agents outside that team when all team members were at
capacity.
## Type of change
- [ ] Bug fix (non-breaking change which fixes an issue)
## Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Changes core assignment selection for both legacy and v2 flows;
misconfiguration of `allow_auto_assign` or team membership could cause
conversations to remain unassigned.
>
> **Overview**
> Prevents auto-assignment from crossing team boundaries by filtering
eligible agents to the conversation’s `team` members (and requiring
`team.allow_auto_assign`) in both the legacy `AutoAssignmentHandler`
path and the v2 `AutoAssignment::AssignmentService` (including the
Enterprise override).
>
> Adds test coverage to ensure team-scoped conversations only assign to
team members, and are skipped when team auto-assign is disabled or no
team members are available; also updates the conversations controller
spec setup to include team membership.
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
67ed2bda0cd8ffd56c7e0253b86369dead2e6155. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
## Summary
Fix hardcoded `Chatwoot` branding in two UI tooltips using the existing
`useBranding` flow so self-hosted/white-label deployments no longer show
the wrong brand text.
## Changes
- LabelSuggestion tooltip now uses:
- `replaceInstallationName($t('LABEL_MGMT.SUGGESTIONS.POWERED_BY'))`
- Message avatar tooltip (native app/external echo) now uses:
- `replaceInstallationName(t('CONVERSATION.NATIVE_APP_ADVISORY'))`
## Why
This follows the existing branding pattern already used in the product
and keeps behavior consistent across deployments.
## Notes
- No change to message logic or API behavior.
- `AGENTS.md` updated with a branding guidance note.
## Fixes
- Fixes https://github.com/chatwoot/chatwoot/issues/13306
- Fixes https://github.com/chatwoot/chatwoot/issues/13466
## Testing
<img width="195" height="155" alt="Screenshot 2026-02-13 at 3 55 39 PM"
src="https://github.com/user-attachments/assets/5b295cdd-6e5d-42c0-bbd7-23ba7052e1c3"
/>
<img width="721" height="152" alt="Screenshot 2026-02-13 at 3 55 48 PM"
src="https://github.com/user-attachments/assets/19cec2a0-451f-4fb3-bd61-7c2e591fc3c7"
/>
## Why
We observed `Webhooks::TwilioEventsJob` failures ending up in Sidekiq
dead jobs when Twilio callback payloads could not be mapped to a
`Channel::TwilioSms` record. In this scenario, channel lookup raised
`ActiveRecord::RecordNotFound`, which caused retries and eventual dead
jobs instead of a graceful drop.
Related Sentry issue/search:
-
https://chatwoot-p3.sentry.io/issues/?project=6382945&query=Webhooks%3A%3ATwilioEventsJob%20ActiveRecord%3A%3ARecordNotFound
## What changed
This PR keeps the existing lookup flow but makes it non-raising:
- `app/services/twilio/incoming_message_service.rb`
- `find_by!` -> `find_by` for account SID + phone lookup
- Added warning log when channel lookup misses
- `app/services/twilio/delivery_status_service.rb`
- `find_by!` -> `find_by` for account SID + phone lookup
- Added warning log when channel lookup misses
## Reproduction
Configure a Twilio webhook callback that reaches Chatwoot but does not
match an existing Twilio channel lookup path. Before this change, the
job raises `RecordNotFound` and can end up in dead jobs after retries.
After this change, the job logs the miss and exits safely.
## Testing
- `bundle exec rspec
spec/services/twilio/incoming_message_service_spec.rb
spec/services/twilio/delivery_status_service_spec.rb`
- `bundle exec rubocop app/services/twilio/incoming_message_service.rb
app/services/twilio/delivery_status_service.rb`