mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
feat: dynamic SIP from agentConfig, logout cleanup, heartbeat
- SIP provider reads credentials from agentConfig (login response) - Auth logout calls sidecar to unlock Redis + Ozonetel logout - AppShell heartbeat every 5 min for CC agents - Login stores agentConfig in localStorage Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import type { ReactNode } from 'react';
|
import { useEffect, type ReactNode } from 'react';
|
||||||
import { useLocation } from 'react-router';
|
import { useLocation } from 'react-router';
|
||||||
import { Sidebar } from './sidebar';
|
import { Sidebar } from './sidebar';
|
||||||
import { SipProvider } from '@/providers/sip-provider';
|
import { SipProvider } from '@/providers/sip-provider';
|
||||||
@@ -13,6 +13,25 @@ export const AppShell = ({ children }: AppShellProps) => {
|
|||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
const { isCCAgent } = useAuth();
|
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 (
|
return (
|
||||||
<SipProvider>
|
<SipProvider>
|
||||||
<div className="flex h-screen bg-primary">
|
<div className="flex h-screen bg-primary">
|
||||||
|
|||||||
@@ -51,6 +51,13 @@ export const LoginPage = () => {
|
|||||||
localStorage.removeItem('helix_remember');
|
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({
|
loginWithUser({
|
||||||
id: u?.id,
|
id: u?.id,
|
||||||
name,
|
name,
|
||||||
|
|||||||
@@ -96,10 +96,21 @@ export const AuthProvider = ({ children }: AuthProviderProps) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const logout = useCallback(() => {
|
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);
|
setUser(DEFAULT_USER);
|
||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
localStorage.removeItem('helix_access_token');
|
localStorage.removeItem('helix_access_token');
|
||||||
localStorage.removeItem('helix_refresh_token');
|
localStorage.removeItem('helix_refresh_token');
|
||||||
|
localStorage.removeItem('helix_agent_config');
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -13,13 +13,28 @@ import {
|
|||||||
import { registerSipStateUpdater, connectSip, disconnectSip, getSipClient } from '@/state/sip-manager';
|
import { registerSipStateUpdater, connectSip, disconnectSip, getSipClient } from '@/state/sip-manager';
|
||||||
import type { SIPConfig } from '@/types/sip';
|
import type { SIPConfig } from '@/types/sip';
|
||||||
|
|
||||||
const DEFAULT_CONFIG: SIPConfig = {
|
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',
|
displayName: import.meta.env.VITE_SIP_DISPLAY_NAME ?? 'Helix Agent',
|
||||||
uri: import.meta.env.VITE_SIP_URI ?? '',
|
uri: import.meta.env.VITE_SIP_URI ?? '',
|
||||||
password: import.meta.env.VITE_SIP_PASSWORD ?? '',
|
password: import.meta.env.VITE_SIP_PASSWORD ?? '',
|
||||||
wsServer: import.meta.env.VITE_SIP_WS_SERVER ?? '',
|
wsServer: import.meta.env.VITE_SIP_WS_SERVER ?? '',
|
||||||
stunServers: 'stun:stun.l.google.com:19302',
|
stunServers: 'stun:stun.l.google.com:19302',
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const SipProvider = ({ children }: PropsWithChildren) => {
|
export const SipProvider = ({ children }: PropsWithChildren) => {
|
||||||
const [, setConnectionStatus] = useAtom(sipConnectionStatusAtom);
|
const [, setConnectionStatus] = useAtom(sipConnectionStatusAtom);
|
||||||
@@ -41,7 +56,7 @@ export const SipProvider = ({ children }: PropsWithChildren) => {
|
|||||||
|
|
||||||
// Auto-connect SIP on mount
|
// Auto-connect SIP on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
connectSip(DEFAULT_CONFIG);
|
connectSip(getSipConfig());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Call duration timer
|
// Call duration timer
|
||||||
@@ -129,7 +144,7 @@ export const useSip = () => {
|
|||||||
isInCall: ['ringing-in', 'ringing-out', 'active'].includes(callState),
|
isInCall: ['ringing-in', 'ringing-out', 'active'].includes(callState),
|
||||||
ozonetelStatus: 'logged-in' as const,
|
ozonetelStatus: 'logged-in' as const,
|
||||||
ozonetelError: null as string | null,
|
ozonetelError: null as string | null,
|
||||||
connect: () => connectSip(DEFAULT_CONFIG),
|
connect: () => connectSip(getSipConfig()),
|
||||||
disconnect: disconnectSip,
|
disconnect: disconnectSip,
|
||||||
makeCall,
|
makeCall,
|
||||||
answer,
|
answer,
|
||||||
|
|||||||
Reference in New Issue
Block a user