Branch review pass:
- useWhatsappCallSession: drop unused `error` ref and unused exports
(hasActiveWhatsappCall, isWhatsappCallMuted), fold sendWhatsappCallBeacon
into a private beaconTerminate helper since only sendWhatsappTerminateBeacon
consumed it externally. Trim WHAT-comments; keep WHY-comments
(browser-quirk explanations, race-condition notes, auth-cookie rationale).
- VoiceCall.vue bubble: fix `data || data` short-circuit that did nothing —
upstream key transform was the same on both branches; collapse to one read.
- calls/useCallSession/actionCable/FloatingCallWidget/VoiceCallButton:
drop comments that just describe what the next line already says.
Net diff: -63 lines across 7 files. No behavior change.
- Remove the [debug] useAlert breadcrumbs that surfaced the accept-call
silent-fail (the !conversation guard). The actual fix from
6ed9500792 stays.
- Rubocop Style/IfUnlessModifier on the WhatsApp voice_enabled jbuilder
block — convert to modifier form.
Console logs aren't visible to the agent on staging — switch to
useAlert toasts so the debug breadcrumbs appear directly in the UI.
Will revert once root cause is identified.
Trace every checkpoint between the green Accept button click and the
backend POST so we can see exactly where the silent failure happens
(0 /accept requests landing on backend, no console errors, no network
requests reported by the user). Logs prefixed [CW Voice] for grep.
Will revert once the root cause is identified.
- FloatingCallWidget: drop !conversation early-return in handleJoinCall —
on a fresh account or after a hard refresh the inbound call's conversation
may not be in the Vuex store yet, which silently no-op'd Accept while
Reject worked (Reject doesn't read the conversation).
- useCallSession: seed the calls store from already-loaded voice_call
messages with status='ringing' on mount and whenever the conversation
list changes. Cable events (voice_call.incoming, message.created) are
one-shot and not replayed on reconnect, so without seeding a refresh
during a ringing call leaves the FloatingCallWidget empty.
- useCallSession: extend the beforeunload warning to fire while a call is
ringing too — losing a ringing inbound to refresh is the same UX hit as
losing an active one.
Twilio voice now uses first-class `Call` records as the source of truth
for call state, instead of storing it on
`conversation.additional_attributes` and `conversation.identifier`. Each
call gets its own record, its own `voice_call` bubble matched by
`call_sid`, and its own conference name keyed off `Call.id`. Multiple
calls on the same conversation (for `lock_to_single_conversation`
inboxes) now work correctly, and the conversation card stays in sync
with the real latest message.
Fixes https://linear.app/chatwoot/issue/PLA-121/lock-to-single-thread
---------
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>