diff --git a/src/components/layout/app-shell.tsx b/src/components/layout/app-shell.tsx index fb2e532..5f24713 100644 --- a/src/components/layout/app-shell.tsx +++ b/src/components/layout/app-shell.tsx @@ -1,4 +1,4 @@ -import type { ReactNode } from 'react'; +import { useEffect, type ReactNode } from 'react'; import { useLocation } from 'react-router'; import { Sidebar } from './sidebar'; import { SipProvider } from '@/providers/sip-provider'; @@ -13,6 +13,25 @@ export const AppShell = ({ children }: AppShellProps) => { const { pathname } = useLocation(); const { isCCAgent } = useAuth(); + // Heartbeat: keep agent session alive in Redis (CC agents only) + useEffect(() => { + if (!isCCAgent) return; + + const beat = () => { + const token = localStorage.getItem('helix_access_token'); + if (token) { + const apiUrl = import.meta.env.VITE_API_URL ?? 'http://localhost:4100'; + fetch(`${apiUrl}/auth/heartbeat`, { + method: 'POST', + headers: { Authorization: `Bearer ${token}` }, + }).catch(() => {}); + } + }; + + const interval = setInterval(beat, 5 * 60 * 1000); + return () => clearInterval(interval); + }, [isCCAgent]); + return (
diff --git a/src/pages/login.tsx b/src/pages/login.tsx index 2385f3a..a02e80e 100644 --- a/src/pages/login.tsx +++ b/src/pages/login.tsx @@ -51,6 +51,13 @@ export const LoginPage = () => { localStorage.removeItem('helix_remember'); } + // Store agent config for SIP provider (CC agents only) + if ((response as any).agentConfig) { + localStorage.setItem('helix_agent_config', JSON.stringify((response as any).agentConfig)); + } else { + localStorage.removeItem('helix_agent_config'); + } + loginWithUser({ id: u?.id, name, diff --git a/src/providers/auth-provider.tsx b/src/providers/auth-provider.tsx index 62ba492..6643974 100644 --- a/src/providers/auth-provider.tsx +++ b/src/providers/auth-provider.tsx @@ -96,10 +96,21 @@ export const AuthProvider = ({ children }: AuthProviderProps) => { }, []); const logout = useCallback(() => { + // Notify sidecar to unlock Redis session + Ozonetel logout + 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(() => {}); + } + setUser(DEFAULT_USER); setIsAuthenticated(false); localStorage.removeItem('helix_access_token'); localStorage.removeItem('helix_refresh_token'); + localStorage.removeItem('helix_agent_config'); localStorage.removeItem(STORAGE_KEY); }, []); diff --git a/src/providers/sip-provider.tsx b/src/providers/sip-provider.tsx index 5bbe777..bc9e156 100644 --- a/src/providers/sip-provider.tsx +++ b/src/providers/sip-provider.tsx @@ -13,12 +13,27 @@ import { import { registerSipStateUpdater, connectSip, disconnectSip, getSipClient } from '@/state/sip-manager'; import type { SIPConfig } from '@/types/sip'; -const DEFAULT_CONFIG: SIPConfig = { - displayName: import.meta.env.VITE_SIP_DISPLAY_NAME ?? 'Helix Agent', - uri: import.meta.env.VITE_SIP_URI ?? '', - password: import.meta.env.VITE_SIP_PASSWORD ?? '', - wsServer: import.meta.env.VITE_SIP_WS_SERVER ?? '', - stunServers: 'stun:stun.l.google.com:19302', +const getSipConfig = (): SIPConfig => { + try { + const stored = localStorage.getItem('helix_agent_config'); + if (stored) { + const config = JSON.parse(stored); + return { + displayName: 'Helix Agent', + uri: config.sipUri, + password: config.sipPassword, + wsServer: config.sipWsServer, + stunServers: 'stun:stun.l.google.com:19302', + }; + } + } catch {} + return { + displayName: import.meta.env.VITE_SIP_DISPLAY_NAME ?? 'Helix Agent', + uri: import.meta.env.VITE_SIP_URI ?? '', + password: import.meta.env.VITE_SIP_PASSWORD ?? '', + wsServer: import.meta.env.VITE_SIP_WS_SERVER ?? '', + stunServers: 'stun:stun.l.google.com:19302', + }; }; export const SipProvider = ({ children }: PropsWithChildren) => { @@ -41,7 +56,7 @@ export const SipProvider = ({ children }: PropsWithChildren) => { // Auto-connect SIP on mount useEffect(() => { - connectSip(DEFAULT_CONFIG); + connectSip(getSipConfig()); }, []); // Call duration timer @@ -129,7 +144,7 @@ export const useSip = () => { isInCall: ['ringing-in', 'ringing-out', 'active'].includes(callState), ozonetelStatus: 'logged-in' as const, ozonetelError: null as string | null, - connect: () => connectSip(DEFAULT_CONFIG), + connect: () => connectSip(getSipConfig()), disconnect: disconnectSip, makeCall, answer,