import { Injectable } from "@nestjs/common"; import { AgentStatus, ProjectStatus, RunnerJobStatus, TaskStatus } from "@prisma/client"; import { PrismaService } from "../prisma/prisma.service"; import type { DashboardSummaryDto, ActiveJobDto, RecentActivityDto, TokenBudgetEntryDto, } from "./dto"; /** * Service for aggregating dashboard summary data. * Executes all queries in parallel to minimize latency. */ @Injectable() export class DashboardService { constructor(private readonly prisma: PrismaService) {} /** * Get aggregated dashboard summary for a workspace */ async getSummary(workspaceId: string): Promise { const now = new Date(); const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000); // Execute all queries in parallel const [ activeAgents, tasksCompleted, totalTasks, tasksInProgress, activeProjects, failedJobsLast24h, totalJobsLast24h, recentActivityRows, activeJobRows, tokenBudgetRows, ] = await Promise.all([ // Active agents: IDLE, WORKING, WAITING this.prisma.agent.count({ where: { workspaceId, status: { in: [AgentStatus.IDLE, AgentStatus.WORKING, AgentStatus.WAITING] }, }, }), // Tasks completed this.prisma.task.count({ where: { workspaceId, status: TaskStatus.COMPLETED, }, }), // Total tasks this.prisma.task.count({ where: { workspaceId }, }), // Tasks in progress this.prisma.task.count({ where: { workspaceId, status: TaskStatus.IN_PROGRESS, }, }), // Active projects this.prisma.project.count({ where: { workspaceId, status: ProjectStatus.ACTIVE, }, }), // Failed jobs in last 24h (for error rate) this.prisma.runnerJob.count({ where: { workspaceId, status: RunnerJobStatus.FAILED, createdAt: { gte: oneDayAgo }, }, }), // Total jobs in last 24h (for error rate) this.prisma.runnerJob.count({ where: { workspaceId, createdAt: { gte: oneDayAgo }, }, }), // Recent activity: last 10 entries this.prisma.activityLog.findMany({ where: { workspaceId }, orderBy: { createdAt: "desc" }, take: 10, }), // Active jobs: PENDING, QUEUED, RUNNING with steps this.prisma.runnerJob.findMany({ where: { workspaceId, status: { in: [RunnerJobStatus.PENDING, RunnerJobStatus.QUEUED, RunnerJobStatus.RUNNING], }, }, include: { steps: { select: { id: true, name: true, status: true, phase: true, }, orderBy: { ordinal: "asc" }, }, }, orderBy: { createdAt: "desc" }, }), // Token budgets for workspace (active, not yet completed) this.prisma.tokenBudget.findMany({ where: { workspaceId, completedAt: null, }, select: { agentId: true, totalTokensUsed: true, allocatedTokens: true, }, }), ]); // Compute error rate const errorRate = totalJobsLast24h > 0 ? (failedJobsLast24h / totalJobsLast24h) * 100 : 0; // Map recent activity const recentActivity: RecentActivityDto[] = recentActivityRows.map((row) => ({ id: row.id, action: row.action, entityType: row.entityType, entityId: row.entityId, details: row.details as Record | null, userId: row.userId, createdAt: row.createdAt.toISOString(), })); // Map active jobs (RunnerJob lacks updatedAt; use startedAt or createdAt as proxy) const activeJobs: ActiveJobDto[] = activeJobRows.map((row) => ({ id: row.id, type: row.type, status: row.status, progressPercent: row.progressPercent, createdAt: row.createdAt.toISOString(), updatedAt: (row.startedAt ?? row.createdAt).toISOString(), steps: row.steps.map((step) => ({ id: step.id, name: step.name, status: step.status, phase: step.phase, })), })); // Map token budget entries const tokenBudget: TokenBudgetEntryDto[] = tokenBudgetRows.map((row) => ({ model: row.agentId, used: row.totalTokensUsed, limit: row.allocatedTokens, })); return { metrics: { activeAgents, tasksCompleted, totalTasks, tasksInProgress, activeProjects, errorRate: Math.round(errorRate * 100) / 100, }, recentActivity, activeJobs, tokenBudget, }; } }