mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 18:51:32 +00:00
feat: M14 — Observability (dashboard charts, agent fleet, stats API, metrics, structured logging, rollback)
Backend: StatsService with 5 aggregation methods, JSON metrics endpoint, slog-based structured logging middleware. Stats API: dashboard summary, certificates-by-status, expiration timeline, job trends, issuance rate. 23 new backend tests. Frontend: Recharts-powered dashboard with 4 charts (status pie, expiration heatmap, job trends line, issuance bar), agent fleet overview page with OS/arch grouping and version breakdown, deployment rollback buttons on version history. 7 new frontend tests. 78 API endpoints, 744+ total tests (658 Go + 86 Vitest). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -55,6 +55,12 @@ import {
|
||||
deleteAgentGroup,
|
||||
getAgentGroupMembers,
|
||||
getHealth,
|
||||
getDashboardSummary,
|
||||
getCertificatesByStatus,
|
||||
getExpirationTimeline,
|
||||
getJobTrends,
|
||||
getIssuanceRate,
|
||||
getMetrics,
|
||||
} from './client';
|
||||
|
||||
// Mock global fetch
|
||||
@@ -617,6 +623,59 @@ describe('API Client', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Stats ─────────────────────────────────────────
|
||||
|
||||
describe('Stats', () => {
|
||||
it('getDashboardSummary calls /api/v1/stats/summary', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockJsonResponse({ total_certificates: 10 }));
|
||||
const result = await getDashboardSummary();
|
||||
expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/stats/summary');
|
||||
expect(result.total_certificates).toBe(10);
|
||||
});
|
||||
|
||||
it('getCertificatesByStatus calls /api/v1/stats/certificates-by-status', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockJsonResponse([{ status: 'Active', count: 5 }]));
|
||||
const result = await getCertificatesByStatus();
|
||||
expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/stats/certificates-by-status');
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('getExpirationTimeline calls with days parameter', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockJsonResponse([]));
|
||||
await getExpirationTimeline(60);
|
||||
expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/stats/expiration-timeline?days=60');
|
||||
});
|
||||
|
||||
it('getExpirationTimeline uses default 30 days', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockJsonResponse([]));
|
||||
await getExpirationTimeline();
|
||||
expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/stats/expiration-timeline?days=30');
|
||||
});
|
||||
|
||||
it('getJobTrends calls with days parameter', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockJsonResponse([]));
|
||||
await getJobTrends(14);
|
||||
expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/stats/job-trends?days=14');
|
||||
});
|
||||
|
||||
it('getIssuanceRate calls with days parameter', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockJsonResponse([]));
|
||||
await getIssuanceRate(7);
|
||||
expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/stats/issuance-rate?days=7');
|
||||
});
|
||||
|
||||
it('getMetrics calls /api/v1/metrics', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockJsonResponse({
|
||||
gauge: { certificate_total: 10 },
|
||||
counter: { job_completed_total: 5 },
|
||||
uptime: { uptime_seconds: 3600 },
|
||||
}));
|
||||
const result = await getMetrics();
|
||||
expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/metrics');
|
||||
expect(result.gauge.certificate_total).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Health ─────────────────────────────────────────
|
||||
|
||||
describe('Health', () => {
|
||||
|
||||
Reference in New Issue
Block a user