diff --git a/src/app/(dashboard)/dashboard/audit/A2aAuditTab.tsx b/src/app/(dashboard)/dashboard/audit/A2aAuditTab.tsx new file mode 100644 index 0000000000..1cd2f7c2dc --- /dev/null +++ b/src/app/(dashboard)/dashboard/audit/A2aAuditTab.tsx @@ -0,0 +1,217 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import { useTranslations } from "next-intl"; +import { Card } from "@/shared/components"; +import type { A2ATask, TaskState } from "@/lib/a2a/taskManager"; + +type TaskListResponse = { + tasks: A2ATask[]; + total: number; + limit: number; + offset: number; +}; + +const A2A_PAGE_SIZE = 25; + +const STATE_STYLES: Record = { + submitted: "border-amber-500/30 bg-amber-500/10 text-amber-600", + working: "border-blue-500/30 bg-blue-500/10 text-blue-600", + completed: "border-emerald-500/30 bg-emerald-500/10 text-emerald-600", + failed: "border-red-500/30 bg-red-500/10 text-red-600", + cancelled: "border-border bg-sidebar/40 text-text-muted", +}; + +function taskDuration(task: A2ATask): string { + const ms = new Date(task.updatedAt).getTime() - new Date(task.createdAt).getTime(); + if (ms < 1000) return `${ms}ms`; + return `${(ms / 1000).toFixed(1)}s`; +} + +export default function A2aAuditTab() { + const t = useTranslations("compliance"); + const [data, setData] = useState({ + tasks: [], + total: 0, + limit: A2A_PAGE_SIZE, + offset: 0, + }); + const [loading, setLoading] = useState(true); + const [skillFilter, setSkillFilter] = useState(""); + const [stateFilter, setStateFilter] = useState("all"); + const [offset, setOffset] = useState(0); + + const fetchTasks = useCallback(async () => { + setLoading(true); + try { + const params = new URLSearchParams(); + params.set("limit", String(A2A_PAGE_SIZE)); + params.set("offset", String(offset)); + if (skillFilter) params.set("skill", skillFilter); + if (stateFilter !== "all") params.set("state", stateFilter); + + const response = await fetch(`/api/a2a/tasks?${params.toString()}`); + const json = (await response.json().catch(() => ({}))) as Partial; + setData({ + tasks: Array.isArray(json.tasks) ? json.tasks : [], + total: Number(json.total || 0), + limit: Number(json.limit || A2A_PAGE_SIZE), + offset: Number(json.offset || offset), + }); + } finally { + setLoading(false); + } + }, [offset, skillFilter, stateFilter]); + + useEffect(() => { + void fetchTasks(); + }, [fetchTasks]); + + return ( +
+ +
+
+

{t("a2aAudit")}

+

{t("a2aAuditDesc")}

+

+ {t("a2aShowingTasks", { count: data.tasks.length, total: data.total })} +

+
+ +
+
+ + +
+ + +
+ +
+
+
+ + + {loading ? ( +
{t("a2aLoadingTasks")}
+ ) : data.tasks.length === 0 ? ( +
+ + device_hub + +

{t("a2aNoTasks")}

+
+ ) : ( +
+ + + + + + + + + + + + + + {data.tasks.map((task) => ( + + + + + + + + + + ))} + +
{t("timestamp")}{t("a2aTaskId")}{t("a2aSkill")}{t("a2aState")}{t("duration")}{t("a2aEvents")}{t("a2aArtifacts")}
+ {new Date(task.createdAt).toLocaleString()} + + {task.id.slice(0, 8)}… + {task.skill} + + {task.state} + + {taskDuration(task)}{task.events.length}{task.artifacts.length}
+
+ )} +
+ +
+ + +
+
+ ); +} diff --git a/src/app/(dashboard)/dashboard/audit/McpAuditTab.tsx b/src/app/(dashboard)/dashboard/audit/McpAuditTab.tsx index 65a9aacd3c..3dda466001 100644 --- a/src/app/(dashboard)/dashboard/audit/McpAuditTab.tsx +++ b/src/app/(dashboard)/dashboard/audit/McpAuditTab.tsx @@ -23,6 +23,13 @@ type McpAuditResponse = { offset: number; }; +type McpAuditStats = { + totalCalls: number; + successRate: number; + avgDurationMs: number; + topTools: Array<{ tool: string; count: number }>; +}; + const MCP_PAGE_SIZE = 25; export default function McpAuditTab() { @@ -33,11 +40,25 @@ export default function McpAuditTab() { limit: MCP_PAGE_SIZE, offset: 0, }); + const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); const [toolFilter, setToolFilter] = useState(""); const [successFilter, setSuccessFilter] = useState<"all" | "true" | "false">("all"); const [offset, setOffset] = useState(0); + const fetchStats = useCallback(async () => { + try { + const res = await fetch("/api/mcp/audit/stats"); + if (res.ok) setStats((await res.json()) as McpAuditStats); + } catch { + // non-fatal + } + }, []); + + useEffect(() => { + void fetchStats(); + }, [fetchStats]); + const fetchAudit = useCallback(async () => { setLoading(true); try { @@ -80,7 +101,10 @@ export default function McpAuditTab() {