2f5178eb4f
1806 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
bd61458720
|
fix(internal-chat): use internal_chat_channel_id in delete payloads (#270)
Backend broadcast payloads for internal_chat.message.deleted and internal_chat.reaction.deleted used channel_id as the key, but the frontend ActionCable handlers (and all other internal-chat events) expect internal_chat_channel_id. This caused deleted messages and removed reactions to stay visible on screen until a manual refresh. Rename the key on the backend so the payloads match the convention shared with message.created/updated and reaction.created, and drop the defensive fallback on the frontend reaction-deleted handler. |
||
|
|
e032fc7774
|
feat(whatsapp): convert inbox between WhatsApp providers (#268)
* feat(whatsapp): allow converting inbox between WhatsApp providers Adds a Convert flow to switch a WhatsApp inbox between the four supported providers (default/360dialog, whatsapp_cloud, baileys, zapi) without losing conversations, agents, or history. - Channel::Whatsapp#convert_provider! runs inside a transaction: disconnects the old provider, clears provider_connection and message_templates, assigns the new provider/config, and triggers webhook setup plus template resync on the new service. - New POST /api/v1/accounts/:id/inboxes/:id/convert_provider endpoint guarded by InboxPolicy#convert_provider? (admin only). - UI adds a Convert button on the inbox Settings page with a type-to-confirm ConvertInboxModal that lists the effects before redirecting to a dedicated route reusing the WhatsApp provider wizard in convert mode (phone number locked, current provider hidden from the picker). * chore(whatsapp): polish convert UI colors and expand specs - Settings: use slate for the Convert trigger and ruby for the modal confirm to mirror the delete gate instead of the less conventional amber variant. - Drop the redundant "current provider is hidden from the list" sentence from the convert wizard description. - Add specs for the post-conversion webhook setup path (triggered and skipped branches) and the sync_templates error-rescue behaviour. * fix: address CodeRabbit review on convert-provider flow - Whitelist provider_config keys in the convert endpoint via permit rather than permit!, and default to an empty hash when omitted so the request no longer crashes. - Pre-validate the new provider config before disconnecting the old session so a bad target config no longer terminates the existing provider; also keep the disconnect bound to the old provider_url. - Guard ConvertInboxModal's submit handler so pressing Enter cannot bypass the type-to-confirm gate, and migrate it to <script setup>. - Reject invalid ?provider= query values in convert mode so hidden providers (Twilio, the current provider) cannot be reached via URL. - Await the inbox fetch in InboxConvert before running the route guard so directly opening the route for a non-WhatsApp inbox redirects. - Remove the unreachable second CloudWhatsapp branch in Whatsapp.vue. * fix: address second CodeRabbit round on convert-provider flow - Unify provider picker validation so create mode also rejects unknown ?provider= values, with a single helper that accepts available providers plus the whatsapp_manual fallback. - Simplify the pre-validation rollback in convert_provider!: the errors snapshot/merge dance was redundant because assign_attributes does not clear errors. - Follow the repo convention of asserting on error.class.name so the rollback spec stays stable under reloading/parallel environments. - Strengthen the controller success spec with provider_connection and message_templates cleanup invariants, and set Content-Type on the templates stub so HTTParty parses the empty data array correctly. * fix: address third CodeRabbit round on convert-provider flow - Add 360Dialog entry to the Whatsapp provider catalog, keep it hidden from the create picker (preserving the existing fork behavior) but expose it in the convert picker where it is a valid target. Restore URL reachability for ?provider=360dialog in create mode. - Scope the WHATSAPP_MANUAL allowance to create mode only: the manual fallback flow is not reachable in convert mode. - Redirect to the inboxes list in InboxConvert when the inbox is still absent after the store fetch, so the page no longer stays blank. - Use an explicit allowlist of WhatsApp providers to gate the Convert button instead of negating Twilio, so adding a new WhatsApp channel type will not silently expose the flow. - Bind the disabled provider display field with :value instead of v-model, since the underlying computed is getter-only. - Add Content-Type: application/json to the templates stub in the model spec so HTTParty parses the empty data array. * fix: address fourth CodeRabbit round on convert-provider flow - Reject no-op conversions that target the same provider as the one already configured, so the endpoint no longer wipes provider connection and message templates on a request that changes nothing. - Call the provider service's disconnect directly so failures abort the conversion instead of being silently swallowed; otherwise the old external session could remain live while the inbox flips to the new provider. - Cover both behaviors with specs. * fix: address fifth CodeRabbit round on convert-provider flow - Reset the Vuelidate state when closing ConvertInboxModal so reopening the gate does not surface stale validation errors. - Call teardown_webhooks before converting away from whatsapp_cloud so the Meta webhook subscription is removed for embedded_signup channels, mirroring the destroy-time cleanup (manual-setup channels keep the existing no-op behavior). Swallow teardown failures so a flaky Meta call does not abort the swap. - Switch the rollback specs to compare message_templates counts instead of the boolean be_present matcher so they remain meaningful if the fixture happens to have an empty templates list. * fix: address sixth CodeRabbit round on convert-provider flow - Derive the convert header's current-provider label from the shared PROVIDER_CATALOG so the picker and header stay in sync. - Assert the full Cloud provider_config payload and the absence of the Baileys-only provider_url key on both the controller success spec and the model atomic-swap spec. - In the sync-error spec, reload and assert that the record was actually flipped to the new provider before the sync rescue fires, so the test can't pass on a pre-save failure. * test: pin 422 error payload on convert_provider negative paths The unsupported-conversion and invalid-config specs only checked the status code, so they would have stayed green if the 422 started coming from a different branch. Pin the response body so each example actually covers the failure case it names. * fix(baileys): save custom host as provider_url, not url The Baileys form was writing the custom endpoint to provider_config['url'] while the backend reads provider_config['provider_url']. That silently broke the custom-host feature for newly created or converted Baileys inboxes: they always fell back to BAILEYS_PROVIDER_DEFAULT_URL. Align the key on both ends. * fix(whatsapp): skip second validation pass in convert_provider! The transaction's save! was re-running validate_provider_config after the old provider's session had already been disconnected, so a transient Graph API failure on the second check could roll back the swap while leaving the external session terminated — the exact inconsistency the pre-flight valid? was meant to rule out. Capture the validated provider_config snapshot after valid? (so fields populated by before_validation callbacks like webhook_verify_token are preserved) and switch the final persist to save!(validate: false) so the earlier check stays authoritative. * fix: normalize provider-conversion failures and pass accountId - The convert_provider action only rescued ActiveRecord::RecordInvalid, so disconnect/teardown failures bubbled up as 500 with no stable payload. Catch StandardError, log the class + message, and return a 422 with a generic user-facing message so the dashboard can surface the error consistently. - Nested settings routes live under /accounts/:accountId, so the router push from Settings.vue must include accountId alongside inboxId. Mirrors how sibling pages navigate to settings_inbox_show. * fix: report missing :provider as 400 and sync modal v-model - The generic rescue StandardError on convert_provider was masking ActionController::ParameterMissing behind a misleading provider-conversion error message. Catch it explicitly before the generic rescue and return 400 with the parameter-missing message. - ConvertInboxModal's closeModal now drives localShow to false so parents using v-model:show stay in sync on every close path, not only when the explicit onClose listener flips the flag. * fix(whatsapp): serialize concurrent convert_provider calls with_lock Without a per-record lock, two admin requests against the same inbox could both pass the pre-flight validation, race the disconnect/save, and then run setup_webhooks/sync_templates in arbitrary order, leaving the persisted provider out of sync with the external configuration. Wrap the whole convert flow in with_lock so the loser blocks until the winner commits; the subsequent no-op guard then rejects a second conversion request targeting the provider the first one just set. * test: harden convert_provider policy + controller failure specs - Pass accountId explicitly in InboxConvert redirects so the route navigation mirrors how Settings.vue reaches settings_inbox_convert. - Add a spec that assigns the agent to the inbox and still expects 401, so a future regression in InboxPolicy#convert_provider? can no longer slip past on the show policy alone. - Add a spec that stubs convert_provider! to raise StandardError and asserts the controller's generic-failure 422 payload, pinning the dashboard contract for provider-side failures. * test: pin convert_provider success response payload Parse the rendered body and assert provider + provider_config so the spec catches regressions where the DB is updated correctly but the serialized response drifts (dashboard store commits response.data). * fix(whatsapp): reset teardown guard after pre-conversion webhook cleanup teardown_webhooks memoizes @webhook_teardown_initiated = true to prevent double execution during destroy. Calling it from convert_provider! leaves that flag set, so a subsequent destroy! or follow-up conversion on the same instance would skip webhook removal silently. Reset the flag in an ensure block so the destroy-time guard stays scoped to destroy only. * fix: include accountId in post-conversion redirect params * test: pin same-provider convert returns 422 * fix(whatsapp): reset template columns when post-conversion sync fails * fix(convert): enforce provider allowlist in InboxConvert route guard * test: broaden Cloud templates stub to match account-scoped path * test(whatsapp): cover cloud to baileys conversion branch |
||
|
|
97b71915aa
|
fix(mailer): fall back to user account for devise email locale (#267)
Devise emails sent from unauthenticated flows (password reset, unlock, confirmation re-send) were always rendered in the default locale because Current.account is nil in that context. Use the user's first account as a fallback so the email respects the account locale configured by the user. |
||
|
|
112385fd9e |
Merge branch 'main' into chore/merge-4.13.0
Resolves 26 conflicts via manual review. Key decisions: - signature: kept fork's send-time architecture (PR #79), discarded upstream's editor-manipulation functions - WhatsApp incoming: combined fork's two-layer locking (source_id + contact phone) with upstream's blocked-contact drop. Fixed pre-existing regression where echoes were silently dropped - InstallationConfig: upstream's simplified coder (validated against legacy YAML-in-jsonb data) - schema.rb: regenerated, stripped kanban tables from other branches, restored f_unaccent SQL function |
||
|
|
135be52431
|
feat: Introduce last responding agent option to automation assign agent (#12326)
Introduce a `Last Responding Agent` options to assign_agents action in automations to cover the following use cases. - Assign conversations to first responding agent : ( automation message created at , if assignee is nil, assign last responding agent ) - Ensure conversations are not resolved with out an assignee : ( automation conversation resolved at : if assignee is nil, assign last responding agent ) and potential other cases. fixes: #1592 |
||
|
|
aee979ee0b
|
fix: add explicit remove assignment actions to macros and automations (#12172)
This updates macros and automations so agents can explicitly remove assigned agents or teams, while keeping the existing `Assign -> None` flow working for backward compatibility. Fixes: #7551 Closes: #7551 ## Why The original macro change exposed unassignment only through `Assign -> None`, which made macros behave differently from automations and left the explicit remove actions inconsistent across the product. This keeps the lower-risk compatibility path and adds the explicit remove actions requested in review. ## What this change does - Adds `Remove Assigned Agent` and `Remove Assigned Team` as explicit actions in macros. - Adds the same explicit remove actions in automations. - Keeps `Assign Agent -> None` and `Assign Team -> None` working for existing behavior and stored payloads. - Preserves backward compatibility for existing macro and automation execution payloads. - Downmerges the latest `develop` and resolves the conflicts while keeping both the new remove actions and current `develop` behavior. ## Validation - Verified both remove actions are available and selectable in the macro editor. - Verified both remove actions are available and selectable in the automation builder. - Applied a disposable macro with `Remove Assigned Agent` and `Remove Assigned Team` on a real conversation and confirmed both fields were cleared. - Applied a disposable macro with `Assign Agent -> None` and `Assign Team -> None` on a real conversation and confirmed both fields were still cleared. |
||
|
|
48533e2a5d
|
fix: strip markdown hard-break backslashes from webhook payloads (#13950) | ||
|
|
b7b6e67df7
|
fix(captain): localize AI summary to account language (#13790)
AI-generated summaries now respect the account's language setting. Previously, summaries were always returned in English regardless of the user's configured language, making section headings like "Customer Intent" and "Action Items" appear in English even for non-English accounts. Previous behavior: <img width="1336" height="790" alt="image" src="https://github.com/user-attachments/assets/5df8b78b-1218-438d-9578-a806b5cb94ac" /> Current Behavior: <img width="1253" height="372" alt="image" src="https://github.com/user-attachments/assets/ae932c97-06da-4baf-9f77-9719bc9162e8" /> ## What changed - Added explicit account locale to the AI system prompt in `Captain::SummaryService` - Updated the summary prompt template to instruct the model to translate section headings ## How to test 1. Configure an account with a non-English language (e.g., Portuguese) 2. Open a conversation with messages 3. Use the Copilot "Summarize" feature 4. Verify that section headings ("Customer Intent", "Conversation Summary", etc.) appear in the account's language --------- Co-authored-by: Aakash Bakhle <48802744+aakashb95@users.noreply.github.com> |
||
|
|
02f70ff611 |
fix: resolve CI failures from presence update method collision
Rename consolidate_contact to consolidate_presence_contact in PresenceUpdate to avoid overriding the 3-arg version from GroupContactMessageHandler when both modules are mixed into IncomingMessageBaileysService. Fix CSAT spec side effects where conversation callbacks triggered ActivityMessageJob unexpectedly during test setup. |
||
|
|
104a05a511 |
fix: fix CI failures from presence subscribe changes
- Use optional chaining on presenceSubscribe call in setActiveChat to handle missing mock in tests - Update setup_channel_provider spec stubs to include autoPresenceSubscribe Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
11e9932e9b
|
feat(whatsapp): show contact typing and recording indicators via baileys presence (#264)
* feat(whatsapp): show contact typing and recording indicators via baileys presence Subscribe to WhatsApp presence updates via the baileys-api provider to display real-time typing and recording indicators in the dashboard. - Handle presence.update webhook events (composing, recording, paused, available) and broadcast via ActionCable - Add conversation.recording event to ActionCable, webhook, and channel listeners for parity with typing_on/typing_off - Show "typing..." / "recording..." in green text on the chat list, replacing the message preview - Show "X is typing" / "X is recording audio" in the conversation view - Add presence_subscribe provider config option (default off) to gate all subscription calls to the baileys-api - Subscribe to presence on conversation open and periodically (1 min) for the top 10 chat list conversations - Consolidate contact LID from presence.update jidAlt payload - Prevent echo-back of contact typing events to the channel Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address review feedback - Filter chat list typing indicator to contact-only events - Add dedupe to presence subscribe bulk calls - Use strong parameters for conversation_ids - Remove redundant YAML quotes in swagger webhook enum Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address review feedback - Extract phone from data[:id] when JID is @s.whatsapp.net (fallback when jidAlt is absent) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address review feedback - Filter recording users in getTypingUsersText to show correct names - Add 10s timeout to presence_subscribe HTTP request Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: scope typing timer per user instead of per conversation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: make presence subscribe best-effort with rescue per channel Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address review feedback - Add messagePreviewClass to typing preview for consistent padding - Fix specs to use WebMock assertions instead of instance spying Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
722e68eecb
|
fix: validate support_email format and handle parse errors in mailer (#13958)
## Description ConversationReplyMailer#parse_email calls Mail::Address.new(email_string).address without error handling. When an account's support_email contains a non-email string (e.g., "Smith Smith"), the mail gem raises Mail::Field::IncompleteParseError, crashing conversation transcript emails. This has caused 1,056 errors on Sentry (EXTERNAL-CHATINC-JX) since Feb 25, all from a single account that has a name stored in the support_email field instead of a valid email address. Closes https://linear.app/chatwoot/issue/CW-6687/mailfieldincompleteparseerror-mailaddresslist-can-not-parse-orsmith ## Type of change Please delete options that are not relevant. - [ ] 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 --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com> |
||
|
|
0592cccca9
|
fix: prevent lost custom_attributes updates from concurrent jsonb writes (#14040)
## Linear ticket https://linear.app/chatwoot/issue/CW-6834/billing-upgrade-didnt-work ## Description A `customer.subscription.updated` Stripe webhook for account 76162 returned 200 OK but did not persist the new `subscribed_quantity`. Root cause: a race condition between the webhook handler and `increment_response_usage` (Captain usage counter), both doing read-modify-write on the `custom_attributes` JSONB column. The webhook wrote `quantity: 6`, then a concurrent `save` from `increment_response_usage` overwrote the entire hash with stale data — restoring `quantity: 5`. Fix: use atomic `jsonb_set` so usage counter updates only touch the single key they care about, instead of rewriting the whole `custom_attributes` hash. `increment_custom_attribute` also performs the increment in SQL, making concurrent increments correct as well. ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? - New regression spec in `handle_stripe_event_service_spec.rb` that simulates concurrent webhook + `increment_response_usage` and asserts both `subscribed_quantity` and `captain_responses_usage` survive - Existing account, billing, captain, and topup specs all pass 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 - [ ] Any dependent changes have been merged and published in downstream modules |
||
|
|
45b6ea6b3f
|
feat: add automation condition to filter private notes (#12102)
## Summary Adds a new automation condition to filter private notes. This allows automation rules to explicitly include or exclude private notes instead of relying on implicit behavior. Fixes: #11208 ## Preview https://github.com/user-attachments/assets/c40f6910-7bbf-4e59-aae5-ad408602927a |
||
|
|
3aca86aa43
|
feat(internal-chat): implement internal chat system for agents (#247)
* feat(internal-chat): implement internal chat system for agents (Phase 1+2 MVP)
Add a Slack/Discord-style internal messaging system for Chatwoot agents with
text channels (public/private), direct messages, reactions, typing indicators,
and real-time updates via ActionCable.
Backend:
- 6 database migrations (categories, channels, members, messages, attachments, reactions)
- 6 models under InternalChat:: namespace with validations and associations
- API controllers for categories, channels, messages, members, and reactions
- Pundit policies for authorization (public/private/DM access control)
- MessageCreateService, TypingStatusManager, DefaultChannelSetupService
- InternalChatListener for real-time broadcasting to channel members
- Event types for internal chat events
- Default category/channel setup for new and existing accounts
Frontend:
- Vuex store modules for channels, messages, and typing status
- API clients for channels and messages
- Vue 3 components: InternalChatLayout, ChannelSidebar, ChannelView,
ChannelHeader, MessageList, MessageBubble, MessageEditor,
EmojiReactionPicker, ReactionDisplay, TypingIndicator
- Sidebar integration with "Internal Chat" menu item
- ActionCable handlers for real-time message/reaction/typing events
- Route definitions and i18n translations
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(internal-chat): add comprehensive specs for models, controllers, policies, services, and listener
- 6 model specs (74 examples) covering associations, validations, scopes, methods
- 5 request specs for all API controllers (categories, channels, messages, members, reactions)
- 4 policy specs testing authorization rules for all actions
- 3 service specs (DefaultChannelSetupService, MessageCreateService, TypingStatusManager)
- 1 listener spec testing real-time broadcasting for all event types
- 6 FactoryBot factories with traits for all InternalChat models
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): fix dispatcher mock in service specs and cursor pagination test
- Allow dispatcher.dispatch in service specs to handle Account.created
callbacks from factory setup before asserting specific event dispatch
- Fix after-cursor pagination test by adding 1 second offset to avoid
timestamp precision issues with iso8601 rounding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): address CodeRabbit review — 7 critical security/correctness fixes
- Scope member creation through Current.account.users to prevent cross-account membership
- Scope member_ids in DM creation through Current.account to prevent cross-account invites
- Scope reaction message lookup through channel account to prevent cross-account access
- Fix Vuex store to commit messages array instead of response envelope
- Add UUID generation callback on Channel model (before_validation)
- Add channel access check to reaction deletion policy
- Validate parent_id belongs to same channel in MessageCreateService
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): address CodeRabbit round 2 + fix ChannelSidebar runtime error
- Re-throw error in fetchMessages instead of swallowing with empty array
- Wrap message + attachment creation in transaction for atomicity
- Fix factory to derive account from message (prevent cross-account fixtures)
- Guard listener against cross-account mismatch (not just missing records)
- Add cross-account regression tests to listener spec
- Fix ChannelSidebar computed properties to default to empty arrays
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(internal-chat): auto-setup default channels on account creation and migration
- Add after_create_commit :setup_internal_chat callback on Account model
- Add data migration to create default channels for existing accounts
- Make DefaultChannelSetupService convergent (find_or_create) instead of
bail-on-exists, so it can sync new members on subsequent runs
- Fix specs to handle default category/channel created by callback
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): avoid Vuex state mutation in sort + align muted styling in fallback section
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): fix store module name mismatch — register as 'internalChat' not 'internalChatChannels'
Components dispatch to 'internalChat/get' but the module was registered
as 'internalChatChannels'. Also fix ActionCable handlers to use
'internalChat/messages/' nested module path.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* i18n(internal-chat): add pt-BR translations for internal chat feature
Backend: default_category_name ('Canais') and default_channel_name ('Geral')
Frontend: all 40+ keys translated to Brazilian Portuguese
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): handle ISO 8601 timestamps in MessageBubble and MessageList
The API returns created_at as ISO strings but messageTimestamp() expects
Unix seconds and MessageList used `* 1000`. Now handles both formats.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(internal-chat): build swagger output for internal chat API endpoints
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(internal-chat): register internal chat tags and paths in swagger index
Add tag definitions and path entries for all 5 internal chat resource
groups in swagger/index.yml and swagger/paths/index.yml. Rebuild output.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* i18n(internal-chat): add SIDEBAR.INTERNAL_CHAT key to pt-BR settings
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): comprehensive review fixes — backend and frontend
Backend:
- Add attachments to message API responses in both controllers
- Add internal_chat_channel_updated listener handler
- Include reactions in message event broadcast data
Frontend:
- Fix ActionCable dispatch paths to use correct action names
(addMessageFromCable, updateMessageFromCable, deleteMessageFromCable)
- Connect typingUsers to internalChatTypingStatus store getter
- Fix message field references (edited → content_attributes.edited_at)
- Fix channel type comparisons (use 'private_channel'/'dm' strings)
- Add parent 'internal_chat' to sidebar activeOn array
- Increment unread_count on ActionCable message receive
- Add loadMore handler for cursor-based pagination
- Remove unused is-direct-message prop from InternalChatLayout
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(internal-chat): implement phases 3-5 — threads, mentions, notifications, polls, drafts
Phase 3 — Threads, Mentions, Notifications:
- MentionService: parse @user mentions, @all (admin only), generate notifications
- NotificationService: create notifications for channel messages (respects mute)
- Add internal_chat_message/mention notification types to Notification model
- ThreadPanel.vue: slide-out panel for threaded replies
- Integrate mentions + notifications into MessageCreateService
Phase 4 — Polls:
- 3 new migrations: polls, poll_options, poll_votes tables
- 3 new models: Poll, PollOption, PollVote with validations
- PollsController: create poll, vote, unvote with routes
- PollService: voting logic with multiple choice + revote support
- PollCreator.vue: modal for creating polls with options
- PollDisplay.vue: vote UI with progress bars and results
- Polls Vuex store module
- INTERNAL_CHAT_POLL_VOTED event type
Phase 5 — Drafts:
- 1 new migration: drafts table
- Draft model with validations
- DraftsController: full CRUD (replace stub)
- DraftsList.vue: list all user drafts with navigation
- Drafts Vuex store module with auto-save
- Draft route and sidebar integration
Phase 6 — Feature Flag:
- Add INTERNAL_CHAT feature flag to features.yml and featureFlags.js
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): fix API routing for drafts and polls, add poll voting ActionCable handler
- Fix draft API client to use channel-scoped PATCH/DELETE endpoints
- Create dedicated polls API client with correct poll-based endpoints
- Update polls store to use InternalChatPollsAPI with pollId-based voting
- Add ActionCable handler for internal_chat.poll.voted events
- Add thread and drafts routes to sidebar activeOn array
- Fix drafts store to pass channelId to API calls
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): fix poll response format and API client routing (review round 2)
- Return message with embedded poll data instead of raw poll response
- Add poll data to message_response in messages controller
- Create dedicated InternalChatPollsAPI client with correct endpoints
- Update PollDisplay.vue to read from message.poll or content_attributes.poll
- Use option.voted flag instead of checking voters array
- Add missing PERCENTAGE i18n key to pt-BR
- Remove unused currentUserId prop from PollDisplay
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): fix poll voting and draft lookup bugs (review round 3)
- Fix draft getter to use internal_chat_channel_id field name
- Split poll set_poll into vote/unvote variants — unvote doesn't need option_id
- Unvote finds user's vote by user_id across all poll options
- Fix ChannelView to extract pollId from message.poll before dispatching
- Fix unvote handler to not require optionId
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address CodeRabbit review feedback (round 3)
- Expose is_dm, favorited, muted on channel API responses
- Normalize poll cable updates into message-shaped patch
- Add file presence validation to MessageAttachment
- Remove duplicate mention notifications from MentionService
- Make data migration rollback safe (IrreversibleMigration)
- Update factory to include file by default
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: return 201 for channel creation and optimize DM lookup
- Return :created (201) instead of :ok (200) for channel creation
- Replace O(n) Ruby scan with SQL-based DM lookup using ARRAY_AGG
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address CodeRabbit review feedback (round 5)
- Broadcast channel event after create for real-time notifications
- Separate create/update strong params to prevent channel_type transitions
- Use strong params for typing_status input
- Replace find_by with detect on preloaded collections to fix N+1
- Preload attachments with blobs in show response
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address CodeRabbit review feedback (round 6)
- Serialize DM creation with advisory lock to prevent duplicates
- Broadcast channel deletion event for real-time UI updates
- Use strong params for mark_unread message_id
- Batch unread count computation to eliminate N+1 in index
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: eliminate N+1 in compute_unread_counts with single JOIN query
Replace per-membership COUNT loop with a single JOIN + GROUP BY query
that returns all unread counts in one database call.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): quality fixes, missing tests, and Playwright E2E setup
Addresses quality issues found during review and fills test coverage gaps
for the internal chat feature.
Backend fixes:
- Return 201 for all create endpoints (messages, categories, polls, reactions, members)
- Fix N+1 queries: replies.size, poll votes, category channels.count, votes.exists?
- Fix pagination has_more logic to check page size instead of total count
- Scope poll vote/unvote to current account (security fix)
- Add internal_chat.messages.deleted i18n key
- Use find_by! in mark_unread for proper 404 on non-members
- Guard time param parsing with rescue ArgumentError
- Align message response format between channels and messages controllers
- Switch notification service to ActionCable-only (avoid push/email crashes)
Frontend fixes:
- Fix pinned message detection (content_attributes.pinned, not message.pinned)
- Fix thread reply count key (replies_count, not thread_replies_count)
- Fix markUnread to pass message_id parameter
- Fix pagination: PREPEND_MESSAGES mutation instead of overwriting
- Fix typing status to read Vuex reactive state, not stale closure
- Fix deleteDraft argument shape (pass { channelId, draftId })
- Fix DM channel filtering (check both is_dm and channel_type)
- Fix DraftsList navigation to use correct channel ID key
- Wire PollCreator to poll button in MessageEditor
- Wire settings event handler on ChannelHeader
- Reset PollCreator isSubmitting on timeout
New RSpec tests (67 examples):
- Factories: polls, poll_options, poll_votes, drafts
- Model specs: Poll, PollOption, PollVote, Draft
- Controller specs: PollsController, DraftsController
- Service specs: PollService, NotificationService, MentionService
Playwright E2E setup (37 tests):
- Install Playwright with Chromium
- Auth helper with Devise Token Auth login flow
- 8 test suites: navigation, channels, messaging, DMs, reactions, threads, polls, mark-read-unread
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address CodeRabbit review feedback (round 7)
Backend:
- Use lambda for UUID default in channels migration
- Wrap poll creation in transaction for atomicity
- Preload replies in thread action to avoid N+1
- Broadcast replies_count + attachments in listener (match REST shape)
- Scope draft listing through accessible channels
- Key draft upserts/deletes by parent_id for thread drafts
Frontend:
- Remove duplicate poll methods from internalChatMessages.js (use internalChatPolls.js)
- Persist toggleMute/toggleFavorite to backend via updateMember API
- Clear active channel on DELETE_CHANNEL mutation
- Skip unread increment for active channel in ActionCable handler
- Filter archived channels from sidebar getters
- Fix ChannelHeader isArchived to check status === 'archived'
- Prevent duplicate reactions in ADD_REACTION mutation
- Merge poll data into existing content_attributes on cable updates
- Guard infinite scroll against duplicate loads
- Add response.ok() check in E2E auth helper, remove hardcoded account ID
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address CodeRabbit review feedback (round 8)
- Remove unused nested typingStatus module from internalChat store
- Add parent_id to draft uniqueness scope and migration index
- Exclude reaction creator from reaction_created broadcast
- Preload attachments and poll associations in thread/messages queries
- Handle `after` fetches with APPEND_MESSAGES mutation
- Wrap channel creation payloads under `channel` key in E2E helpers
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: rewrite Playwright E2E tests to use actual UI interactions
Completely rewrote all 8 E2E test suites to work with the live app:
- Test through actual UI interactions, not API bypass
- Use correct Portuguese (pt_BR) locale strings
- Use structural selectors matching real Vue component DOM
- Dynamic account ID from login response (no hardcoded values)
- 3 parallel workers, increased timeouts for reliability
- API calls only for preconditions (seeding test data)
29 tests passing across navigation, channels, messaging, DMs,
reactions, threads, polls, and mark-read-unread suites.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use partial unique indexes for draft uniqueness with NULL parent_id
PostgreSQL treats NULL as distinct in unique constraints, so a composite
index on (user_id, channel_id, parent_id) allows duplicate root drafts.
Split into two partial indexes: one for root drafts (WHERE parent_id IS
NULL) and one for thread drafts (WHERE parent_id IS NOT NULL).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address CodeRabbit review feedback (round 9)
- Remove duplicate index on internal_chat_polls.internal_chat_message_id
(keep only unique index)
- Add options validation in polls create (return 400 instead of 500)
- Add expiration check to unvote action (match vote behavior)
- Use strong params in messages update action
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address CodeRabbit review feedback (round 10)
- Change channel associations from destroy_async to destroy (FK
constraints are ON DELETE RESTRICT, blocking async deletion)
- Remove unused internal_chat notification types and PRIMARY_ACTORS
entry (notification service uses ActionCable only, no DB records)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address CodeRabbit review feedback (round 11)
- Scope category_id to current account in channels controller (security)
- Defer message-created event in poll creation until after transaction
- Change message associations from destroy_async to destroy (FK compat)
- Validate option belongs to poll in poll_service
- Use strong params for emoji in reactions controller
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address CodeRabbit review feedback (round 12)
Backend (9 fixes):
- Gate message update/destroy by channel accessibility in policy
- Guard content_attributes nil before merge in polls controller
- Fix after-cursor pagination to use limit() instead of last()
- Wrap revote in transaction for atomicity in poll service
- Make unvote option-specific for multi-choice polls
- Exclude own messages from unread count
- Make channel activity update monotonic (only write if newer)
- Include actor in message/reaction broadcasts (multi-tab support)
- Return 400 for empty member create instead of 201
Frontend (8 fixes):
- Show uncategorized channels even when categories exist
- Clear editor on channel switch when no draft exists
- Soft-delete messages in store (update in place, don't remove)
- Guard ThreadPanel against out-of-order fetch responses
- Replace hardcoded channel label with i18n key in DraftsList
- Add accessible name to settings button in ChannelHeader
- Add aria-label to search field in ChannelSidebar
- Make MessageBubble actions keyboard-accessible via focus-within
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address CodeRabbit review feedback (round 13)
- Fix keyword argument mismatch in reactions dispatch_reaction_event
- Add user_id to reaction cable broadcast for shape consistency with REST
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): quality fixes, expanded RSpec + Playwright E2E tests
Fix isArchived computed (checked .archived instead of .status), fix
ReactionDisplay user identification (.user?.id vs .user_id), update
17 spec assertions from :success to :created on create endpoints,
add 32 new RSpec examples (polls, drafts, services), and rewrite
8 Playwright E2E test files with correct selectors, proper test
isolation, and dynamic user ID discovery.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address CodeRabbit review feedback (round 14)
Prevent duplicate votes on same option in multi-choice polls with
explicit BadRequest guard. Add internal_chat webhook events to
ALLOWED_WEBHOOK_EVENTS so users can subscribe to them.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: include poll data in ActionCable broadcast for poll messages
Extract base_message_data helper and enrich message_event_data with
poll options when the message has an associated poll, ensuring
realtime subscribers receive the same poll data as REST API clients.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address CodeRabbit review feedback (round 15)
Backend: wrap single-choice revotes in transaction, capture member
tokens before channel destroy, exclude own messages from unread count,
strip attachments from deleted messages, enrich poll broadcast payload.
Frontend: use getCurrentRole getter, fix public-results poll display,
sync thread replies via store, add close button a11y, pass option_id
to unvote API, pass parent_id to deleteDraft API.
Models: handle nil last_read_at for new members, skip content
validation for attachment-only messages, align PollService guards
with controller, change category dependent to nullify.
Swagger: add attachments to message schema, fix create status to 201.
E2E: remove fragile waitForTimeout.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): fix 21 UX/functional issues
Address 21 UX gaps discovered during product testing:
Sidebar & Navigation:
- Fix search icon overlap, extend search to description + DM members
- Add create channel/DM/category buttons and modals
- Show DM member names instead of null
- Include members data in channel index API for DMs
Message Interactions:
- Add delete confirmation dialog
- Implement inline message editing with cancel support
- Toggle emoji reactions (add/remove)
- Support multiple pinned messages with click-to-scroll
- Prevent thread replies from appearing in main chat
- Fix reply count live updates
- Hide pin button on thread messages
- Improve deleted message styling with greyed-out card
- Replace spinner with skeleton loading
- Add markdown toolbar (bold/italic/code)
- Fix thread editing and add vote/unvote handlers
Features & Polish:
- Implement channel settings slide-over panel
- Fix thread loading not affecting main channel spinner
- Fix poll creation field name mismatch with backend API
- Fix drafts: show channel names, handle DM navigation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): use Dialog modal for delete confirmation
Replace window.confirm with the project's Dialog component for
message delete confirmation, providing a consistent UI experience.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address CodeRabbit review feedback (round 16)
- Require content field in message update OpenAPI schema
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address CodeRabbit review feedback (round 17)
- Sanitize advisory lock SQL with sanitize_sql_array
- Use semantic button for pinned message banner
- Add aria-label to ChannelSettings close button
- Add type="button" to all ChannelSettings buttons
- Gate channel/DM/category creation to admins
- Replace hardcoded 'Direct Message' with i18n key
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address CodeRabbit review feedback (round 18)
- Wrap DM creation payload in channel key for consistency
- Replace raw text in category select with i18n key
- Add IME composition guard to prevent premature send
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(internal-chat): UX round 2, rich editor, team members, drag-and-drop
- Reduce sidebar spacing between search bar and drafts
- Fix search icon overlapping placeholder text
- Replace inline category form with Dialog modal
- Add collapsible sidebar sections with localStorage persistence
- Add drag-and-drop channels across categories (admin-only, vuedraggable)
- Replace textarea editor with WootWriter ProseMirror rich text editor
- Replace regex markdown rendering with shared MessageFormatter
- Wire draft auto-save pipeline with WootWriter (3s debounce watcher)
- Add team + agent selection when creating private channels
- Auto-add all agents when creating public channels
- Sync team members to linked channels via TeamMember callback
- Fix member list not loading on first settings panel open
- Complete PT-BR translations for all internal chat strings
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(internal-chat): UX round 3, Enter-to-send, mentions, copy link, poll modal
- Send with Enter (not Cmd+Enter), Shift+Enter for newlines
- Enable @mentions via WootWriter suggestions plugin
- Refocus editor after sending a message
- Copy link to message button in hover toolbar
- Poll creator refactored to Dialog with confirm-discard on close
- Channel type uses Switch instead of dropdown
- Category uses components-next Select instead of native select
- Skeleton loading: only on initial load, spinner for pagination
- Scroll position preserved when loading older messages
- Mute/Favorite buttons fixed (store members updated after fetch)
- Add/remove channel members after creation (admin-only)
- Save draft immediately when switching channels
- Settings sidebar remembers open/closed state via localStorage
- Search icon overlap fixed (increased padding)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): DM settings, copy updates, input refocus, member UX
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): member edit for private only, emoji overflow, reaction tooltips
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): thread count sync, scroll loading, copy link, thread/settings exclusivity
- Fix thread reply count doubling (remove duplicate INCREMENT_REPLY_COUNT from sendThreadReply, cable handles it)
- Fix copy link button (use window.location.origin + pathname as fallback)
- Hide poll button in thread editor
- Add "Also send in #channel" checkbox for thread replies
- Increase scroll threshold for loading older messages (100px instead of 0)
- Track and stop loading when oldest message reached
- Thread and settings panels are mutually exclusive
- Refocus editor after send with delayed focus
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): scroll to linked message via ?messageId= query param
Read messageId from route query on mount, scroll to and highlight the
target message after messages load, then clean the query param.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): prevent editor from becoming unfocusable after send
Root cause: passing disabled prop to WootWriter applies pointer-events-none
and ProseMirror does not re-enable contenteditable when disabled returns to
false. Fix: never disable the WootWriter, use a local isSending guard to
prevent double-sends. Refocus 300ms after send for ProseMirror state reload.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): simplify send guard, no artificial timeout
Content is cleared immediately before emit, so canSend naturally
returns false (empty content). No isSending guard needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): align poll option remove buttons vertically
Increase padding to p-1.5 and add flex-shrink-0 for consistent sizing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): close poll modal after creating poll
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): poll option X button alignment, discard modal on submit
- Button uses explicit 34px height matching input, no items-center
- Reset form before closing dialog so hasUnsavedChanges is false
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): close settings sidebar when clicking reply
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): update poll UI after voting, fix re-vote error
Vote/unvote actions now dispatch updateMessageFromCable with the API
response to update poll state locally. Pass channelId to enable this.
Clicking an already-voted option correctly triggers unvote.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): include poll data in message response, add timer and voters
Backend: message_response now includes poll data (options, votes, voted
status, voters for public polls) via eager-loaded poll association.
This fixes polls not rendering after page reload.
Frontend PollDisplay:
- Countdown timer showing time remaining until poll closes
- Read-only state when expired (div instead of button, no hover)
- Voter names shown below each option (public polls or admin)
- Prefer content_attributes.poll over message.poll for fresh data
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): include channel_id in poll voted cable broadcast
The poll_event_data was missing internal_chat_channel_id, so the
frontend cable handler could not route the update to the correct
channel's message store.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): poll vote highlight, typing off, reaction broadcast, translations
- Preserve per-user voted flags when merging cable poll broadcast
- Send typing_off after 3s of no typing activity
- Include internal_chat_channel_id in reaction event broadcasts
- Fix reaction deleted handler to also check channel_id field
- Simplify "also send in" copy (works for both channels and DMs)
- Add PT-BR translation for ALSO_SEND_IN_CHANNEL
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(internal-chat): unified reaction popover with all emoji groups
Clicking any reaction badge opens a single popover showing all reactions
grouped by emoji with user names. Current user can remove their own
reaction via X button. Replaces per-reaction popover with unified view.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): wire close DM button to archive and navigate home
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(internal-chat): close DM via hidden flag on channel membership
Add hidden boolean to channel_members table. Close DM sets hidden=true
via member update API. Sidebar filters out hidden DMs. New messages on
a DM channel automatically unhide all members via listener callback.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): reaction popover, user names, file upload support
- Include user name in reaction API response (was missing)
- Redesign reaction popover: flat list with emoji + name per row,
aligned X button for removing own reactions
- Add file upload: paperclip button opens file picker, attached files
shown as chips with remove, sent via FormData with message
- Store action and API client support files parameter
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): reaction user names, unreact button, attachment rendering
- Include user name in reactions across all endpoints (messages_controller,
listener base_message_data)
- Make unreact X button always visible (bg-n-alpha-2 background)
- Render message attachments as downloadable links with paperclip icon
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(internal-chat): image preview for attachments in messages and editor
Messages: images render inline with max-h-60, non-images as download links.
Editor: image files show thumbnail preview, non-images show file icon + name.
Remove button as floating circle on top-right corner of each attachment.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): attachment preview matches conversation pattern
- File previews show name + size (e.g. "2 MB") in a horizontal card
- Image thumbnails as 32px squares, non-images show document emoji
- Remove button is a visible X icon (not a floating circle)
- Layout matches AttachmentsPreview from conversation ReplyBox
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): auto-detect image file type from MIME on upload
MessageCreateService now detects file type from content_type instead of
defaulting to :file. Images are correctly tagged as :image so they
render inline in message bubbles.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): include file_url in cable broadcast, fix filename display
Listener attachment_event_data now includes file_url so attachments
render correctly on real-time messages without page refresh.
MessageBubble extracts filename from URL or falls back to file_type+ext.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): pin attachments, edit members modal, settings persistence
- Skip content validation when pinning/unpinning (fixes pin on file-only messages)
- Add EditMembersModal with search, add, and remove members for private channels
- Fix settings sidebar always opening: @close now calls handleToggleSettings
which updates localStorage, not just sets ref to false
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): fix X buttons for attachment remove and reaction unreact
Replace Icon component with inline SVG cross for reliable rendering.
Both attachment remove and reaction unreact buttons now show a visible
X icon at all times with proper vertical alignment.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): allow any user to pin messages, not just sender/admin
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): restore Icon component for X buttons (size-4 in size-6 container)
SVG inline approach didn't render. Reverted to Icon i-lucide-x with
larger sizes (size-4 icon in size-6 button) which renders reliably.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): use p-1 + size-4 pattern for X buttons (matches message toolbar)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(internal-chat): thread indicator on messages from threads, allow pinning all
- Show "Thread" badge with icon on messages that have parent_id,
clicking it opens the parent thread
- Remove parent_id restriction from canPin, any non-deleted message
can be pinned
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): thread indicator, poll close loop, thread navigation
- Hide thread indicator inside thread panel (inThread prop)
- Open parent thread when clicking thread badge on messages with parent_id
- Fix PollCreator infinite close loop (handleClose no longer calls
dialogRef.close, since Dialog already triggered the close)
- Look up parent message in store when opening thread from indicator
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(internal-chat): poll duration translations and clickable switch labels
- Duration options use i18n keys (EN + PT-BR: 24 horas, 7 dias, etc.)
- Multiple choice and Public results switches toggle by clicking label
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(internal_chat): enhance message handling and search functionality
- Added broadcasting of typing off events in InternalChatListener.
- Included member user IDs in channel data for better context.
- Updated message model to allow optional sender association.
- Implemented team mention expansion in MentionService to include team members.
- Enhanced message creation service to store mentioned user IDs in content attributes.
- Introduced a new SearchService for searching channels, DMs, and messages.
- Updated API responses to include has_unread_mention flag for channels.
- Added tests for user deletion behavior in internal chat, ensuring message preservation and reaction handling.
- Improved draft model to allow coexistence of root and thread drafts.
- Added unique indexes for drafts to prevent duplicate entries.
- Implemented foreign key constraints with appropriate delete behaviors for internal chat models.
* feat(internal-chat): swagger docs, webhook events, search UX improvements
Add Swagger documentation for drafts, polls, and search endpoints.
Wire internal_chat_message_deleted and internal_chat_channel_updated
webhook events to the UI and listener. Improve search empty state with
min-chars hint and friendly no-results message. Update CLAUDE.md to
include pt_BR translations.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(internal-chat): add draft count display in channel sidebar
* chore: remove playwright config and dependencies
* feat(internal-chat): polish UX, swagger updates, and migration consolidation
- Editor toolbar shortcuts (@ and #) with instant popover trigger,
including accent-insensitive matching and wider conversation popover
- Localized last activity time and inbox name on conversation preview cards
- Thread + main interplay: also-send-in-channel mirror, parent_id filter,
per-message conversation link, hidden buttons inside thread view,
reactions update across both lists, scroll-to-message behavior
- Search service uses f_unaccent for messages, channel names and user names
via dedicated GIN trigram functional indexes
- Renamed InternalChat::ProGating to InternalChat::Limits with neutral
semantics
- Consolidated 17 internal chat migrations into 3 (tables, default channels,
unaccent search) and added a rake task to ensure the f_unaccent function
exists before db:schema:load
- Swagger paths and definitions updated to match the current state of the
feature (also_send_in_channel, status codes, pro-required responses,
hidden member flag, search meta fields, etc.)
* fix(internal-chat): use Rake task augmentation for db:schema:load hook
The previous `Rake::Task['db:schema:load'].enhance(...)` guarded by
`task_defined?` silently no-op'd in CI when the rake file loaded before
ActiveRecord's rake_tasks block ran. Re-opening `db:schema:load` via
Rake's `task name => deps` DSL augments the existing task regardless of
load order, ensuring the f_unaccent function is created before schema.rb
references it.
* fix(internal-chat): enhance db:schema:load from Rakefile after load_tasks
Adding the prereq inside lib/tasks/internal_chat_search.rake (via either
`Rake::Task#enhance` or task DSL augmentation) was being silently dropped
in CI, presumably due to load order between application rake files and
ActiveRecord's `rake_tasks` block. Moving the `enhance` to the Rakefile
itself, after `Rails.application.load_tasks`, guarantees both
`db:schema:load` and `db:internal_chat:ensure_search_functions` are
defined before the prereq is added.
Also leaves a debug `puts` in the task body so future regressions are
visible from CI logs.
* chore(internal-chat): add diagnostic logging to f_unaccent hook
* fix(internal-chat): install f_unaccent on all envs iterated by db:schema:load
Rails' `db:schema:load` in development env iterates over BOTH the
development and test databases (see
`ActiveRecord::Tasks::DatabaseTasks.each_current_environment`), but our
hook was only installing the function on the currently-connected
database. CI defaults to development env (no `RAILS_ENV` set), so the
function landed on `chatwoot_dev` while `chatwoot_test` remained
without it, causing the schema load to fail when creating the functional
indexes against the test DB.
The hook now mirrors the same iteration logic and installs the function
on every relevant config, restoring the original AR connection
afterwards.
* fix(internal-chat): align listener spec with current broadcast payload
- internal_chat_message_created now emits two broadcasts (the message
itself plus an automatic typing_off), so the spec switches to
`allow`/`have_received` to assert the message broadcast without caring
about the additional typing_off call.
- internal_chat_reaction_created payload uses `message_id`, not
`internal_chat_message_id`. Update the spec expectation to match.
* chore(internal-chat): remove redundant DSL augmentation in rake task
* fix(internal-chat): harden gates, kill N+1s and reduce race risk
Closes review findings raised on the internal chat PR:
- Restrict role mass-assignment in ChannelMembersController so only
account administrators can promote new members to channel admin.
- Wrap private-channel create/unarchive in a Postgres advisory lock per
account so concurrent requests can no longer bypass the freemium limit.
- Replace `replies.size` and `votes.size` (per-broadcast queries) with
`replies_count` / `votes_count` counter caches.
- Make `update_channel_activity` an atomic compare-and-set update so
concurrent message creates can never regress `last_activity_at`.
- Optimize `Poll#total_votes_count` to use the cached column and eager-
loaded options instead of a per-poll `votes.count` query.
- Add `internal_chat_messages.account_id` foreign key (`ON DELETE
CASCADE`) to prevent orphan rows.
- Escape HTML in `ChannelSidebar.highlightMatch` to close a v-html XSS
via incomplete tags in message search snippets.
- Cleanup `typingOffTimer` on `ChannelView` unmount.
- Add stable sort to `getChannelsByCategory` (alphabetical) and
`getDMChannels` (last activity) to prevent UI reorder thrash.
- Localize `PollDisplay` time-remaining strings (en + pt-BR).
- Add specs covering the 90-day search history filter and the search
controller endpoint, plus regenerate the consolidated migration
with the new columns and FK.
* docs(swagger): note role mass-assignment restriction on channel members
Document that the `role` field on the channel member create payload is
silently coerced to `member` for callers that are not account
administrators, matching the controller behavior introduced in the
previous commit.
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
b0a8fa70d0
|
fix(signatures): allow admins to manage inbox signatures without explicit membership (#260)
Administrators have access to all inboxes in an account but the validate_inbox_access filter only checked InboxMember records, returning 401 for admins not explicitly added as inbox members. |
||
|
|
f13f3ba446
|
fix: log only on system api key failures (#13968)
Removes sentry flooding of unnecessary rubyllm logs of wrong API key. Logs only system api key error since it would be P0. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
bd14e96ed9
|
chore: allow article to create without content (#14007) | ||
|
|
8cf6e8907f
|
release v4.12.0-fazer-ai.47 (#259)
* fix(whatsapp): add idempotent message sending to prevent duplicates on timeout retry When sending media messages via Baileys, Net::ReadTimeout causes Sidekiq to retry the job, potentially sending the same message multiple times. This adds a chatwootMessageId parameter to the Baileys API request, enabling server-side deduplication via Redis. Also increases HTTP timeout to 120s and channel lock to 130s to reduce false timeouts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address review feedback - Use error.class.name assertions for parallel/reloading safety - Assert reconnect endpoint was not called on 409 (stronger assertion) * fix: address review feedback (round 2) - Only release channel lock in ensure if it was actually acquired (prevents clearing another worker's lock on timeout) - Assert chatwootMessageId in reproduction spec body matcher --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
00837019b5
|
fix(captain): display handoff message to customer in V2 flow (#13885)
HandoffTool changes conversation status but only posts a private note.
ResponseBuilderJob now detects the tool flag and creates the public
handoff message that was previously only shown in V1.
# Pull Request Template
## Description
Captain V2 was silently forwarding conversations to humans without
showing a handoff message to the customer. The conversation appeared to
just stop
responding.
Root cause: In V2, HandoffTool calls bot_handoff! during agent
execution, which changes conversation status from pending to open. By
the time control returns
to ResponseBuilderJob#process_response, the conversation_pending? guard
returns early - skipping create_handoff_message entirely. The V1 flow
didn't have this
problem because AssistantChatService just returns a string token
(conversation_handoff) and lets ResponseBuilderJob handle everything.
What changed:
1. AgentRunnerService now surfaces the handoff_tool_called flag (already
tracked internally for usage metadata) in its response hash.
2. ResponseBuilderJob#handoff_requested? detects handoffs from both V1
(response token) and V2 (tool flag).
3. ResponseBuilderJob#process_response checks handoff_requested? before
the conversation_pending? guard, so V2 handoffs are processed even when
the status has
already changed.
4. ResponseBuilderJob#process_action('handoff') captures
conversation_pending? before calling bot_handoff! and uses that snapshot
to guard both bot_handoff!
and the OOO message - preventing double-execution when V2's HandoffTool
already ran them.
New V2 handoff flow:
AgentRunnerService
→ agent calls HandoffTool (creates private note, calls bot_handoff!)
→ returns response with handoff_tool_called: true
ResponseBuilderJob#process_response
→ handoff_requested? detects the flag
→ process_action('handoff')
→ create_handoff_message (public message for customer)
→ bot_handoff! skipped (conversation_pending? is false)
→ OOO skipped (conversation_pending? is false)
Fixes #13881
## Type of change
Please delete options that are not relevant.
- [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?
- Update existing response_builder_job_spec.rb covering the V2 handoff
path, V2 normal response path, and V1 regression
- Updated existing agent_runner_service_spec.rb expectations for the new
handoff_tool_called key and added a context for when the flag is true
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [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: Aakash Bakhle <48802744+aakashb95@users.noreply.github.com>
Co-authored-by: aakashb95 <aakashbakhle@gmail.com>
|
||
|
|
e5107604a0
|
feat: account enrichment using context.dev [UPM-27] (#13978)
## Account branding enrichment during signup This PR does the following ### Replace Firecrawl with Context.dev Switches the enterprise brand lookup from Firecrawl to Context.dev for better data quality, built-in caching, and automatic filtering of free/disposable email providers. The service interface changes from URL to email input to match Context.dev's email endpoint. OSS still falls back to basic HTML scraping with a normalized output shape across both paths. The enterprise path intentionally does not fall back to HTML scraping on failure — speed matters more than completeness. We want the user on the editable onboarding form fast, and a slow fallback scrape is worse than letting them fill it in. Requires `CONTEXT_DEV_API_KEY` in Super Admin → App Config. Without it, falls back to OSS HTML scraping. ### Add job to enrich account details After account creation, `Account::BrandingEnrichmentJob` looks up the signup email and pre-fills the account name, colors, logos, social links, and industry into `custom_attributes['brand_info']`. The job signals completion via a short-lived Redis key (30s TTL) + an ActionCable broadcast (`account.enrichment_completed`). The Redis key lets the frontend distinguish "still running" from "finished with no results." |
||
|
|
871f2f4d56
|
fix: harden fetching on upload endpoint (#14012) | ||
|
|
4f94ad4a75
|
feat: ensure signup verification [UPM-14] (#13858)
Previously, signing up gave immediate access to the app. Now, unconfirmed users are redirected to a verification page where they can resend the confirmation email. - After signup, the user is routed to `/auth/verify-email` instead of the dashboard - After login, unconfirmed users are redirected to the verification page - The dashboard route guard catches unconfirmed users and redirects them - `active_for_authentication?` is removed from the sessions controller so unconfirmed users can authenticate — the frontend gates access instead - If the user visits the verification page after already confirming, they're automatically redirected to the dashboard - No session is issued until the user is verified <details><summary>Demo</summary> <p> #### Fresh Signup https://github.com/user-attachments/assets/abb735e5-7c8e-44a2-801c-96d9e4823e51 #### Google Fresh Signup https://github.com/user-attachments/assets/ab9e389a-a604-4a9d-b492-219e6d94ee3f #### Create new account from Dashboard https://github.com/user-attachments/assets/c456690d-1946-4e0b-834b-ad8efcea8369 </p> </details> --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> |
||
|
|
118270d2e8
|
fix(agent-bot): Update listener spec to match signed webhook arguments (#14006)
Fixes failing `agent_bot_listener_spec.rb` tests for `conversation_status_changed` events. After #13892 added webhook signing, `process_webhook_bot_event` passes `:agent_bot_webhook` and `secret:`/`delivery_id:` kwargs to `AgentBots::WebhookJob.perform_later`, but two spec expectations were not updated to match the new call signature. ## What changed - Updated `perform_later` expectations in `conversation_status_changed` specs to include the `:agent_bot_webhook` type and `secret` keyword arguments. 🤖 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.6 (1M context) <noreply@anthropic.com> |
||
|
|
50d6ebaaca
|
fix(agent-bot): Dispatch conversation_status_changed event to agent bots (#14002)
Agent bots assigned to a conversation were not receiving `conversation_status_changed` webhook events. This meant bots could not react to status transitions like moving a conversation to **pending**. The deprecated `conversation_opened` and `conversation_resolved` events were still being delivered, but the newer unified `conversation_status_changed` event was silently dropped because `AgentBotListener` had no handler for it. ## What changed - Added `conversation_status_changed` handler to `AgentBotListener`, matching the pattern already used by `WebhookListener`. The payload includes `changed_attributes` so bots know which status transition occurred. ## How to test 1. Configure an agent bot with an `outgoing_url` (e.g. a webhook.site endpoint). 2. Assign the bot to an inbox or conversation. 3. Change a conversation's status to **pending** (or any other status). 4. Verify the bot receives a `conversation_status_changed` event with the correct `changed_attributes`. 🤖 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.6 (1M context) <noreply@anthropic.com> |
||
|
|
95463230cb
|
feat: sign webhooks for API channel and agentbots (#13892)
Account webhooks sign outgoing payloads with HMAC-SHA256, but agent bot and API inbox webhooks were delivered unsigned. This PR adds the same signing to both. Each model gets a dedicated `secret` column rather than reusing the agent bot's `access_token` (for API auth back into Chatwoot) or the API inbox's `hmac_token` (for inbound contact identity verification). These serve different trust boundaries and shouldn't be coupled — rotating a signing secret shouldn't invalidate API access or contact verification. The existing `Webhooks::Trigger` already signs when a secret is present, so the backend change is just passing `secret:` through to the jobs. Shared token logic is extracted into a `WebhookSecretable` concern included by `Webhook`, `AgentBot`, and `Channel::Api`. The frontend reuses the existing `AccessToken` component for secret display. Secrets are admin-only and excluded from enterprise audit logs. ### How to test Point an agent bot or API inbox webhook URL at a request inspector. Send a message and verify `X-Chatwoot-Signature` and `X-Chatwoot-Timestamp` headers are present. Reset the secret from settings and confirm subsequent deliveries use the new value. --------- Co-authored-by: Sojan Jose <sojan@pepalo.com> |
||
|
|
f4d66566d0
|
fix(agent-bot): Include changed_attributes in conversation_updated webhook (#14001)
The `conversation_updated` webhook sent to AgentBots did not include
`changed_attributes`, making it impossible for bots to distinguish
between different types of conversation updates (e.g. bot assignment vs
label change vs status change).
This aligns the AgentBot webhook payload with the existing
`WebhookListener` behavior, which already includes `changed_attributes`.
## How to reproduce
1. Assign an AgentBot to a conversation
2. Then update the conversation (e.g. add a label)
3. **Before fix:** Both events arrive with identical payload structure —
bot cannot tell them apart
4. **After fix:** Each event includes `changed_attributes` showing
exactly what changed
## What changed
- **`AgentBotListener#conversation_updated`**: Added
`changed_attributes` to the webhook payload using
`extract_changed_attributes` (same pattern as `WebhookListener`)
## How to test
1. Assign an AgentBot to a conversation via API
2. Check the webhook payload — should include:
```json
"changed_attributes": [
{ "assignee_agent_bot_id": { "previous_value": null, "current_value": 7
} }
]
```
3. Update the conversation (e.g. add a label)
4. Check the webhook payload — `changed_attributes` should reflect the
label change, not bot assignment
🤖 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.6 (1M context) <noreply@anthropic.com>
|
||
|
|
5fd3d5e036
|
feat: allow zero conversation limit capacity policy (#13964)
## Description Two improvements to Agent Capacity Policy: **1. Support exclusion via zero conversation limit** Allow `conversation_limit` to be `0` on inbox capacity limits. Agents with a zero limit are excluded from auto-assignment for that inbox while remaining members for manual assignment. **2. Fix exclusion rules duration input** - Default changed from `10` to `null` so time-based exclusion isn't applied unless explicitly set. - Minimum lowered from 10 to 1 minute. - `DurationInput` updated to handle `null` values correctly. ## Type of change Please delete options that are not relevant. - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? - Added model and capacity service specs for zero-limit exclusion behavior. - Tested manually via UI flows ## Checklist: - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> |
||
|
|
84242aae7b
|
release v4.12.0-fazer-ai.44 (#253)
* fix(whatsapp): use payload filename for documents to avoid Content-Disposition parsing issues When receiving documents via the WhatsApp Cloud API, filenames with spaces or special characters were mangled due to the Down gem's case-sensitive Content-Disposition header parsing. Now uses the filename from the WhatsApp message payload when available, falling back to Content-Disposition for other attachment types. * fix: scope payload filename preference to document messages only * fix: revert document-only scope, apply payload filename for all attachment types |
||
|
|
441fe4db11
|
fix: scope external_url override to Instagram DM conversations only (#13982)
Previously, all incoming messages from Facebook channel with instagram_id had their attachment data_url and thumb_url overridden with external_url. This caused issues for non-Instagram conversations originating from Facebook Message where the file URL should be used instead. Narrows the override to only apply when the conversation type is instagram_direct_message, which is the only case where Instagram's CDN URLs need to be used directly. Fixes https://linear.app/chatwoot/issue/CW-6722/videos-are-missing-in-facebook-conversation --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
b815eb9ce0
|
fix(agent-bot): Dispatch webhook event on agent bot assignment (#13975)
When an AgentBot is assigned to a conversation after the first message
has already been received, the bot does not respond because it never
receives any event. The `message_created` event fires before the bot is
assigned, and the bot has no way to know it was assigned.
Chatwoot already dispatches a `CONVERSATION_UPDATED` event when
`assignee_agent_bot_id` changes, but `AgentBotListener` wasn't listening
for it. This fix adds a `conversation_updated` handler so the bot
receives a webhook with the conversation context when assigned.
## How to reproduce
1. Customer sends a message → conversation created, `message_created`
fires
2. System processes the message (adds labels, custom attributes)
3. System assigns an AgentBot to the conversation via API
4. **Before fix:** Bot receives no event and never responds
5. **After fix:** Bot receives `conversation_updated` event with
conversation payload
## What changed
- **`AgentBotListener`**: Added `conversation_updated` handler that
sends the conversation webhook payload to the assigned bot when the
conversation is updated
## How to test
1. Create an AgentBot with an `outgoing_url` pointing to a webhook
inspector (e.g. webhook.site)
2. Send a message to create a conversation
3. Assign the AgentBot to the conversation via API:
```
POST /api/v1/accounts/{id}/conversations/{id}/assignments
{ "assignee_id": <bot_id>, "assignee_type": "AgentBot" }
```
4. Verify the bot receives a `conversation_updated` event at its webhook
URL
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
b3d0af84c4
|
fix(widget): Queue SDK-set conversation attributes and labels for first message (#13912)
### Description
When integrating the web widget via the JS SDK, customers call
setConversationCustomAttributes and setLabel on chatwoot:ready — before
any conversation exists. These API calls silently fail because the
backend endpoints require an existing conversation. When the visitor
sends their first message, the conversation is created without those
attributes/labels, so the message_created webhook payload is missing the
expected metadata.
This change queues SDK-set conversation custom attributes and labels in
the widget store when no conversation exists yet, and includes them in
the API request when the first message (or attachment) creates the
conversation. The backend now permits and applies these params during
conversation creation — before the message is saved and webhooks fire.
### How to test
1. Configure a web widget without a pre-chat form.
2. Open the widget on a test page and run the following in the browser
console after chatwoot:ready:
`window.$chatwoot.setConversationCustomAttributes({ plan: 'enterprise'
});`
`window.$chatwoot.setLabel('vip');` // must be a label that exists in
the account
3. Send the first message from the widget.
4. Verify in the Chatwoot dashboard that the conversation has plan:
enterprise in custom attributes and the vip label applied.
5. Set up a webhook subscriber for `message_created` confirm the first
payload includes the conversation metadata.
6. Verify that calling `setConversationCustomAttributes` / `setLabel` on
an existing conversation still works as before (direct API path, no
regression).
7. Verify the pre-chat form flow still works as expected.
|
||
|
|
d83beb2148
|
fix: Populate extension and include content_type in attachment webhook payload (#13945)
Attachment webhook event payloads (`message_created`) were missing the
file extension and content type. The `extension` column existed but was
never populated, and `content_type` was not included in the payload at
all.
## What changed
- Added `before_save :set_extension` callback to extract file extension
from the filename when saving an attachment.
- Added `content_type` (from ActiveStorage) to the `file_metadata` used
in `push_event_data`.
### Before
```json
{
"extension": null,
"data_url": "...",
"file_size": 11960
}
```
### After
```json
{
"extension": "pdf",
"content_type": "application/pdf",
"data_url": "...",
"file_size": 11960
}
```
## How to reproduce
1. Send a message with a file attachment (e.g., PDF) via any channel
2. Inspect the `message_created` webhook payload
3. Observe `extension` is `null` and `content_type` is missing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
8daf6cf6cb
|
feat: captain custom tools v1 (#13890)
# Pull Request Template ## Description Adds custom tool support to v1 ## 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. <img width="1816" height="958" alt="CleanShot 2026-03-24 at 11 37 33@2x" src="https://github.com/user-attachments/assets/2777a953-8b65-4a2d-88ec-39f395b3fb47" /> <img width="378" height="488" alt="CleanShot 2026-03-24 at 11 38 18@2x" src="https://github.com/user-attachments/assets/f6973c99-efd0-40e4-90fe-4472a2f63cea" /> <img width="1884" height="1452" alt="CleanShot 2026-03-24 at 11 38 32@2x" src="https://github.com/user-attachments/assets/9fba4fc4-0c33-46da-888a-52ec6bad6130" /> ## Checklist: - [x] My code follows the style guidelines of this project - [ ] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Shivam Mishra <scm.mymail@gmail.com> |
||
|
|
211fb1102d
|
chore: rotate oauth password if unconfirmed (#13878)
When a user signs up with an email they don't own and sets a password, that password remains valid even after the real owner later signs in via OAuth. This means the original registrant — who never proved ownership of the email — retains working credentials on the account. This change closes that gap by rotating the password to a random value whenever an unconfirmed user completes an OAuth sign-in. The check (`oauth_user_needs_password_reset?`) is evaluated before `skip_confirmation!` runs, since confirmation would flip `confirmed_at` and mask the condition. If the user was unconfirmed, the stored password is replaced with a secure random string that satisfies the password policy. This applies to both the web and mobile OAuth callback paths, as well as the sign-up path where the password is rotated before the reset token is generated. Users who lose access to password-based login as a side effect can recover through the standard "Forgot password" flow at any time. Since they've already proven email ownership via OAuth, this is a low-friction recovery path |
||
|
|
79c193ee9e
|
fix(whatsapp): resolve phone_number conflict when converting inbox between providers (#252)
* fix(whatsapp): resolve phone_number conflict when converting inbox between providers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: move transfer_identifier_to inside transaction for atomic consolidation * fix: reassign message sender before destroying merged contact * fix: resolve identifier conflicts account-wide in adopt and consolidate paths * fix: scope sender reassignment to moved conversations only --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
4cce7f6ad8
|
fix(line): Use non-expiring URLs for image and video messages (#13949)
Images and videos sent from Chatwoot to LINE inboxes fail to display on the LINE mobile app — users see expired markers, broken thumbnails, or missing images. This happens because LINE mobile lazy-loads images rather than downloading them immediately, and the ActiveStorage signed URLs expire after 5 minutes. Closes https://linear.app/chatwoot/issue/CW-6696/line-messaging-with-image-or-video-may-not-show-when-client-inactive ## How to reproduce 1. Create a LINE inbox and start a chat from the LINE mobile app 2. Close the LINE mobile app 3. Send an image from Chatwoot to that chat 4. Wait 7-8 minutes (past the 5-minute URL expiration) 5. Open the LINE mobile app — the image is broken/expired ## What changed - **`originalContentUrl`**: switched from `download_url` (signed, 5-min expiry) to `file_url` (permanent redirect-based URL) - **`previewImageUrl`**: switched to `thumb_url` (250px resized thumbnail meeting LINE's 1MB/240x240 recommendation), with fallback to `file_url` for non-image attachments like video 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Sojan Jose <sojan@pepalo.com> |
||
|
|
1987ac3d97
|
fix: remove bulk_auto_assignment_job cron schedule (#13877) | ||
|
|
42441dbd28
|
feat: add GuideJar embed support in HC (#13944) | ||
|
|
7651c18b48
|
feat: firecrawl branding api [UPM-15] (#13903)
Adds `WebsiteBrandingService` (OSS) with an Enterprise override using
Firecrawl v2 to extract branding and business data from a URL for
onboarding auto-fill.
OSS version uses HTTParty + Nokogiri to extract:
- Business name (og:site_name or title)
- Language (html lang)
- Favicon
- Social links from `<a>` tags
Enterprise version makes a single Firecrawl call to fetch:
- Structured JSON (name, language, industry via LLM)
- Branding (favicon, primary color)
- Page links
Falls back to OSS if Firecrawl is unavailable or fails.
Social handles (WhatsApp, Facebook, Instagram, Telegram, TikTok, LINE)
are parsed deterministically via a shared `SocialLinkParser`.
> We use links for socials, since the LLM extraction was unreliable,
mostly returned empty, and hallucinated in some rare scenarios
## How to test
```ruby
# OSS (no Firecrawl key needed)
WebsiteBrandingService.new('chatwoot.com').perform
# Enterprise (requires CAPTAIN_FIRECRAWL_API_KEY)
WebsiteBrandingService.new('notion.so').perform
WebsiteBrandingService.new('postman.com').perform
```
Verify the returned hash includes business_name, language,
industry_category, social_handles, and branding with
favicon/primary_color.
<img width="908" height="393" alt="image"
src="https://github.com/user-attachments/assets/e3696887-d366-485a-89a0-8e1a9698a788"
/>
|
||
|
|
04acc16609
|
fix: skip pay call if invoice already paid after finalize (#13924)
## Description When a customer downgrades from Enterprise to Business, they may retain unused Stripe credit balance. During an AI credits topup, Stripe::Invoice.finalize_invoice auto-applies that credit balance to the invoice. If the credit balance fully covers the invoice amount, Stripe marks it as paid immediately upon finalization. Calling Stripe::Invoice.pay on an already-paid invoice throws an error, breaking the topup flow. This fix retrieves the invoice status after finalization and skips the pay call if Stripe has already settled it via credits. ## Type of change Please delete options that are not relevant. - [ ] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? Tested against Stripe test mode with the following scenarios: - Full credit balance payment: Customer has enough Stripe credit balance to cover the entire invoice. Invoice is marked paid after finalize_invoice — pay is correctly skipped. Credits are fulfilled successfully. - Partial credit balance payment: Customer has some Stripe credit balance but not enough to cover the full amount. Invoice remains open after finalization — pay is called and charges the remaining amount to the default payment method. Credits are fulfilled successfully. - Zero credit balance (normal payment): Customer has no Stripe credit balance. Invoice remains open after finalization — pay charges the full amount. Credits are fulfilled successfully. ## 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 |
||
|
|
9efd554693
|
fix: resolve V2 capacity bypass in team assignment (#13904)
## Description When Assignment V2 is enabled, the V2 capacity policies (AgentCapacityPolicy / InboxCapacityLimit) are not respected during team-based assignment paths. The system falls back to the legacy V1 max_assignment_limit, and since V1 is deprecated and typically unconfigured in V2 setups, agents receive unlimited assignments regardless of their V2 capacity. Root cause: Inbox class directly defined member_ids_with_assignment_capacity, which shadowed the Enterprise::InboxAgentAvailability module override in Ruby's method resolution order (MRO). This made the V2 capacity check unreachable (dead code) for any code path using member_ids_with_assignment_capacity. ## Type of change - [ ] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? ⏺ Before the fix 1. Enable assignment_v2 + advanced_assignment on account 2. Create AgentCapacityPolicy with InboxCapacityLimit = 1 for an inbox 3. Assign the policy to an agent (e.g., John) 4. Create 1 open conversation assigned to John (now at capacity) 5. Create a new unassigned conversation in the same inbox 6. Assign a team (containing John) to that conversation 7. Result: John gets assigned despite being at capacity ⏺ After the fix Same steps 1–6. 7. Result: John is NOT assigned — conversation stays unassigned (no agents with capacity available) ## 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 |
||
|
|
4381be5f3e
|
feat: disable helpcenter on hacker plans (#12068)
This change blocks Help Center access for default/Hacker-plan accounts and closes the downgrade gap that could leave `help_center` enabled after a subscription falls back to the default cloud plan. Fixes: none Closes: none ## Why Default-plan accounts should not be able to access the Help Center, but the downgrade fallback path only reset the plan name and did not reconcile premium feature flags. That meant some accounts could keep `help_center` enabled even after landing back on the Hacker/default plan. ## What this change does - blocks Help Center portal and article access for default/Hacker-plan accounts - reconciles premium feature flags when a subscription falls back to the default cloud plan, so `help_center` is disabled immediately instead of waiting for a later webhook - preserves existing account `custom_attributes` during Stripe customer recreation instead of overwriting them - adds Enterprise coverage for the default-plan access checks on hosted and custom-domain Help Center routes - fixes the public access check to use the resolved portal object so blocked requests return the intended response instead of raising an error ## Validation 1. Create or use an account on the default/Hacker cloud plan with an active portal. 2. Visit the portal home page and a published article on both the Chatwoot-hosted URL and a configured custom domain. 3. Confirm the Help Center is blocked for that account. 4. Downgrade a paid account back to the default/Hacker plan through the Stripe webhook flow. 5. Confirm `help_center` is disabled right after the downgrade fallback is processed and the account can no longer access the Help Center. --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: Sojan Jose <sojan@pepalo.com> |
||
|
|
cac7438fff
|
fix: Email Channel links are not working (backend) (#13898)
# Pull Request Template ## Description This PR fixes the link formatting issue on the backend by adding `:autolink` to `ChatwootMarkdownRenderer#render_message`, ensuring all URLs are converted to `<a>` tags. Fixes https://linear.app/chatwoot/issue/CW-6682/email-channel-links-are-not-working ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Sojan Jose <sojan@pepalo.com> |
||
|
|
4517c50227
|
feat: support bulk select and delete for documents (#13907) | ||
|
|
4c4b70da25
|
fix: Skip email rate limiting for self-hosted instances (#13915)
Self-hosted installations were incorrectly hitting the daily email rate limit of 100, seeded from `installation_config`. Since self-hosted users control their own infrastructure, email rate limiting should only apply to Chatwoot Cloud. Closes #13913 |
||
|
|
23786bcb52
|
chore: mark conversation notifications as read on visit (#13906) | ||
|
|
e4c3f0ac2f
|
feat: fallback on phone number to update lead (#13910)
When syncing contacts to LeadSquared, the `Lead.CreateOrUpdate` API defaults to searching by email. If a contact has no email (or a different email) but a phone number matching an existing lead, the API fails with `MXDuplicateEntryException` instead of finding and updating the existing lead. This accounted for ~69% of all LeadSquared integration errors, and cascaded into "Lead not found" failures when posting transcript and conversation activities (~14% of errors). ## What changed - `LeadClient#create_or_update_lead` now catches `MXDuplicateEntryException` and retries the request once with `SearchBy=Phone` appended to the body, telling the API to match on phone number instead - Once the retry succeeds, the returned lead ID is stored on the contact (existing behavior), so all future events use the direct `update_lead` path and never hit the duplicate error again ## How to reproduce 1. Create a lead in LeadSquared with phone number `+91-75076767676` and email `a@example.com` 2. In Chatwoot, create a contact with the same phone number but a different email (or no email) 3. Trigger a contact sync (via conversation creation or contact update) 4. Before fix: `MXDuplicateEntryException` error in logs, contact fails to sync 5. After fix: retry with `SearchBy=Phone` finds and updates the existing lead, stores the lead ID on the contact |
||
|
|
742c5cc1f4
|
feat(dialogflow): make language_code configurable instead of hardcoded (#13221)
# 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. - Add language_code setting to Dialogflow integration configuration - Support 'auto' mode to detect language from contact's additional_attributes - Fallback to 'en-US' when no language is configured or detected - Include comprehensive language options (22 languages) - Add tests for language code configuration scenarios Fixes #3071 ## Type of change Please delete options that are not relevant. - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected) - [ ] This change requires a documentation update ## How Has This Been Tested? 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. <img width="815" height="506" alt="Screenshot 2026-01-10 220410" src="https://github.com/user-attachments/assets/26d2619c-ed42-4c9a-a41d-9fb07ef91a30" /> ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [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: Sojan Jose <sojan@pepalo.com> |
||
|
|
e0e321b8e2
|
fix: Annotaterb model annotation incomplete migration (#13132)
This pull request fixes the model annotation tooling due to previous incomplete migration from `annotate` to `annotaterb` gem (#12600). It also improves the handling of serialized values in the `InstallationConfig` model by ensuring a default value is set, simplifying the code, and removing a workaround for YAML deserialization. **Annotation tooling updates:** * Added `.annotaterb.yml` to configure the `annotate_rb` gem with project-specific options, centralizing annotation settings. * Replaced the custom `auto_annotate_models.rake` task with the standard rake task from `annotate_rb`, and added `lib/tasks/annotate_rb.rake` to load annotation tasks in development environments. [[1]](diffhunk://#diff-9450d2359e45f1db407b3871dde787a25d60bb721aed179a65ffd2692e95fb4bL1-L61) [[2]](diffhunk://#diff-578cdfc7ad56637e42472ea891ea286dff8803d9a1750afdbfeafec164d9b8b2R1-R8) **Model serialization improvements:** * Updated the `InstallationConfig` model to set a default value for the `serialized_value` attribute, ensuring it always has a hash with indifferent access and removing the need for a deserialization workaround in the `value` method. [[1]](diffhunk://#diff-b4bdde42c1ad0f584073818bd43dbd865b1b3b50d4701b131979f900d7c68297L22-R22) [[2]](diffhunk://#diff-b4bdde42c1ad0f584073818bd43dbd865b1b3b50d4701b131979f900d7c68297L36-L39) --------- Co-authored-by: Sojan Jose <sojan@pepalo.com> |