diff --git a/src/components/call-desk/active-call-card.tsx b/src/components/call-desk/active-call-card.tsx index 2d975eb..77bec51 100644 --- a/src/components/call-desk/active-call-card.tsx +++ b/src/components/call-desk/active-call-card.tsx @@ -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, diff --git a/src/providers/sip-provider.tsx b/src/providers/sip-provider.tsx index 8213265..dcc9bf5 100644 --- a/src/providers/sip-provider.tsx +++ b/src/providers/sip-provider.tsx @@ -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}; };