mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-05-18 20:08:19 +00:00
feat(performance-alerts): rules-engine-driven alerts, persisted as PerformanceAlert
Phase A+B of the alerts overhaul: - New PerformanceFactsProvider exposes agent.idleMinutes (from AgentSession), agent.busyMinutes, agent.totalCallsToday, agent.bookedCallsToday, agent.conversionPercent - Implement EscalateActionHandler (was a stub): persists a PerformanceAlert row, dedupes per agent+type+IST date so a 5-min cron can't spam, updates value if it changes - New PerformanceConsumer: setInterval every 5 min, reads on_schedule rules referencing agent.* facts, evaluates per agent, dispatches escalate actions - Two starter rules in hospital-starter.json: excessive-idle (>60min) and low-conversion (<15% with >10 calls today). NPS deferred — no source signal exists yet - New PerformanceAlertsController: GET /api/supervisor/performance-alerts (active list), POST /:id/dismiss, POST /dismiss-all - Rules engine now injects EscalateActionHandler via DI so the action has access to PlatformGraphqlService for persistence Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
91
src/supervisor/performance-alerts.controller.ts
Normal file
91
src/supervisor/performance-alerts.controller.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { Controller, Get, Post, Param, Logger } from '@nestjs/common';
|
||||
import { PlatformGraphqlService } from '../platform/platform-graphql.service';
|
||||
|
||||
/**
|
||||
* Read API for the supervisor notification bell. Returns active (non-
|
||||
* dismissed) PerformanceAlert rows the rules engine has emitted.
|
||||
*
|
||||
* Frontend polls every 60s. Dismiss is per-alert.
|
||||
*/
|
||||
@Controller('api/supervisor/performance-alerts')
|
||||
export class PerformanceAlertsController {
|
||||
private readonly logger = new Logger(PerformanceAlertsController.name);
|
||||
|
||||
constructor(private readonly platform: PlatformGraphqlService) {}
|
||||
|
||||
@Get()
|
||||
async list() {
|
||||
const data = await this.platform.query<any>(
|
||||
`{ performanceAlerts(
|
||||
first: 50,
|
||||
filter: { dismissedAt: { is: NULL } },
|
||||
orderBy: [{ firedAt: DescNullsLast }]
|
||||
) {
|
||||
edges { node {
|
||||
id alertType severity message value ruleId firedAt
|
||||
agent { id name }
|
||||
} }
|
||||
} }`,
|
||||
);
|
||||
const edges = data?.performanceAlerts?.edges ?? [];
|
||||
return {
|
||||
alerts: edges.map((e: any) => {
|
||||
const n = e.node;
|
||||
return {
|
||||
id: n.id,
|
||||
agent: n.agent?.name ?? 'Unknown',
|
||||
agentId: n.agent?.id ?? null,
|
||||
type: this.toLabel(n.alertType),
|
||||
severity: (n.severity ?? 'WARNING').toLowerCase(),
|
||||
value: n.value ?? '',
|
||||
message: n.message,
|
||||
firedAt: n.firedAt,
|
||||
ruleId: n.ruleId,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@Post(':id/dismiss')
|
||||
async dismiss(@Param('id') id: string) {
|
||||
try {
|
||||
await this.platform.query<any>(
|
||||
`mutation($id: UUID!, $data: PerformanceAlertUpdateInput!) { updatePerformanceAlert(id: $id, data: $data) { id } }`,
|
||||
{ id, data: { dismissedAt: new Date().toISOString() } },
|
||||
);
|
||||
return { status: 'ok' };
|
||||
} catch (err: any) {
|
||||
this.logger.warn(`[ALERTS] Dismiss failed for ${id}: ${err?.message ?? err}`);
|
||||
return { status: 'error', message: String(err?.message ?? err) };
|
||||
}
|
||||
}
|
||||
|
||||
private toLabel(alertType: string | null | undefined): string {
|
||||
switch (alertType) {
|
||||
case 'EXCESSIVE_IDLE': return 'Excessive Idle Time';
|
||||
case 'LOW_NPS': return 'Low NPS';
|
||||
case 'LOW_CONVERSION': return 'Low Conversion';
|
||||
default: return alertType ?? 'Alert';
|
||||
}
|
||||
}
|
||||
|
||||
@Post('dismiss-all')
|
||||
async dismissAll() {
|
||||
const now = new Date().toISOString();
|
||||
const data = await this.platform.query<any>(
|
||||
`{ performanceAlerts(first: 100, filter: { dismissedAt: { is: NULL } }) { edges { node { id } } } }`,
|
||||
);
|
||||
const ids = (data?.performanceAlerts?.edges ?? []).map((e: any) => e.node.id);
|
||||
let dismissed = 0;
|
||||
for (const id of ids) {
|
||||
try {
|
||||
await this.platform.query<any>(
|
||||
`mutation($id: UUID!, $data: PerformanceAlertUpdateInput!) { updatePerformanceAlert(id: $id, data: $data) { id } }`,
|
||||
{ id, data: { dismissedAt: now } },
|
||||
);
|
||||
dismissed++;
|
||||
} catch {}
|
||||
}
|
||||
return { status: 'ok', dismissed };
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { PlatformModule } from '../platform/platform.module';
|
||||
import { OzonetelAgentModule } from '../ozonetel/ozonetel-agent.module';
|
||||
import { SupervisorController } from './supervisor.controller';
|
||||
import { SupervisorBargeController } from './supervisor-barge.controller';
|
||||
import { PerformanceAlertsController } from './performance-alerts.controller';
|
||||
import { SupervisorService } from './supervisor.service';
|
||||
import { AgentHistoryService } from './agent-history.service';
|
||||
import { OzonetelAdminAuthService } from '../ozonetel/ozonetel-admin-auth.service';
|
||||
@@ -12,7 +13,7 @@ import { OzonetelAdminAuthService } from '../ozonetel/ozonetel-admin-auth.servic
|
||||
// — it causes a circular dependency via AuthModule.
|
||||
@Module({
|
||||
imports: [PlatformModule, forwardRef(() => OzonetelAgentModule)],
|
||||
controllers: [SupervisorController, SupervisorBargeController],
|
||||
controllers: [SupervisorController, SupervisorBargeController, PerformanceAlertsController],
|
||||
providers: [SupervisorService, AgentHistoryService, OzonetelAdminAuthService],
|
||||
exports: [SupervisorService, AgentHistoryService, OzonetelAdminAuthService],
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user