mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-09 07:08:59 +00:00
feat(Pre-2.1.0-E): GUI completeness — 5 new pages, clickable nav, verification badges
Wire all remaining backend features to the frontend GUI: New pages: - DigestPage: preview digest HTML via iframe + send with confirmation - ObservabilityPage: health status, metrics gauges, Prometheus config + live output - JobDetailPage: full job details, verification section, timeline, audit events - IssuerDetailPage: redacted config, test connection, issued certificates list - TargetDetailPage: config, agent link, deployment history with verification Existing page updates: - JobsPage: clickable job IDs, verification column with VerificationBadge - IssuersPage: clickable issuer names linking to detail page - TargetsPage: clickable target names linking to detail page - Sidebar: Digest and Observability nav items - 5 new routes in main.tsx API client: getJob, getIssuer, getTarget, getJobVerification, getPrometheusMetrics Tests: 7 new Vitest tests (203 total), testing-guide Part 37 (17 manual tests) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -78,6 +78,11 @@ import {
|
||||
triggerNetworkScan,
|
||||
previewDigest,
|
||||
sendDigest,
|
||||
getJob,
|
||||
getJobVerification,
|
||||
getIssuer,
|
||||
getTarget,
|
||||
getPrometheusMetrics,
|
||||
} from './client';
|
||||
|
||||
// Mock global fetch
|
||||
@@ -1006,4 +1011,99 @@ describe('API Client', () => {
|
||||
expect(result.message).toBe('digest sent');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Job Detail ────────────────────────────
|
||||
|
||||
describe('Job Detail', () => {
|
||||
it('getJob fetches single job by ID', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'job-1', type: 'Deployment', status: 'Completed' }));
|
||||
const result = await getJob('job-1');
|
||||
expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/jobs/job-1');
|
||||
expect(result.id).toBe('job-1');
|
||||
expect(result.type).toBe('Deployment');
|
||||
});
|
||||
|
||||
it('getJobVerification fetches verification result', async () => {
|
||||
const verificationData = {
|
||||
job_id: 'job-1',
|
||||
target_id: 't-nginx1',
|
||||
verified: true,
|
||||
actual_fingerprint: 'abc123',
|
||||
expected_fingerprint: 'abc123',
|
||||
verified_at: '2026-03-28T12:00:00Z',
|
||||
};
|
||||
mockFetch.mockReturnValueOnce(mockJsonResponse(verificationData));
|
||||
const result = await getJobVerification('job-1');
|
||||
expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/jobs/job-1/verification');
|
||||
expect(result.verified).toBe(true);
|
||||
expect(result.actual_fingerprint).toBe('abc123');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Issuer Detail ─────────────────────────
|
||||
|
||||
describe('Issuer Detail', () => {
|
||||
it('getIssuer fetches single issuer by ID', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 'iss-local', name: 'Local CA', type: 'local_ca', status: 'active' }));
|
||||
const result = await getIssuer('iss-local');
|
||||
expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/issuers/iss-local');
|
||||
expect(result.name).toBe('Local CA');
|
||||
expect(result.type).toBe('local_ca');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Target Detail ─────────────────────────
|
||||
|
||||
describe('Target Detail', () => {
|
||||
it('getTarget fetches single target by ID', async () => {
|
||||
mockFetch.mockReturnValueOnce(mockJsonResponse({ id: 't-nginx1', name: 'Web Server', type: 'nginx', hostname: 'web1.example.com' }));
|
||||
const result = await getTarget('t-nginx1');
|
||||
expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/targets/t-nginx1');
|
||||
expect(result.name).toBe('Web Server');
|
||||
expect(result.type).toBe('nginx');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Prometheus Metrics ────────────────────
|
||||
|
||||
describe('Prometheus Metrics', () => {
|
||||
it('getPrometheusMetrics fetches text format', async () => {
|
||||
const metricsText = '# HELP certctl_certificate_total Total certificates\ncertctl_certificate_total 10';
|
||||
mockFetch.mockReturnValueOnce(
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: () => Promise.resolve(metricsText),
|
||||
} as Response)
|
||||
);
|
||||
const result = await getPrometheusMetrics();
|
||||
expect(mockFetch.mock.calls[0][0]).toBe('/api/v1/metrics/prometheus');
|
||||
expect(result).toContain('certctl_certificate_total');
|
||||
});
|
||||
|
||||
it('getPrometheusMetrics throws on error', async () => {
|
||||
mockFetch.mockReturnValueOnce(
|
||||
Promise.resolve({
|
||||
ok: false,
|
||||
status: 500,
|
||||
text: () => Promise.resolve('error'),
|
||||
} as Response)
|
||||
);
|
||||
await expect(getPrometheusMetrics()).rejects.toThrow('Prometheus metrics failed: 500');
|
||||
});
|
||||
|
||||
it('getPrometheusMetrics includes auth header', async () => {
|
||||
setApiKey('prom-key');
|
||||
mockFetch.mockReturnValueOnce(
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: () => Promise.resolve('metrics'),
|
||||
} as Response)
|
||||
);
|
||||
await getPrometheusMetrics();
|
||||
const [, init] = mockFetch.mock.calls[0];
|
||||
expect(init.headers['Authorization']).toBe('Bearer prom-key');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -365,5 +365,32 @@ export const previewDigest = () => {
|
||||
export const sendDigest = () =>
|
||||
fetchJSON<{ message: string }>(`${BASE}/digest/send`, { method: 'POST' });
|
||||
|
||||
// Jobs (single)
|
||||
export const getJob = (id: string) =>
|
||||
fetchJSON<Job>(`${BASE}/jobs/${id}`);
|
||||
|
||||
// Job Verification
|
||||
export const getJobVerification = (id: string) =>
|
||||
fetchJSON<{ job_id: string; target_id: string; verified: boolean; actual_fingerprint: string; expected_fingerprint: string; verified_at: string; error?: string }>(`${BASE}/jobs/${id}/verification`);
|
||||
|
||||
// Issuers (single)
|
||||
export const getIssuer = (id: string) =>
|
||||
fetchJSON<Issuer>(`${BASE}/issuers/${id}`);
|
||||
|
||||
// Targets (single)
|
||||
export const getTarget = (id: string) =>
|
||||
fetchJSON<Target>(`${BASE}/targets/${id}`);
|
||||
|
||||
// Prometheus metrics (text format)
|
||||
export const getPrometheusMetrics = () => {
|
||||
const headers: Record<string, string> = {};
|
||||
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
|
||||
return fetch(`${BASE}/metrics/prometheus`, { headers })
|
||||
.then(r => {
|
||||
if (!r.ok) throw new Error(`Prometheus metrics failed: ${r.status}`);
|
||||
return r.text();
|
||||
});
|
||||
};
|
||||
|
||||
// Health
|
||||
export const getHealth = () => fetchJSON<{ status: string }>('/health');
|
||||
|
||||
Reference in New Issue
Block a user