From fc731a7f60e1f65d1d0ec6f26bdcd684bca53045 Mon Sep 17 00:00:00 2001 From: Sergio Arroutbi Date: Wed, 13 May 2026 18:57:35 +0200 Subject: [PATCH] Add registrar metrics to Performance page (FR-095) Extend PerformanceSummary with registrar_reachable, registrar_latency_ms, and registered_agent_count. Add a Registrar Metrics section with two KPI cards (Registrar Status with latency subtitle, Registered Agents count). No circuit breaker card for the registrar per NFR-017. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Sergio Arroutbi --- src/api/__tests__/performance.test.ts | 5 ++ src/api/performance.ts | 4 ++ src/pages/Performance/Performance.tsx | 43 ++++++++++++++ .../__tests__/Performance.test.tsx | 56 ++++++++++++++++++- src/types/index.ts | 3 + 5 files changed, 110 insertions(+), 1 deletion(-) diff --git a/src/api/__tests__/performance.test.ts b/src/api/__tests__/performance.test.ts index 76971c2..e973fd4 100644 --- a/src/api/__tests__/performance.test.ts +++ b/src/api/__tests__/performance.test.ts @@ -18,6 +18,11 @@ describe('performanceApi', () => { expect(mockGet).toHaveBeenCalledWith('/performance/summary'); }); + it('registrar calls GET /performance/registrar', async () => { + await performanceApi.registrar(); + expect(mockGet).toHaveBeenCalledWith('/performance/registrar'); + }); + it('database calls GET /system/database', async () => { await performanceApi.database(); expect(mockGet).toHaveBeenCalledWith('/system/database'); diff --git a/src/api/performance.ts b/src/api/performance.ts index e05feab..a1466ff 100644 --- a/src/api/performance.ts +++ b/src/api/performance.ts @@ -6,6 +6,10 @@ export const performanceApi = { return apiClient.get('/performance/summary'); }, + registrar() { + return apiClient.get('/performance/registrar'); + }, + database() { return apiClient.get('/system/database'); }, diff --git a/src/pages/Performance/Performance.tsx b/src/pages/Performance/Performance.tsx index 84171b4..02ef2b7 100644 --- a/src/pages/Performance/Performance.tsx +++ b/src/pages/Performance/Performance.tsx @@ -1,6 +1,9 @@ +import { useMemo } from 'react'; import { useQuery } from '@tanstack/react-query'; import { KpiCard } from '@/components/common/KpiCard'; import { performanceApi } from '@/api/performance'; +import { agentsApi } from '@/api/agents'; +import type { IntegrationService } from '@/types'; const CIRCUIT_BREAKER_LABELS: Record = { closed: 'Closed', @@ -29,6 +32,31 @@ export function Performance() { select: (res) => res.data, }); + const { data: services } = useQuery({ + queryKey: ['integrations', 'status'], + queryFn: () => performanceApi.integrations(), + select: (res) => res.data, + }); + + const registrar = useMemo(() => { + const list = (Array.isArray(services) ? services : []) as IntegrationService[]; + return list.find((s) => s.name.toLowerCase().includes('registrar')) ?? null; + }, [services]); + + const { data: agentData } = useQuery({ + queryKey: ['agents', 'count'], + queryFn: () => agentsApi.list({ per_page: 1 }), + select: (res) => res.data, + }); + + const registrarUp = registrar + ? registrar.status.toLowerCase() === 'up' + : perf?.registrar_reachable ?? null; + + const registrarLatency = registrar?.latency_ms ?? perf?.registrar_latency_ms ?? null; + + const registeredAgentCount = perf?.registered_agent_count ?? agentData?.total_items ?? null; + return (
@@ -60,6 +88,21 @@ export function Performance() { />
+

Registrar Metrics

+
+ + +
+

Verifier Cluster Metrics

diff --git a/src/pages/Performance/__tests__/Performance.test.tsx b/src/pages/Performance/__tests__/Performance.test.tsx index 2ada288..ffe19ab 100644 --- a/src/pages/Performance/__tests__/Performance.test.tsx +++ b/src/pages/Performance/__tests__/Performance.test.tsx @@ -15,8 +15,23 @@ vi.mock('@/api/performance', () => ({ estimated_attestation_rate: 120, capacity_utilization_pct: 55.3, database_status: 'ok', + registered_agent_count: 12, }, }), + integrations: vi.fn().mockResolvedValue({ + data: [ + { name: 'keylime-registrar', endpoint: 'http://localhost:3001', status: 'UP', latency_ms: 18 }, + { name: 'keylime-verifier', endpoint: 'http://localhost:3000', status: 'UP', latency_ms: 42 }, + ], + }), + }, +})); + +vi.mock('@/api/agents', () => ({ + agentsApi: { + list: vi.fn().mockResolvedValue({ + data: { items: [], page: 1, page_size: 1, total_items: 12, total_pages: 12 }, + }), }, })); @@ -40,7 +55,8 @@ describe('Performance', () => { it('renders KPI cards with performance data', async () => { renderWithProviders(); - expect(await screen.findByText('Reachable')).toBeInTheDocument(); + const reachables = await screen.findAllByText('Reachable'); + expect(reachables).toHaveLength(2); expect(await screen.findByText('42 ms')).toBeInTheDocument(); expect(await screen.findByText('Closed')).toBeInTheDocument(); expect(await screen.findByText('120/s')).toBeInTheDocument(); @@ -54,6 +70,13 @@ describe('Performance', () => { expect(screen.getByText('Circuit Breaker Status')).toBeInTheDocument(); }); + it('renders registrar KPI cards from integrations data', async () => { + renderWithProviders(); + expect(await screen.findByText('Registrar Metrics')).toBeInTheDocument(); + expect(await screen.findByText('18 ms')).toBeInTheDocument(); + expect(await screen.findByText('12')).toBeInTheDocument(); + }); + it('shows danger variant for unreachable verifier', async () => { const { performanceApi } = await import('@/api/performance'); vi.mocked(performanceApi.summary).mockResolvedValueOnce({ @@ -67,11 +90,39 @@ describe('Performance', () => { database_status: 'ok', }, } as never); + vi.mocked(performanceApi.integrations).mockResolvedValueOnce({ + data: [ + { name: 'keylime-registrar', endpoint: 'http://localhost:3001', status: 'UP', latency_ms: 10 }, + ], + } as never); renderWithProviders(); expect(await screen.findByText('Unreachable')).toBeInTheDocument(); expect(await screen.findByText('Open')).toBeInTheDocument(); }); + it('shows danger variant for unreachable registrar', async () => { + const { performanceApi } = await import('@/api/performance'); + vi.mocked(performanceApi.summary).mockResolvedValueOnce({ + data: { + verifier_reachable: true, + verifier_latency_ms: 20, + circuit_breaker_state: 'closed', + agent_count: 5, + estimated_attestation_rate: 100, + capacity_utilization_pct: 40.0, + database_status: 'ok', + }, + } as never); + vi.mocked(performanceApi.integrations).mockResolvedValueOnce({ + data: [ + { name: 'keylime-registrar', endpoint: 'http://localhost:3001', status: 'DOWN', latency_ms: 0 }, + ], + } as never); + renderWithProviders(); + const unreachables = await screen.findAllByText('Unreachable'); + expect(unreachables).toHaveLength(1); + }); + it('shows warning variant for high capacity', async () => { const { performanceApi } = await import('@/api/performance'); vi.mocked(performanceApi.summary).mockResolvedValueOnce({ @@ -92,7 +143,10 @@ describe('Performance', () => { it('renders dashes when no data', async () => { const { performanceApi } = await import('@/api/performance'); + const { agentsApi } = await import('@/api/agents'); vi.mocked(performanceApi.summary).mockResolvedValueOnce({ data: null } as never); + vi.mocked(performanceApi.integrations).mockResolvedValueOnce({ data: [] } as never); + vi.mocked(agentsApi.list).mockResolvedValueOnce({ data: null } as never); renderWithProviders(); const dashes = screen.getAllByText('--'); expect(dashes.length).toBeGreaterThanOrEqual(4); diff --git a/src/types/index.ts b/src/types/index.ts index 3f56c7c..aa6f038 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -41,6 +41,9 @@ export interface PerformanceSummary { estimated_attestation_rate: number | null; capacity_utilization_pct: number | null; database_status: string; + registrar_reachable?: boolean; + registrar_latency_ms?: number | null; + registered_agent_count?: number; } export interface TimeRange {