Files
BizOne-portal/backend/src/dashboard/dashboard.service.ts

183 lines
5.6 KiB
TypeScript

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<Record<string, number>>((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',
},
};
}
}