From 29c135dfe2d1564ad764d40f39811560a05cf348 Mon Sep 17 00:00:00 2001 From: galuis116 Date: Thu, 16 Jul 2026 09:11:38 -0400 Subject: [PATCH] feat(ui): maintainer dashboard panel for per-tool MCP usage counts New McpToolUsageCard, wired into MaintainerPanel's qualityDashboard, showing per-tool call counts, success/failure rates, and a local-vs-remote split over the dashboard's window. The backend aggregation (from the PostHog telemetry wrappers #6235/ #6236/#6358 already write to) is tracked separately -- matching AcceptanceRateCard's own established precedent, this card assumes qualityDashboard.mcpToolUsage may be absent from the payload today and degrades to a "not yet available" empty state until that aggregation lands, rather than assuming a value or blocking on it shipping first. Uses AnalyticsCardShell for chrome/state handling and TableScroll's accessible-table pattern (caption, scope=col headers, focusable region) for the per-tool breakdown, matching this codebase's existing dashboard conventions. Closes #6241 --- .../site/app-panels/maintainer-panel.test.tsx | 87 ++++++++++++++ .../site/app-panels/maintainer-panel.tsx | 7 ++ .../app-panels/mcp-tool-usage-card.test.tsx | 107 ++++++++++++++++++ .../site/app-panels/mcp-tool-usage-card.tsx | 104 +++++++++++++++++ 4 files changed, 305 insertions(+) create mode 100644 apps/loopover-ui/src/components/site/app-panels/mcp-tool-usage-card.test.tsx create mode 100644 apps/loopover-ui/src/components/site/app-panels/mcp-tool-usage-card.tsx diff --git a/apps/loopover-ui/src/components/site/app-panels/maintainer-panel.test.tsx b/apps/loopover-ui/src/components/site/app-panels/maintainer-panel.test.tsx index cdeccad914..6d751d6e0f 100644 --- a/apps/loopover-ui/src/components/site/app-panels/maintainer-panel.test.tsx +++ b/apps/loopover-ui/src/components/site/app-panels/maintainer-panel.test.tsx @@ -113,3 +113,90 @@ describe("MaintainerPanel install health — Orb broker mode (#selfhost-runtime- expect(screen.queryAllByText(/n\/a \(broker\)/)).toHaveLength(2); // scoped to the brokered install only }); }); + +describe("MaintainerPanel MCP tool usage panel (#6241)", () => { + // MaintainerPanel's own isEmpty check requires a non-empty health OR reviewability array to reach + // the real dashboard content branch at all -- an all-empty payload short-circuits to its own + // top-level EmptyState before any qualityDashboard card (including this one) ever renders. + const nonEmptyHealth = [ + { + installationId: 1, + accountLogin: "an-owner", + installedReposCount: 1, + status: "healthy" as const, + missingPermissions: [], + missingEvents: [], + checkedAt: "2026-07-03T00:00:00.000Z", + authMode: "local" as const, + }, + ]; + + it("shows the not-yet-available empty state when mcpToolUsage is absent from the payload", () => { + useSession.mockReturnValue({ + session: { login: "maint", roles: ["maintainer"] }, + hydrated: true, + }); + useApiResource.mockReturnValue({ + status: "ready", + data: { + metrics: [], + health: nonEmptyHealth, + reviewability: [], + settingsPreview: { removed: [], added: [] }, + qualityDashboard: { topContributors: [], gateOutcomeBreakdown: emptyGateOutcomeBreakdown }, + }, + reload: () => {}, + error: null, + }); + + render(); + + // Scoped to this card's own copy, not the generic "Not yet available" title QueueHealthCard's + // own (also-absent) empty state shares. + expect( + screen.getByText( + "Per-tool MCP usage appears here once tool-call telemetry is aggregated into the dashboard payload.", + ), + ).toBeTruthy(); + }); + + it("renders real per-tool rows once mcpToolUsage is present in the payload", () => { + useSession.mockReturnValue({ + session: { login: "maint", roles: ["maintainer"] }, + hydrated: true, + }); + useApiResource.mockReturnValue({ + status: "ready", + data: { + metrics: [], + health: nonEmptyHealth, + reviewability: [], + settingsPreview: { removed: [], added: [] }, + qualityDashboard: { + topContributors: [], + gateOutcomeBreakdown: emptyGateOutcomeBreakdown, + mcpToolUsage: { + windowDays: 14, + tools: [ + { + tool: "loopover_check_slop_risk", + callCount: 5, + successCount: 5, + failureCount: 0, + localCallCount: 5, + remoteCallCount: 0, + }, + ], + }, + }, + }, + reload: () => {}, + error: null, + }); + + render(); + + expect(screen.getByText("loopover_check_slop_risk")).toBeTruthy(); + expect(screen.getByText("14d window")).toBeTruthy(); + }); +}); diff --git a/apps/loopover-ui/src/components/site/app-panels/maintainer-panel.tsx b/apps/loopover-ui/src/components/site/app-panels/maintainer-panel.tsx index dcd0bc59be..2a2e54f1c9 100644 --- a/apps/loopover-ui/src/components/site/app-panels/maintainer-panel.tsx +++ b/apps/loopover-ui/src/components/site/app-panels/maintainer-panel.tsx @@ -24,6 +24,10 @@ import type { MaintainerTopContributor } from "@/components/site/app-panels/cont import { GateOutcomeCard } from "@/components/site/app-panels/gate-outcome-card"; import type { GateOutcomeCardData } from "@/components/site/app-panels/gate-outcome-card-model"; import { GateRampControl } from "@/components/site/app-panels/gate-ramp-control"; +import { + McpToolUsageCard, + type McpToolUsageSummary, +} from "@/components/site/app-panels/mcp-tool-usage-card"; import { QueueHealthCard, type MaintainerQueueHealth, @@ -96,6 +100,7 @@ type MaintainerDashboard = { qualityDashboard: { topContributors: MaintainerTopContributor[]; gateOutcomeBreakdown: GateOutcomeCardData; + mcpToolUsage?: McpToolUsageSummary; queueHealth?: MaintainerQueueHealth; slopDuplicateTrend?: MaintainerSlopDuplicateTrend; }; @@ -436,6 +441,8 @@ function MaintainerDashboardView({ + + {data.qualityDashboard.slopDuplicateTrend ? ( diff --git a/apps/loopover-ui/src/components/site/app-panels/mcp-tool-usage-card.test.tsx b/apps/loopover-ui/src/components/site/app-panels/mcp-tool-usage-card.test.tsx new file mode 100644 index 0000000000..2b4172f7c8 --- /dev/null +++ b/apps/loopover-ui/src/components/site/app-panels/mcp-tool-usage-card.test.tsx @@ -0,0 +1,107 @@ +import { render, screen, within } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import { + McpToolUsageCard, + type McpToolUsageSummary, +} from "@/components/site/app-panels/mcp-tool-usage-card"; + +const FORBIDDEN_PUBLIC_TERMS = + /wallet|hotkey|coldkey|mnemonic|reward|payout|farming|raw trust|trust score|scoreability|credibility|private ranking/i; + +function usage(overrides: Partial = {}): McpToolUsageSummary { + return { + windowDays: 30, + tools: [ + { + tool: "loopover_check_slop_risk", + callCount: 40, + successCount: 38, + failureCount: 2, + localCallCount: 30, + remoteCallCount: 10, + }, + { + tool: "loopover_predict_gate", + callCount: 10, + successCount: 10, + failureCount: 0, + localCallCount: 0, + remoteCallCount: 10, + }, + ], + ...overrides, + }; +} + +describe("McpToolUsageCard (#6241)", () => { + it("shows the 'not yet available' empty state when usage is undefined", () => { + render(); + expect(screen.getByText("Not yet available")).toBeTruthy(); + expect( + screen.getByText( + "Per-tool MCP usage appears here once tool-call telemetry is aggregated into the dashboard payload.", + ), + ).toBeTruthy(); + }); + + it("shows a distinct 'no calls yet' empty state when the payload exists but has zero tools", () => { + render(); + expect(screen.getByText("No MCP tool calls yet")).toBeTruthy(); + expect( + screen.getByText( + "No loopover_* tool calls were recorded across local or remote servers in this window.", + ), + ).toBeTruthy(); + }); + + it("renders one row per tool, sorted by call count descending, with success rate and local/remote split", () => { + render(); + expect(screen.getByText("30d window")).toBeTruthy(); + + const table = screen.getByRole("table", { + name: "Per-tool MCP call counts, success rate, and local vs. remote call split.", + }); + const rows = within(table).getAllByRole("row").slice(1); // drop the header row + expect(rows).toHaveLength(2); + // Sorted descending by callCount: loopover_check_slop_risk (40) before loopover_predict_gate (10). + expect(within(rows[0]!).getByText("loopover_check_slop_risk")).toBeTruthy(); + expect(within(rows[0]!).getByText("95%")).toBeTruthy(); // 38/40 + expect(within(rows[1]!).getByText("loopover_predict_gate")).toBeTruthy(); + expect(within(rows[1]!).getByText("100%")).toBeTruthy(); // 10/10 + }); + + it("shows a dash success rate for a tool with zero calls (never divides by zero)", () => { + render( + , + ); + expect(screen.getByText("—")).toBeTruthy(); + }); + + it("wraps the table in a keyboard-focusable, labelled scroll region (#794 a11y pattern)", () => { + render(); + const region = screen.getByRole("region", { name: "MCP tool usage by tool" }); + expect(region.tabIndex).toBe(0); + const table = within(region).getByRole("table"); + expect(within(table).getByRole("columnheader", { name: "Tool" })).toBeTruthy(); + expect(within(table).getByRole("columnheader", { name: "Success rate" })).toBeTruthy(); + }); + + it("never surfaces forbidden reward/wallet/score terms", () => { + const { container } = render(); + expect(container.textContent ?? "").not.toMatch(FORBIDDEN_PUBLIC_TERMS); + }); +}); diff --git a/apps/loopover-ui/src/components/site/app-panels/mcp-tool-usage-card.tsx b/apps/loopover-ui/src/components/site/app-panels/mcp-tool-usage-card.tsx new file mode 100644 index 0000000000..6aa42635a0 --- /dev/null +++ b/apps/loopover-ui/src/components/site/app-panels/mcp-tool-usage-card.tsx @@ -0,0 +1,104 @@ +import { AnalyticsCardShell } from "@/components/site/app-panels/analytics-card-shell"; +import { StatusPill } from "@/components/site/control-primitives"; +import { TableScroll } from "@/components/site/data-table"; + +/** One MCP tool's aggregate call counts over the dashboard's window. All counts are aggregate-only — + * no call arguments, repo names, or other per-call detail (matches the PostHog telemetry wrappers' + * own privacy boundary in `src/mcp/telemetry.ts` / `packages/loopover-mcp/lib/telemetry.js`). */ +export type McpToolUsageEntry = { + tool: string; + callCount: number; + successCount: number; + failureCount: number; + localCallCount: number; + remoteCallCount: number; +}; + +export type McpToolUsageSummary = { + windowDays: number; + tools: McpToolUsageEntry[]; +}; + +function successRate(entry: McpToolUsageEntry): number | null { + return entry.callCount > 0 ? entry.successCount / entry.callCount : null; +} + +function formatRate(rate: number | null): string { + return rate === null ? "—" : `${Math.round(rate * 100)}%`; +} + +/** Maintainer dashboard panel (#6241, part of #6228): per-tool MCP call counts, success/failure rates, and a + * local-vs-remote split, over the dashboard's selectable window. Backend aggregation (from the PostHog + * telemetry wrappers #6235/#6236/#6358 already write to) is tracked separately, so — matching + * AcceptanceRateCard's own precedent — this card assumes the field may be absent from the dashboard payload + * today and degrades to a "not yet available" empty state until it lands, rather than assuming a value. */ +export function McpToolUsageCard({ usage }: { usage?: McpToolUsageSummary }) { + if (!usage || usage.tools.length === 0) { + return ( + + ); + } + + const sorted = [...usage.tools].sort((a, b) => b.callCount - a.callCount); + + return ( + +
+ {usage.windowDays}d window +
+ + + + + + + + + + + + + + {sorted.map((entry) => ( + + + + + + + + ))} + +
+ Per-tool MCP call counts, success rate, and local vs. remote call split. +
+ Tool + + Calls + + Success rate + + Local + + Remote +
{entry.tool}{entry.callCount} + {formatRate(successRate(entry))} + {entry.localCallCount}{entry.remoteCallCount}
+
+
+ ); +}