fix: SIP disconnect on callState change + dispose sends agentId

- Fixed useEffect dependency bug: callState in deps caused cleanup
  (disconnectSip) to fire on every state transition, killing SIP
  mid-dial. Now uses useRef for callState in beforeunload handler
  with empty deps array — cleanup only fires on unmount.
- sendBeacon auto-dispose now includes agentId from agent config
- Disposition modal submit now includes agentId from agent config

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 15:49:34 +05:30
parent fb92da113e
commit 6a2fc47226
2 changed files with 15 additions and 4 deletions

View File

@@ -90,9 +90,11 @@ export const ActiveCallCard = ({ lead, callerPhone, missedCallId, onCallComplete
// Submit disposition to sidecar
if (callUcid) {
const agentCfg = JSON.parse(localStorage.getItem('helix_agent_config') ?? '{}');
const disposePayload = {
ucid: callUcid,
disposition,
agentId: agentCfg.ozonetelAgentId,
callerPhone,
direction: callDirectionRef.current,
durationSec: callDuration,

View File

@@ -1,4 +1,4 @@
import { useEffect, useCallback, type PropsWithChildren } from 'react';
import { useEffect, useCallback, useRef, type PropsWithChildren } from 'react';
import { useAtom, useSetAtom } from 'jotai';
import {
sipConnectionStatusAtom,
@@ -93,22 +93,31 @@ export const SipProvider = ({ children }: PropsWithChildren) => {
// Layer 2: Fire sendBeacon to auto-dispose if user confirms leave
// These two layers protect against the "agent refreshes mid-call → stuck in ACW" bug.
// Layer 3 (server-side ACW timeout) lives in supervisor.service.ts.
//
// IMPORTANT: beforeunload reads callState via a ref (not the dep array)
// because adding callState to deps causes the cleanup to fire on every
// state transition → disconnectSip() → kills the call mid-flight.
const callStateRef = useRef(callState);
callStateRef.current = callState;
useEffect(() => {
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
const ucid = localStorage.getItem('helix_active_ucid');
const state = callStateRef.current;
// Layer 1: Show browser "Leave page?" dialog during active calls
if (callState === 'active' || callState === 'ringing-in' || callState === 'ringing-out') {
if (state === 'active' || state === 'ringing-in' || state === 'ringing-out') {
e.preventDefault();
e.returnValue = '';
}
// Layer 2: Fire disposition beacon if there's an active UCID
// sendBeacon is guaranteed to fire even during page unload
if (ucid) {
const agentCfg = JSON.parse(localStorage.getItem('helix_agent_config') ?? '{}');
const payload = JSON.stringify({
ucid,
disposition: 'CALLBACK_REQUESTED',
agentId: agentCfg.ozonetelAgentId,
autoDisposed: true,
});
navigator.sendBeacon('/api/ozonetel/dispose', new Blob([payload], { type: 'application/json' }));
@@ -125,7 +134,7 @@ export const SipProvider = ({ children }: PropsWithChildren) => {
window.removeEventListener('unload', handleUnload);
disconnectSip();
};
}, [callState]);
}, []); // empty deps — runs once on mount, cleanup only on unmount
return <>{children}</>;
};