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 { 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 (
|
||||
<SipProvider>
|
||||
<div className="flex h-screen bg-primary">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user