From 9a016a2ed00c05b5f8e6691a789a4cc478aecbe7 Mon Sep 17 00:00:00 2001 From: saridsa2 Date: Fri, 17 Apr 2026 05:45:14 +0530 Subject: [PATCH] =?UTF-8?q?feat:=20real-time=20active=20call=20SSE=20?= =?UTF-8?q?=E2=80=94=20hold/unhold=20status=20for=20supervisor=20live=20mo?= =?UTF-8?q?nitor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SupervisorService: added activeCallSubject (RxJS Subject), emits on all activeCalls Map mutations (Answered, Calling, Disconnect, Hold, Unhold) - SupervisorController: new @Sse('active-calls/stream') endpoint - OzonetelAgentController: callControl HOLD/UNHOLD updates activeCalls Map status via supervisor.updateCallStatus() Co-Authored-By: Claude Opus 4.6 (1M context) --- src/ozonetel/ozonetel-agent.controller.ts | 7 +++++++ src/supervisor/supervisor.controller.ts | 10 ++++++++++ src/supervisor/supervisor.service.ts | 20 ++++++++++++++++---- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/ozonetel/ozonetel-agent.controller.ts b/src/ozonetel/ozonetel-agent.controller.ts index 0bc8411..ff84148 100644 --- a/src/ozonetel/ozonetel-agent.controller.ts +++ b/src/ozonetel/ozonetel-agent.controller.ts @@ -382,6 +382,13 @@ export class OzonetelAgentController { try { const result = await this.ozonetelAgent.callControl(body); + + if (body.action === 'HOLD') { + this.supervisor.updateCallStatus(body.ucid, 'on-hold'); + } else if (body.action === 'UNHOLD') { + this.supervisor.updateCallStatus(body.ucid, 'active'); + } + return result; } catch (error: any) { const message = error.response?.data?.message ?? error.message ?? 'Call control failed'; diff --git a/src/supervisor/supervisor.controller.ts b/src/supervisor/supervisor.controller.ts index 67e993e..8fdaa7b 100644 --- a/src/supervisor/supervisor.controller.ts +++ b/src/supervisor/supervisor.controller.ts @@ -13,6 +13,16 @@ export class SupervisorController { return this.supervisor.getActiveCalls(); } + @Sse('active-calls/stream') + streamActiveCalls(): Observable { + this.logger.log('[SSE] Active calls stream opened'); + return this.supervisor.activeCallSubject.pipe( + map(event => ({ + data: JSON.stringify(event), + } as MessageEvent)), + ); + } + @Get('team-performance') async getTeamPerformance(@Query('date') date?: string) { const targetDate = date ?? new Date().toISOString().split('T')[0]; diff --git a/src/supervisor/supervisor.service.ts b/src/supervisor/supervisor.service.ts index d08ac16..056fbb0 100644 --- a/src/supervisor/supervisor.service.ts +++ b/src/supervisor/supervisor.service.ts @@ -36,6 +36,7 @@ export class SupervisorService implements OnModuleInit { private readonly agentStates = new Map(); private readonly acwTimers = new Map(); readonly agentStateSubject = new Subject<{ agentId: string; state: AgentOzonetelState | string; timestamp: string }>(); + readonly activeCallSubject = new Subject<{ type: 'update' | 'remove'; call?: ActiveCall; ucid: string }>(); // Worklist update stream — emitted when a missed call is created or // assigned. Frontend SSE listener triggers an immediate worklist // refresh so agents see new missed calls without waiting for the 30s poll. @@ -95,10 +96,9 @@ export class SupervisorService implements OnModuleInit { this.logger.warn(`Ignoring call event for offline agent ${agentId} (${ucid})`); return; } - this.activeCalls.set(ucid, { - ucid, agentId, callerNumber, - callType, startTime: eventTime, status: 'active', - }); + const call: ActiveCall = { ucid, agentId, callerNumber, callType, startTime: eventTime, status: 'active' }; + this.activeCalls.set(ucid, call); + this.activeCallSubject.next({ type: 'update', call, ucid }); this.logger.log(`Active call: ${agentId} ↔ ${callerNumber} (${ucid})`); // Persist CALL_START as AgentEvent on the "Answered" moment @@ -130,6 +130,7 @@ export class SupervisorService implements OnModuleInit { } else if (action === 'Disconnect') { const wasActive = this.activeCalls.get(ucid); this.activeCalls.delete(ucid); + this.activeCallSubject.next({ type: 'remove', ucid }); this.logger.log(`Call ended: ${ucid}`); // Persist CALL_END — pair against the start for duration. @@ -294,6 +295,17 @@ export class SupervisorService implements OnModuleInit { // definitely stale (e.g. Disconnect webhook was dropped). private static readonly NON_CALL_AGENT_STATES = new Set(['ready', 'offline', 'paused']); + updateCallStatus(ucid: string, status: 'active' | 'on-hold') { + const call = this.activeCalls.get(ucid); + if (!call) { + this.logger.warn(`[CALL-STATUS] No active call found for UCID ${ucid}`); + return; + } + call.status = status; + this.activeCallSubject.next({ type: 'update', call, ucid }); + this.logger.log(`[CALL-STATUS] ${ucid} → ${status} (agent=${call.agentId})`); + } + getActiveCalls(): ActiveCall[] { // Sweep stale entries before returning. The activeCalls Map is a // best-effort in-memory projection of Ozonetel call events — if