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 // Submit disposition to sidecar
if (callUcid) { if (callUcid) {
const agentCfg = JSON.parse(localStorage.getItem('helix_agent_config') ?? '{}');
const disposePayload = { const disposePayload = {
ucid: callUcid, ucid: callUcid,
disposition, disposition,
agentId: agentCfg.ozonetelAgentId,
callerPhone, callerPhone,
direction: callDirectionRef.current, direction: callDirectionRef.current,
durationSec: callDuration, 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 { useAtom, useSetAtom } from 'jotai';
import { import {
sipConnectionStatusAtom, sipConnectionStatusAtom,
@@ -93,22 +93,31 @@ export const SipProvider = ({ children }: PropsWithChildren) => {
// Layer 2: Fire sendBeacon to auto-dispose if user confirms leave // 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. // 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. // 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(() => { useEffect(() => {
const handleBeforeUnload = (e: BeforeUnloadEvent) => { const handleBeforeUnload = (e: BeforeUnloadEvent) => {
const ucid = localStorage.getItem('helix_active_ucid'); const ucid = localStorage.getItem('helix_active_ucid');
const state = callStateRef.current;
// Layer 1: Show browser "Leave page?" dialog during active calls // 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.preventDefault();
e.returnValue = ''; e.returnValue = '';
} }
// Layer 2: Fire disposition beacon if there's an active UCID // Layer 2: Fire disposition beacon if there's an active UCID
// sendBeacon is guaranteed to fire even during page unload
if (ucid) { if (ucid) {
const agentCfg = JSON.parse(localStorage.getItem('helix_agent_config') ?? '{}');
const payload = JSON.stringify({ const payload = JSON.stringify({
ucid, ucid,
disposition: 'CALLBACK_REQUESTED', disposition: 'CALLBACK_REQUESTED',
agentId: agentCfg.ozonetelAgentId,
autoDisposed: true, autoDisposed: true,
}); });
navigator.sendBeacon('/api/ozonetel/dispose', new Blob([payload], { type: 'application/json' })); navigator.sendBeacon('/api/ozonetel/dispose', new Blob([payload], { type: 'application/json' }));
@@ -125,7 +134,7 @@ export const SipProvider = ({ children }: PropsWithChildren) => {
window.removeEventListener('unload', handleUnload); window.removeEventListener('unload', handleUnload);
disconnectSip(); disconnectSip();
}; };
}, [callState]); }, []); // empty deps — runs once on mount, cleanup only on unmount
return <>{children}</>; return <>{children}</>;
}; };