mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-09 13:08:53 +00:00
fix(web,ci): close TS↔Go type drift across 5 entities (D-2 master)
Closes five 2026-04-24 audit findings (all P2, all category cat-f /
diff-05x06-*) by reconciling the TypeScript interfaces in
web/src/api/types.ts with the on-wire JSON shape Go's
internal/domain/*.go structs actually emit. D-1 closed the same pattern
for one entity (Certificate / ManagedCertificate); D-2 covers the
remaining five.
Per-entity verdicts (audit's "stricter side is the contract"):
Agent — TRIM 5 phantoms (last_heartbeat, capabilities, tags,
created_at, updated_at). Go emits last_heartbeat_at only.
Target — ADD 2 (retired_at?, retired_reason?) — I-004 fields.
DiscCert — ADD pem_data? — real field, real Go emit, omitempty.
Issuer — TRIM phantom status. Go has Enabled bool only.
Notif — TRIM phantom subject. Go has Message string only.
Certificate — verify-only; D-1 closure confirmed clean at recon.
Consumer fixes (same commit as the trim):
- AgentDetailPage.tsx — remove dead Capabilities + Tags sections (always
rendered empty); replace agent.created_at/updated_at row with the
Go-emitted registered_at; widen heartbeatStatus() to accept undefined.
- AgentsPage.tsx — same heartbeatStatus widening.
- IssuersPage.tsx + IssuerDetailPage.tsx — issuerStatus() now derives
from `enabled` exclusively; the dead `issuer.status || 'Unknown'`
fallback is gone.
- NotificationsPage.tsx — drop dead `|| n.subject` fallback.
- NotificationsPage.test.tsx — drop dead `subject:` from mocks.
- api/utils.ts::timeAgo widened to accept string | undefined | null.
- api/types.test.ts — Agent (I-004) fixture trimmed of the 5 phantoms.
Tests (Vitest):
- 5 new describe blocks in web/src/api/types.test.ts:
- Agent interface (D-2 phantom-fields trim) — 2 it blocks
- Target interface (D-2 retirement fields) — 2 it blocks
- DiscoveredCertificate interface (D-2 pem_data ADD) — 2 it blocks
- Issuer interface (D-2 status phantom trim) — 1 it block
- Notification interface (D-2 subject phantom trim) — 1 it block
- Each block uses the literal-construction pattern from D-1; trimmed
fields are pinned via excess-property comments that compile-fail when
uncommented if a phantom is reintroduced.
CI regression guardrail:
- .github/workflows/ci.yml — existing D-1 step renamed to "Forbidden
StatusBadge dead-key + TS phantom-field regression guard (D-1 + D-2)".
Three new awk-windowed greps over Agent / Issuer / Notification
interfaces in types.ts. The Agent grep includes a `grep -v
'last_heartbeat_at'` filter to avoid false positives on the
legitimate Go-emitted heartbeat field.
Documentation:
- CHANGELOG.md — new D-2 section above B-1 under [unreleased] with full
Added/Removed/Audit findings closed/Known follow-ups breakdown.
- docs/architecture.md — Web Dashboard section gains a new "TS ↔ Go
type contract rule (D-1 + D-2 closure)" paragraph capturing the
stricter-side-wins rule and the CI guardrail it's anchored by.
- coverage-gap-audit-2026-04-24-v5/unified-audit.md — Live Tracker score
20/47 → 25/47 (P2: 6/27 → 11/27). Per-finding ✅ RESOLVED Status
blocks added to all 5 diff-05x06-* entries plus the verify-only
Certificate entry. Closed-bundle index gets D-2 row.
Verification (all gates green):
- cd web && tsc --noEmit → clean
- cd web && vitest run --reporter=dot → 9 files, 302 tests passing
(was 294 → +8 D-2 cases)
- cd web && vite build → clean
- go vet ./internal/... ./cmd/... → clean (no Go touched)
- golangci-lint v2.11.4 run ./... → 0 issues
- D-2 Agent guardrail dry-run → empty (good)
- D-2 Issuer guardrail dry-run → empty (good)
- D-2 Notification guardrail dry-run → empty (good)
- D-2 Target ADD-shape sanity → 2 retirement fields present
- D-2 DiscCert ADD-shape sanity → pem_data present
- D-1 Certificate guardrail still clean → empty (good)
- OpenAPI YAML parses → 89 paths
Audit findings closed:
- diff-05x06-7cdf4e78ae24 (P2, Agent TS↔Go drift)
- diff-05x06-2044a46f4dd0 (P2, Target TS↔DeploymentTarget Go drift)
- diff-05x06-85ab6b98a2f7 (P2, DiscoveredCertificate TS↔Go drift)
- diff-05x06-97fab8783a5c (P2, Issuer TS↔Go drift)
- diff-05x06-caba9eb3620e (P2, Notification TS↔NotificationEvent drift)
- diff-05x06-af18a8d7ef41 (P2) — verified clean since D-1; no edit
Deferred follow-ups:
- Issuer richer status view (enabled × test_status) — UX scope, not drift.
- Real Agent metadata (capabilities, tags) — backend feature, not drift.
- DiscoveredCertificate pem_data list-response perf — separate backend change.
This commit is contained in:
+261
-11
@@ -1,6 +1,14 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { POLICY_TYPES, POLICY_SEVERITIES } from './types';
|
||||
import type { Agent, Certificate, CertificateVersion } from './types';
|
||||
import type {
|
||||
Agent,
|
||||
Certificate,
|
||||
CertificateVersion,
|
||||
DiscoveredCertificate,
|
||||
Issuer,
|
||||
Notification,
|
||||
Target,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* Regression tests for the policy enum tuples.
|
||||
@@ -85,6 +93,9 @@ describe('Agent interface (I-004 retirement)', () => {
|
||||
// Construct an Agent with the retirement fields set. If Phase 2b names
|
||||
// them anything other than retired_at / retired_reason, this fails to
|
||||
// compile — which is exactly what the Red stage wants.
|
||||
// D-2 (master): the post-D-2 Agent shape no longer carries
|
||||
// last_heartbeat / capabilities / tags / created_at / updated_at —
|
||||
// those were TS phantoms the Go-side struct never emitted.
|
||||
const retired: Agent = {
|
||||
id: 'ag-1',
|
||||
name: 'decom-01',
|
||||
@@ -94,13 +105,8 @@ describe('Agent interface (I-004 retirement)', () => {
|
||||
architecture: 'amd64',
|
||||
status: 'Offline',
|
||||
version: '2.1.0',
|
||||
last_heartbeat: '2026-01-01T00:00:00Z',
|
||||
last_heartbeat_at: '2026-01-01T00:00:00Z',
|
||||
capabilities: [],
|
||||
tags: {},
|
||||
registered_at: '2024-01-01T00:00:00Z',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2026-01-01T00:00:00Z',
|
||||
retired_at: '2026-01-01T00:00:00Z',
|
||||
retired_reason: 'old hardware',
|
||||
};
|
||||
@@ -111,6 +117,7 @@ describe('Agent interface (I-004 retirement)', () => {
|
||||
it('accepts an Agent without retired_at / retired_reason (optional fields)', () => {
|
||||
// Active agents should not carry retirement metadata. If Phase 2b makes
|
||||
// the fields required, this block fails to compile.
|
||||
// D-2 (master): post-D-2 Agent shape (see sibling describe block).
|
||||
const active: Agent = {
|
||||
id: 'ag-2',
|
||||
name: 'web01',
|
||||
@@ -120,13 +127,8 @@ describe('Agent interface (I-004 retirement)', () => {
|
||||
architecture: 'amd64',
|
||||
status: 'Online',
|
||||
version: '2.1.0',
|
||||
last_heartbeat: '2026-04-18T12:00:00Z',
|
||||
last_heartbeat_at: '2026-04-18T12:00:00Z',
|
||||
capabilities: ['deploy', 'scan'],
|
||||
tags: {},
|
||||
registered_at: '2024-06-01T00:00:00Z',
|
||||
created_at: '2024-06-01T00:00:00Z',
|
||||
updated_at: '2026-04-18T12:00:00Z',
|
||||
};
|
||||
expect(active.retired_at).toBeUndefined();
|
||||
expect(active.retired_reason).toBeUndefined();
|
||||
@@ -220,3 +222,251 @@ describe('Certificate interface (D-5 phantom-fields trim)', () => {
|
||||
expect(v.key_size).toBe(256);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* D-2 (diff-05x06-7cdf4e78ae24, master): Agent TS phantom-fields trim.
|
||||
*
|
||||
* Pre-D-2 the `Agent` interface declared five fields that the Go-side
|
||||
* struct (`internal/domain/connector.go::Agent`) does NOT emit on the
|
||||
* wire: `last_heartbeat`, `capabilities`, `tags`, `created_at`,
|
||||
* `updated_at`. Two of them had real consumers (`AgentDetailPage.tsx`
|
||||
* read `agent.capabilities` and `agent.tags`) — both always rendered the
|
||||
* empty-state branch because the runtime values were always `undefined`.
|
||||
*
|
||||
* Post-D-2 a `agent.capabilities` access is a TS compile error, forcing
|
||||
* every consumer to acknowledge the field is not part of the Agent
|
||||
* contract. The Go-side struct emits exactly: id, name, hostname, status,
|
||||
* last_heartbeat_at (note the `_at` suffix — this is the real heartbeat
|
||||
* field and stays), registered_at, os, architecture, ip_address, version,
|
||||
* retired_at?, retired_reason?.
|
||||
*/
|
||||
describe('Agent interface (D-2 phantom-fields trim)', () => {
|
||||
it('does NOT declare last_heartbeat / capabilities / tags / created_at / updated_at', () => {
|
||||
// Construct an Agent with ONLY the post-D-2 field set. If a future
|
||||
// PR re-adds any of the five trimmed fields, the excess-property
|
||||
// comments below become live TS errors when uncommented (and the
|
||||
// CI guardrail in .github/workflows/ci.yml fires regardless).
|
||||
const a: Agent = {
|
||||
id: 'ag-test',
|
||||
name: 'web-01',
|
||||
hostname: 'web-01.prod',
|
||||
status: 'Online',
|
||||
last_heartbeat_at: '2026-04-25T12:00:00Z',
|
||||
registered_at: '2024-06-01T00:00:00Z',
|
||||
os: 'linux',
|
||||
architecture: 'amd64',
|
||||
ip_address: '10.0.0.1',
|
||||
version: '2.1.0',
|
||||
};
|
||||
expect(a.id).toBe('ag-test');
|
||||
expect(a.last_heartbeat_at).toBe('2026-04-25T12:00:00Z');
|
||||
|
||||
// Excess-property check (each MUST be a TS error if uncommented):
|
||||
// const broken1: Agent = { ...a, last_heartbeat: '2026-...' }; // ❌ TS2353
|
||||
// const broken2: Agent = { ...a, capabilities: ['deploy'] }; // ❌ TS2353
|
||||
// const broken3: Agent = { ...a, tags: { env: 'prod' } }; // ❌ TS2353
|
||||
// const broken4: Agent = { ...a, created_at: '...' }; // ❌ TS2353
|
||||
// const broken5: Agent = { ...a, updated_at: '...' }; // ❌ TS2353
|
||||
});
|
||||
|
||||
it('keeps last_heartbeat_at (the real Go-emitted heartbeat field)', () => {
|
||||
// Negative-prevention guard: the awk-windowed CI grep for the trimmed
|
||||
// `last_heartbeat` field must NOT trip on the legitimate
|
||||
// `last_heartbeat_at`. This test pins that the legitimate field stays.
|
||||
const a: Agent = {
|
||||
id: 'ag-2',
|
||||
name: 'web-02',
|
||||
hostname: 'web-02.prod',
|
||||
status: 'Offline',
|
||||
registered_at: '2024-06-01T00:00:00Z',
|
||||
os: 'linux',
|
||||
architecture: 'amd64',
|
||||
ip_address: '10.0.0.2',
|
||||
version: '2.1.0',
|
||||
};
|
||||
expect(a.last_heartbeat_at).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* D-2 (diff-05x06-2044a46f4dd0, master): Target retirement-fields ADD.
|
||||
*
|
||||
* Pre-D-2 the Go-side `DeploymentTarget` struct
|
||||
* (`internal/domain/connector.go:24`) emitted `retired_at` and
|
||||
* `retired_reason` (I-004 soft-retirement, mirroring the Agent
|
||||
* treatment), but the TS `Target` interface did not declare them.
|
||||
* Consumers wanting to surface the retired state in the GUI had to
|
||||
* use `(target as any).retired_at` escapes that lost type-checking.
|
||||
*
|
||||
* Post-D-2 the TS interface declares both as optional nullable strings,
|
||||
* mirroring the existing Agent retirement-fields shape.
|
||||
*/
|
||||
describe('Target interface (D-2 retirement fields)', () => {
|
||||
it('accepts retired_at and retired_reason as optional nullable strings', () => {
|
||||
const retired: Target = {
|
||||
id: 't-decom-01',
|
||||
name: 'old-iis-server',
|
||||
type: 'iis',
|
||||
agent_id: 'ag-old',
|
||||
config: {},
|
||||
enabled: false,
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
retired_at: '2026-03-01T00:00:00Z',
|
||||
retired_reason: 'replaced by new iis-server',
|
||||
};
|
||||
expect(retired.retired_at).toBe('2026-03-01T00:00:00Z');
|
||||
expect(retired.retired_reason).toBe('replaced by new iis-server');
|
||||
});
|
||||
|
||||
it('accepts a Target without the retirement fields (active row)', () => {
|
||||
const active: Target = {
|
||||
id: 't-1',
|
||||
name: 'iis-server',
|
||||
type: 'iis',
|
||||
agent_id: 'ag-1',
|
||||
config: {},
|
||||
enabled: true,
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
};
|
||||
expect(active.retired_at).toBeUndefined();
|
||||
expect(active.retired_reason).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* D-2 (diff-05x06-85ab6b98a2f7, master): DiscoveredCertificate pem_data ADD.
|
||||
*
|
||||
* Pre-D-2 the Go-side `DiscoveredCertificate` struct
|
||||
* (`internal/domain/discovery.go::DiscoveredCertificate.PEMData`) emitted
|
||||
* `pem_data` (omitempty — populated by repo SELECT, agent ingestion at
|
||||
* cmd/agent/main.go:1021, and connector scans at
|
||||
* internal/connector/discovery/azurekv/azurekv.go:234), but the TS
|
||||
* `DiscoveredCertificate` interface did not declare it. Consumers wanting
|
||||
* to inspect or download the raw PEM had to use `(d as any).pem_data`.
|
||||
*
|
||||
* Post-D-2 the TS interface declares it as `pem_data?: string`, optional
|
||||
* because the Go side uses `omitempty` (empty string → not emitted).
|
||||
*
|
||||
* Performance note (deferred follow-up): the LIST endpoint also loads
|
||||
* pem_data via the same repo SELECT; for large discovered-cert tables
|
||||
* this can ship kilobytes per row. Optimising the list response to omit
|
||||
* pem_data is a separate backend change.
|
||||
*/
|
||||
describe('DiscoveredCertificate interface (D-2 pem_data ADD)', () => {
|
||||
it('accepts pem_data as an optional string', () => {
|
||||
const d: DiscoveredCertificate = {
|
||||
id: 'dc-1',
|
||||
fingerprint_sha256: 'a'.repeat(64),
|
||||
common_name: 'discovered.example.com',
|
||||
sans: [],
|
||||
serial_number: '01:02:03',
|
||||
issuer_dn: 'CN=Test CA',
|
||||
subject_dn: 'CN=discovered.example.com',
|
||||
key_algorithm: 'ECDSA',
|
||||
key_size: 256,
|
||||
is_ca: false,
|
||||
source_path: '/etc/ssl/certs/disc.pem',
|
||||
source_format: 'pem',
|
||||
agent_id: 'ag-1',
|
||||
status: 'Unmanaged',
|
||||
first_seen_at: '2026-04-25T12:00:00Z',
|
||||
last_seen_at: '2026-04-25T12:00:00Z',
|
||||
created_at: '2026-04-25T12:00:00Z',
|
||||
updated_at: '2026-04-25T12:00:00Z',
|
||||
pem_data: '-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----\n',
|
||||
};
|
||||
expect(d.pem_data).toContain('BEGIN CERTIFICATE');
|
||||
});
|
||||
|
||||
it('accepts a DiscoveredCertificate without pem_data (list-response shape)', () => {
|
||||
const d: DiscoveredCertificate = {
|
||||
id: 'dc-2',
|
||||
fingerprint_sha256: 'b'.repeat(64),
|
||||
common_name: 'list.example.com',
|
||||
sans: [],
|
||||
serial_number: '04:05:06',
|
||||
issuer_dn: 'CN=Test CA',
|
||||
subject_dn: 'CN=list.example.com',
|
||||
key_algorithm: 'ECDSA',
|
||||
key_size: 256,
|
||||
is_ca: false,
|
||||
source_path: '/etc/ssl/certs/list.pem',
|
||||
source_format: 'pem',
|
||||
agent_id: 'ag-1',
|
||||
status: 'Unmanaged',
|
||||
first_seen_at: '2026-04-25T12:00:00Z',
|
||||
last_seen_at: '2026-04-25T12:00:00Z',
|
||||
created_at: '2026-04-25T12:00:00Z',
|
||||
updated_at: '2026-04-25T12:00:00Z',
|
||||
};
|
||||
expect(d.pem_data).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* D-2 (diff-05x06-97fab8783a5c, master): Issuer status phantom trim.
|
||||
*
|
||||
* Pre-D-2 the TS `Issuer` interface declared a required `status: string`
|
||||
* field that the Go-side struct (`internal/domain/connector.go::Issuer`)
|
||||
* never emitted — the Go struct has only `Enabled bool`. The TS interface
|
||||
* comment claimed "Backend returns enabled boolean; status is derived
|
||||
* from this" but no derivation logic existed: `IssuersPage.tsx::~line 23`
|
||||
* read `issuer.status || 'Unknown'` and always rendered 'Unknown'.
|
||||
*
|
||||
* Post-D-2 the `status` field is removed; the consumer now derives the
|
||||
* displayed status from `enabled` at render time.
|
||||
*/
|
||||
describe('Issuer interface (D-2 status phantom trim)', () => {
|
||||
it('does NOT declare a phantom `status` field — derive from `enabled`', () => {
|
||||
// Construct a fully-populated Issuer with the post-D-2 shape.
|
||||
// If `status` is re-added, this construction fails with "missing
|
||||
// required" (TS2741) when status is required, or the excess-property
|
||||
// comment below trips when it's added back as optional.
|
||||
const i: Issuer = {
|
||||
id: 'iss-test',
|
||||
name: 'Test ACME',
|
||||
type: 'acme',
|
||||
config: {},
|
||||
enabled: true,
|
||||
created_at: '2026-01-01T00:00:00Z',
|
||||
};
|
||||
expect(i.id).toBe('iss-test');
|
||||
expect(i.enabled).toBe(true);
|
||||
|
||||
// Excess-property check:
|
||||
// const broken: Issuer = { ...i, status: 'Active' }; // ❌ TS2353
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* D-2 (diff-05x06-caba9eb3620e, master): Notification subject phantom trim.
|
||||
*
|
||||
* Pre-D-2 the TS `Notification` interface declared `subject?: string` —
|
||||
* the field was acknowledged in the existing comment as "a historical
|
||||
* frontend-only field the backend never emits" but kept on the interface
|
||||
* "so legacy fixtures and the pendingNotif test mock still type
|
||||
* correctly." Real consumer at `NotificationsPage.tsx::~line 241` had
|
||||
* `{n.message || n.subject}` as a fallback that always fell through to
|
||||
* `n.message` (since `n.subject` was always undefined).
|
||||
*
|
||||
* Post-D-2 the field is removed; the consumer drops the dead fallback
|
||||
* and the test fixtures drop the dead `subject:` initializer.
|
||||
*/
|
||||
describe('Notification interface (D-2 subject phantom trim)', () => {
|
||||
it('does NOT declare the phantom `subject` field', () => {
|
||||
const n: Notification = {
|
||||
id: 'no-test',
|
||||
type: 'CertificateExpiring',
|
||||
channel: 'email',
|
||||
recipient: 'ops@example.com',
|
||||
message: 'Certificate api.example.com expires in 14 days',
|
||||
status: 'pending',
|
||||
created_at: '2026-04-25T12:00:00Z',
|
||||
};
|
||||
expect(n.id).toBe('no-test');
|
||||
expect(n.message).toContain('14 days');
|
||||
|
||||
// Excess-property check:
|
||||
// const broken: Notification = { ...n, subject: 'Cert expiring' }; // ❌ TS2353
|
||||
});
|
||||
});
|
||||
|
||||
+63
-12
@@ -67,6 +67,23 @@ export interface CertificateVersion {
|
||||
// API contract. See docs/architecture.md ER-diagram note and
|
||||
// coverage-gap-audit-2026-04-24-v5/unified-audit.md cat-s5-apikey_leak
|
||||
// for the closure rationale.
|
||||
//
|
||||
// D-2 (diff-05x06-7cdf4e78ae24, master): pre-D-2 this interface declared
|
||||
// five fields the Go-side struct (internal/domain/connector.go::Agent)
|
||||
// does NOT emit on the wire: `last_heartbeat` (the real field is
|
||||
// `last_heartbeat_at`; the bare-name was a sibling typo never rejected
|
||||
// at compile time), `capabilities`, `tags`, `created_at`, `updated_at`.
|
||||
// Two of them had real consumers (AgentDetailPage rendered
|
||||
// `agent.capabilities` and `agent.tags`) — both always rendered the
|
||||
// empty-state branch because the runtime values were always undefined.
|
||||
// Post-D-2 the interface field set matches the Go-emitted JSON exactly:
|
||||
// id, name, hostname, status, last_heartbeat_at, registered_at, os,
|
||||
// architecture, ip_address, version, retired_at?, retired_reason?. A
|
||||
// `agent.capabilities` access is now a TS compile error. The CI guardrail
|
||||
// in .github/workflows/ci.yml (`Forbidden StatusBadge dead-key + TS
|
||||
// phantom-field regression guard (D-1 + D-2)`) blocks reintroduction of
|
||||
// the trimmed field names while explicitly excluding `last_heartbeat_at`
|
||||
// from the `last_heartbeat` regex.
|
||||
export interface Agent {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -76,13 +93,8 @@ export interface Agent {
|
||||
architecture: string;
|
||||
status: string;
|
||||
version: string;
|
||||
last_heartbeat: string;
|
||||
last_heartbeat_at: string;
|
||||
capabilities: string[];
|
||||
tags: Record<string, string>;
|
||||
last_heartbeat_at?: string;
|
||||
registered_at: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
// I-004: soft-retirement fields. When retired_at is non-null, the agent is
|
||||
// tombstoned — it will never heartbeat again and cascaded targets have been
|
||||
// retired alongside it. The retired tab on AgentsPage uses these to show the
|
||||
@@ -159,9 +171,17 @@ export interface Job {
|
||||
* without chasing server logs.
|
||||
*
|
||||
* `sent_at` and `error` are the pre-I-005 audit fields on the backend struct.
|
||||
* `subject` is a historical frontend-only field the backend never emits; it's
|
||||
* kept optional so legacy fixtures and the pendingNotif test mock still type
|
||||
* correctly without forcing a rewrite of every existing consumer.
|
||||
*
|
||||
* D-2 (diff-05x06-caba9eb3620e, master): pre-D-2 this interface carried a
|
||||
* phantom `subject?: string` field documented as "kept optional so legacy
|
||||
* fixtures and the pendingNotif test mock still type correctly without
|
||||
* forcing a rewrite of every existing consumer." The Go-side struct
|
||||
* (`internal/domain/notification.go::NotificationEvent`) never emitted it,
|
||||
* so `n.subject` was always `undefined` at runtime. The one real consumer
|
||||
* (NotificationsPage rendering `{n.message || n.subject}`) always fell
|
||||
* through to `n.message`. Post-D-2 the field is removed; the consumer
|
||||
* drops the dead `|| n.subject` fallback and the test fixtures drop the
|
||||
* dead `subject:` initializer. The CI guardrail blocks reintroduction.
|
||||
*
|
||||
* Status values follow the backend NotificationStatus constants:
|
||||
* pending · sent · failed · dead · read
|
||||
@@ -174,7 +194,6 @@ export interface Notification {
|
||||
type: string;
|
||||
channel: string;
|
||||
recipient: string;
|
||||
subject?: string;
|
||||
message: string;
|
||||
status: string;
|
||||
certificate_id?: string;
|
||||
@@ -269,13 +288,21 @@ export interface RenewalPolicy {
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// D-2 (diff-05x06-97fab8783a5c, master): pre-D-2 this interface declared
|
||||
// a required `status: string` field that the Go-side struct
|
||||
// (`internal/domain/connector.go::Issuer`) never emitted — the Go struct
|
||||
// has only `Enabled bool`. The TS comment claimed "status is derived from
|
||||
// this" but no derivation ever existed: `IssuersPage.tsx` read
|
||||
// `issuer.status || 'Unknown'` and always rendered 'Unknown'. Post-D-2
|
||||
// the phantom is removed; render sites derive the displayed status from
|
||||
// `enabled` (and optionally `test_status`) at the call site. The CI
|
||||
// guardrail in .github/workflows/ci.yml blocks reintroduction.
|
||||
export interface Issuer {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
config: Record<string, unknown>;
|
||||
status: string;
|
||||
/** Backend returns enabled boolean; status is derived from this */
|
||||
/** Backend returns enabled boolean; render sites derive status labels from this */
|
||||
enabled: boolean;
|
||||
/** Timestamp of last connection test */
|
||||
last_tested_at?: string;
|
||||
@@ -287,6 +314,14 @@ export interface Issuer {
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
// D-2 (diff-05x06-2044a46f4dd0, master): pre-D-2 this interface lacked
|
||||
// `retired_at` and `retired_reason` even though the Go-side struct
|
||||
// (`internal/domain/connector.go::DeploymentTarget`) emits both as part
|
||||
// of the I-004 soft-retirement model. Consumers wanting to surface the
|
||||
// retired state had to escape via `(target as any).retired_at`. Post-D-2
|
||||
// the TS interface declares both as optional nullable strings, mirroring
|
||||
// the Agent retirement-fields shape (an Agent retire cascades to all
|
||||
// associated Targets per service.RetireAgent → repository.RetireTarget).
|
||||
export interface Target {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -297,6 +332,8 @@ export interface Target {
|
||||
last_tested_at?: string;
|
||||
test_status?: string;
|
||||
source?: string;
|
||||
retired_at?: string | null;
|
||||
retired_reason?: string | null;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
@@ -403,6 +440,19 @@ export interface IssuanceRateDataPoint {
|
||||
}
|
||||
|
||||
// Discovery types
|
||||
//
|
||||
// D-2 (diff-05x06-85ab6b98a2f7, master): pre-D-2 this interface lacked
|
||||
// `pem_data` even though the Go-side struct
|
||||
// (`internal/domain/discovery.go::DiscoveredCertificate.PEMData`,
|
||||
// json:"pem_data,omitempty") emits it on the wire. The field is
|
||||
// populated by the agent's filesystem scanner
|
||||
// (cmd/agent/main.go::buildDiscoveryReport), the cloud-secret-manager
|
||||
// connectors (e.g. internal/connector/discovery/azurekv/azurekv.go), and
|
||||
// the repo SELECT that materialises the row from PostgreSQL. Post-D-2
|
||||
// the TS interface declares `pem_data?: string`, optional because the
|
||||
// Go side uses `omitempty` (empty string → not emitted). Performance
|
||||
// follow-up: the LIST endpoint loads pem_data via the same repo SELECT;
|
||||
// a future change should gate emission on the per-id detail path only.
|
||||
export interface DiscoveredCertificate {
|
||||
id: string;
|
||||
fingerprint_sha256: string;
|
||||
@@ -416,6 +466,7 @@ export interface DiscoveredCertificate {
|
||||
key_algorithm: string;
|
||||
key_size: number;
|
||||
is_ca: boolean;
|
||||
pem_data?: string;
|
||||
source_path: string;
|
||||
source_format: string;
|
||||
agent_id: string;
|
||||
|
||||
@@ -8,7 +8,12 @@ export function formatDateTime(iso: string | undefined | null): string {
|
||||
return new Date(iso).toLocaleString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
export function timeAgo(iso: string): string {
|
||||
// D-2 (master): widened to accept undefined/null since several Go-side
|
||||
// timestamp fields are emitted as `omitempty` (e.g. Agent.last_heartbeat_at
|
||||
// for never-heartbeated agents). Pre-D-2 the TS interfaces declared
|
||||
// these as required strings, masking the case; post-D-2 the optionality
|
||||
// is propagated end-to-end and the helper handles it explicitly.
|
||||
export function timeAgo(iso: string | undefined | null): string {
|
||||
if (!iso) return '—';
|
||||
const now = Date.now();
|
||||
const then = new Date(iso).getTime();
|
||||
|
||||
Reference in New Issue
Block a user