import { Injectable } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; @Injectable() export class DashboardService { constructor(private readonly prisma: PrismaService) {} async summary() { const totalContacts = await this.prisma.contact.count(); const totalWebhookEvents = await this.prisma.webhookEvent.count(); const failedWebhookEvents = await this.prisma.webhookEvent.count({ where: { processingStatus: 'failed' }, }); return { totalContacts, totalWebhookEvents, deliveredRate: 0, webhookHealth: failedWebhookEvents > 0 ? 'degraded' : 'healthy', }; } async analyticsSummary() { const now = new Date(); const last24Hours = new Date(now.getTime() - 24 * 60 * 60 * 1000); const lastHour = new Date(now.getTime() - 60 * 60 * 1000); const [ totalJobs, pendingJobs, processingJobs, failedJobs24h, totalWebhookEvents, verifiedWebhookEvents, pendingWebhookEvents, auditTrailTotal, recentJobs, recentWebhookEvents, ] = await this.prisma.$transaction([ this.prisma.job.count(), this.prisma.job.count({ where: { status: 'queued' } }), this.prisma.job.count({ where: { status: 'processing' } }), this.prisma.job.count({ where: { status: 'failed', updatedAt: { gte: last24Hours }, }, }), this.prisma.webhookEvent.count(), this.prisma.webhookEvent.count({ where: { verified: true } }), this.prisma.webhookEvent.count({ where: { processingStatus: { not: 'processed' }, }, }), this.prisma.auditLog.count(), this.prisma.job.findMany({ where: { createdAt: { gte: lastHour }, }, select: { queueName: true, status: true, attempts: true, maxAttempts: true, createdAt: true, processedAt: true, }, }), this.prisma.webhookEvent.findMany({ where: { createdAt: { gte: lastHour }, }, select: { createdAt: true, verified: true, processingStatus: true, }, }), ]); const workerGroups = recentJobs.reduce>((acc, job) => { const key = `${job.queueName}-worker`; acc[key] = (acc[key] || 0) + 1; return acc; }, {}); const workerHealth = Object.entries(workerGroups).map(([name, count]) => { const load = Math.max(4, Math.min(100, count * 12)); return { name, load, tone: load >= 70 ? 'success' : 'warning', }; }); const latencySamples = recentJobs .filter((job) => job.processedAt) .map((job) => job.processedAt!.getTime() - job.createdAt.getTime()) .filter((value) => Number.isFinite(value) && value >= 0); const averageLatencyMs = latencySamples.length > 0 ? Math.round(latencySamples.reduce((sum, value) => sum + value, 0) / latencySamples.length) : 120; const processedLastHour = recentJobs.filter((job) => job.status === 'processed').length; const webhookLastHour = recentWebhookEvents.length; const verifiedWebhookRate = totalWebhookEvents > 0 ? Math.round((verifiedWebhookEvents / totalWebhookEvents) * 100) : 0; const throughputPerMinute = Math.max(1, Math.round((processedLastHour + webhookLastHour) / 60)); const averageAttempts = recentJobs.length > 0 ? Math.round( recentJobs.reduce((sum, job) => sum + Math.min(job.attempts, job.maxAttempts), 0) / recentJobs.length, ) : 0; const databaseConnectionsEstimate = Math.max( 1, Math.min(200, recentJobs.length + recentWebhookEvents.length + Math.min(auditTrailTotal, 120)), ); const memoryUsageGbEstimate = Number( (1.8 + (recentJobs.length + recentWebhookEvents.length + pendingWebhookEvents) * 0.07).toFixed(1), ); const apiLatencyBars = [ Math.max(4, Math.min(100, 32 + pendingJobs * 2)), Math.max(4, Math.min(100, 42 + processingJobs * 8)), Math.max(4, Math.min(100, 36 + failedJobs24h * 6)), Math.max(4, Math.min(100, 56 + pendingWebhookEvents * 8)), Math.max(4, Math.min(100, 28 + averageAttempts * 9)), Math.max(4, Math.min(100, 22 + processingJobs * 7)), Math.max(4, Math.min(100, 30 + verifiedWebhookRate)), ]; const memoryBars = [ Math.max(4, Math.min(100, 48 + pendingJobs * 3)), Math.max(4, Math.min(100, 54 + processingJobs * 5)), Math.max(4, Math.min(100, 58 + failedJobs24h * 4)), Math.max(4, Math.min(100, 50 + pendingWebhookEvents * 7)), Math.max(4, Math.min(100, 55 + verifiedWebhookRate)), Math.max(4, Math.min(100, 62 + processingJobs * 4)), Math.max(4, Math.min(100, 64 + averageAttempts * 5)), ]; return { generatedAt: now.toISOString(), queue: { pendingJobs, processingJobs, failedJobs24h, }, workers: workerHealth, throughput: { perMinute: throughputPerMinute, verifiedWebhookRate, jobsLastHour: recentJobs.length, webhooksLastHour: webhookLastHour, }, metrics: { apiLatencyMs: averageLatencyMs, apiLatencyBars, databaseConnectionsEstimate, databaseUsagePercent: Math.max(4, Math.min(100, Math.round((databaseConnectionsEstimate / 200) * 100))), memoryUsageGbEstimate, memoryBars, }, totals: { totalJobs, totalWebhookEvents, pendingWebhookEvents, auditTrailTotal, }, health: { status: 'ok', database: 'ok', }, }; } }