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:
shankar0123
2026-03-22 19:46:13 -04:00
parent 2f65dd1a61
commit ee75f149ae
21 changed files with 2125 additions and 28 deletions
+20 -1
View File
@@ -1,4 +1,4 @@
import type { Certificate, CertificateVersion, Agent, Job, Notification, AuditEvent, PolicyRule, PolicyViolation, Issuer, Target, CertificateProfile, Owner, Team, AgentGroup, PaginatedResponse } from './types';
import type { Certificate, CertificateVersion, Agent, Job, Notification, AuditEvent, PolicyRule, PolicyViolation, Issuer, Target, CertificateProfile, Owner, Team, AgentGroup, PaginatedResponse, DashboardSummary, CertificateStatusCount, ExpirationBucket, JobTrendDataPoint, IssuanceRateDataPoint, MetricsResponse } from './types';
const BASE = '/api/v1';
@@ -257,5 +257,24 @@ export const approveRenewal = (jobId: string) =>
export const rejectRenewal = (jobId: string, reason: string) =>
fetchJSON<{ message: string }>(`${BASE}/jobs/${jobId}/reject`, { method: 'POST', body: JSON.stringify({ reason }) });
// Stats
export const getDashboardSummary = () =>
fetchJSON<DashboardSummary>(`${BASE}/stats/summary`);
export const getCertificatesByStatus = () =>
fetchJSON<CertificateStatusCount[]>(`${BASE}/stats/certificates-by-status`);
export const getExpirationTimeline = (days = 30) =>
fetchJSON<ExpirationBucket[]>(`${BASE}/stats/expiration-timeline?days=${days}`);
export const getJobTrends = (days = 30) =>
fetchJSON<JobTrendDataPoint[]>(`${BASE}/stats/job-trends?days=${days}`);
export const getIssuanceRate = (days = 30) =>
fetchJSON<IssuanceRateDataPoint[]>(`${BASE}/stats/issuance-rate?days=${days}`);
export const getMetrics = () =>
fetchJSON<MetricsResponse>(`${BASE}/metrics`);
// Health
export const getHealth = () => fetchJSON<{ status: string }>('/health');