Make the wait's interrupt explicit (as Intercom does) so users can predict when the
rule runs. An "Ends the wait if …" line adapts to the rule's episode: status change
for conversation rules, customer reply for outgoing message rules, agent reply for
incoming ones — plus "or the conditions no longer match" universally. Behaviour is
unchanged; this only surfaces it.
Adopt the simpler Wait concept (as Intercom et al. use): the toggle is labelled
"Wait", the duration reads "Wait for [4] [Hours]", and the block moves to just
before Actions (it applies to the whole rule; if we add ordered steps later it can
become a proper Wait action). The "Runs after 4h" list badge is unchanged, and the
engineering layer (execution_delay column, delayed_automations flag) keeps its name.
Enables Uzbek as a selectable Chatwoot language and wires the `uz`
locale into the dashboard, widget, and survey translation loaders.
## Closes
Closes https://github.com/chatwoot/chatwoot/issues/15030
## Why
Uzbek is now available in the Chatwoot Crowdin project. The widget
translation has been proofread and is 100% translated and approved.
## What changed
- Adds Uzbek to the supported language registry
- Registers the `uz` locale with the dashboard, widget, and survey i18n
loaders
## Validation
- Proofread the Uzbek widget translation in Crowdin
- Verified placeholder preservation and corrected wording where needed
- Confirmed the widget file is 100% translated and 100% approved
This draft intentionally remains blocked until the Crowdin sync adds the
generated Uzbek locale files. It should only be marked ready and merged
after that sync lands.
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>
Restores the Inbox Channels dialog’s Facebook-gating test coverage by
isolating account feature dependencies that are unrelated to these
cases. This keeps the onboarding checks focused on whether Facebook is
configured and avoids initializing router and account-store state.
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Instagram inbox creation is available again on Chatwoot Cloud. Users can
discover and connect Instagram during onboarding or from Add Inbox,
while WhatsApp restrictions and existing-inbox Instagram advisories
remain unchanged.
Closes https://linear.app/chatwoot/issue/CW-7549/enable-instagram
## How to test
1. On Chatwoot Cloud, open Add Inbox and confirm Instagram can be
selected.
2. Confirm **Continue with Instagram** is enabled and starts the OAuth
flow.
3. In onboarding, confirm Instagram is displayed and can start OAuth.
4. Confirm WhatsApp embedded signup remains restricted.
## What changed
- Removed the Cloud-only Instagram filter and OAuth guard from
onboarding.
- Re-enabled the regular Instagram inbox creation action on Cloud.
- Removed the obsolete “Instagram inbox creation is temporarily
unavailable” copy.
- Updated the onboarding expectation for Chatwoot Cloud.
---------
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
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`.
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>
Show the reauthorization banner whenever an embedded signup inbox has a
reauthorization error instead of suppressing it when the manual
migration flow is enabled. Reword the manual migration banner and dialog
as a recommendation to reconnect with your own Meta app, and switch
their styling from amber warning to blue info.
## Description
The conversation sidebar counts (`GET /conversations/meta`) are polled
on a debounced cadence selected from the last known conversation count
(`allCount` in the `conversationStats` store).
`allCount` starts at `0` and is only set after a **successful** meta
response. When the meta query is slow or failing under load, `allCount`
stays `0`, which selected the fastest cadence, so every client polled as
aggressively as possible exactly when the endpoint was already
struggling. On large accounts with many concurrent agents this
multiplies into sustained load that keeps the query slow, a
self-reinforcing loop.
This PR makes two changes:
1. **Treat an unknown count (`0`) as a large account** and poll at the
slowest cadence instead of the fastest (`getMetaDebounceKey`, extracted
as a pure, testable function). This breaks the failure loop.
2. **Raise the debounce intervals across all tiers** so counts are
polled less frequently under sustained activity:
- fast: max 1 poll / 2s -> 1 / 5s
- mid: 1 / 10s -> 1 / 20s
- slow (large/unknown accounts): 1 / 20s -> 1 / 30s
First-load counts still render immediately (the debounce fires on the
leading edge for the very first call), so the higher intervals only
affect the sustained poll rate, not initial render.
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
Enabling a delay on a rule with delay-unsafe conditions called onEventChange, which
resets conditions AND actions to defaults, silently replacing the user's configured
actions with assign_agent. Reset only the conditions (getDefaultConditions) and leave
the actions intact.
Event-narrowing wrongly hid valid delayed triggers like conversation_created
(new conversation idle 10m -> assign), forcing the event to conversation_updated.
The real guardrail is the Status/Inbox condition narrowing, which stays; the event
list no longer needs restricting since every event supports a delayed rule with
the right conditions.
inbox_id never changes after a conversation is created, so filtering a delayed
conversation-level rule by inbox is safe: the status episode key still tracks the
only mutable dimension, and inbox rides along as a static fire-time re-check. Allow
status + inbox_id in the validation and the narrowed condition dropdown.
When a delay is on, the condition attribute dropdown for conversation-level events
offers only Status, and attribute_changed is dropped from the operator list for any
delayed rule. Toggling the delay on resets conditions the delayed rule can't use.
This makes unsupported delayed rules unconstructable, so the inline restriction
warning (and its i18n strings) are removed.
Delayed execution is now a toggle chosen first (above Event). Turning it on limits
the Event dropdown to the events where a delayed rule is meaningful (Conversation
Updated for status waits, Message Created for awaiting-agent/reply-chase) and snaps
an unsupported event to a supported one, so users can't build a delayed rule on an
event the engine can't handle.
The edit dialog's delay radios/value were synced once in open() from the automation
model, whose prop only settles a tick later — so the first edit click showed the
previous rule's delay (or immediate). Thread the clicked rule's execution_delay
directly into open() so the delay controls hydrate correctly on the first click.
- episode_key_for falls back to the incoming message's created_at when
waiting_since is still nil (it is written after MESSAGE_CREATED dispatches),
so awaiting-agent episodes no longer arm as awaiting_agent:0 and get skipped
as episode_moved on the first customer message after an agent reply.
- Editing a rule's trigger, conditions, or actions (not just the delay) now
discards its armed pending rows so they can't fire against a definition they
were never armed under; deleting rather than skipping frees the episode slot
so the new definition re-arms on the next matching event.
- The rule form only offers a delay when the config supports it: the delayed
option is disabled with an explanation for attribute_changed conditions and
for non-status conditions on conversation events, and the backend's specific
error is surfaced on save instead of a generic message.
Agents can no longer send replies in Instagram conversations on Chatwoot
Cloud while the temporary Meta platform restriction is active. The reply
box locks into Private Note mode — the same behavior as an expired
24-hour reply window — so teams can still collaborate internally, with
the existing amber restriction banner above the conversation explaining
why. Self-hosted installations are unaffected.
Follow-up to #14974.
## How to test
1. On a Chatwoot Cloud environment (`isOnChatwootCloud` true), open any
Instagram conversation.
2. The composer should be locked to Private Note mode: the Reply/Private
Note toggle is disabled, and sending creates a private note — even for
conversations within the 24-hour reply window.
3. Switching between conversations should keep the composer in Private
Note mode for Instagram conversations.
4. On a self-hosted environment, Instagram conversations should behave
as before (reply allowed within the messaging window).
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
- 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>
## 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>
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>
# Pull Request Template
## Description
Increases description for Captain.
Why?
We are planning to include business context in description and 255 char
limit on the column and 200 char limit on the UI are very limiting to
get proper context.
## Type of change
Improvement to accommodate business context
## 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:
- [ ] 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
Instagram inbox creation and WhatsApp embedded signup on Chatwoot Cloud
now reflect the temporary Meta restriction. Instagram is hidden from
onboarding on Cloud, while the regular Instagram inbox creation page
shows a disabled action with a status-linked amber warning. WhatsApp
embedded signup on Cloud stays visible with its connect action disabled.
WhatsApp Call setup always uses the manual WhatsApp form.
Existing Instagram conversations and Instagram inbox settings on Cloud
also show amber warning banners with the public incident link.
Self-hosted installations keep their existing Instagram, WhatsApp, and
WhatsApp Call setup behavior because the temporary restriction is based
only on the Chatwoot Cloud environment check.
---------
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
# Pull Request Template
## Description
This PR makes reply editor variables insert their resolved value (for
example, the contact's name) instead of the raw `{{contact.name}}`
placeholder, matching canned response behavior. This works both when
picking a variable from the `{{` menu and when an agent manually types
out `{{contact.name}}` — it resolves the moment the closing `}}` is
typed.
If a variable has no value, the `{{placeholder}}` is kept so the backend
can still resolve it when the message is sent. Private notes are left
untouched.
For safety, a resolved value that itself contains Liquid syntax `({{ }}`
or `{% %})` also keeps its placeholder, so customer-controlled fields
can never inject Liquid into the outgoing message.
Fixes
https://linear.app/chatwoot/issue/CW-7528/reply-editor-inserts-variable-placeholder-instead-of-the-value
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
1. Open a conversation and add a reply.
2. Type `{{` and pick a variable that has a value (e.g. Contact name) →
it inserts the actual value.
3. Manually type `{{contact.name}}` and close the braces → it
auto-resolves to the value.
4. Insert/type a variable with no value → the `{{placeholder}}` stays;
confirm it resolves correctly on send.
5. Repeat in a private note → placeholders are left as-is.
## 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
Re-enables the "WhatsApp Calls" inbox creation option, which disappeared
when embedded signup was disabled on production. The channel card now
only requires the `channel_voice` account feature, and the creation flow
offers the manual WhatsApp Cloud API setup form. Once the channel is
created with manual credentials, calling is enabled automatically — the
same post-setup step the embedded signup flow used to perform.
When an installation has embedded signup configured, the flow still uses
it; the manual form is the fallback (and the effective path on Chatwoot
Cloud today).
## How to test
1. Enable the `channel_voice` feature on the account.
2. Go to Add Inbox → the "WhatsApp Calls" card is visible again → select
it.
3. Fill in the manual Cloud API credentials (inbox name, phone number,
phone number ID, business account ID, API key) and submit.
4. The inbox is created and calling is enabled:
`provider_config.calling_enabled` is true and the Calls tab toggle is
on. If the number isn't enrolled in the Business Calling API, an alert
explains the enable failure but the messaging inbox is still created.
## What changed
- `ChannelItem.vue`: the `whatsapp_call` card is gated only on
`channel_voice` (previously also required the embedded signup app ID).
- `CloudWhatsapp.vue`: new `enableCallingOnComplete` prop that calls the
enable-calling API after channel creation.
- `WhatsappCall.vue`: renders embedded signup when available, otherwise
the manual setup form — both with calling enabled on completion.
Updates the suspended account page with the revised policy copy and adds
a visible Contact support action that opens the embedded Chatwoot
support widget.
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Updates the suspended account screen copy to use clearer policy and
safety-oriented language, while giving users a direct support path if
they believe the suspension is a mistake.
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
## 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
Reworks the Captain assistant "hours saved" metric so it produces a
believable number. It previously multiplied assistant reply count by
customer reply-wait time, which inflated the figure into the millions of
hours. It now estimates saved time as replies × a fixed ~2 min per-reply
effort assumption. On the overview card the value renders in days once
it passes 100 hours, and the label/hint copy was updated to match.
## How to test
1. Open the Captain assistant overview page.
2. Check the "Time saved" card shows a sensible value (hours, or days
past 100h).
3. Hover the hint and confirm it reflects the per-reply estimate.
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>