feat: add public AI traffic dashboard and filter homepage tasks
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 8s

This commit is contained in:
OG T
2026-06-07 16:21:09 +08:00
parent c0841de16d
commit 309b5aa1ec
2 changed files with 271 additions and 0 deletions

View File

@@ -4,6 +4,13 @@ import Link from "next/link";
export const dynamic = "force-dynamic";
export default async function Home() {
const tasks = await prisma.task.findMany({
where: {
title: {
not: {
startsWith: "GitHub Issue:",
},
},
},
orderBy: { created_at: "desc" }
});
@@ -100,6 +107,11 @@ export default async function Home() {
<p className="text-gray-500 text-sm">
Agent
</p>
<div className="mt-6">
<Link href="/traffic" className="inline-flex items-center gap-2 text-emerald-400 hover:text-emerald-300">
AI
</Link>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,259 @@
import Link from "next/link";
import { prisma } from "@/lib/prisma";
import { isIP } from "node:net";
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;
}
function isInternalActorId(value: string | null | undefined) {
if (!value) return true;
const actorId = value.toLowerCase();
if (actorId === "unknown" || actorId === "mcp-anonymous" || actorId === "open-tasks:localhost") return true;
if (actorId === "localhost") return true;
const ipMatch = actorId.match(/^open-tasks:([a-z0-9.:_-]+)$/);
if (!ipMatch?.[1]) return false;
const actorIp = ipMatch[1];
if (actorIp.startsWith("127.") || actorIp.startsWith("10.") || actorIp.startsWith("192.168.")) return true;
if (actorIp.startsWith("172.")) {
const secondOctet = Number(actorIp.split(".")[1]);
return secondOctet >= 16 && secondOctet <= 31;
}
if (actorIp === "localhost" || actorIp === "unknown" || actorIp.startsWith("fc") || actorIp.startsWith("fd")) return true;
if (actorIp === "::1" || actorIp.startsWith("fe80")) return true;
if (isIP(actorIp) === 6 && actorIp.startsWith("fc")) return true;
if (isIP(actorIp) === 6 && actorIp.startsWith("fd")) return true;
if (isIP(actorIp) === 4 || isIP(actorIp) === 6) return false;
return false;
}
function isAuthorizedToken(token: string | undefined, tokenHeader: string | undefined) {
if (!token) return true;
return tokenHeader === token;
}
async function getTrafficSummary(minutes: number) {
const since = new Date(Date.now() - minutes * 60 * 1000);
const [summaryRows, actorSummaryRows, externalActorRows, totalRows, latestEvents] = await Promise.all([
prisma.auditEvent.groupBy({
by: ["action"],
where: {
createdAt: { gte: since },
},
_count: { _all: true },
}),
prisma.auditEvent.groupBy({
by: ["actorType"],
where: {
createdAt: { gte: since },
},
_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 } },
}),
prisma.auditEvent.findMany({
where: {
createdAt: { gte: since },
},
orderBy: {
createdAt: "desc",
},
take: 120,
select: {
id: true,
action: true,
actorType: true,
actorId: true,
entityType: true,
entityId: true,
reason: true,
metadata: true,
createdAt: true,
},
}),
]);
const actionSummary = Object.fromEntries(summaryRows.map((row) => [row.action, row._count._all]));
const actorSummary = Object.fromEntries(actorSummaryRows.map((row) => [row.actorType, row._count._all]));
const externalActorSummary = externalActorRows
.map((row) => ({
actorId: row.actorId || "unknown",
events: row._count._all,
}))
.filter((row) => !isInternalActorId(row.actorId))
.sort((a, b) => b.events - a.events)
.slice(0, 20);
const channelSummary = Object.entries(actionSummary).reduce(
(acc, [action, count]) => {
if (action.startsWith("EXTERNAL_")) {
acc.external += count;
} else {
acc.internal += count;
}
return acc;
},
{ external: 0, internal: 0 } as Record<string, number>
);
const externalEventTypes = Object.entries(actionSummary)
.filter(([action]) => action.startsWith("EXTERNAL_"))
.map(([action, count]) => ({ action, count }));
const recentEvents = latestEvents.map((event) => {
const metadata = asRecordJson(event.metadata);
return {
...event,
surface: metadata?.surface,
level: metadata?.level,
};
});
return {
periodMinutes: minutes,
totalEvents: totalRows,
actionSummary,
actorSummary,
channelSummary,
externalActorSummary,
externalEventTypes,
recentExternalEvents: recentEvents.filter((event) => event.action.startsWith("EXTERNAL_") && !isInternalActorId(event.actorId)),
};
}
export default async function TrafficDashboard({
searchParams,
}: {
searchParams: Promise<{ minutes?: string; token?: string }>;
}) {
const resolved = await searchParams;
const token = resolved?.token;
const minutes = Math.max(parseInt(resolved?.minutes || "1440", 10), 5);
if (!isAuthorizedToken(MONITOR_TOKEN, token)) {
return (
<div className="min-h-screen bg-gray-950 text-gray-100 p-8 font-sans">
<div className="max-w-5xl mx-auto">
<h1 className="text-3xl font-bold mb-4">VibeWork </h1>
<p className="text-gray-300 mb-4"> Token </p>
<p className="text-sm text-gray-500"><code>/traffic?token=YOUR_TOKEN&minutes=60</code></p>
</div>
</div>
);
}
const summary = await getTrafficSummary(minutes);
return (
<div className="min-h-screen bg-gray-950 text-gray-100 p-8 font-sans">
<div className="max-w-6xl mx-auto space-y-8">
<div className="flex justify-between items-center">
<h1 className="text-3xl font-bold">VibeWork </h1>
<Link href="/" className="text-blue-400 hover:text-blue-300">
</Link>
</div>
<div className="text-sm text-gray-400">
{summary.periodMinutes} `EXTERNAL_*`
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-4">
<div className="text-gray-400 text-sm"></div>
<div className="text-3xl font-bold mt-2">{summary.totalEvents}</div>
</div>
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-4">
<div className="text-gray-400 text-sm"></div>
<div className="text-3xl font-bold mt-2 text-emerald-300">{summary.channelSummary.external}</div>
</div>
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-4">
<div className="text-gray-400 text-sm">/</div>
<div className="text-3xl font-bold mt-2 text-blue-300">{summary.channelSummary.internal}</div>
</div>
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-4">
<div className="text-gray-400 text-sm"></div>
<div className="text-lg font-bold mt-2">{summary.externalEventTypes.length}</div>
</div>
</div>
<div className="grid gap-4 lg:grid-cols-2">
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-6">
<h2 className="text-xl font-semibold mb-4"> Actor 20</h2>
<div className="space-y-2 max-h-80 overflow-auto">
{summary.externalActorSummary.length === 0 ? (
<p className="text-gray-500"> Actor</p>
) : (
summary.externalActorSummary.map((actor) => (
<div key={actor.actorId} className="flex justify-between text-sm">
<span className="text-gray-300">{actor.actorId}</span>
<span className="text-emerald-300">{actor.events}</span>
</div>
))
)}
</div>
</div>
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-6">
<h2 className="text-xl font-semibold mb-4">Actor </h2>
<div className="space-y-2">
{Object.entries(summary.actorSummary).map(([actorType, count]) => (
<div key={actorType} className="flex justify-between text-sm">
<span className="text-gray-300">{actorType}</span>
<span className="text-blue-300">{count}</span>
</div>
))}
{Object.keys(summary.actorSummary).length === 0 ? (
<p className="text-gray-500 text-sm"></p>
) : null}
</div>
</div>
</div>
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-6">
<h2 className="text-xl font-semibold mb-4">top 120</h2>
<div className="space-y-2 max-h-96 overflow-auto">
{summary.recentExternalEvents.length === 0 ? (
<p className="text-gray-500"></p>
) : (
summary.recentExternalEvents.map((event) => {
const ts = new Date(event.createdAt).toLocaleString("zh-TW", { timeZone: "Asia/Taipei" });
return (
<div key={event.id} className="border-b border-gray-800 py-2 text-sm">
<div className="font-mono text-emerald-300">{event.action}</div>
<div className="text-gray-400">
actor={event.actorType}:{event.actorId || "unknown"} | entity={event.entityType}/{event.entityId} | {ts}
</div>
{event.reason ? <div className="text-gray-500 text-xs mt-1">{event.reason}</div> : null}
</div>
);
})
)}
</div>
</div>
</div>
</div>
);
}