feat: Decisions page — aggregated view of all decision points across traces

Adds /dashboard/decisions page with colored type badges, search,
filters (by type), sort (newest/oldest/costliest), pagination,
and links to parent traces. New /api/decisions endpoint with
Prisma queries. Removes 'Soon' badge from sidebar nav.
This commit is contained in:
Vectry
2026-02-10 02:24:00 +00:00
parent 145b1669e7
commit 92b98f2d6f
3 changed files with 530 additions and 2 deletions

View File

@@ -0,0 +1,127 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { Prisma } from "@agentlens/database";
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get("page") ?? "1", 10);
const limit = parseInt(searchParams.get("limit") ?? "20", 10);
const type = searchParams.get("type");
const search = searchParams.get("search");
const sort = searchParams.get("sort") ?? "newest";
// Validate pagination
if (isNaN(page) || page < 1) {
return NextResponse.json(
{ error: "Invalid page parameter. Must be a positive integer." },
{ status: 400 }
);
}
if (isNaN(limit) || limit < 1 || limit > 100) {
return NextResponse.json(
{ error: "Invalid limit parameter. Must be between 1 and 100." },
{ status: 400 }
);
}
// Validate type
const validTypes = [
"TOOL_SELECTION",
"ROUTING",
"RETRY",
"ESCALATION",
"MEMORY_RETRIEVAL",
"PLANNING",
"CUSTOM",
];
if (type && !validTypes.includes(type)) {
return NextResponse.json(
{ error: `Invalid type. Must be one of: ${validTypes.join(", ")}` },
{ status: 400 }
);
}
// Validate sort
const validSorts = ["newest", "oldest", "costliest"];
if (!validSorts.includes(sort)) {
return NextResponse.json(
{ error: `Invalid sort. Must be one of: ${validSorts.join(", ")}` },
{ status: 400 }
);
}
// Build where clause
const where: Prisma.DecisionPointWhereInput = {};
if (type) {
where.type = type as Prisma.EnumDecisionTypeFilter["equals"];
}
if (search) {
where.reasoning = {
contains: search,
mode: "insensitive",
};
}
// Build order by
let orderBy: Prisma.DecisionPointOrderByWithRelationInput;
switch (sort) {
case "oldest":
orderBy = { timestamp: "asc" };
break;
case "costliest":
orderBy = { costUsd: "desc" };
break;
case "newest":
default:
orderBy = { timestamp: "desc" };
break;
}
// Count total
const total = await prisma.decisionPoint.count({ where });
// Pagination
const skip = (page - 1) * limit;
const totalPages = Math.ceil(total / limit);
// Fetch decisions with parent trace and span
const decisions = await prisma.decisionPoint.findMany({
where,
include: {
trace: {
select: {
id: true,
name: true,
},
},
span: {
select: {
id: true,
name: true,
},
},
},
orderBy,
skip,
take: limit,
});
return NextResponse.json(
{
decisions,
total,
page,
limit,
totalPages,
},
{ status: 200 }
);
} catch (error) {
console.error("Error listing decisions:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}