Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
188 lines
4.8 KiB
TypeScript
188 lines
4.8 KiB
TypeScript
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<DashboardSummaryDto> {
|
|
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<string, unknown> | 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,
|
|
};
|
|
}
|
|
}
|