From beb43a8bea64494993394ccd2b413bfe56e14c97 Mon Sep 17 00:00:00 2001
From: diegosouzapw
Date: Sat, 16 May 2026 09:57:40 -0300
Subject: [PATCH] feat(dashboard): add A2A audit page, stats bar on MCP audit,
fix sidebar duplicates
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Add /dashboard/audit/a2a page with A2aAuditTab: lists tasks with skill/state
filters, colored state badges, duration, events and artifacts counts
- Add "A2A Audit" item to Audit sidebar group (Monitoring section)
- Remove duplicate "MCP Audit" from MCP Server sidebar group — it stays
only in the Audit group under Monitoring
- Improve McpAuditTab: fetch /api/mcp/audit/stats and show 4-card stat bar
(calls 24h, success rate, avg duration, top tool) above the filters
- Add audit-a2a to HIDEABLE_SIDEBAR_ITEM_IDS
- Add i18n keys: auditA2a in sidebar + header sections, a2a* in compliance namespace
---
.../dashboard/audit/A2aAuditTab.tsx | 217 ++++++++++++++++++
.../dashboard/audit/McpAuditTab.tsx | 74 +++++-
.../(dashboard)/dashboard/audit/a2a/page.tsx | 7 +
src/i18n/messages/en.json | 22 +-
src/shared/constants/sidebarVisibility.ts | 7 +-
5 files changed, 321 insertions(+), 6 deletions(-)
create mode 100644 src/app/(dashboard)/dashboard/audit/A2aAuditTab.tsx
create mode 100644 src/app/(dashboard)/dashboard/audit/a2a/page.tsx
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")}
+
+ ) : (
+
+
+
+
+ | {t("timestamp")} |
+ {t("a2aTaskId")} |
+ {t("a2aSkill")} |
+ {t("a2aState")} |
+ {t("duration")} |
+ {t("a2aEvents")} |
+ {t("a2aArtifacts")} |
+
+
+
+ {data.tasks.map((task) => (
+
+ |
+ {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() {