feat: enrich traffic API with external actor ranking and focused event streams
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 7s

This commit is contained in:
OG T
2026-06-07 16:00:24 +08:00
parent 031d5af8b0
commit 2aa4fece7d

View File

@@ -5,6 +5,13 @@ export const dynamic = "force-dynamic";
const MONITOR_TOKEN = process.env.TRAFFIC_MONITOR_TOKEN;
function asRecordJson(value: unknown): Record<string, unknown> | undefined {
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
return value as Record<string, unknown>;
}
return undefined;
}
export async function GET(request: NextRequest) {
if (MONITOR_TOKEN) {
const token = request.headers.get("x-traffic-token");
@@ -13,9 +20,11 @@ export async function GET(request: NextRequest) {
}
}
const since = new Date(Date.now() - 24 * 60 * 60 * 1000);
const query = request.nextUrl.searchParams;
const minutes = Math.max(parseInt(query.get("minutes") || "1440", 10), 5);
const since = new Date(Date.now() - minutes * 60 * 1000);
const [summaryRows, actorSummaryRows, totalRows, latestEvents] = await Promise.all([
const [summaryRows, actorSummaryRows, externalActorRows, totalRows, latestEvents] = await Promise.all([
prisma.auditEvent.groupBy({
by: ["action"],
where: {
@@ -30,6 +39,16 @@ export async function GET(request: NextRequest) {
},
_count: { _all: true },
}),
prisma.auditEvent.groupBy({
by: ["actorId"],
where: {
createdAt: { gte: since },
action: {
startsWith: "EXTERNAL_",
},
},
_count: { _all: true },
}),
prisma.auditEvent.count({
where: { createdAt: { gte: since } },
}),
@@ -40,7 +59,7 @@ export async function GET(request: NextRequest) {
orderBy: {
createdAt: "desc",
},
take: 80,
take: 120,
select: {
id: true,
action: true,
@@ -63,6 +82,14 @@ export async function GET(request: NextRequest) {
actorSummaryRows.map((row) => [row.actorType, row._count._all])
);
const externalActorSummary = externalActorRows
.map((row) => ({
actorId: row.actorId || "unknown",
events: row._count._all,
}))
.sort((a, b) => b.events - a.events)
.slice(0, 20);
const channelSummary = Object.entries(actionSummary).reduce(
(acc, [action, count]) => {
if (action.startsWith("EXTERNAL_")) {
@@ -75,16 +102,16 @@ export async function GET(request: NextRequest) {
{ external: 0, internal: 0 } as Record<string, number>
);
const externalEventTypes = Object.entries(actionSummary).filter(([action]) => action.startsWith("EXTERNAL_")).map(([action, count]) => ({ action, count }));
const internalEventTypes = Object.entries(actionSummary).filter(([action]) => !action.startsWith("EXTERNAL_")).map(([action, count]) => ({ action, count }));
const externalEventTypes = Object.entries(actionSummary)
.filter(([action]) => action.startsWith("EXTERNAL_"))
.map(([action, count]) => ({ action, count }));
const internalEventTypes = Object.entries(actionSummary)
.filter(([action]) => !action.startsWith("EXTERNAL_"))
.map(([action, count]) => ({ action, count }));
const recentEvents = latestEvents.map((event) => {
// metadata is JSONB in Prisma; read only known optional keys for monitoring views.
const metadata =
event.metadata && typeof event.metadata === "object" && !Array.isArray(event.metadata)
? (event.metadata as Record<string, unknown>)
: undefined;
const metadata = asRecordJson(event.metadata);
return {
id: event.id,
action: event.action,
@@ -99,15 +126,25 @@ export async function GET(request: NextRequest) {
};
});
const recentExternalEvents = recentEvents.filter((event) =>
event.action.startsWith("EXTERNAL_")
);
const recentInternalEvents = recentEvents.filter(
(event) => !event.action.startsWith("EXTERNAL_")
);
return NextResponse.json({
period_hours: 24,
period_minutes: minutes,
total_events: totalRows,
action_summary: actionSummary,
channel_summary: channelSummary,
actor_summary: actorSummary,
external_actor_summary: externalActorSummary,
external_event_types: externalEventTypes,
internal_event_types: internalEventTypes,
recent_events: recentEvents,
recent_external_events: recentExternalEvents,
recent_internal_events: recentInternalEvents,
updated_at: new Date().toISOString(),
});
}