e70bbfaaa5efc5d84b490b8a9e3967cd324bb360
1864
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
90861f8809 |
feat(whatsapp): gate embedded signup inbox creation (#15046)
WhatsApp inbox creation now shows Embedded Signup for Chatwoot Cloud accounts only when the new `whatsapp_embedded_signup_inbox_creation` feature flag is enabled. Cloud accounts without the flag go directly to manual WhatsApp Cloud API setup, while self-hosted installations with a configured WhatsApp App ID retain their existing Embedded Signup flow. --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> |
||
|
|
c420edce58 |
feat: branded email layouts for email inboxes (#14936)
## Description
Adds an API-only branded email layout feature for Email inbox replies.
Administrators can configure an account-level fallback layout and
per-email-inbox overrides with Liquid HTML using `{{ content_for_layout
}}`, and eligible outbound email replies/transcripts render through the
scoped layout when the account feature flag `branded_email_templates` is
enabled.
The feature is disabled by default and is manually controlled through
the normal account feature flag mechanism.
Fixes
https://linear.app/chatwoot/issue/CW-7514/branded-html-email-templates-per-inboxbrand
## Type of change
- [x] New feature (non-breaking change which adds functionality)
- [x] This change requires a documentation update
## How to test
1. Start Chatwoot locally and sign in as an administrator.
2. Enable the account feature flag for the account you are testing:
```ruby
account = Account.find(<account_id>)
account.enable_features!(:branded_email_templates)
```
3. Create or pick an Email inbox, then note the `account_id` and
`inbox_id`.
4. Configure an account-level fallback layout through the API using
authenticated admin headers:
```http
PATCH /api/v1/accounts/:account_id/branded_email_layout
Content-Type: application/json
{
"branded_email_layout": "<html><body><header>Account Brand</header>{{
content_for_layout }}<footer>Account footer</footer></body></html>"
}
```
5. Confirm `GET /api/v1/accounts/:account_id/branded_email_layout`
returns the saved account layout.
6. Configure an inbox-level override for the Email inbox:
```http
PATCH /api/v1/accounts/:account_id/inboxes/:inbox_id
Content-Type: application/json
{
"branded_email_layout": "<html><body><header>Inbox Brand</header>{{
content_for_layout }}<footer>Inbox footer</footer></body></html>"
}
```
7. Confirm `GET /api/v1/accounts/:account_id/inboxes/:inbox_id` returns
the inbox `branded_email_layout`.
8. Send an Email inbox reply and verify the outbound email body is
wrapped with the inbox layout around the generated reply content.
9. Clear the inbox layout by sending a blank value, then send another
reply and verify it falls back to the account layout:
```http
PATCH /api/v1/accounts/:account_id/inboxes/:inbox_id
Content-Type: application/json
{
"branded_email_layout": ""
}
```
10. Clear the account layout with a blank value and verify Email replies
return to the existing no-layout behavior.
11. Verify validation behavior:
- Updating either API with a layout that omits `{{ content_for_layout
}}` returns `422`.
- Updating either API with invalid Liquid returns `422`.
- Updating a non-Email inbox with `branded_email_layout` returns `422`.
- Disabling `branded_email_templates` and updating a layout returns
`422`.
## How Has This Been Tested?
Validation:
- `bundle exec rspec spec/models/email_template_spec.rb
spec/controllers/api/v1/accounts/branded_email_layouts_controller_spec.rb
spec/controllers/api/v1/accounts/inboxes_controller_spec.rb
spec/lib/email_templates/db_resolver_service_spec.rb
spec/mailers/conversation_reply_mailer_spec.rb`
- `bundle exec rspec
spec/enterprise/services/enterprise/billing/handle_stripe_event_service_spec.rb
spec/enterprise/services/internal/reconcile_plan_config_service_spec.rb`
- `bundle exec rubocop` on changed Ruby files, excluding generated
`db/schema.rb`
- `git diff --check` and `git diff --cached --check`
- YAML parsing for changed config/Swagger files
- `bundle exec rails routes -g branded_email_layout`
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have made corresponding changes to the documentation
- [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
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] My changes generate no new warnings
- [ ] Any dependent changes have been merged and published in downstream
modules
|
||
|
|
331875cdaa | feat: Add popular content per locale (#14939) | ||
|
|
081e08c7c6 |
fix(whatsapp): skip template sync for suspended accounts (#15047)
Suspended accounts no longer participate in scheduled WhatsApp template syncs. This avoids unnecessary external API calls while keeping the existing refresh behavior unchanged for active accounts. Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> |
||
|
|
9749a3dc96 |
feat: capture captain sessions for v2 assistant responses [CW-7485] (#14971)
Records a `Captain::Session` row for every Captain V2 assistant response delivered in a conversation, so we can show how a response was generated and report on credit, FAQ, and document usage. Stacked on #14970 (the `captain_sessions` model). ## What changed - `FaqLookupTool` now records the retrieved FAQ ids (and their backing document ids) into the shared run state, accumulated across tool calls. - `AgentRunnerService` exposes the raw ai-agents run result via `last_run_result`; the `generate_response` return shape is unchanged, so the playground path is unaffected. - New `Captain::Assistant::SessionCaptureService` builds the session: scenario resolved from the answering agent name, model from `assistant.agent_model`, token usage plus the trimmed current-turn conversation history stored in `run_context`. - `ResponseBuilderJob` captures after delivery: `credits_consumed` mirrors the actual charge (1.0 for a billed response, 0.0 for handoffs, where the session points at the customer-facing handoff message). Capture runs outside the delivery transaction and swallows its own failures, so a logging bug can never block or roll back a customer reply. V1 responses and copilot are out of scope; copilot capture comes next. ## How to test On an account with `captain_integration_v2` enabled and an inbox connected to an assistant with approved FAQs, send a customer message on a pending conversation. After the assistant replies, a `Captain::Session` row should exist with the conversation as subject, the reply message as result, the FAQs/documents used, and the run context for that turn. Asking for a human agent should produce a zero-credit session pointing at the handoff message. <img width="2428" height="1058" alt="CleanShot 2026-07-15 at 17 25 40@2x" src="https://github.com/user-attachments/assets/d8e44923-c17b-494f-8c33-c8fa4219438c" /> |
||
|
|
8dd0d08322 |
refactor: align conversation direct uploads with standard account auth (#15039)
Conversation attachment uploads now go through the same authentication that every other account-scoped API endpoint uses. Agents continue to attach files exactly as before, and the upload request is now tied to the agent's dashboard session instead of a separately serialized access token. Because the upload request is now authenticated, the dashboard proves the agent's session directly instead of passing `currentUser.access_token`. This keeps uploads working alongside the profile access-token changes in #14973, including on accounts where that token is serialized as empty. ## What changed - `Api::V1::Accounts::Conversations::DirectUploadsController` now runs the standard account auth stack: API access token when the `api_access_token` header is present, dashboard session (devise-token-auth) otherwise, with agent-bot tokens rejected. Previously it inherited `ActiveStorage::DirectUploadsController` directly and did not run any authentication. - `EnsureCurrentAccountHelper#ensure_current_account` now returns `401` when a request has neither an authenticated user nor a bot resource, instead of continuing. This closes the same gap for any controller that relies on the helper. - The dashboard direct-upload paths (`useFileUpload.js` and the legacy `fileUploadMixin.js`) now attach the agent's session headers to the upload request via a new `directUploadsHelper.js`, instead of sending `currentUser.access_token`. ## How to test 1. As a logged-in agent, open a conversation and attach a file. Upload should succeed as before, on installs with direct uploads enabled. 2. Confirm attachments still work for an agent on an account whose profile access token is not serialized (e.g. a Cloud plan without `api_and_webhooks`). 3. Send a `POST` to `/api/v1/accounts/:account_id/conversations/:conversation_id/direct_uploads` with no credentials, an empty `api_access_token`, or an invalid token, and confirm it returns `401`. 4. Confirm a valid agent of the account (via API token or session) gets `200`, while an agent of a different account gets `401`. |
||
|
|
2891a72cb9 | feat(whatsapp): enable reconfigure for embedded signup inboxes (#15038) | ||
|
|
522e3c4d3f |
feat: enforce api_and_webhooks feature for token API and account webhooks (#14973)
This gates API-token access and outgoing account webhooks behind the `api_and_webhooks` account feature introduced in #14972. On Chatwoot Cloud, Hacker accounts lose token-authenticated account API access and account webhook delivery, while paid accounts retain them through the billing-plan feature reconcile. Community and self-hosted installations continue to work without any upgrade-time interruption. ## What changed - Added `Account#api_and_webhooks_enabled?` as the single backend kill switch. Core returns enabled; the Enterprise override consults the account flag on Chatwoot Cloud and remains enabled off-Cloud. - Account-scoped v1 and v2 requests authenticated with a user or agent-bot API token now return `403 Forbidden` when the feature is disabled. Invalid tokens still return 401, and dashboard session requests are unaffected. - Profile responses return an empty access token when none of the user's accounts has access. The stored token is preserved, and the profile UI disables its token controls with paid-plan copy on Cloud. - Account webhook delivery stops when the feature is disabled. Webhook CRUD remains available to session-authenticated dashboard requests, API-inbox webhooks continue to be delivered, and the Cloud dashboard shows a webhook paywall instead of the webhook list. - Removed the database backfill migration. Existing paid Cloud accounts should be enabled with the one-off script below before enforcement is deployed. ## Existing paid-account rollout Run this as an ad-hoc Rails runner script on Chatwoot Cloud. It intentionally targets only the Startups, Business, and Enterprise plans and does not add `api_and_webhooks` to `manually_managed_features`, so future billing reconciles remain authoritative. ```rb paid_plan_names = %w[Startups Business Enterprise] accounts = Account.where("custom_attributes ->> 'plan_name' IN (?)", paid_plan_names) total = accounts.count enabled = 0 skipped = 0 puts "Enabling api_and_webhooks for #{total} paid account(s)..." accounts.find_each(batch_size: 500).with_index(1) do |account, processed| if account.feature_enabled?('api_and_webhooks') skipped += 1 else account.enable_features!('api_and_webhooks') enabled += 1 end puts "Processed #{processed}/#{total}..." if (processed % 1000).zero? end puts "Done! Enabled: #{enabled}, Skipped: #{skipped}, Total: #{total}" ``` For example, save the snippet outside the repository as `enable_api_and_webhooks.rb`, then run: ```sh bundle exec rails runner /path/to/enable_api_and_webhooks.rb ``` ## How to test - On Cloud, use a Hacker account and confirm token-authenticated requests to account-scoped v1 and v2 endpoints return 403, while the same dashboard actions continue to work through session authentication. - Confirm profile access-token controls are disabled with paid-plan copy when all accounts are ineligible, and remain available when at least one account has the feature. - Confirm the Webhooks settings page shows the billing paywall for a Cloud account without the feature; admins get the billing action and agents get the existing ask-an-admin message. - Confirm outgoing account webhooks stop for an ineligible Cloud account while API-inbox webhooks still deliver. - Confirm community and self-hosted installations retain API and webhook behavior after upgrading, even when an existing account does not have the stored feature bit. ### Screenshots ## Cloud <img width="2590" height="642" alt="CleanShot 2026-07-15 at 15 13 14@2x" src="https://github.com/user-attachments/assets/431a7bd8-1742-4e7a-b312-d3ad92015f9b" /> <img width="2152" height="994" alt="CleanShot 2026-07-15 at 15 14 37@2x" src="https://github.com/user-attachments/assets/475dda48-d1c5-4be5-a3c3-7a96b9713724" /> --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> |
||
|
|
0fccb7dacd | feat: stage edits to published articles as drafts (#14842) | ||
|
|
354c2cab6b |
fix(captain): handle resolved conversation context (#14433)
# Pull Request Template ## Description Fixes: https://github.com/chatwoot/chatwoot/issues/13880 Uses approaches discussed from: https://github.com/chatwoot/chatwoot/pull/13883 Activity messages pertaining to resolve are included along with an instruction for the LLM to choose whether to consider them or not along ## 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 and with 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> |
||
|
|
9c68eed676 |
fix(captain): send custom tool API key headers (#15008)
Captain custom tools configured with API key authentication now send the configured key in the requested HTTP header. Existing tools begin working without needing to be recreated or reconfigured, while credentials remain protected across redirects. ## How to reproduce 1. Create a Captain custom tool using API Key authentication. 2. Configure `X-API-Key` as the header name and save the tool. 3. Invoke the tool and inspect the incoming request. 4. Before this change, the API key header is absent; after this change, the configured endpoint receives it. ## What changed The UI persists API key authentication as `name` and `key`, but the request builder also required an unused `location: header` property. The request builder now treats API key authentication as header-based, matching the only mode exposed by the UI. Custom authentication headers are also registered as sensitive with `SafeFetch`. They are retained for the configured endpoint and same-origin redirects, but stripped when a redirect crosses origins to prevent credential leakage. Factory, request, and redirect specs cover the real UI payload and both public and private-network fetch paths. |
||
|
|
6d38b4d39c |
fix(captain): improve complex migration instructions (#15002)
Improves Captain V1 → V2 migration for complex legacy instructions so mandatory triggers, workflows, language rules, and escalation behavior remain active while query-dependent product knowledge is prepared as pending FAQ candidates. ## What changed - Added explicit preservation rules for mandatory triggers, verification steps, escalation conditions, exceptions, and language behavior. - Added an auditor that checks the draft and fixes any issues before manual review. - Kept the existing migration application contract and schema limits unchanged - Added focused regression coverage for the complex-prompt classifier contract. ## How to reproduce Generate a migration draft for an assistant with dense legacy instructions containing mandatory handoff triggers, verification rules, product facts, and multi-step workflows. The resulting draft should keep actions active, place query-dependent facts in FAQ candidates, and avoid silently dropping or reversing source requirements. Focused Captain migration specs and RuboCop checks pass locally. |
||
|
|
fd625981e9 |
fix(captain): keep custom tools available in Captain V2 (#15015)
Captain V2 assistants can now use every enabled custom tool from their account through the main assistant. The change keeps existing custom tool access when an assistant has no migrated scenarios, so switching from V1 does not remove the capability without warning. ## How to reproduce 1. Create and enable an account custom tool. 2. Use an assistant with no custom instructions and no generated scenarios. 3. Enable Captain V2 for the account. 4. Before this change, the main assistant receives only FAQ lookup and handoff. After this change, it also receives the enabled account custom tool. ## What changed The main V2 assistant now loads enabled custom tools through its account association. Scenario agents still load only the tools named in their scenario instructions. The account custom tool limit keeps the added tool count bounded. Focused model coverage verifies enabled tools, disabled tools, account isolation, FAQ lookup, and handoff. Existing V1 assistant, V2 scenario, and V2 runner coverage passes. RuboCop passes. |
||
|
|
49c442751d |
fix: require admin for dashboard app mutations (#14831)
## Summary Restricts account-wide Dashboard App creation, updates, and deletion to administrators while keeping read access available to authenticated account users. ## Why Dashboard Apps are account-level integrations displayed in conversation views. Agents should be able to use them, but only administrators should be able to change their configuration. ## What changed - authorize Dashboard App actions through `DashboardAppPolicy` - allow index and show access for authenticated account users - restrict create, update, and destroy actions to administrators - add request coverage for administrator and agent mutation behavior ## Validation `bundle exec rspec spec/controllers/api/v1/accounts/dashboard_apps_controller_spec.rb` 17 examples, 0 failures. `bundle exec rubocop app/controllers/api/v1/accounts/dashboard_apps_controller.rb app/policies/dashboard_app_policy.rb` 2 files inspected, no offenses detected. --------- Co-authored-by: Gaurav Singhal <gauravsinghal@Gauravs-Mac-mini.local> Co-authored-by: Sojan Jose <sojan@pepalo.com> |
||
|
|
13db36609d |
fix: hide agent bot access tokens from agents (#14830)
## Summary Keeps Agent Bot list and show access available to agents while restricting account bot access tokens to administrators. ## Why Agents need Agent Bot metadata for existing product workflows, but the bot access token can be replayed against bot-authorized APIs and should not be exposed to them. ## What changed - serialize `access_token` only for administrators - verify agents can read Agent Bot metadata without receiving the token - verify administrators still receive the token from index and show responses ## Validation `bundle exec rspec spec/controllers/api/v1/accounts/agent_bots_controller_spec.rb` 27 examples, 0 failures. Related follow-up: [CW-7595](https://linear.app/chatwoot/issue/CW-7595/standardize-one-time-credential-disclosure-across-chatwoot-apis) --------- Co-authored-by: Gaurav Singhal <gauravsinghal@Gauravs-Mac-mini.local> Co-authored-by: Sojan Jose <sojan@pepalo.com> |
||
|
|
9328f8739c |
fix: clear whatsapp webhook override when manual cloud inbox is deleted (#15010)
Deleting a manually-configured WhatsApp Cloud inbox left its
phone-number-level webhook override still pointing at Chatwoot on Meta's
side. The number kept routing inbound events to us after the inbox was
gone, which blocked the customer's own app — subscribed separately on
the same WABA — from receiving messages, since the phone-level override
takes priority over the app-level subscription. Deleting the inbox now
releases the override, as it already did for embedded-signup inboxes.
## What changed
The setup and teardown paths gated on opposite halves of the same
condition. `Channel::Whatsapp#should_auto_setup_webhooks?` sets the
override for `whatsapp_cloud` inboxes where `source !=
'embedded_signup'` (i.e. manual ones), while
`Whatsapp::WebhookTeardownService#should_teardown_webhook?` only cleared
it when `source == 'embedded_signup'`. The two sets are disjoint, so
manual inboxes were exactly the ones that set an override on create and
never cleared it on destroy. Embedded-signup inboxes were unaffected
because `EmbeddedSignupService` calls `setup_webhooks` explicitly.
Dropping the `source` check from the teardown guard is the whole fix.
Manual `whatsapp_cloud` channels can't persist without `api_key`,
`phone_number_id` and `business_account_id` (`validate_provider_config`
verifies all three against Meta), so the remaining presence guards and
both API calls have everything they need. The WABA-level `DELETE
/subscribed_apps` now also fires for manual inboxes when the last one on
a WABA is removed, which is symmetric with manual setup subscribing the
app in the first place; the token only unsubscribes the app it belongs
to, so a customer's separate app subscription is untouched.
This fixes the leak going forward. Numbers already stranded still need
the override cleared with the customer's own token, since we no longer
hold their `api_key` once the inbox is deleted.
## How to reproduce
1. Create a WhatsApp Cloud inbox using manual API keys (not embedded
signup).
2. Confirm the override is set: `GET
/v22.0/{phone_number_id}?fields=webhook_configuration` shows
`phone_number` pointing at your Chatwoot install.
3. Delete the inbox.
4. Before this change, the override still points at Chatwoot. After it,
`webhook_configuration` no longer carries the phone-level override and
events fall back to the WABA/app-level subscription.
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
|
||
|
|
1b6a80d84d |
fix(captain): improve conversation completion evaluation (#14967)
# Pull Request Template ## Description Please include a summary of the change and issue(s) fixed. Also, mention relevant motivation, context, and any dependencies that this change requires. Fixes https://linear.app/chatwoot/issue/AI-136/check-conversation-status-while-auto-resolving - After 60mins of inactivity, we run a job that decides if pending conversations are resolvable or need handoff - the prompt was a bit conservative and didn't have conversation state context ## 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 ran a sample eval ## 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> |
||
|
|
9c444315a6 |
feat: add api_and_webhooks feature flag reconciled from billing plan (#14972)
This introduces a new `api_and_webhooks` account feature flag that will
control access to the token-authenticated API and account webhooks. The
flag is part of the Startup plan features, so paid plans — including
trials of paid plans — get it through the billing reconcile, while
accounts on the default (Hacker) plan don't, with
`manually_managed_features` available as a per-account override. The
flag defaults to enabled, and nothing enforces it yet, so this PR is
behavior-neutral — enforcement lands in a follow-up.
## What changed
- Added `api_and_webhooks` to `features.yml` (first flag on the
`feature_flags_ext_1` column, default enabled).
- Added the flag to `STARTUP_PLAN_FEATURES` in
`Enterprise::Billing::ReconcilePlanFeaturesService`, so all paid tiers
get it and the default plan loses it on reconcile.
- Added the flag to the manually manageable features list so it can be
granted per account via Super Admin.
```rb
# Enables the api_and_webhooks feature for all existing accounts and marks it
# as manually managed so cloud billing reconciles never strip it.
#
# NOT committed to source control — run manually on production.
#
# Usage:
# bundle exec rails runner enable_api_and_webhooks.rb
# ACCOUNT_ID=123 bundle exec rails runner enable_api_and_webhooks.rb
#
# Idempotent: accounts already grandfathered are skipped; safe to re-run.
probe = Internal::Accounts::InternalAttributesService.new(Account.new)
abort 'api_and_webhooks is not in valid_feature_list — deploy the feature flag PR first.' unless probe.valid_feature_list.include?('api_and_webhooks')
account_id = ENV.fetch('ACCOUNT_ID', nil)
accounts = account_id.present? ? Account.where(id: account_id) : Account.all
abort "Account with ID #{account_id} not found" if account_id.present? && accounts.empty?
total = accounts.count
puts "Grandfathering api_and_webhooks for #{total} account(s)..."
puts "Started at: #{Time.current}"
updated = 0
skipped = 0
errored = 0
accounts.find_each(batch_size: 500) do |account|
service = Internal::Accounts::InternalAttributesService.new(account)
features = service.manually_managed_features
if features.include?('api_and_webhooks') && account.feature_enabled?('api_and_webhooks')
skipped += 1
else
service.manually_managed_features = features + ['api_and_webhooks'] unless features.include?('api_and_webhooks')
account.enable_features!('api_and_webhooks')
updated += 1
end
processed = updated + skipped + errored
puts "Processed #{processed}/#{total}..." if (processed % 1000).zero?
rescue StandardError => e
errored += 1
puts "Account #{account.id}: FAILED - #{e.message}"
end
puts "Done! Updated: #{updated}, Skipped: #{skipped}, Errored: #{errored}, Total: #{total}"
```
|
||
|
|
df7f137657 |
feat: add captain sessions model [CW-7485] (#14970)
This adds a `captain_sessions` table to log every Captain run, starting with Assistant Responses and Copilot Responses. Each session records the assistant, model, credits consumed, the FAQs/documents/scenario that contributed to the response, and the full run context — giving customers visibility into how a response was generated and giving us durable stats on credit, FAQ, and document usage (which today only exist as ephemeral trace metadata and an aggregate account counter). ## What changed - New `Captain::Session` model with a `session_type` enum (`assistant`, `copilot`). The subject (`Conversation` / `CopilotThread`) and result (`Message` / `CopilotMessage`) classes are inferred from the session type, so the table stores plain `subject_id` / `result_id` ids. `result_id` is nullable so failed runs that still consumed credits can be logged. - Composite indexes on `[session_type, subject_id]`, `[session_type, result_id]`, and `[account_id, session_type, created_at]` for lookup and usage-stats queries. - Factory and model specs. This PR is schema + model only; the writer/instrumentation that records sessions from the assistant and copilot flows will follow. --------- Co-authored-by: Sony Mathew <sony@chatwoot.com> |
||
|
|
08260f3be7 |
feat: show crawled document details and FAQ counts in Captain (#14863)
- Add a document details view that surfaces crawled content, source metadata, and generated FAQ counts. - Rename the document card action to open details and show the FAQ count inline in the list. - Return `responses_count` from the documents API efficiently and expose document content in the show payload. - Update related Captain copy to reflect the new details-oriented flow. **Preview** <img width="1640" height="1596" alt="CleanShot 2026-06-26 at 09 25 15@2x" src="https://github.com/user-attachments/assets/0c408fae-7d37-422a-8869-ece466292cb1" /> --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> Co-authored-by: Sony Mathew <sony@chatwoot.com> Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com> |
||
|
|
11c65f3b9a |
feat: Intercom import workflow (#14922)
## Description Adds an admin-only Intercom import workflow under Settings > Data. Admins can connect an Intercom access token, start named historical contact/conversation imports, monitor active and previous import runs, review paginated skip/error logs, download skip logs, and route imported conversations into source-bucket API inboxes that can be renamed later. The import path stores durable source mappings, batches Intercom contact/conversation pages through Sidekiq, records already-imported records as skipped, and writes historical messages without normal outbound delivery callbacks. The PR also includes the Intercom import PRD/TDD document for review context. Closes [CW-7519](https://linear.app/chatwoot/issue/CW-7519/explore-intercom-import) ## 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) - [x] This change requires a documentation update ## How Has This Been Tested? Tested importing using actual data through integration. Screenshots: <img width="1800" height="948" alt="Screenshot 2026-07-02 at 10 48 48 PM" src="https://github.com/user-attachments/assets/e74d9ed6-0bca-47de-b6ef-e589afcddfde" /> <img width="1800" height="1008" alt="Screenshot 2026-07-02 at 10 49 03 PM" src="https://github.com/user-attachments/assets/1bd12fdb-0a47-4287-ac1d-ea308e70a9cd" /> <img width="1800" height="1005" alt="Screenshot 2026-07-02 at 10 49 21 PM" src="https://github.com/user-attachments/assets/3d8145f5-1794-4cc3-b3fa-de5cd80e6ca3" /> <img width="1800" height="1002" alt="Screenshot 2026-07-02 at 10 49 38 PM" src="https://github.com/user-attachments/assets/6f818efd-4193-43c2-84eb-66970dca4490" /> Passed locally: ```sh eval "$(rbenv init -)" && bundle exec rspec spec/models/data_import_spec.rb spec/jobs/data_import_job_spec.rb spec/requests/api/v1/accounts/data_imports_spec.rb spec/requests/api/v1/accounts/integrations/intercom_spec.rb spec/jobs/data_imports/intercom/import_jobs_spec.rb spec/services/data_imports/intercom/importer_spec.rb spec/services/data_imports/intercom/placeholder_inbox_builder_spec.rb spec/services/data_imports/intercom/source_bucket_spec.rb ``` ```sh eval "$(rbenv init -)" && bundle exec rubocop app/controllers/api/v1/accounts/data_imports_controller.rb app/controllers/api/v1/accounts/integrations/intercom_controller.rb app/jobs/data_imports/intercom app/models/data_import.rb app/models/data_import_error.rb app/models/data_import_item.rb app/models/data_import_mapping.rb app/models/integrations/hook.rb app/policies/data_import_policy.rb app/policies/hook_policy.rb app/services/data_imports/intercom db/migrate/20260702000000_expand_data_imports_for_intercom_imports.rb db/migrate/20260702000001_create_data_import_items.rb db/migrate/20260702000002_create_data_import_mappings.rb db/migrate/20260702000003_create_data_import_errors.rb spec/jobs/data_imports/intercom spec/requests/api/v1/accounts/data_imports_spec.rb spec/requests/api/v1/accounts/integrations/intercom_spec.rb spec/services/data_imports/intercom ``` ```sh pnpm exec eslint app/javascript/dashboard/api/dataImports.js app/javascript/dashboard/api/integrations.js app/javascript/dashboard/routes/dashboard/settings/data/Index.vue app/javascript/dashboard/routes/dashboard/settings/data/Show.vue app/javascript/dashboard/routes/dashboard/settings/data/data.routes.js app/javascript/dashboard/routes/dashboard/settings/data/importStatus.js app/javascript/dashboard/routes/dashboard/settings/integrations/Intercom.vue app/javascript/dashboard/routes/dashboard/settings/integrations/integrations.routes.js app/javascript/dashboard/routes/dashboard/settings/settings.routes.js app/javascript/dashboard/components-next/sidebar/Sidebar.vue app/javascript/dashboard/routes/dashboard/settings/inbox/Index.vue ``` ```sh git diff --check ``` Note: the RSpec boot logs the existing local `chatwoot_dev` purge warning because other database sessions are open, then continues and completes with 52 examples, 0 failures. ## 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 - [x] 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 --------- Co-authored-by: Shivam Mishra <scm.mymail@gmail.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: iamsivin <iamsivin@gmail.com> |
||
|
|
8fc5c7a5c8 |
feat: add captain general guidelines migration helpers (#14909)
This PR adds internal tooling and planning docs for migrating existing Captain assistant instructions into the new General Guidelines structure. **Summary** This PR adds a controlled migration path for moving existing Captain V1 assistant instructions into the structured Captain architecture. It introduces a classifier that reads the current `config.instructions` and produces reviewed migration drafts with separate sections for: - assistant description / business context - response guidelines - guardrails - scenario candidates - conversation messages - FAQ/document candidates - needs-review items The migration is intentionally staged. It only targets V1-style assistants that still have custom instructions, are connected to inboxes, and do not already have structured response guidelines, guardrails, or scenario records. When applied, the task writes the extracted business context to the assistant description, response guidelines to `response_guidelines`, guardrails to `guardrails`, and stores scenario candidates / FAQ candidates / review notes under `config["assistant_migration"]`. Scenario candidates are also flattened into response guidelines for now so customer behavior is preserved before we create real `Captain::Scenario` records in a later rollout. The applier stores the original assistant values under migration metadata so conversation message config can be restored if needed. It does not create scenario records yet. **How to generate drafts** For specific assistant IDs: ```bash bundle exec rake captain:assistant_migration:generate \ IDS=546,636,819 \ LIMIT=0 \ OUTPUT=tmp/captain_migration_drafts.jsonl ``` For the first 50 eligible assistants: ```bash bundle exec rake captain:assistant_migration:generate \ OUTPUT=tmp/captain_migration_drafts.jsonl ``` For all eligible assistants: ```bash bundle exec rake captain:assistant_migration:generate \ LIMIT=0 \ OUTPUT=tmp/captain_migration_drafts.jsonl ``` **How to apply drafts** Dry run first: ```bash bundle exec rake captain:assistant_migration:apply \ INPUT=tmp/captain_migration_drafts.jsonl \ DRY_RUN=true ``` Apply changes: ```bash bundle exec rake captain:assistant_migration:apply \ INPUT=tmp/captain_migration_drafts.jsonl \ DRY_RUN=false ``` **How to restore conversation messages** If extracted `welcome_message`, `handoff_message`, or `resolution_message` need to be reverted to their pre-migration values: ```bash bundle exec rake captain:assistant_migration:restore_messages \ IDS=546,636,819 \ DRY_RUN=true ``` ```bash bundle exec rake captain:assistant_migration:restore_messages \ IDS=546,636,819 \ DRY_RUN=false ``` **Notes** - `LIMIT=0` means no limit. - `generate` overwrites the output file. - The apply task skips assistants that are no longer V1 migration candidates. - This PR does not create `Captain::Scenario` records; scenario candidates are staged in assistant config for a future migration. --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> Co-authored-by: aakashb95 <aakashbakhle@gmail.com> Co-authored-by: Aakash Bakhle <48802744+aakashb95@users.noreply.github.com> |
||
|
|
9c10fe4eb1 |
fix: default assignment age exclusion (#14998)
## Description Auto-assignment was skipping the 7-day staleness check entirely for inboxes that don't have an assignment policy attached. Those inboxes would pull unassigned conversations of any age off the backlog and hand them to agents — including conversations untouched for months — while the activity log still credited "Default Policy" for the assignment. This makes the default behaviour match what that label implies: with no policy configured, conversations with no activity in the last 7 days are now excluded, the same window a freshly created policy uses. ## 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 |
||
|
|
6b3800e25b |
fix: pre-chat form contact custom attributes lost for existing contacts (#14956)
Contact custom attributes filled in the pre-chat form (e.g. a CPF field)
were silently lost when the visitor's email or phone matched an existing
contact. The widget sent the attributes in a separate request that raced
the conversation create: creating the conversation merges the widget
contact into the existing contact, so the attribute update landed on the
destroyed contact and vanished without an error. New visitors were
unaffected, which made the bug hard to spot.
The fix sends contact custom attributes inside the same request that
identifies the contact. The widget conversation create endpoint now
accepts `contact.custom_attributes` and applies them through
`ContactIdentifyAction` in the same transaction as the merge, so the
submitted values always land on the surviving contact. The campaign path
folds the attributes into the existing contact update call the same way.
<details>
<summary>Reproduction script (rails runner, concurrent
threads)</summary>
```ruby
# Concurrent reproduction of the pre-chat form race that loses contact
# custom attributes when the visitor's email matches an existing contact.
#
# The old widget fired two unawaited requests on pre-chat submit. Each
# iteration replays them as real concurrent threads:
# - create thread = POST /widget/conversations: resolves the widget contact,
# then runs ContactIdentifyAction (which merges the widget contact into the
# existing contact) inside a transaction held open while the conversation
# and message are created.
# - patch thread = PATCH /widget/contact: resolves the widget contact via its
# contact inbox and applies the custom attributes through
# ContactIdentifyAction, exactly like Widget::ContactsController#update.
#
# Run with: bundle exec rails runner confirm_prechat_race.rb
# Creates throwaway contacts on Account.first and deletes them afterwards.
ITERATIONS = 20
CPF = '123.456.789-09'.freeze
account = Account.first!
inbox = account.inboxes.first!
losses = 0
ITERATIONS.times do |i|
existing = account.contacts.create!(name: 'Existing Contact', email: "race-existing-#{SecureRandom.hex(6)}@example.com")
temp = account.contacts.create!(name: 'Widget Visitor')
contact_inbox = ContactInbox.create!(contact: temp, inbox: inbox, source_id: SecureRandom.uuid)
begin
create_request = Thread.new do
ActiveRecord::Base.connection_pool.with_connection do
widget_contact = ContactInbox.find(contact_inbox.id).contact # set_contact before_action
ActiveRecord::Base.transaction do
ContactIdentifyAction.new(
contact: widget_contact,
params: { email: existing.email, phone_number: nil, name: 'Widget Visitor' },
retain_original_contact_name: true,
discard_invalid_attrs: true
).perform
sleep(0.02) # conversation + message creation keeps the transaction open
end
end
end
patch_request = Thread.new do
ActiveRecord::Base.connection_pool.with_connection do
sleep(rand * 0.02) # network jitter between the two requests
widget_contact = ContactInbox.find(contact_inbox.id).contact # set_contact before_action
ContactIdentifyAction.new(
contact: widget_contact,
params: { custom_attributes: { cpf: CPF } },
discard_invalid_attrs: true
).perform
end
end
create_request.join
patch_request.join
survivor = existing.reload
if survivor.custom_attributes['cpf'] == CPF
puts "iteration #{i + 1}: CPF survived"
else
losses += 1
puts "iteration #{i + 1}: CPF LOST (survivor custom_attributes: #{survivor.custom_attributes.inspect})"
end
ensure
account.contacts.where(id: [existing.id, temp.id]).find_each(&:destroy!)
end
end
puts
puts "#{losses}/#{ITERATIONS} iterations lost the CPF -- race #{losses.positive? ? 'confirmed' : 'not reproduced in this run'}"
```
Result: **20/20 iterations lose the CPF** (consistent across repeated
runs). The conversation create holds its transaction open across the
contact merge, so the fast attribute update either resolves the
soon-to-be-destroyed widget contact or blocks on its row lock and then
updates 0 rows — silently, with no error. This is why the customer sees
the loss every time, not intermittently.
Note: the script replays the old two-request flow at the service layer,
so it reproduces the loss even with this fix applied — the fix works by
removing the second request from the widget, not by changing the raced
code paths. The new request spec (`saves contact custom attributes on
the surviving contact when merged into an existing contact`) covers the
fixed contract.
</details>
## Closes
- Reported via support conversation:
https://app.chatwoot.com/app/accounts/1/conversations/84465
## How to reproduce
1. Create a website inbox with a pre-chat form that has a contact custom
attribute field (e.g. CPF).
2. Create a contact with a known email address.
3. As a visitor, open the widget and fill the pre-chat form using that
same email plus a value for the custom attribute.
4. Before: the attribute never appears on the contact profile. After: it
is saved on the existing contact, overwriting any stale value.
## What changed
- `POST /api/v1/widget/conversations` now permits
`contact.custom_attributes` and passes it to `ContactIdentifyAction`,
which deep-merges it atomically with the contact merge.
- The widget pre-chat form sends contact custom attributes inside the
conversation create payload instead of a separate `setCustomAttributes`
call; the campaign path includes them in the `contacts/update` payload.
|
||
|
|
b560720e08 |
feat: allow switching embedded signup whatsapp embedded inboxes to manual setup (#14975)
Since embedded signup has been disabled on production, WhatsApp inboxes that were created through it have no way to be managed going forward. This PR lets admins transfer an embedded signup inbox to a manual Cloud API setup: the inbox settings Configuration tab now shows the webhook verification token and a "Switch to Manual Setup" form (pre-populated with the Phone Number ID, Business Account ID, and API key stored during the embedded signup journey) instead of the old Reconfigure button. Saving the form updates the channel's `provider_config` without the `source: embedded_signup` marker, so the inbox becomes a regular manually-configured WhatsApp Cloud inbox. The backend re-validates the submitted credentials against Meta before accepting the change. ## How to test 1. Open the settings page of a WhatsApp inbox created via embedded signup → Configuration tab. 2. The Reconfigure button is gone; you see the webhook verification token and a Switch to Manual Setup form pre-filled with the stored credentials. 3. Configure the webhook in your own Meta app using the verification token, enter a permanent access token from that app, and click "Switch to Manual Setup". 4. On success the page switches to the standard manual configuration view (verify token, API key update), and messaging continues to work with the new credentials. Invalid credentials are rejected with an error. ## What changed - `ConfigurationPage.vue`: replaced the embedded-signup Reconfigure section (and the hidden reauthorize component) with the manual transfer form. - New `WHATSAPP_MANUAL_TRANSFER_*` translation keys. --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: iamsivin <iamsivin@gmail.com> |
||
|
|
848e94bcf2 |
fix: throttle filtered unread count rebuilds (#14980)
Reduces database pressure from filtered unread-count cache rebuilds during high-traffic account rollouts by serving stale snapshots longer and limiting inline saved-filter rebuild fanout. ## Closes None ## What changed - Increase filtered unread-count refresh throttling from 30 seconds to 5 minutes. - Increase the stale snapshot window from 30 minutes to 1 hour. - Reduce inline saved-filter count rebuilds per request from 10 to 3. - Update unread-count specs to assert refresh and stale behavior through the shared constants. ## How to test - Enable `conversation_unread_counts` and `unread_count_for_filters` for an account with conversation custom filters. - Open the dashboard and verify unread-count badges still return values. - Mutate conversations and verify stale filtered counts are served while rebuilds are throttled, instead of repeatedly rebuilding every 30 seconds. |
||
|
|
d57354c8b5 |
feat: tighten conversation FAQ generation prompt (#14957)
Tightens the resolved-conversation FAQ generator so it only proposes durable, reusable FAQ candidates supported by human support-agent messages. The implementation now sends a conversation-FAQ-specific transcript to the LLM: customer messages plus real human support-agent messages only, excluding bot, private, activity, and template messages. ## Closes - https://linear.app/chatwoot/issue/CW-7494/tighten-conversation-faq-generation-prompt ## What changed - Added a human-only transcript builder in `ConversationFaqService` instead of using the generic `conversation.to_llm_text` output. - Excluded bot/agent-bot messages before the LLM call, which removes the main bot-line leakage class deterministically. - Preserved native-channel human replies where outgoing messages are stored as `external_echo` without a `User` sender. - Kept a prompt decision gate requiring each FAQ to be backed by a complete public human-agent answer. - Added generic no-FAQ classes for spam, wrong-service conversations, private account/payment/order/certificate/troubleshooting cases, support workflow mechanics, and direct-link/file/quote outputs. - Added a separate `conversation_faq_generation` model route defaulting to `gpt-5.2`, while keeping `document_faq_generation` on its existing `gpt-4.1-mini` default. Conversation FAQ generation passes that feature default ahead of the legacy global `CAPTAIN_OPEN_AI_MODEL` setting unless an account-level override is configured. - Kept the prompt domain-neutral so it can still generate reusable product, service, policy, setup, and process FAQs outside SaaS contexts. ## Sampling notes - Production Langfuse traces showed `llm.captain.conversation_faq` calls using `gpt-4.1` in the sampled account set. - Locally, `Llm::FeatureRouter.resolve(feature: 'conversation_faq_generation')` now resolves to `gpt-5.2`. - Reviewed recent production `llm.captain.conversation_faq` traces across 13+ accounts in compact form. - Replayed 20 full traces across 10 accounts/domains, including education, hosting, retail/auto, APIs, logistics, tax/fiscal workflows, and Chatwoot account 1. - Explicit `gpt-5.2` replay with human-only conversation history returned no FAQ for 15/20 traces. - A comparison replay with `gpt-4.1-mini` returned no FAQ for only 7/20 traces, bringing back several private/order/payment/support-workflow cases. - Remaining non-empty `gpt-5.2` outputs are now mostly borderline/possibly useful human-agent-derived FAQs rather than obvious bot-sourced answers. ## How to test - Resolve conversations where the answer came only from the bot; no pending FAQ should be generated. - Resolve spam, unrelated, wrong-service, or private payment/order/account conversations; no pending FAQ should be generated. - Resolve conversations that require account/order/payment/login/private verification or a human handoff; no pending FAQ should be generated. - Resolve a conversation where a human agent gives a stable, reusable help-center answer; the generated pending FAQ should be general and self-contained. |
||
|
|
20227403ce |
fix: return public contact inbox payload after update (#14946)
# Pull Request Template ## Description This keeps the public inbox contact update response on the public contact inbox serializer shape after applying the contact identify update. ## 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 to not work as expected) - [ ] This change requires a documentation update ## How Has This Been Tested? - `bundle exec rspec spec/controllers/public/api/v1/inbox/contacts_controller_spec.rb` ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [ ] I have commented 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 Fixes [https://linear.app/chatwoot/issue/CW-6937](https://linear.app/chatwoot/issue/CW-6937) Fixes [https://linear.app/chatwoot/issue/CW-7464](https://linear.app/chatwoot/issue/CW-7464) Fixes [https://linear.app/chatwoot/issue/CW-7457](https://linear.app/chatwoot/issue/CW-7457) |
||
|
|
0a13dbedfb |
fix: force account picker on microsoft email inbox OAuth (#14794)
## Description The Microsoft authorize URL had no `prompt` parameter. With Microsoft's default behavior, a browser carrying an active Microsoft session is silently signed in to that account. If the same account is already attached to an inbox in the same Chatwoot account, the callback treats the OAuth response as a re-auth and routes the user to the existing inbox settings page instead of letting them create the new inbox they intended. Adding `prompt=select_account` interrupts SSO and always presents the Microsoft account picker, so the user explicitly chooses the mailbox they want to connect. This is distinct from `prompt=consent`, which was removed in [#13962](https://github.com/chatwoot/chatwoot/pull/13962) to fix the admin consent loop. `select_account` only interrupts SSO and does not retrigger the consent dialog. Closes INF-76 ## 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? `bundle exec rspec spec/controllers/api/v1/accounts/microsoft/authorization_controller_spec.rb` (3 examples, 0 failures). The regression test introduced in [#13962](https://github.com/chatwoot/chatwoot/pull/13962) is updated to assert `prompt=select_account` instead of asserting absence of the parameter. ## 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 |
||
|
|
66cfb26c77 |
feat: Add unread count filters feature flag (1/6) (#14885)
## Description Adds the account-level `unread_count_for_filters` feature flag as the dark-launch gate for filtered sidebar unread counts. This reuses the deprecated `quoted_email_reply` flag slot, resets the reused bit for existing accounts, and removes stale defaults so new accounts do not reference the old flag. This also adds the feature where we are now calculating the unread counts for built in filters like mentions, participating and unattended along with unread count for saved filters/folders. Closes [CW-7262](https://linear.app/chatwoot/issue/CW-7262/unread-counts-for-filters-folders) ## 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 |
||
|
|
873d16f54c |
feat: Extend account feature flag storage (#14947)
# Pull Request Template ## Description Extends account-level feature flags by adding a second bigint bitset column, `feature_flags_ext_2`, while preserving the existing `flag_shih_tzu` feature check and enable/disable APIs. Existing flags continue to live on `feature_flags`; future flags can opt into the extension column through `config/features.yml` metadata. Fixes [CW-7238](https://linear.app/chatwoot/issue/CW-7238/feature-flag-extension) ## 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? - `eval "$(rbenv init -)" && RAILS_ENV=test POSTGRES_DATABASE=chatwoot_test_31a6 bundle exec rspec spec/models/concerns/featurable_spec.rb spec/models/account_spec.rb spec/lib/config_loader_spec.rb spec/controllers/platform/api/v1/accounts_controller_spec.rb spec/controllers/super_admin/accounts_controller_spec.rb spec/enterprise/models/account_spec.rb` - 144 examples, 0 failures - `eval "$(rbenv init -)" && bundle exec rubocop app/models/concerns/featurable.rb app/models/account.rb db/migrate/20260706215758_add_feature_flags_ext_2_to_accounts.rb spec/models/concerns/featurable_spec.rb spec/models/account_spec.rb spec/lib/config_loader_spec.rb spec/enterprise/models/account_spec.rb` - 7 files inspected, no offenses detected - `ruby -ryaml -e "features = YAML.safe_load(File.read('config/features.yml')); abort unless features.size == 63; puts features.group_by { |f| f['column'] || 'feature_flags' }.transform_values(&:size).inspect"` - `{"feature_flags" => 63}` - `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 - [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 |
||
|
|
f6c18f5225 |
feat: account calls dashboard index endpoint (#14780)
## Description Adds a backend endpoint that powers an account-wide calls dashboard, letting users list and filter all calls in the account. ## Linear Ticket - https://linear.app/chatwoot/issue/UPM-28/voice-call-dashboard-view ## Type of change - [ ] New feature (non-breaking change which adds functionality) ## 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 |
||
|
|
0e07a27c74 |
fix: enforce inbox limits at model level (#14949)
Fixes https://linear.app/chatwoot/issue/CW-7559/inbox-limit-abuse ## Why The regular inbox API checked limits in the controller, but WhatsApp embedded signup creates inboxes through a service using `Inbox.create!`. That let Enterprise account inbox limits be skipped for embedded signup. ## What this change does - Adds an Inbox create-time validation hook in OSS and implements the limit check in the Enterprise Inbox module. - Removes the duplicate controller/helper limit check so the model is the single enforcement point. - Preserves the existing `402 Payment Required` API response for account inbox limit failures. - Keeps updates to existing inboxes allowed when an account is already at its inbox limit. ## Validation - `bundle exec rspec spec/controllers/api/v1/accounts/inboxes_controller_spec.rb spec/enterprise/models/inbox_spec.rb` |
||
|
|
ce8c8e9a11 |
fix: sanitize control characters in team names (#14865)
Team names created via the API could contain control characters (for
example a trailing newline). Because the team-delete confirmation dialog
requires you to retype the team name and matches it against the stored
value, a hidden control character meant the typed name never matched —
leaving the team impossible to delete from the UI. This sanitizes team
names on save so they stay clean and deletable.
#### How to reproduce
1. Create a team via `POST /api/v1/accounts/{account_id}/teams` with
`{"name": "test\n"}`.
2. The team is created with the trailing newline stored in `name`.
3. In **Settings → Teams**, click delete and type the team name to
confirm — the match fails, so the team cannot be deleted.
#### What changed
- `app/models/team.rb`: the existing `before_validation` now strips
control characters and surrounding whitespace before downcasing the
name. Names that reduce to blank (e.g. only newlines/tabs) are rejected
loudly by the existing `presence` validation.
- Fixing at the model layer covers the API and every other create/update
path, rather than relying on the frontend confirm-dialog `.trim()`
(which only handles leading/trailing whitespace, not internal control
characters).
Note: this prevents new malformed names. Any team already saved with a
control character can be made deletable again simply by renaming it (an
update re-runs the same sanitization).
| Input | Stored as | Result |
|---|---|---|
| `"test\n"` | `"test"` | valid, deletable |
| `"te\nst"` (internal) | `"test"` | valid |
| `"\t\n "` (only control/ws) | — | rejected: "Name must not be blank" |
| `"Customer Support"` | `"customer support"` | unchanged behavior |
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
a5fcecb3f6 |
feat: assistant overview page [CW-7408] (#14889)
This PR adds a Captain Assistant **Overview** page to show some KPI metrics (conversations handled, auto-resolution, handoff, hours saved, reopen-after-resolve, conversation depth) with trend deltas vs the previous window, a real knowledge card, and a lazily-loaded, cached LLM welcome summary. ### Highlights - **Two contextual banners** on the overview: - **Inbox banner** — prompts the user to connect an inbox when the assistant has none, so it can actually do work. - **Coverage banner** — warns when FAQ coverage is below 85% with more than 100 responses pending review, linking straight to the pending queue. Dismissal persists per-assistant for 24h via localStorage. - **Batched stats builder** (`Captain::AssistantStatsBuilder`) computes both windows in single FILTER-aggregated scans to cut round trips, behind new `stats`/`summary` endpoints. - **Cards included but intentionally left dummy / not rendered yet:** `ResponseQualityCard` (flagged responses) and `CreditUsageCard` (credit usage + daily chart). Credits are an account-wide counter with no per-assistant or daily history, so there is no real data to back them yet; they ship in the codebase but are not wired into the page. ### Index migration - Replaces `index_messages_on_sender_type_and_sender_id` with `index_messages_on_sender_and_created` `(sender_type, sender_id, created_at)`. - **Why it helps:** the per-assistant windowed lookups filter `sender_*` *and* a `created_at` range. The old 2-column index matched every lifetime row for the assistant and filtered the time slice at the heap (~89% of rows discarded); adding `created_at` as a range column lets Postgres scan only the window, and fixes the row-count estimate so the planner picks a hash join over a nested loop on `reporting_events`. - **Why dropping the old index is safe:** the new index is a left-prefix superset `(sender_type, sender_id, ...)`, so every query the old one served is still served. No code references it by name, and dropping it keeps write amplification on `messages` neutral. Built/dropped with `CONCURRENTLY` and `if_not_exists`/`if_exists` guards. ## Preview <img width="2572" height="1754" alt="CleanShot 2026-06-29 at 22 38 51@2x" src="https://github.com/user-attachments/assets/3798d09e-7850-48e4-b2cd-508533f15cea" /> ## Banners #### Inbox connect alert <img width="2178" height="612" alt="CleanShot 2026-06-30 at 14 26 55@2x" src="https://github.com/user-attachments/assets/373c371c-bb7d-4291-a0f9-620673078302" /> #### Coverage alert <img width="2178" height="612" alt="CleanShot 2026-06-30 at 14 25 41@2x" src="https://github.com/user-attachments/assets/e12d6308-11b6-4ba2-88a2-8a3077dd3e8f" /> --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> |
||
|
|
c8bfbadae2 |
fix: validate identifier hash on widget contact update (#14884)
## Description The widget contact update endpoint (`PATCH /api/v1/widget/contact`) applied updates that set an `identifier` without validating the `identifier_hash`, unlike `set_user`, which already does. This change runs the same validation on the update path whenever an `identifier` is supplied, keeping identity validation consistent across the widget contact endpoints. Anonymous updates that don't pass an identifier (prechat name/email/phone and custom attributes) keep working unchanged, including on inboxes with mandatory identity validation. Fixes https://linear.app/chatwoot/issue/CW-7118 --------- Co-authored-by: Shivam Mishra <scm.mymail@gmail.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> |
||
|
|
83dda621c5 |
feat: default new accounts to captain v2 (#14917)
# Pull Request Template ## Description defaults new accounts to captain v2 ## 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 |
||
|
|
eab859cd04 |
fix: Sanitize query canceled API errors (#14933)
# Pull Request Template ## Description Message creation API failures caused by PostgreSQL query cancellations now return a generic retryable error instead of exposing raw database internals such as `PG::QueryCanceled`, tuple identifiers, or relation names to customers. Existing validation failures continue to return their specific validation messages. Closes [CW-7538](https://linear.app/chatwoot/issue/CW-7538/do-not-expose-pgquerycanceled-details-in-messages-api-errors) ## 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? Reproduced the messages API path by raising `ActiveRecord::QueryCanceled` from `Messages::MessageBuilder` and verified the response contains the customer-safe localized error without `PG::QueryCanceled` details. Validation run locally: - `bundle exec rspec spec/controllers/api/v1/accounts/conversations/messages_controller_spec.rb` - `bundle exec rubocop app/controllers/concerns/request_exception_handler.rb spec/controllers/api/v1/accounts/conversations/messages_controller_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 - [ ] 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 |
||
|
|
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 |