develop
1876
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b823764199 | refactor: optimize agent bot assignment dropdown (#14866) | ||
|
|
11deffdd5d |
feat: billing brl pix new users (#14617)
## Linear ticket - https://linear.app/chatwoot/issue/CW-7253/billing-brl-pix-new-users ## Description New accounts that sign up in Brazilian Portuguese are now billed in BRL instead of USD. Their Stripe customer is created with a Brazil address and Portuguese locale (so the Stripe portal offers Real prices and PIX), and the AI credit top-up flow shows packages priced in the account's billing currency. Currency support is config-driven, so adding another currency later is a configuration change rather than a code change. ## Type of change - [ ] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? - https://www.loom.com/share/c8d3d08c1b844ed6b820438d4209491a ## Screenshot <img width="904" height="440" alt="image" src="https://github.com/user-attachments/assets/6f19fad8-e6af-46ea-b99f-b0265bb9eeec" /> ## 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 |
||
|
|
8818d276b9 |
fix(captain): read OpenAI key from InstallationConfig in article search terms (#14915)
generate_article_search_terms still pulled ENV['OPENAI_API_KEY'], left over from before the Jan 2025 Captain migration moved the key into InstallationConfig as CAPTAIN_OPEN_AI_API_KEY. Every other Captain LLM call site got updated then; this one (used by Portal::ArticleIndexingJob for help center article embedding search terms) didn't, so it sent a blank bearer token unless you also happened to have the old env var set. Also drops the stale OPENAI_API_KEY line from .env.example and points to where the key actually lives now (Super Admin > App Configs > Captain). --------- Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com> Co-authored-by: Sony Mathew <sony@chatwoot.com> |
||
|
|
b8545019e1 |
feat: add catch-all failure handling to article writer (#14881)
Help-center onboarding jobs were getting stuck in the `generating` state
indefinitely. `Onboarding::HelpCenterArticleWriterJob` only finalized
the generation counter for two exception types
(`Firecrawl::FirecrawlError`, `ArticleBuildFailed`). Any other error
exhausted Sidekiq's default retries and landed in the dead set without
ever bumping `finished`, leaving state at `total - 1` / `generating`
until the 7-day Redis TTL expired. Observed in production: 4 generations
wedged at exactly `finished = total - 1`, four days after the run, with
no further progress — the onboarding UI showed "generating" the whole
time.
This PR adds a `discard_on StandardError` catch-all to
`HelpCenterArticleWriterJob`, after the existing `retry_on
Firecrawl::FirecrawlError` and `discard_on ArticleBuildFailed` handlers.
ActiveJob matches in declaration order, first match wins, so existing
behavior is unchanged — the catch-all only absorbs errors that
previously fell through to unhandled retry-then-dead-set. It routes
through the same `on_writer_failure` → `finalize` path, so state always
progresses to `completed`.
<details><summary>
##### Script to remove dead jobs
</summary>
<p>
```rb
# One-off: unstick Onboarding::HelpCenterGenerationState keys that are wedged in
# "generating" because a writer job died on an unhandled exception (neither
# FirecrawlError nor ArticleBuildFailed) and exhausted Sidekiq retries without
# ever calling record_article_finished.
#
# These portals have real articles (finished is 1 short of total). Marking them
# "completed" is the honest terminal state: generation is done, one article failed.
#
# Run on a prod box (dry-run first, then REMOVE_DRY_RUN=1):
# RAILS_ENV=production bundle exec rails runner scripts/help_center_investigation/unstick_generating.rb
#
# To actually write, set REMOVE_DRY_RUN=1 in the environment.
pattern = format(Redis::Alfred::HELP_CENTER_GENERATION, id: '*')
dry_run = ENV['REMOVE_DRY_RUN'].blank?
stuck = []
Redis::Alfred.with do |conn|
conn.scan_each(match: pattern, count: 1000) do |key|
h = conn.hgetall(key)
next unless h['status'] == 'generating'
gen_id = key.sub('HELP_CENTER_GENERATION::', '')
stuck << { gen_id: gen_id, total: h['total'], finished: h['finished'], key: key }
unless dry_run
conn.hset(key, 'status', 'completed')
conn.expire(key, Onboarding::HelpCenterGenerationState::TTL)
end
end
end
puts "#{dry_run ? '[DRY RUN] ' : ''}Found #{stuck.size} stuck 'generating' states:"
stuck.each do |s|
puts " gen=#{s[:gen_id]} total=#{s[:total]} finished=#{s[:finished]} -> #{dry_run ? 'would mark completed' : 'marked completed'}"
end
puts
puts 'Re-run with REMOVE_DRY_RUN=1 to apply.' if dry_run
````
</p>
</details>
|
||
|
|
6a7ca9dd3b |
feat: Add report bar drilldown drawer (#14626)
## Description Adds drilldown support for report bar charts powered by `ReportContainer`. Clicking a non-zero report bar now opens a right-side drawer with the conversations or messages that contributed to that bucket, with each row linking to the underlying conversation and message rows linking with `messageId`. This includes a new `GET /api/v2/accounts/:account_id/reports/drilldown` endpoint, backend drilldown builders/serializers, generic chart click emission, local drawer state via `useReportDrilldown`, compact drilldown cards, pagination, stale-response protection, and validation for unsupported drilldown dimensions. Fixes # CW-4497 https://linear.app/chatwoot/issue/CW-4497/drill-down-on-agent-conversations-report ## 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? Ran the focused backend and frontend checks for the drilldown endpoint, builder, chart click handling, drawer/card UI, API helper, and stale-response handling. Here are the screenshots on how it looks like: <img width="1792" height="1199" alt="Screenshot 2026-06-02 at 11 32 11 PM" src="https://github.com/user-attachments/assets/6bdb8832-b9df-4bf3-9a2a-beaefe203b6e" /> <img width="1791" height="1230" alt="Screenshot 2026-06-02 at 11 32 34 PM" src="https://github.com/user-attachments/assets/36e92eb7-3208-4855-87f4-0c7f316df54d" /> <img width="1784" height="1235" alt="Screenshot 2026-06-02 at 11 32 46 PM" src="https://github.com/user-attachments/assets/f7a53916-74f2-4622-9305-042e0ac9e877" /> ## 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 - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com> Co-authored-by: Shivam Mishra <scm.mymail@gmail.com> |
||
|
|
6c9efc4e92 |
fix: assign outbound voice call conversation to the calling agent (#14906)
Outbound voice calls were being auto-assigned to the wrong agent. When an agent placed an outbound call, the conversation was created without an assignee, so inboxes with auto-assignment enabled would round-robin it to a different agent instead of keeping it with the person who actually made the call. This made it hard to tell which agent was on an active call. ## What changed - Set the calling agent as the conversation's assignee when creating an outbound voice call conversation. - This prevents the generic auto-assignment handler from treating the conversation as unassigned and reassigning it. - Added specs covering the assignment, including a regression case with inbox auto-assignment enabled. **Note:** this applies to newly placed calls; it does not retroactively fix conversations that were already mis-assigned. --------- Co-authored-by: Tanmay Deep Sharma <tanmaydeepsharma21@gmail.com> Co-authored-by: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com> |
||
|
|
34d8741b9b | feat: drag to reorder help center articles across pages (#14910) | ||
|
|
7bf76057c2 |
fix: SLA handling for blocked contacts (#14861)
# Pull Request Template ## Description Blocked contacts are now excluded from SLA assignment, processing, reports, and conversation SLA UI while they remain blocked. Existing SLA records are preserved, and SLA behavior resumes if the contact is unblocked. Fixes https://linear.app/chatwoot/issue/CW-7435/sla-should-not-trigger-for-blocked-contacts ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? - `bundle exec rspec spec/enterprise/models/conversation_spec.rb spec/enterprise/models/applied_sla_spec.rb spec/enterprise/services/enterprise/action_service_spec.rb spec/enterprise/services/sla/evaluate_applied_sla_service_spec.rb spec/enterprise/jobs/sla/process_account_applied_slas_job_spec.rb spec/enterprise/controllers/api/v1/accounts/applied_slas_controller_spec.rb spec/enterprise/controllers/api/v1/accounts/conversations_controller_spec.rb spec/enterprise/controllers/enterprise/api/v1/accounts/conversations_controller_spec.rb spec/enterprise/presenters/conversations/event_data_presenter_spec.rb` — 78 examples, 0 failures - `bundle exec rubocop enterprise/app/controllers/api/v1/accounts/applied_slas_controller.rb enterprise/app/jobs/sla/process_account_applied_slas_job.rb enterprise/app/models/applied_sla.rb enterprise/app/models/enterprise/concerns/conversation.rb enterprise/app/presenters/enterprise/conversations/event_data_presenter.rb enterprise/app/services/enterprise/action_service.rb enterprise/app/services/sla/evaluate_applied_sla_service.rb lib/tasks/apply_sla.rake spec/enterprise/controllers/api/v1/accounts/applied_slas_controller_spec.rb spec/enterprise/controllers/api/v1/accounts/conversations_controller_spec.rb spec/enterprise/controllers/enterprise/api/v1/accounts/conversations_controller_spec.rb spec/enterprise/jobs/sla/process_account_applied_slas_job_spec.rb spec/enterprise/models/applied_sla_spec.rb spec/enterprise/models/conversation_spec.rb spec/enterprise/presenters/conversations/event_data_presenter_spec.rb spec/enterprise/services/enterprise/action_service_spec.rb spec/enterprise/services/sla/evaluate_applied_sla_service_spec.rb` — no offenses - `pnpm exec vitest --no-watch --no-cache --no-coverage app/javascript/dashboard/components/widgets/conversation/specs/ConversationCard.spec.js` — 2 tests passed - `pnpm exec eslint app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreviewWithMeta.vue app/javascript/dashboard/components-next/Conversation/ConversationCard/ConversationCardExpanded.vue app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue app/javascript/dashboard/components/widgets/conversation/specs/ConversationCard.spec.js` — passed with existing raw-text warnings in `ConversationHeader.vue` - `git diff --cached --check` — clean ## 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 - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> |
||
|
|
a3c7f3b204 |
fix: finalize WhatsApp calls when terminate webhook overtakes connect (#14836)
## Description Some inbound WhatsApp calls stayed stuck in "ringing" forever. When a caller hung up within ~1s of dialing, Meta delivered the terminate webhook before the connect webhook. The terminate arrived with no call record yet and was dropped, then connect created the call in ringing with nothing left to close it. These calls now correctly land as missed (no_answer), and an agent who taps Accept on a call that already ended gets a clean "call ended" instead of a generic error. ## Type of change - [ ] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? - local UI testing ## 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 |
||
|
|
49b0ab0e1f |
fix: Consider business hours when computing SLA breaches (#13392)
- Fixes SLA breach computation to respect the "Only during business hours" setting - Backend now pre-computes SLA deadlines, simplifying frontend logic ## How it works Before: SLA deadlines were calculated using wall-clock time, ignoring business hours. After: When an SLA policy has "Only during business hours" enabled and the inbox has working hours configured, the deadline is calculated by adding threshold time only during business hours. **How you check if a conversation has a SLA hit or miss?** <img width="474" height="510" alt="Screenshot 2026-01-28 at 7 06 53 PM" src="https://github.com/user-attachments/assets/54ec8581-18b8-45c6-a356-de8c778ea78d" /> **Example:** - Conversation created: Friday 4:30 PM - FRT threshold: 1 hour - Business hours: Mon-Fri 9 AM - 5 PM | | Breach time | |--|--| | Before | Friday 5:30 PM | | After | Monday 9:30 AM | ## Test plan - [x] Create an SLA policy with "Only during business hours" enabled - [x] Configure inbox with business hours (e.g., Mon-Fri 9-5) - [x] Conversation created during business hours - Create a conversation on Wednesday 10:00 AM UTC - Expected: FRT deadline shows Wednesday 12:00 PM UTC (2 business hours later) - [x] Conversation created before business hours - Create a conversation on Wednesday 7:00 AM UTC - Expected: FRT deadline shows Wednesday 11:00 AM UTC (counting starts at 9 AM) - [x] Conversation created after business hours - Create a conversation on Wednesday 6:00 PM UTC - Expected: FRT deadline shows Thursday 11:00 AM UTC (counting starts next day 9 AM) - [x] Conversation created on weekend - Create a conversation on Saturday 10:00 AM UTC - Expected: FRT deadline shows Monday 11:00 AM UTC (skips weekend) - [x] Threshold spans weekend - Create a conversation on Friday 4:00 PM UTC with 2-hour FRT - Expected: FRT deadline shows Monday 10:00 AM UTC (1h Friday + 1h Monday) - [x] SLA without business hours - Create an SLA policy with only_during_business_hours: false - Create a conversation on Friday 4:00 PM UTC with 2-hour FRT - Expected: FRT deadline shows Friday 6:00 PM UTC (wall-clock time) - [x] All Day marked as closed_all_day - Create a conversation on Tuesday 4:00 PM UTC with 2-hour FRT - Expected: FRT deadline shows Thursday 10:00 AM UTC - [x] All Day marked as open_all_day - Create a conversation on Saturday 10:00 AM UTC with 2-hour FRT - Expected: FRT deadline shows Saturday 12:00 PM UTC - [x] UI displays correct countdown - Verify conversation card shows correct SLA timer - Verify timer shows flame icon when breached - Verify timer shows alarm icon when within threshold - Time updates automatically when time passes - [x] Verify the breach with a different timezone than your local timezone --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: Sojan Jose <sojan@pepalo.com> Co-authored-by: Sony Mathew <sony@chatwoot.com> Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com> |
||
|
|
926a9d8a69 |
fix(captain): default temperature to 0.5 and remove UI control (#14879)
# Pull Request Template ## Description - Default temperature to 0.5 and remove UI control - No migrations needed for existing accounts, their current settings are preserved ## Type of change - [x] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. locally and spec ## 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 - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] Any dependent changes have been merged and published in downstream modules |
||
|
|
ce2e10e89e |
fix: tighten captain v2 (#14883)
# Pull Request Template ## Description Tightens v2 prompt and config to match v1 ## Type of change Please delete options that are not relevant. - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. locally ## 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 - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] Any dependent changes have been merged and published in downstream modules |
||
|
|
56275b750e |
fix: respect companies feature flag for auto-association (#14886)
# Pull Request Template ## Description This PR stops new contacts from getting an auto-assigned company name when the Companies feature is disabled. Since #14496, email-domain company auto-association also updates a contact's `company_name`. However, the callback isn't gated behind the Companies feature flag, so accounts without the feature enabled still auto-create companies and overwrite any `company_name` provided via the SDK/`setUser`. This PR gates `should_associate_company?` behind `account.feature_enabled?('companies')`, so auto-association only runs when the Companies feature is enabled. Fixes https://linear.app/chatwoot/issue/CW-7462/setuser-overwrites-contact-company-name-for-accounts-that-dont-use ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## 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 - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules |
||
|
|
299bc6c0a4 |
fix: recover from stale LeadSquared lead ids on activity sync (#14818)
LeadSquared sync now recovers automatically when a contact's cached lead has been deleted or merged on the LeadSquared side. Previously the stale lead id was never cleared, so every new conversation or contact update for that contact failed with "Lead not found" (`MXInvalidEntityReferenceException`) indefinitely. ## What changed - Activity sync: on a "Lead not found" error while posting a conversation/transcript activity, clear the cached `leadsquared_id`, re-resolve the contact to a fresh lead, and retry the activity once (guarded against loops and duplicate leads). - Contact sync: on the same error while updating an existing lead, clear the cached id and create a fresh lead instead. - Fix `get_lead_id` to actually return early for unidentifiable contacts (the guard previously fell through). ## How to reproduce 1. For a LeadSquared-enabled account, point a contact's cached lead id at a lead that no longer exists in LeadSquared. 2. Update the contact, or create/resolve a conversation for it. 3. Before: the sync fails repeatedly with "Lead not found" and never self-corrects. After: the stale id is cleared, a fresh lead is resolved/created, and subsequent syncs reuse the healed id. --------- Co-authored-by: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com> |
||
|
|
7522457740 |
feat: v2 - generations get trace level attributes (#14878)
# Pull Request Template ~~Note: merge only after https://github.com/chatwoot/ai-agents/pull/74 has been merged~~ ## Description Before: <img width="436" height="617" alt="image" src="https://github.com/user-attachments/assets/fd5be8dc-abab-4e01-b251-366648b998ea" /> After: <img width="446" height="577" alt="image" src="https://github.com/user-attachments/assets/8d96d2f2-c126-4cca-b3a2-f2a62b204adf" /> <img width="441" height="558" alt="image" src="https://github.com/user-attachments/assets/89da4157-48a5-4fdc-9c67-9bf987e31638" /> ## Type of change - [x] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. locally and specs ## 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 - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Sony Mathew <sony@chatwoot.com> |
||
|
|
d0b1c055e8 |
chore: Track cloud plan activation conversions (#14834)
## Summary - track cloud plan activation conversions when an attributed account moves from the configured default cloud plan to a paid plan - use the Stripe webhook event time as the activation timestamp so the 30-day signup attribution window reflects the actual upgrade event - send the Stripe subscription amount and currency for the conversion value - mark the account attribution after enqueueing so later plan updates do not send duplicate activation conversions ## Notes - Marketing tracker: https://linear.app/chatwoot/issue/MAR-113 - Cloud implementation: https://linear.app/chatwoot/issue/LEA-34 - Stripe billing stays responsible for subscription state and value calculation. - Cloud plan activation conversion tracking is handled by a small dedicated service that owns the activation rule, duplicate marker, and conversion enqueue. - Website attribution cookie capture remains separate in the marketing attribution service. - There is no frontend change and no new user-facing configuration. - Conversion upload still no-ops outside Chatwoot Cloud and when attribution has no supported click identifier. |
||
|
|
cf134deb37 |
fix: Preserve Captain LLM defaults (#14858)
# Pull Request Template ## Description Adjusts the Captain LLM feature defaults after feature routing so the defaults stay intentional and avoid unintended high-cost model upgrades. Assistant, copilot, and onboarding content generation now default to `gpt-4.1`; audio transcription keeps `gpt-4o-mini-transcribe` as the default while exposing `whisper-1` as an available account override option. Related: https://linear.app/chatwoot/issue/CW-7425/test-new-models ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? - `ruby -ryaml -e 'yaml = YAML.load_file("config/llm.yml"); features = yaml.fetch("features"); models = yaml.fetch("models"); features.each { |name, cfg| missing = Array(cfg["models"]) - models.keys; raise "#{name}: missing #{missing.join(",")}" if missing.any?; raise "#{name}: default not in models" unless Array(cfg["models"]).include?(cfg["default"]) }'` - `node -e "JSON.parse(require('fs').readFileSync('app/javascript/dashboard/i18n/locale/en/settings.json', 'utf8'))"` - `pnpm exec prettier --check app/javascript/dashboard/i18n/locale/en/settings.json` - `bundle exec rspec spec/lib/llm/feature_router_spec.rb spec/enterprise/services/llm/base_ai_service_spec.rb spec/enterprise/models/concerns/agentable_spec.rb spec/enterprise/services/messages/audio_transcription_service_spec.rb spec/models/concerns/captain_featurable_spec.rb spec/models/account_spec.rb spec/controllers/api/v1/accounts/captain/preferences_controller_spec.rb spec/controllers/super_admin/accounts_controller_spec.rb` - `bundle exec rspec spec/enterprise/services/captain/llm/article_translation_service_spec.rb spec/enterprise/services/messages/audio_transcription_service_spec.rb spec/enterprise/services/llm/base_ai_service_spec.rb` - `RUBOCOP_CACHE_ROOT=/private/tmp/rubocop_cache bundle exec rubocop enterprise/app/services/captain/llm/article_translation_service.rb enterprise/app/services/messages/audio_transcription_service.rb spec/enterprise/services/captain/llm/article_translation_service_spec.rb spec/enterprise/services/llm/base_ai_service_spec.rb spec/enterprise/services/messages/audio_transcription_service_spec.rb` - `git diff --check` ## 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 - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules |
||
|
|
b8b62ad0f1 |
feat: Harden model override preferences (5/6) (#14846)
## Description Hardens the Captain model override preferences API so account-level overrides follow the same feature-router contract used by runtime LLM calls. The API now permits model and feature keys from `llm.yml`, removes blank model overrides, rejects invalid saved model combinations, and returns each feature's effective model, provider, and source for UI clients. Fixes https://linear.app/chatwoot/issue/CW-7425/test-new-models ## Type of change - [ ] 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? Verified the account preferences API and account model validation behavior for valid overrides, invalid model values, unknown feature keys, blank override removal, and effective model/provider/source payload metadata. - `eval "$(rbenv init -)" && bundle exec rspec spec/controllers/api/v1/accounts/captain/preferences_controller_spec.rb spec/models/account_spec.rb spec/models/concerns/captain_featurable_spec.rb spec/lib/llm/feature_router_spec.rb` - `eval "$(rbenv init -)" && bundle exec rubocop app/controllers/api/v1/accounts/captain/preferences_controller.rb app/models/concerns/account_settings_schema.rb app/models/concerns/captain_featurable.rb spec/controllers/api/v1/accounts/captain/preferences_controller_spec.rb spec/models/account_spec.rb spec/models/concerns/captain_featurable_spec.rb spec/lib/llm/feature_router_spec.rb` - `git diff --check` ## 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 - [ ] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules |
||
|
|
4e26c5b4bb |
feat: Route system LLM jobs (4/6) (#14843)
## Description Routes the remaining system-only and legacy-sensitive LLM jobs through feature-level model configuration, while preserving system credential usage and usage-accounting behavior. This adds dedicated defaults for help center article generation, onboarding content generation, query translation, transcription, and search embeddings so these flows can be configured per account without falling back to installation-wide model settings. Fixes https://linear.app/chatwoot/issue/CW-7425/test-new-models ## Type of change - [ ] 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? Verified the feature routing defaults and account overrides for the touched Captain/system LLM paths, including the legacy OpenAI transcription and paginated FAQ services. - `eval "$(rbenv init -)" && bundle exec rspec spec/lib/captain/base_task_service_spec.rb spec/lib/llm/models_spec.rb spec/models/concerns/captain_featurable_spec.rb spec/controllers/api/v1/accounts/captain/preferences_controller_spec.rb spec/enterprise/services/captain/llm/paginated_faq_generator_service_spec.rb spec/enterprise/services/captain/llm/pdf_processing_service_spec.rb spec/enterprise/services/messages/audio_transcription_service_spec.rb spec/enterprise/services/onboarding/help_center_article_builder_spec.rb spec/enterprise/services/captain/onboarding/website_analyzer_service_spec.rb` - `eval "$(rbenv init -)" && bundle exec rubocop app/controllers/api/v1/accounts/captain/preferences_controller.rb app/models/concerns/account_settings_schema.rb lib/captain/base_task_service.rb enterprise/app/services/captain/llm/article_translation_service.rb enterprise/app/services/captain/llm/article_writer_service.rb enterprise/app/services/captain/llm/embedding_service.rb enterprise/app/services/captain/llm/help_center_curation_service.rb enterprise/app/services/captain/llm/paginated_faq_generator_service.rb enterprise/app/services/captain/llm/translate_query_service.rb enterprise/app/services/captain/llm/widget_tagline_service.rb enterprise/app/services/captain/onboarding/website_analyzer_service.rb enterprise/app/services/messages/audio_transcription_service.rb spec/enterprise/services/messages/audio_transcription_service_spec.rb spec/lib/captain/base_task_service_spec.rb` - `ruby -e "require 'yaml'; config = YAML.load_file('config/llm.yml'); %w[document_faq_generation help_center_article_generation onboarding_content_generation help_center_query_translation audio_transcription help_center_search].each { |feature| abort(%(missing #{feature})) unless config.dig('features', feature) }; abort('wrong article default') unless config.dig('features', 'help_center_article_generation', 'default') == 'gpt-5.2'; puts 'llm.yml ok'"` - `git diff --check` ## 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 - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules |
||
|
|
43e0f8a141 |
feat: Route Enterprise LLM services (3/6) (#14841)
# Pull Request Template ## Description Routes Enterprise assistant, copilot, FAQ, contact memory, action-classifier, and false-promise detector LLM paths through feature-specific model resolution. `Llm::BaseAiService` now accepts feature/account context and uses `Llm::FeatureRouter` when that context is present, while retaining the installation-model fallback for unmigrated callers. This also adds a `document_faq_generation` feature default for generative FAQ/document content. Linear: https://linear.app/chatwoot/issue/CW-7425/test-new-models Depends on #14840 ## Type of change - [ ] 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? - `bundle exec rspec spec/lib/llm/models_spec.rb spec/enterprise/services/llm/base_ai_service_spec.rb spec/enterprise/services/captain/copilot/chat_service_spec.rb spec/enterprise/services/captain/llm/assistant_chat_service_spec.rb spec/enterprise/services/captain/llm/faq_generator_service_spec.rb spec/enterprise/services/captain/llm/conversation_faq_service_spec.rb spec/enterprise/services/captain/llm/assistant_action_classifier_service_spec.rb spec/enterprise/services/captain/llm/assistant_false_promise_service_spec.rb spec/enterprise/jobs/captain/conversation/response_builder_job_spec.rb` passed with 112 examples, 0 failures. - `bundle exec rspec spec/models/concerns/captain_featurable_spec.rb spec/models/account_spec.rb spec/controllers/api/v1/accounts/captain/preferences_controller_spec.rb spec/lib/llm/feature_router_spec.rb` passed with 87 examples, 0 failures. - `bundle exec rubocop enterprise/app/services/llm/base_ai_service.rb enterprise/app/services/captain/copilot/chat_service.rb enterprise/app/services/captain/llm/assistant_chat_service.rb enterprise/app/services/captain/llm/faq_generator_service.rb enterprise/app/services/captain/llm/conversation_faq_service.rb enterprise/app/services/captain/llm/contact_notes_service.rb enterprise/app/services/captain/llm/contact_attributes_service.rb enterprise/app/services/captain/llm/assistant_action_classifier_service.rb enterprise/app/services/captain/llm/assistant_false_promise_service.rb spec/enterprise/services/llm/base_ai_service_spec.rb spec/enterprise/services/captain/copilot/chat_service_spec.rb spec/enterprise/services/captain/llm/assistant_chat_service_spec.rb spec/enterprise/services/captain/llm/faq_generator_service_spec.rb spec/enterprise/services/captain/llm/conversation_faq_service_spec.rb spec/enterprise/services/captain/llm/assistant_action_classifier_service_spec.rb spec/enterprise/services/captain/llm/assistant_false_promise_service_spec.rb spec/enterprise/jobs/captain/conversation/response_builder_job_spec.rb` passed with no offenses. - `bundle exec ruby -e "require 'yaml'; config = YAML.load_file('config/llm.yml'); abort('missing document_faq_generation') unless config.dig('features', 'document_faq_generation'); abort('missing default') unless config.dig('features', 'document_faq_generation', 'default'); puts 'llm.yml ok'"` passed. - `git diff --check` passed. ## 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 - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules |
||
|
|
91d8a4e2a3 |
feat: Route reply box LLM tasks (2/6) (#14840)
# Pull Request Template ## Description Routes OSS reply-box and small Captain tasks through feature-specific LLM model resolution. Rewrite, reply suggestion, summary, follow-up, and CSAT utility analysis now resolve through the `editor` feature; label suggestion resolves through `label_suggestion`. Existing credentials, account OpenAI hook behavior, and instrumentation event names remain unchanged. Linear: https://linear.app/chatwoot/issue/CW-7425/test-new-models Depends on #14839 ## Type of change - [ ] 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? - `bundle exec rspec spec/lib/captain/base_task_service_spec.rb spec/lib/captain/rewrite_service_spec.rb spec/lib/captain/reply_suggestion_service_spec.rb spec/lib/captain/summary_service_spec.rb spec/lib/captain/label_suggestion_service_spec.rb spec/lib/captain/csat_utility_analysis_service_spec.rb spec/lib/captain/follow_up_service_spec.rb` passed with 81 examples, 0 failures. - `bundle exec rubocop lib/captain/base_task_service.rb lib/captain/rewrite_service.rb lib/captain/reply_suggestion_service.rb lib/captain/summary_service.rb lib/captain/label_suggestion_service.rb lib/captain/csat_utility_analysis_service.rb lib/captain/follow_up_service.rb enterprise/lib/enterprise/captain/reply_suggestion_service.rb spec/lib/captain/base_task_service_spec.rb spec/lib/captain/rewrite_service_spec.rb spec/lib/captain/reply_suggestion_service_spec.rb spec/lib/captain/summary_service_spec.rb spec/lib/captain/label_suggestion_service_spec.rb spec/lib/captain/csat_utility_analysis_service_spec.rb spec/lib/captain/follow_up_service_spec.rb` passed with no offenses. - `git diff --check` passed. ## 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 - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules |
||
|
|
8b977b35a8 |
feat: Add LLM feature router (1/6) (#14839)
## Description Adds the foundation for feature-specific LLM model routing so Captain AI features can resolve their effective provider/model from code defaults and account-level overrides. This fixes the provider metadata key in `config/llm.yml`, adds `Llm::FeatureRouter`, and routes existing `CaptainFeaturable` model defaults through the shared resolver. Fixes https://linear.app/chatwoot/issue/CW-7425/test-new-models ## Type of change - [ ] 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? - `bundle exec rspec spec/lib/llm/models_spec.rb spec/lib/llm/feature_router_spec.rb spec/models/concerns/captain_featurable_spec.rb` - 23 examples, 0 failures - `bundle exec rubocop lib/llm/models.rb lib/llm/feature_router.rb app/models/concerns/captain_featurable.rb spec/lib/llm/models_spec.rb spec/lib/llm/feature_router_spec.rb spec/models/concerns/captain_featurable_spec.rb` - no offenses - `bundle exec ruby -e "require 'yaml'; config = YAML.load_file('config/llm.yml'); abort('missing providers') unless config['providers']; abort('missing models') unless config['models']; abort('missing features') unless config['features']; puts 'llm.yml ok'"` - `git diff --check` ## 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 - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules |
||
|
|
62cbeae95f |
feat: onboarding inboxes UI (#14565)
After entering their account details, new admins land on an **Inbox setup** screen that shows what we've already set up for them and lets them connect their conversation channels without leaving onboarding. It surfaces the auto-created live chat widget (and Help Center on Enterprise), highlights channels detected from their website, and offers a **View all** dialog to connect any supported channel inline. ### Channel status | Channel | How it connects | Status | PR | |---|---|---|---| | Live chat (Website) | Auto-created during setup | ✅ Done | https://github.com/chatwoot/chatwoot/pull/14314 | | WhatsApp | Meta embedded signup | ✅ Done | https://github.com/chatwoot/chatwoot/pull/14619 | | Facebook | Login + page picker | ✅ Done | https://github.com/chatwoot/chatwoot/pull/14619 | | Instagram | OAuth redirect | ✅ Done | https://github.com/chatwoot/chatwoot/pull/14568 | | TikTok | OAuth redirect | ✅ Done | https://github.com/chatwoot/chatwoot/pull/14569 | | LINE | Inline credential form | ✅ Done | — | | Telegram | Inline credential form | ✅ Done | — | | Gmail / Outlook | OAuth (email) | ⚠️ Disabled — coming in a follow-up | https://github.com/chatwoot/chatwoot/pull/14567 | | SMS / API / Voice / Other email | — | ⛔ Unavailable | — | --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> |
||
|
|
7c7459b734 |
fix(whatsapp): override webhook at phone number level (#13817)
## Description Move WhatsApp webhook callback override from WABA level to phone number level, allowing multiple phone numbers on the same WABA to have independent callback URLs. ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? - Connect a WhatsApp Cloud inbox via embedded signup - Verify webhook setup succeeds and messages are received - Connect a second phone number on the same WABA — both should receive messages independently - Delete an inbox and verify only that phone number's override is cleared ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes --------- Co-authored-by: tds-1 <tds-1@users.noreply.github.com> Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
0849d2e070 |
chore: Track cloud signup conversions (#14852)
## Summary - enqueue `cloud_signup` conversion tracking after website attribution is stored on a new cloud account - keep authenticated add-workspace requests out of attribution/conversion tracking - reuse the existing marketing conversion tracking service and config already available on `develop` ## Notes - This PR only wires the signup path. - Billing/plan activation conversion tracking is intentionally left out for a separate rollout. - The conversion service still no-ops outside Chatwoot Cloud and when stored attribution has no supported click identifier. |
||
|
|
cd1a214d98 |
chore: Add marketing conversion tracking foundation (#14851)
## Summary Adds the minimal foundation for Cloud marketing conversion tracking: - locked internal installation config for conversion tracking credentials and event mappings - generic background job and service for uploading conversion events from stored attribution - focused coverage for Cloud gating, click-id selection, payload shape, and optional conversion value This PR intentionally does not wire signup or plan activation yet. The next step is to validate the service from Rails console against existing attributed accounts, then add the event hooks in a follow-up PR. ## Notes The config remains locked and is surfaced under the Internal settings group next to the existing Cloud plan configuration. The service assumes the locked config is present and valid; config mistakes should surface instead of being silently ignored. |
||
|
|
f9cc702030 |
fix(help-center): documentation layout on custom domain root, locale (#14850)
Help centers using the documentation layout now render correctly in two cases that previously fell back to the wrong output. Opening a portal at its custom-domain root (e.g. docs.example.com/ ) now shows the full documentation home in place instead of the classic layout, and portals whose locale is a region variant that Chatwoot doesn't ship a translation for (e.g. th_TH , fr_ML ) now show their categories in the sidebar on article pages. Closes https://linear.app/chatwoot/issue/CW-7437/portal-layout-is-not-properly-working-in-custom-domain ## How to test 1. Create a portal with the documentation layout and a custom domain (e.g. example.chat.test ), with at least one category containing a published article. 2. Visit the custom-domain root ( http://example.chat.test:3000/ ) → it should show the full documentation home (sidebar, topbar), not the classic layout, with no redirect. 3. Set the portal's locale to a region variant Chatwoot doesn't translate, e.g. th_TH , with categories/articles under that locale. 4. Open an article → the sidebar should list the article's category and sibling articles. Before the fix the sidebar was empty for these locales. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
74db16158d |
feat: migrate dyte integration to cloudflare realtimekit (#14752)
Dyte is sunsetting its existing infrastructure after the Cloudflare acquisition, so this migrates Chatwoot’s video call integration to Cloudflare RealtimeKit. The integration now uses Cloudflare Account ID, RealtimeKit App ID, and a Cloudflare API token with Realtime Admin permissions. Meeting creation and participant token generation now call Cloudflare’s RealtimeKit APIs, while the existing Chatwoot call experience remains unchanged for agents and customers. This also adds setup-time credential validation, so admins get clearer errors when the API token is invalid, the Cloudflare account or permissions are incorrect, or the RealtimeKit App ID does not belong to the selected account. Fixes https://linear.app/chatwoot/issue/PLA-176/migrate-dyte-integration-to-cloudflare-realtimekit **How to test** 1. Go to Settings → Integrations → Cloudflare RealtimeKit. 2. Add a Cloudflare Account ID, RealtimeKit App ID, and API token with Realtime Admin permissions. 3. Confirm the integration saves successfully with valid credentials. 4. Try invalid credentials and confirm the error identifies whether the token, account/permissions, or app ID is wrong. 5. Start a video call from a conversation and confirm the RealtimeKit meeting opens. --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> Co-authored-by: Sony Mathew <sony@chatwoot.com> |
||
|
|
e785620921 |
feat: read structured X-Chatwoot-* headers for mobile session metadata (#14762)
Phase 2 of [INF-75](https://linear.app/chatwoot/issue/INF-75). Follows up on [#14753](https://github.com/chatwoot/chatwoot/pull/14753) (Phase 1 UA pattern fallback, already merged). When the request carries an \`X-Chatwoot-Client-Name\` header, \`UserSessionTrackingService\` now prefers the five structured \`X-Chatwoot-*\` headers over the User-Agent for populating the \`user_sessions\` row: | Header | Mapped column | |---|---| | \`X-Chatwoot-Client-Name\` | \`browser_name\` | | \`X-Chatwoot-Client-Version\` | \`browser_version\` | | \`X-Chatwoot-Device-Model\` | \`platform_name\` | | \`X-Chatwoot-Platform-Version\` | \`platform_version\` | | \`X-Chatwoot-Platform\` (+ model) | \`device_name\` (\`iPhone\` / \`iPad\` / \`Android\`) | When the header is absent or blank, the existing \`Browser.new\` path runs, with the Phase 1 legacy UA fallback still acting as the floor. Real browsers are untouched. A follow-up PR in [chatwoot-mobile-app](https://github.com/chatwoot/chatwoot-mobile-app) will start sending these headers from the React Native build. Until that ships, this PR is a no-op for production traffic, so it can land independently. Fixes INF-75. |
||
|
|
0af9900be2 |
fix: model for false promise harness (#14824)
fixes model for false promise harness |
||
|
|
84fda8a323 |
fix(captain): move false promise harness setting to account (#14823)
# Pull Request Template ## Description moves the harness to account settings rather than assistant settings ## Type of change refactor ## How Has This Been Tested? Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. locally and specs ## 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 - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] Any dependent changes have been merged and published in downstream modules |
||
|
|
1c7e60880d |
fix: add harness on false promises (#14672)
# Pull Request Template ## Description https://linear.app/chatwoot/issue/AI-179/false-promise-soft-handoff-guard ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. Against negative cases identified by evals ## 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 |
||
|
|
8e27ca4282 |
fix(firecrawl): get url in case sourceUrl is not available (#14820)
# Pull Request Template ## Description Fixes inconsistency by adding a fallback to use `url` instead of `sourceUrl` when Firecrawl returns empty `sourceUrl` in production. We switched to v2 endpoints of Firecrawl in https://github.com/chatwoot/chatwoot/pull/14624 and it worked locally, but seems to fail for some cases in production. ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. locally ## 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 - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] Any dependent changes have been merged and published in downstream modules |
||
|
|
ee5e20551d |
fix(facebook): handle messenger post attachment type (#14813)
Facebook Messenger messages that share a post no longer break message ingestion. Following the recent sticker change, Meta now sends a shared post as a new `post` attachment type. Chatwoot didn't recognise `post` as a valid attachment file type, so the webhook job crashed and the message — and any others in the same batch — failed to sync. Shared posts now appear in the conversation as a fallback link (title + URL), the same way `share` attachments already do. This is the same class of bug as #14793 (sticker), just a different attachment type. #### Root cause `post` was neither skipped as unsupported nor mapped to a valid `file_type`, so `attachment_params` produced `file_type: :post`. The `Attachment#file_type` enum has no `post` value, so it raised `ArgumentError: 'post' is not a valid file_type`, crashing `Webhooks::FacebookEventsJob`. #### Fix The Facebook builder already maps `share -> :fallback` because shared posts point to facebook.com page URLs rather than downloadable media. A `post` is the same kind of attachment, so map both `share` and `post` to `fallback` (stores the title/link, no download attempt). Also read the fallback title from `payload.title`, since the post payload nests it there rather than at the top level. ## How to reproduce 1. Connect a Facebook Page inbox. 2. Share a Facebook post into Messenger to that page. 3. Before this change: `Webhooks::FacebookEventsJob` raises `ArgumentError: 'post' is not a valid file_type` and the message is dropped. 4. After this change: the shared post shows up in the conversation as a fallback link. Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
df79c0bbde |
feat: exclude stale conversations via assignment policy age threshold (#14766)
## Linear ticket - https://linear.app/chatwoot/issue/CW-7137/assignment-v2-backlog-flush-overloads-agents ## Description Assignment policies now skip stale unassigned conversations automatically. Each policy carries an age threshold (defaults to 7 days), so auto-assignment only picks up recent backlog instead of draining very old, forgotten conversations. The threshold is configurable per policy and can be cleared to assign conversations regardless of age. Previously this control existed only on Enterprise capacity policies (`exclude_older_than_hours`); it now lives on the assignment policy itself, so every V2 inbox benefits without needing a capacity policy. ## Type of change - [ ] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? - Local UI flows ## Screenshots? <img width="645" height="822" alt="image" src="https://github.com/user-attachments/assets/ec58db86-c1fa-4f9e-be87-2e24ff02e077" /> ## 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 --------- Co-authored-by: Sony Mathew <sony@chatwoot.com> |
||
|
|
647cfc2d83 |
feat: distinguish agent-declined calls with a rejected status (#14783)
## Linear ticket - https://linear.app/chatwoot/issue/CW-7374/agent-declined-voice-calls-miscounted-as-failed ## Description Agent rejections were stored with status: failed, so call reports lumped deliberate declines together with real technical failure. Fix: give declines their own terminal rejected status (Twilio + WhatsApp paths), keeping end_reason: agent_rejected. Genuine provider/network failures stay failed. Frontend renders declines exactly as before; existing rows backfilled via migration. ## Type of change - [ ] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? - Tested on UI ## 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 --------- Co-authored-by: Sony Mathew <sony@chatwoot.com> |
||
|
|
e86222034e |
fix(auth): record attribution for oauth signups (#14796)
Google OAuth signups now persist the same first-party attribution cookies as email signups on Chatwoot Cloud. This keeps attribution capture owned by the website and reuses the existing Enterprise-only account attribution service. The OAuth callback only records the already-shaped first-touch and last-touch cookie payload after a new account is created. ## What changed - Added Enterprise-only attribution persistence to the Google OAuth signup account creation path. - Reuses `Internal::Accounts::MarketingAttributionService`. - Keeps attribution best-effort so failures do not interrupt OAuth signup. - Leaves existing email signup and SAML behavior unchanged. ## How to test - Start from a Chatwoot Cloud-like setup with attribution cookies present. - Sign up using the Google OAuth button. - Confirm the created account has `internal_attributes['marketing_attribution']` with `first_touch` and `last_touch`. - Confirm existing Google OAuth login still redirects normally. Validation run locally: - `bundle exec rspec spec/enterprise/controllers/enterprise/devise_overrides/google_oauth_attribution_spec.rb` - `bundle exec rspec spec/controllers/devise/omniauth_callbacks_controller_spec.rb spec/enterprise/controllers/enterprise/devise_overrides/google_oauth_attribution_spec.rb` - `bundle exec rubocop enterprise/app/controllers/enterprise/devise_overrides/omniauth_callbacks_controller.rb spec/enterprise/controllers/enterprise/devise_overrides/google_oauth_attribution_spec.rb` |
||
|
|
2b37fef0e0 |
fix(companies): sync contact company names (#14759)
Fixes company-contact name drift when a company is renamed or deleted. Closes: N/A ## Why Contacts keep a denormalized `additional_attributes.company_name` for display and filtering. Company rename/delete flows could leave that copied value stale even though the actual `company_id` relationship changed. ## What changed - Enqueues an async company contact-name sync job when a company name changes. - Moves company deletion into `Companies::DeleteJob`. - The delete job unlinks linked contacts, clears only the copied `company_name`, and then deletes the company. - Uses bulk JSON updates for the cleanup path so contact records are not saved, which avoids contact update callbacks, webhook dispatch, and automation side effects. ## How to test - Link a contact to a company, rename the company, and confirm the contact company name updates after the job runs. - Delete a company with linked contacts and confirm the delete job removes the company, unassigns linked contacts, and preserves other contact additional attributes. --------- Co-authored-by: Sony Mathew <sony@chatwoot.com> Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com> |
||
|
|
a4be38f8f3 |
fix(facebook): handle messenger sticker attachment type (#14793)
Facebook Messenger sticker messages no longer break message ingestion. Meta recently changed its webhook payloads so a sticker now arrives as a new `sticker` attachment type (alongside the existing `image` attachment during the transition period). Chatwoot didn't recognise `sticker` as a valid attachment file type, so the webhook job crashed and the message — and any others in the same batch — failed to sync. Stickers now appear in the conversation as a single image, just like before. Fixes https://linear.app/chatwoot/issue/PLA-177 ## How to reproduce 1. Connect a Facebook Page inbox. 2. Send a sticker from Messenger to that page. 3. Before this change: the `Webhooks::FacebookEventsJob` raises `ArgumentError: 'sticker' is not a valid file_type` and the message is dropped. 4. After this change: the sticker shows up in the conversation as a single image attachment. --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6a262287d2 |
feat(captain): allow agents to report Captain messages (#14799)
Adds a cloud-only flow for agents to flag incorrect or problematic Captain (AI) responses. Right-clicking a Captain message surfaces a "Report message" option that opens a dialog to pick a problem type and add a description, persisted to a new captain_message_reports table for the team to review. <img width="636" height="542" alt="Screenshot 2026-06-20 at 9 15 56 AM" src="https://github.com/user-attachments/assets/afaa233d-6bd6-455a-8a33-a3796a3e3ef6" /> <img width="580" height="502" alt="Screenshot 2026-06-20 at 9 16 03 AM" src="https://github.com/user-attachments/assets/2d220d99-98cc-4c5e-a325-778ceb4f7bc9" /> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
dfd656a7d9 |
feat(account): persist signup attribution (#14761)
This keeps Chatwoot-side attribution persistence intentionally small and Enterprise-only. The website owns attribution capture, normalization, and source classification. Chatwoot Cloud only reads the already-shaped first-party attribution cookies during the current web signup account creation path and stores the decoded payload in internal account metadata. Self-hosted installs remain unchanged in this repo. ## What changed - Added Enterprise-only attribution persistence to the current Cloud web signup account creation path. - Stores attribution only when `ChatwootApp.chatwoot_cloud?` is true. - Reads the existing first-touch and last-touch attribution cookies. - Saves only the documented scalar attribution fields under account internal metadata. - Preserves raw attribution values and leaves escaping to display boundaries. - Bounds stored attribution values to the website field-size limit. - Keeps OSS controller code unchanged. - Keeps signup attribution request coverage in Enterprise specs. - Avoids backend attribution derivation, referrer parsing, or fallback classification. - Skips authenticated add-workspace flows so additional workspaces are not counted as signup attribution. - Does not hook unused account creation paths or OmniAuth account creation. ## How to test - `bundle exec rubocop spec/controllers/api/v1/accounts_controller_spec.rb spec/enterprise/controllers/api/v1/accounts_controller_spec.rb enterprise/app/controllers/enterprise/api/v1/accounts_settings.rb enterprise/app/services/internal/accounts/marketing_attribution_service.rb spec/enterprise/services/internal/accounts/marketing_attribution_service_spec.rb` - `bundle exec rspec spec/controllers/api/v1/accounts_controller_spec.rb spec/enterprise/controllers/api/v1/accounts_controller_spec.rb spec/enterprise/services/internal/accounts/marketing_attribution_service_spec.rb` - On a cloud-like setup, create an account through the current web signup path with attribution cookies and confirm account internal metadata is populated. - On a non-cloud setup or authenticated add-workspace flow, confirm account-create behavior is unchanged and no attribution is stored. |
||
|
|
f66b551c7d |
revert: Sidebar unread counts for filters (CW-7262) (#14769)
## Description Reverts [#14726](https://github.com/chatwoot/chatwoot/pull/14726) (\"feat: Add sidebar unread counts for filters (CW-7262)\"), which shipped in 4.15.0. After 4.15.0 rolled out to prod the unread-counts-for-filters code path caused a cascading incident: - `Counter#ensure_filters_cache!` fires on every `/unread_counts/index` and `update_last_seen` request. - On cache miss it calls `Builder#build_filters_for!`, which: - invokes `store.clear_user_filters!` -> `delete_matching` -> a Redis `SCAN_each` over a per-user pattern keyspace, and - runs 4 fresh SQL passes per user (mentions, participating, unattended, and per-folder `Conversations::FilterService` queries). - Threads blocked in the SCAN held their DB connections, the connection pool exhausted, Sidekiq jobs were discarded with `ActiveJob::DeserializationError: could not obtain a connection from the pool`, and the enqueued queue blew past 200K. Related: [CW-7262](https://linear.app/chatwoot/issue/CW-7262/unread-counts-for-filters-folders) ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) - [ ] 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? ## 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 - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules |
||
|
|
c70a57407d |
fix: label Chatwoot Mobile sessions instead of 'Unknown Device' (#14753)
## Description Sessions created by logins from the Chatwoot Mobile app currently render as "Unknown Device" in the dashboard sessions UI. The mobile app's HTTP layer sends: - Android: `User-Agent: okhttp/4.9.2` - iOS: `User-Agent: Chatwoot/<build> CFNetwork/<v> Darwin/<v>` Neither pattern is classifiable by the `browser` gem, so \`browser_name\`, \`platform_name\`, and \`device_name\` all end up "Unknown". This change adds a backend-only fallback in \`UserSessionTrackingService\`. When \`Browser.new(ua)\` returns "Unknown Browser" and the UA matches a known Chatwoot Mobile pattern, the labels are overridden to \`Chatwoot Mobile\` + \`Android\` / \`iPhone\`. The Vue sessions list (\`ActiveSessions.vue\`) then renders "Chatwoot Mobile on Android" or "Chatwoot Mobile on iPhone" with the smartphone icon. This is the immediate floor. A follow-up will add structured \`X-Chatwoot-*\` headers from the mobile app so we can render full version + device model. Fixes INF-75. ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? - Added specs ## 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 - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] Any dependent changes have been merged and published in downstream modules |
||
|
|
fe6368b42e |
feat: Add sidebar unread counts for filters (CW-7262) (#14726)
## Description Extends the conversation unread-count system so the left sidebar can show unread badges for Mentions, Participating, Unattended, and saved conversation folders. Folder badges reuse the existing `custom_filters` conversation filter semantics, store user-scoped Redis sets lazily, and skip unsupported folder filters so invalid saved folders continue to render without a badge. The Unattended badge counts all visible unread open conversations that match the existing unattended conversation scope. Closes - [CW-7262](https://linear.app/chatwoot/issue/CW-7262/unread-counts-for-filters-folders) ## What changed - Added user-scoped unread-count Redis keys and cache builders for mentions, participating conversations, unattended conversations, and saved folder filters. - Reused `Conversations::FilterService` through a relation-returning path so folder counts match the folder conversation list behavior. - Invalidated user filter caches from mention, participant, custom-filter, and relevant conversation update events. - Extended the unread-count endpoint payload and sidebar Vuex/sidebar rendering for the new badge counts, including the Unattended sidebar item. - Added Ruby, Enterprise, request, listener, and frontend store coverage for the new unread-count dimensions. ## Type of change - [x] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? - Created local validation folders for `john@acme.inc` and confirmed the unread-count payload includes open, resolved, and high-priority folder badges while excluding the invalid unsupported folder. - Added coverage for the Unattended badge rule: all visible unread open conversations matching `Conversation.unattended`. - Ran focused unread-count Ruby specs, including service, listener, request, and Enterprise counter coverage. - Ran frontend unread-count store specs. - Ran RuboCop on the touched Ruby files. - Ran ESLint through the project script; it completed with warnings in existing unrelated files and no errors. <img width="369" height="525" alt="Screenshot 2026-06-13 at 10 51 39 PM" src="https://github.com/user-attachments/assets/36b1d2c4-dac1-4f6f-9c0e-7ef5a6cc2975" /> ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [x] Documentation changes are not required for this internal unread-count behavior - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] No dependent downstream changes are required --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> |
||
|
|
64b0ebd8dc |
feat: Script to migrate images between providers (#9117)
# Pull Request Template ## Description Storage migration functionality, which allows the transfer of images from one on-premises provider to another (e.g. AWS, Google, etc.) using Active Storage. I ran the unit and integration tests and they all passed correctly. **The command to execute the migration is `FROM=from_service TO=to_service rake storage:migrate`** Fixes #7907 ## Type of change Please delete options that are not relevant. - [x] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? I have tested the storage migration functionality by running the provided unit and integration tests. Additionally, I manually tested the migration process in a local development environment by following these steps: Set up two storage services (e.g., AWS S3 and Google Cloud Storage) with valid configurations. Execute the `FROM=local TO=amazon rake storage:migrate` task with appropriate FROM and TO arguments to migrate blobs between the services. Verified that the blobs were successfully transferred to the target storage service. Checked for any error messages or unexpected behavior during the migration process. ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Sony Mathew <sony@chatwoot.com> Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com> |
||
|
|
41a3ab6dfa |
feat(companies): add contact company selector (#14496)
Adds a company selector to the contact details form so agents can associate a contact with an existing company directly from the contact page. Closes - None Why Contacts already expose company information through the CRM fields, but the form only accepted free-text company names. As we split company CRM work into smaller PRs, this keeps the contact page aligned with the structured company model while preserving the existing company-name behavior used by automations. What changed - Shows a company dropdown in the contact details form when the Companies feature is enabled. - Keeps legacy free-text company names editable when a contact has no structured `company_id`. - Allows Enterprise contact create/update APIs to accept account-scoped `company_id`. - Syncs `additional_attributes.company_name` when a contact is associated with a company, including the existing email-domain auto-association path. - Serializes `company_id` in the contact model payload so the form can show the current association. How to test 1. Enable Companies for an account and open a contact details page. 2. In Edit contact details, use the Company field to select an existing company. 3. Save the contact and refresh the page. 4. Confirm the selected company remains visible and the contact is associated with that company. 5. Confirm contacts with only a legacy free-text company name still show the text input instead of an empty selector. --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: iamsivin <iamsivin@gmail.com> Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com> |
||
|
|
de137e8297 |
feat: Capture CTWA referral metadata for WhatsApp conversations (#14681)
Adds support for capturing Click-to-WhatsApp ad referral metadata from incoming WhatsApp messages. This stores Meta Cloud API `referral` payloads on the incoming message `content_attributes` and normalizes Twilio `Referral*` callback fields into the same shape. The UI display is intentionally deferred until Instagram, Messenger, and TikTok referral payloads are captured as well, so we can design one cross-channel referral surface instead of a WhatsApp-only sidebar block. Why message-level storage Meta sends CTWA referral details as part of the inbound message payload, not as a stable conversation-level webhook. The referral represents the exact ad click that produced that specific customer message, and later messages in the same conversation may not carry the same context. Storing the normalized referral on the message preserves the original webhook semantics, avoids adding conversation-level attribution that could become stale or ambiguous, and keeps support for both Cloud API and Twilio payloads aligned behind `content_attributes.referral`. Fixes https://linear.app/chatwoot/issue/CW-7090/surface-meta-ctwa-referral-on-incoming-whatsapp-messages-cloud-api Closes https://github.com/chatwoot/chatwoot/issues/13995, https://github.com/chatwoot/chatwoot/issues/12560, https://github.com/chatwoot/chatwoot/issues/13006 Related community PRs - https://github.com/chatwoot/chatwoot/pull/13130 - https://github.com/chatwoot/chatwoot/pull/14180 - https://github.com/chatwoot/chatwoot/pull/14121 Related follow-ups - https://linear.app/chatwoot/issue/CW-7301/capture-instagram-ad-referral-metadata-on-incoming-messages - https://linear.app/chatwoot/issue/CW-7302/capture-messenger-ad-referral-metadata-on-facebook-page-conversations - https://linear.app/chatwoot/issue/CW-7303/capture-tiktok-ad-referral-metadata-from-im-referral-msg-events How to test 1. Send or replay a WhatsApp Cloud API inbound message that contains a `messages[0].referral` payload from a Click-to-WhatsApp ad. 2. Confirm the generated incoming message stores the payload under `content_attributes.referral`. 3. Repeat with a Twilio WhatsApp callback containing `Referral*` fields and confirm the stored message has the same normalized `content_attributes.referral` structure. --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> |
||
|
|
a3d05ef55d | chore: Update emoji picker in widget (#14741) | ||
|
|
dda25c0b51 |
fix(portal): emit valid BCP 47 html lang attribute (#14680)
Locales are stored with underscores (e.g. pt_BR), but the HTML lang attribute requires hyphens (pt-BR). Convert via a helper in the public portal layouts. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> |
||
|
|
4816e923b6 |
chore: added a new sort by unread option for conversations (CW-7152) (#14512)
## Description Added a new option for the sort by option in conversation filters called unread. This is to filter out unread conversations. Fixes # CW-7152 ## Type of change Please delete options that are not relevant. - [ ] 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? Tested manually in local. <img width="1397" height="603" alt="Screenshot 2026-05-20 at 10 40 20 PM" src="https://github.com/user-attachments/assets/6c60263e-907b-419f-a7ed-06010bbe8736" /> ## 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 - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules |