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:
2026-03-23 21:24:47 +05:30
parent b9b7ee275f
commit 3afa4f20b2
4 changed files with 61 additions and 9 deletions

View File

@@ -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">

View File

@@ -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,

View File

@@ -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);
}, []); }, []);

View File

@@ -13,12 +13,27 @@ 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 => {
displayName: import.meta.env.VITE_SIP_DISPLAY_NAME ?? 'Helix Agent', try {
uri: import.meta.env.VITE_SIP_URI ?? '', const stored = localStorage.getItem('helix_agent_config');
password: import.meta.env.VITE_SIP_PASSWORD ?? '', if (stored) {
wsServer: import.meta.env.VITE_SIP_WS_SERVER ?? '', const config = JSON.parse(stored);
stunServers: 'stun:stun.l.google.com:19302', 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) => { export const SipProvider = ({ children }: PropsWithChildren) => {
@@ -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,