feat: SSE agent state, UCID fix, maint module, QA bug fixes

- Fix outbound disposition: store UCID from dial API response (root cause of silent disposition failure)
- SSE agent state: real-time Ozonetel state drives status toggle (ready/break/calling/in-call/acw)
- Maint module with OTP-protected endpoints (force-ready, unlock-agent, backfill, fix-timestamps)
- Maint OTP modal with PinInput component, keyboard shortcuts (Ctrl+Shift+R/U/B/T)
- Force-logout via SSE: admin unlock pushes force-logout to connected browsers
- Silence JsSIP debug flood, add structured lifecycle logging ([SIP], [DIAL], [DISPOSE], [AGENT-STATE])
- Centralize date formatting with IST-aware formatters across 11 files
- Fix call history: non-overlapping aggregates (completed/missed), correct timestamp display
- Auto-dismiss CallWidget ended/failed state after 3 seconds
- Remove floating "Helix Phone" idle badge from all pages
- Fix dead code in agent-state endpoint (auto-assign was unreachable after return)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 22:03:48 +05:30
parent ae94a390df
commit 488f524f84
21 changed files with 462 additions and 107 deletions

View File

@@ -0,0 +1,78 @@
import { useState, useEffect, useRef } from 'react';
import { notify } from '@/lib/toast';
export type OzonetelState = 'ready' | 'break' | 'training' | 'calling' | 'in-call' | 'acw' | 'offline';
const API_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:4100';
export const useAgentState = (agentId: string | null): OzonetelState => {
const [state, setState] = useState<OzonetelState>('offline');
const prevStateRef = useRef<OzonetelState>('offline');
const esRef = useRef<EventSource | null>(null);
useEffect(() => {
if (!agentId) {
setState('offline');
return;
}
// Fetch current state on connect
fetch(`${API_URL}/api/supervisor/agent-state?agentId=${agentId}`)
.then(res => res.json())
.then(data => {
if (data.state) {
console.log(`[SSE] Initial state for ${agentId}: ${data.state}`);
prevStateRef.current = data.state;
setState(data.state);
}
})
.catch(() => {});
// Open SSE stream
const url = `${API_URL}/api/supervisor/agent-state/stream?agentId=${agentId}`;
console.log(`[SSE] Connecting: ${url}`);
const es = new EventSource(url);
esRef.current = es;
es.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
console.log(`[SSE] State update: ${agentId}${data.state}`);
// Force-logout: only triggered by explicit admin action, not normal Ozonetel logout
if (data.state === 'force-logout') {
console.log('[SSE] Force-logout received — clearing session');
notify.info('Session Ended', 'Your session was ended by an administrator.');
es.close();
localStorage.removeItem('helix_access_token');
localStorage.removeItem('helix_refresh_token');
localStorage.removeItem('helix_agent_config');
localStorage.removeItem('helix_user');
import('@/state/sip-manager').then(({ disconnectSip }) => disconnectSip()).catch(() => {});
setTimeout(() => { window.location.href = '/login'; }, 1500);
return;
}
prevStateRef.current = data.state;
setState(data.state);
} catch {
console.warn('[SSE] Failed to parse event:', event.data);
}
};
es.onerror = () => {
console.warn('[SSE] Connection error — will auto-reconnect');
};
return () => {
console.log('[SSE] Closing connection');
es.close();
esRef.current = null;
};
}, [agentId]);
return state;
};