From 13e81ba9fb237e2d9d42c5ddf14857e234b3af76 Mon Sep 17 00:00:00 2001 From: saridsa2 Date: Tue, 24 Mar 2026 15:22:39 +0530 Subject: [PATCH 1/2] fix: await logout before navigating, prevent cancelled fetch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Logout is now async — awaits sidecar /auth/logout before clearing tokens - confirmSignOut awaits logout() before navigate('/login') - 5 second timeout on logout fetch to prevent indefinite hang - Added console.warn on logout failure Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/layout/sidebar.tsx | 4 ++-- src/providers/auth-provider.tsx | 17 +++++++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/components/layout/sidebar.tsx b/src/components/layout/sidebar.tsx index 9a5eeb5..82f5be0 100644 --- a/src/components/layout/sidebar.tsx +++ b/src/components/layout/sidebar.tsx @@ -132,9 +132,9 @@ export const Sidebar = ({ activeUrl = "/" }: SidebarProps) => { setLogoutOpen(true); }; - const confirmSignOut = () => { + const confirmSignOut = async () => { setLogoutOpen(false); - logout(); + await logout(); navigate('/login'); }; diff --git a/src/providers/auth-provider.tsx b/src/providers/auth-provider.tsx index 6643974..ea88821 100644 --- a/src/providers/auth-provider.tsx +++ b/src/providers/auth-provider.tsx @@ -95,15 +95,20 @@ export const AuthProvider = ({ children }: AuthProviderProps) => { setIsAuthenticated(true); }, []); - const logout = useCallback(() => { - // Notify sidecar to unlock Redis session + Ozonetel logout + const logout = useCallback(async () => { + // Notify sidecar to unlock Redis session + Ozonetel logout — await before clearing tokens const token = localStorage.getItem('helix_access_token'); if (token) { const apiUrl = import.meta.env.VITE_API_URL ?? 'http://localhost:4100'; - fetch(`${apiUrl}/auth/logout`, { - method: 'POST', - headers: { Authorization: `Bearer ${token}` }, - }).catch(() => {}); + try { + await fetch(`${apiUrl}/auth/logout`, { + method: 'POST', + headers: { Authorization: `Bearer ${token}` }, + signal: AbortSignal.timeout(5000), + }); + } catch (err) { + console.warn('Logout cleanup failed:', err); + } } setUser(DEFAULT_USER); From 710609dfee1844402c2a69e742c6c6aae774ae5d Mon Sep 17 00:00:00 2001 From: saridsa2 Date: Tue, 24 Mar 2026 18:49:10 +0530 Subject: [PATCH 2/2] refactor: centralise outbound dial into useSip().dialOutbound() - Single dialOutbound() in sip-provider handles all outbound state: callState, callerNumber, outboundPending, API call, error recovery - ClickToCallButton, PhoneActionCell, Dialler all use dialOutbound() - Removed direct Jotai atom manipulation from calling components - Removed setOutboundPending imports from components - SIP disconnects on provider unmount + auth logout - Dialler input is now editable (type or numpad) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../call-desk/click-to-call-button.tsx | 27 ++-------------- .../call-desk/phone-action-cell.tsx | 22 ++----------- src/pages/call-desk.tsx | 17 ++++++---- src/providers/auth-provider.tsx | 6 ++++ src/providers/sip-provider.tsx | 31 ++++++++++++++++--- 5 files changed, 48 insertions(+), 55 deletions(-) diff --git a/src/components/call-desk/click-to-call-button.tsx b/src/components/call-desk/click-to-call-button.tsx index 3c8904d..37721e1 100644 --- a/src/components/call-desk/click-to-call-button.tsx +++ b/src/components/call-desk/click-to-call-button.tsx @@ -2,14 +2,10 @@ import type { FC } from 'react'; import { useState } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faPhone } from '@fortawesome/pro-duotone-svg-icons'; -import { useSetAtom } from 'jotai'; const Phone01: FC<{ className?: string } & Record> = ({ className, ...rest }) => ; import { Button } from '@/components/base/buttons/button'; import { useSip } from '@/providers/sip-provider'; -import { sipCallStateAtom, sipCallerNumberAtom, sipCallUcidAtom } from '@/state/sip-state'; -import { setOutboundPending } from '@/state/sip-manager'; -import { apiClient } from '@/lib/api-client'; import { notify } from '@/lib/toast'; interface ClickToCallButtonProps { @@ -20,33 +16,14 @@ interface ClickToCallButtonProps { } export const ClickToCallButton = ({ phoneNumber, label, size = 'sm' }: ClickToCallButtonProps) => { - const { isRegistered, isInCall } = useSip(); + const { isRegistered, isInCall, dialOutbound } = useSip(); const [dialing, setDialing] = useState(false); - const setCallState = useSetAtom(sipCallStateAtom); - const setCallerNumber = useSetAtom(sipCallerNumberAtom); - const setCallUcid = useSetAtom(sipCallUcidAtom); const handleDial = async () => { setDialing(true); - - // Show call UI immediately - setCallState('ringing-out'); - setCallerNumber(phoneNumber); - setOutboundPending(true); - // Safety: reset flag if SIP INVITE doesn't arrive within 30s - const safetyTimer = setTimeout(() => setOutboundPending(false), 30000); - try { - const result = await apiClient.post<{ ucid?: string; status?: string }>('/api/ozonetel/dial', { phoneNumber }); - if (result?.ucid) { - setCallUcid(result.ucid); - } + await dialOutbound(phoneNumber); } catch { - clearTimeout(safetyTimer); - setCallState('idle'); - setCallerNumber(null); - setOutboundPending(false); - setCallUcid(null); notify.error('Dial Failed', 'Could not place the call'); } finally { setDialing(false); diff --git a/src/components/call-desk/phone-action-cell.tsx b/src/components/call-desk/phone-action-cell.tsx index a1464b7..07ceb2e 100644 --- a/src/components/call-desk/phone-action-cell.tsx +++ b/src/components/call-desk/phone-action-cell.tsx @@ -1,11 +1,7 @@ import { useState, useRef, useEffect } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faPhone, faCommentDots, faEllipsisVertical, faMessageDots } from '@fortawesome/pro-duotone-svg-icons'; -import { useSetAtom } from 'jotai'; import { useSip } from '@/providers/sip-provider'; -import { sipCallStateAtom, sipCallerNumberAtom, sipCallUcidAtom } from '@/state/sip-state'; -import { setOutboundPending } from '@/state/sip-manager'; -import { apiClient } from '@/lib/api-client'; import { notify } from '@/lib/toast'; import { cx } from '@/utils/cx'; @@ -16,10 +12,7 @@ type PhoneActionCellProps = { }; export const PhoneActionCell = ({ phoneNumber, displayNumber, leadId: _leadId }: PhoneActionCellProps) => { - const { isRegistered, isInCall } = useSip(); - const setCallState = useSetAtom(sipCallStateAtom); - const setCallerNumber = useSetAtom(sipCallerNumberAtom); - const setCallUcid = useSetAtom(sipCallUcidAtom); + const { isRegistered, isInCall, dialOutbound } = useSip(); const [menuOpen, setMenuOpen] = useState(false); const [dialing, setDialing] = useState(false); const menuRef = useRef(null); @@ -41,20 +34,9 @@ export const PhoneActionCell = ({ phoneNumber, displayNumber, leadId: _leadId }: if (!isRegistered || isInCall || dialing) return; setMenuOpen(false); setDialing(true); - setCallState('ringing-out'); - setCallerNumber(phoneNumber); - setOutboundPending(true); - const safetyTimer = setTimeout(() => setOutboundPending(false), 30000); - try { - const result = await apiClient.post<{ ucid?: string }>('/api/ozonetel/dial', { phoneNumber }); - if (result?.ucid) setCallUcid(result.ucid); + await dialOutbound(phoneNumber); } catch { - clearTimeout(safetyTimer); - setCallState('idle'); - setCallerNumber(null); - setOutboundPending(false); - setCallUcid(null); notify.error('Dial Failed', 'Could not place the call'); } finally { setDialing(false); diff --git a/src/pages/call-desk.tsx b/src/pages/call-desk.tsx index 41b2cd8..14e75d9 100644 --- a/src/pages/call-desk.tsx +++ b/src/pages/call-desk.tsx @@ -12,14 +12,13 @@ import { ActiveCallCard } from '@/components/call-desk/active-call-card'; import { Badge } from '@/components/base/badges/badges'; import { AgentStatusToggle } from '@/components/call-desk/agent-status-toggle'; -import { apiClient } from '@/lib/api-client'; import { notify } from '@/lib/toast'; import { cx } from '@/utils/cx'; export const CallDeskPage = () => { const { user } = useAuth(); const { leadActivities } = useData(); - const { connectionStatus, isRegistered, callState, callerNumber, callUcid } = useSip(); + const { connectionStatus, isRegistered, callState, callerNumber, callUcid, dialOutbound } = useSip(); const { missedCalls, followUps, marketingLeads, totalPending, loading } = useWorklist(); const [selectedLead, setSelectedLead] = useState(null); const [contextOpen, setContextOpen] = useState(true); @@ -34,7 +33,7 @@ export const CallDeskPage = () => { if (num.length < 10) { notify.error('Enter a valid phone number'); return; } setDialling(true); try { - await apiClient.post('/api/ozonetel/dial', { phoneNumber: num }); + await dialOutbound(num); setDiallerOpen(false); setDialNumber(''); } catch { @@ -95,9 +94,15 @@ export const CallDeskPage = () => {
- - {dialNumber || Enter number} - + setDialNumber(e.target.value)} + onKeyDown={e => e.key === 'Enter' && handleDial()} + placeholder="Enter number" + autoFocus + className="flex-1 bg-transparent text-lg font-semibold text-primary tracking-wider text-center placeholder:text-placeholder placeholder:font-normal placeholder:text-sm outline-none" + /> {dialNumber && (