Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/api/__tests__/performance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import { performanceApi } from '../performance';
beforeEach(() => vi.clearAllMocks());

describe('performanceApi', () => {
it('system calls GET /system/performance', async () => {
await performanceApi.system();
expect(mockGet).toHaveBeenCalledWith('/system/performance');
it('summary calls GET /performance/summary', async () => {
await performanceApi.summary();
expect(mockGet).toHaveBeenCalledWith('/performance/summary');
});

it('database calls GET /system/database', async () => {
Expand Down
6 changes: 3 additions & 3 deletions src/api/performance.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import apiClient from './client';
import type { SystemPerformance, IntegrationService } from '@/types';
import type { PerformanceSummary, IntegrationService } from '@/types';

export const performanceApi = {
system() {
return apiClient.get<SystemPerformance>('/system/performance');
summary() {
return apiClient.get<PerformanceSummary>('/performance/summary');
},

database() {
Expand Down
47 changes: 34 additions & 13 deletions src/pages/Performance/Performance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,30 @@ import { useQuery } from '@tanstack/react-query';
import { KpiCard } from '@/components/common/KpiCard';
import { performanceApi } from '@/api/performance';

const CIRCUIT_BREAKER_LABELS: Record<string, string> = {
closed: 'Closed',
open: 'Open',
half_open: 'Half-Open',
};

type CbVariant = 'success' | 'danger' | 'warning';

const CIRCUIT_BREAKER_VARIANTS: Record<string, CbVariant> = {
closed: 'success',
open: 'danger',
half_open: 'warning',
};

function capacityVariant(pct: number): 'default' | 'warning' | 'danger' {
if (pct > 90) return 'danger';
if (pct > 70) return 'warning';
return 'default';
}

export function Performance() {
const { data: perf } = useQuery({
queryKey: ['system', 'performance'],
queryFn: () => performanceApi.system(),
queryKey: ['performance', 'summary'],
queryFn: () => performanceApi.summary(),
select: (res) => res.data,
});

Expand All @@ -18,24 +38,25 @@ export function Performance() {

<div className="kpi-grid">
<KpiCard
title="CPU Usage"
value={perf ? `${perf.cpu_percent.toFixed(1)}%` : '--'}
variant={perf && perf.cpu_percent > 80 ? 'danger' : 'default'}
title="Verifier Status"
value={perf ? (perf.verifier_reachable ? 'Reachable' : 'Unreachable') : '--'}
subtitle={perf?.verifier_latency_ms != null ? `${perf.verifier_latency_ms} ms` : undefined}
variant={perf ? (perf.verifier_reachable ? 'success' : 'danger') : 'default'}
/>
<KpiCard
title="Memory Usage"
value={perf ? `${perf.memory_percent.toFixed(1)}%` : '--'}
variant={perf && perf.memory_percent > 80 ? 'danger' : 'default'}
title="Circuit Breaker"
value={perf ? (CIRCUIT_BREAKER_LABELS[perf.circuit_breaker_state] ?? perf.circuit_breaker_state) : '--'}
variant={perf ? (CIRCUIT_BREAKER_VARIANTS[perf.circuit_breaker_state] ?? 'default') : 'default'}
/>
<KpiCard
title="Attestations/sec"
value={perf?.attestations_per_sec ?? '--'}
title="Attestation Rate"
value={perf?.estimated_attestation_rate != null ? `${perf.estimated_attestation_rate}/s` : '--'}
variant="success"
/>
<KpiCard
title="Queue Depth"
value={perf?.queue_depth ?? '--'}
variant={perf && perf.queue_depth > 100 ? 'warning' : 'default'}
title="Capacity"
value={perf?.capacity_utilization_pct != null ? `${perf.capacity_utilization_pct.toFixed(1)}%` : '--'}
variant={perf?.capacity_utilization_pct != null ? capacityVariant(perf.capacity_utilization_pct) : 'default'}
/>
</div>

Expand Down
60 changes: 36 additions & 24 deletions src/pages/Performance/__tests__/Performance.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ import { Performance } from '../Performance';

vi.mock('@/api/performance', () => ({
performanceApi: {
system: vi.fn().mockResolvedValue({
summary: vi.fn().mockResolvedValue({
data: {
cpu_percent: 45.2,
memory_percent: 62.8,
attestations_per_sec: 120,
queue_depth: 15,
verifier_reachable: true,
verifier_latency_ms: 42,
circuit_breaker_state: 'closed',
agent_count: 8,
estimated_attestation_rate: 120,
capacity_utilization_pct: 55.3,
database_status: 'ok',
},
}),
},
Expand All @@ -37,10 +40,11 @@ describe('Performance', () => {

it('renders KPI cards with performance data', async () => {
renderWithProviders(<Performance />);
expect(await screen.findByText('45.2%')).toBeInTheDocument();
expect(await screen.findByText('62.8%')).toBeInTheDocument();
expect(await screen.findByText('120')).toBeInTheDocument();
expect(await screen.findByText('15')).toBeInTheDocument();
expect(await screen.findByText('Reachable')).toBeInTheDocument();
expect(await screen.findByText('42 ms')).toBeInTheDocument();
expect(await screen.findByText('Closed')).toBeInTheDocument();
expect(await screen.findByText('120/s')).toBeInTheDocument();
expect(await screen.findByText('55.3%')).toBeInTheDocument();
});

it('renders placeholder sections', () => {
Expand All @@ -50,37 +54,45 @@ describe('Performance', () => {
expect(screen.getByText('Circuit Breaker Status')).toBeInTheDocument();
});

it('shows danger variant for high CPU usage', async () => {
it('shows danger variant for unreachable verifier', async () => {
const { performanceApi } = await import('@/api/performance');
vi.mocked(performanceApi.system).mockResolvedValueOnce({
vi.mocked(performanceApi.summary).mockResolvedValueOnce({
data: {
cpu_percent: 92.5,
memory_percent: 30.0,
attestations_per_sec: 50,
queue_depth: 5,
verifier_reachable: false,
verifier_latency_ms: null,
circuit_breaker_state: 'open',
agent_count: 0,
estimated_attestation_rate: null,
capacity_utilization_pct: null,
database_status: 'ok',
},
} as never);
renderWithProviders(<Performance />);
expect(await screen.findByText('92.5%')).toBeInTheDocument();
expect(await screen.findByText('Unreachable')).toBeInTheDocument();
expect(await screen.findByText('Open')).toBeInTheDocument();
});

it('shows warning variant for high queue depth', async () => {
it('shows warning variant for high capacity', async () => {
const { performanceApi } = await import('@/api/performance');
vi.mocked(performanceApi.system).mockResolvedValueOnce({
vi.mocked(performanceApi.summary).mockResolvedValueOnce({
data: {
cpu_percent: 10.0,
memory_percent: 20.0,
attestations_per_sec: 80,
queue_depth: 150,
verifier_reachable: true,
verifier_latency_ms: 15,
circuit_breaker_state: 'half_open',
agent_count: 50,
estimated_attestation_rate: 200,
capacity_utilization_pct: 78.5,
database_status: 'ok',
},
} as never);
renderWithProviders(<Performance />);
expect(await screen.findByText('150')).toBeInTheDocument();
expect(await screen.findByText('Half-Open')).toBeInTheDocument();
expect(await screen.findByText('78.5%')).toBeInTheDocument();
});

it('renders dashes when no data', async () => {
const { performanceApi } = await import('@/api/performance');
vi.mocked(performanceApi.system).mockResolvedValueOnce({ data: null } as never);
vi.mocked(performanceApi.summary).mockResolvedValueOnce({ data: null } as never);
renderWithProviders(<Performance />);
const dashes = screen.getAllByText('--');
expect(dashes.length).toBeGreaterThanOrEqual(4);
Expand Down
15 changes: 8 additions & 7 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,14 @@ export interface IntegrationService {
latency_ms?: number;
}

export interface SystemPerformance {
cpu_percent: number;
memory_percent: number;
open_fds: number;
thread_count: number;
attestations_per_sec: number;
queue_depth: number;
export interface PerformanceSummary {
verifier_reachable: boolean;
verifier_latency_ms: number | null;
circuit_breaker_state: 'closed' | 'open' | 'half_open';
agent_count: number;
estimated_attestation_rate: number | null;
capacity_utilization_pct: number | null;
database_status: string;
}

export interface TimeRange {
Expand Down