The previous beacon used navigator.sendBeacon, which can't set custom
request headers — and Chatwoot's API requires devise-token-auth headers
(access-token / client / uid) on every call. So the beacon hit the
terminate endpoint, got 401, and Meta kept the call alive until the
carrier-side timeout (~60s) — exactly the bug reported: refresh during
an in-progress call leaves it ringing on Meta's side.
Switch to fetch with keepalive:true so the request survives page unload
AND lets us rehydrate the auth headers from the cw_d_session_info cookie
the dashboard sets at login. credentials:'same-origin' keeps any session
cookies along for the ride. Empty body is well under the 64KB keepalive
quota so it actually flushes before the tab dies.
Browser-direct WebRTC has no rejoin path, so any call that outlives the
agent's tab becomes permanently orphaned on Meta's side. The existing
pagehide beacon only covered the active (accepted) call — extend it to
every ringing inbound too, so a hard refresh during ringing releases the
call instead of leaving Meta to time it out.
- useWhatsappCallSession: factor sendWhatsappCallBeacon(callId) out of
the active-call wrapper so any caller can post terminate for any callId
without going through the activeCallId / intentionallyClosing guard.
- useCallSession.handlePageHide: after the active-call beacon, iterate
the calls store for any ringing WhatsApp call and beacon /terminate
for each. terminate is the right endpoint because the backend records
it as 'no_answer' when the call was still ringing — accurate UX shape.
Mirror the Twilio voice configuration flow: split the WhatsApp Cloud
calling-enabled toggle out of the generic Configuration tab into a
dedicated Calls tab so the surface for telephony settings is consistent
across providers.
- Added WhatsappCallingPage.vue with the toggle, business phone display,
and a how-it-works blurb (no extra credentials needed since the
embedded-signup token already grants the call scopes).
- Settings.vue: register the page and surface a Calls tab on
WhatsApp Cloud + embedded-signup inboxes.
- ConfigurationPage.vue: drop the inline calling-enabled section, the
watcher that auto-saved on every toggle flip, and the now-orphan
updateWhatsAppCallingEnabled method.
- en/inboxMgmt.json: TABS.CALLS label + WHATSAPP_CALLING.* strings.
- 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.