feat: SSE push for worklist updates — instant missed-call notifications
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

New worklist SSE stream replaces the 30s frontend poll. When the
missed-call webhook creates a Call record, it emits a worklist-updated
event via the supervisor's worklistSubject. All connected agents
receive the event immediately.

- supervisor.service.ts: worklistSubject + emitWorklistUpdate()
- supervisor.controller.ts: @Sse('worklist/stream') broadcast endpoint
- missed-call-webhook.controller.ts: emits after createCall() with
  callerPhone + callerName for toast notification
- worklist.module.ts: imports SupervisorModule (forwardRef)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-16 18:32:57 +05:30
parent a6f4c51ca9
commit 9cf0f69dde
4 changed files with 37 additions and 2 deletions

View File

@@ -52,4 +52,18 @@ export class SupervisorController {
} as MessageEvent)),
);
}
// Worklist SSE — broadcast to all connected agents. When a missed
// call is created by the webhook, this fires immediately so agents
// don't wait for the 30s worklist poll. The payload includes the
// caller's phone + name for a toast notification.
@Sse('worklist/stream')
streamWorklistUpdates(): Observable<MessageEvent> {
this.logger.log('[SSE] Worklist stream opened');
return this.supervisor.worklistSubject.pipe(
map(event => ({
data: JSON.stringify(event),
} as MessageEvent)),
);
}
}

View File

@@ -36,6 +36,15 @@ export class SupervisorService implements OnModuleInit {
private readonly agentStates = new Map<string, AgentStateEntry>();
private readonly acwTimers = new Map<string, NodeJS.Timeout>();
readonly agentStateSubject = new Subject<{ agentId: string; state: AgentOzonetelState | string; timestamp: 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.
readonly worklistSubject = new Subject<{ type: string; callerPhone?: string; callerName?: string; callId?: string; timestamp: string }>();
emitWorklistUpdate(data: { type: string; callerPhone?: string; callerName?: string; callId?: string }) {
this.worklistSubject.next({ ...data, timestamp: new Date().toISOString() });
this.logger.log(`[WORKLIST-SSE] ${data.type} phone=${data.callerPhone ?? '?'} name=${data.callerName ?? '?'}`);
}
// Barge session tracking — key is agentId
private readonly bargeSessions = new Map<string, {

View File

@@ -1,7 +1,8 @@
import { Controller, Post, Body, Headers, Logger } from '@nestjs/common';
import { Controller, Post, Body, Headers, Logger, Inject, forwardRef } from '@nestjs/common';
import { PlatformGraphqlService } from '../platform/platform-graphql.service';
import { AgentLookupService } from '../platform/agent-lookup.service';
import { CallerResolutionService } from '../caller/caller-resolution.service';
import { SupervisorService } from '../supervisor/supervisor.service';
import { ConfigService } from '@nestjs/config';
// Ozonetel sends all timestamps in IST — convert to UTC for storage
@@ -24,6 +25,7 @@ export class MissedCallWebhookController {
private readonly config: ConfigService,
private readonly caller: CallerResolutionService,
private readonly agentLookup: AgentLookupService,
@Inject(forwardRef(() => SupervisorService)) private readonly supervisor: SupervisorService,
) {
this.apiKey = config.get<string>('platform.apiKey') ?? '';
}
@@ -126,6 +128,15 @@ export class MissedCallWebhookController {
this.logger.log(`Created call record: ${callId} (${callStatus})${resolved.leadName ? ` linked to ${resolved.leadName}` : ''}`);
// Push worklist SSE so agents see new calls instantly
// instead of waiting for the 30s frontend poll.
this.supervisor.emitWorklistUpdate({
type: callStatus === 'MISSED' ? 'missed-call' : 'inbound-call',
callerPhone: callerPhone,
callerName: resolved.leadName ?? undefined,
callId,
});
// Step 3: Lead-side side-effects (activity log + contact stats)
if (resolved.leadId) {
const summary = callStatus === 'MISSED'

View File

@@ -4,6 +4,7 @@ import { OzonetelAgentModule } from '../ozonetel/ozonetel-agent.module';
import { AuthModule } from '../auth/auth.module';
import { RulesEngineModule } from '../rules-engine/rules-engine.module';
import { CallerResolutionModule } from '../caller/caller-resolution.module';
import { SupervisorModule } from '../supervisor/supervisor.module';
import { TelephonyConfigService } from '../config/telephony-config.service';
import { WorklistController } from './worklist.controller';
import { WorklistService } from './worklist.service';
@@ -12,7 +13,7 @@ import { MissedCallWebhookController } from './missed-call-webhook.controller';
import { KookooCallbackController } from './kookoo-callback.controller';
@Module({
imports: [PlatformModule, forwardRef(() => OzonetelAgentModule), forwardRef(() => AuthModule), RulesEngineModule, forwardRef(() => CallerResolutionModule)],
imports: [PlatformModule, forwardRef(() => OzonetelAgentModule), forwardRef(() => AuthModule), RulesEngineModule, forwardRef(() => CallerResolutionModule), forwardRef(() => SupervisorModule)],
controllers: [WorklistController, MissedCallWebhookController, KookooCallbackController],
providers: [WorklistService, MissedQueueService, TelephonyConfigService],
exports: [MissedQueueService],