Files
stack/apps/api/src/dashboard/dashboard.service.ts
Jason Woltje 7581d26567
All checks were successful
ci/woodpecker/push/api Pipeline was successful
ci/woodpecker/push/web Pipeline was successful
Phase 2: Task Ingestion Pipeline (#459) (#460)
Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
2026-02-23 00:54:55 +00:00

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,
};
}
}