feat(m28+m29+m30): ACME ARI, email digest, and Helm chart

M28: ACME Renewal Information (RFC 9702) — CA-directed renewal timing
with cert ID computation, directory endpoint discovery, graceful
degradation for non-ARI CAs. 19 tests.

M29: Email notifier wiring + scheduled certificate digest — SMTP
connector bridged to service layer via NotifierAdapter, DigestService
with HTML email template, 7th scheduler loop (24h), digest preview/send
API endpoints and GUI card. 21 tests.

M30: Production-ready Helm chart — server Deployment, PostgreSQL
StatefulSet, agent DaemonSet, ConfigMaps, Secrets, Ingress, security
contexts, health probes, example values for dev/prod/ACME scenarios.

Also: OpenAPI spec updates, MCP tool additions, CI helm-lint job,
documentation updates across 5 doc files and README.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Shankar
2026-03-28 21:18:35 -04:00
parent 7cbcf69d72
commit 3f1f94f56b
61 changed files with 6106 additions and 27 deletions
+89 -2
View File
@@ -1,4 +1,5 @@
import { useQuery } from '@tanstack/react-query';
import { useState } from 'react';
import { useQuery, useMutation } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
import {
BarChart, Bar, LineChart, Line, PieChart, Pie, Cell,
@@ -7,7 +8,7 @@ import {
import {
getCertificates, getAgents, getJobs, getNotifications, getHealth,
getDashboardSummary, getCertificatesByStatus, getExpirationTimeline,
getJobTrends, getIssuanceRate,
getJobTrends, getIssuanceRate, previewDigest, sendDigest,
} from '../api/client';
import PageHeader from '../components/PageHeader';
import StatusBadge from '../components/StatusBadge';
@@ -75,6 +76,89 @@ const CustomTooltip = ({ active, payload, label }: any) => {
);
};
function DigestCard() {
const [previewHtml, setPreviewHtml] = useState<string | null>(null);
const [showPreview, setShowPreview] = useState(false);
const previewMutation = useMutation({
mutationFn: previewDigest,
onSuccess: (html) => {
setPreviewHtml(html);
setShowPreview(true);
},
});
const sendMutation = useMutation({ mutationFn: sendDigest });
return (
<>
<div className="bg-surface border border-surface-border rounded p-5 shadow-sm">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-semibold text-ink-muted">Certificate Digest</h3>
<p className="text-xs text-ink-faint mt-0.5">Send an email summary of certificate status to configured recipients</p>
</div>
<div className="flex gap-2">
<button
onClick={() => previewMutation.mutate()}
disabled={previewMutation.isPending}
className="btn btn-secondary text-xs"
>
{previewMutation.isPending ? 'Loading...' : 'Preview'}
</button>
<button
onClick={() => sendMutation.mutate()}
disabled={sendMutation.isPending}
className="btn btn-primary text-xs"
>
{sendMutation.isPending ? 'Sending...' : 'Send Now'}
</button>
</div>
</div>
{sendMutation.isSuccess && (
<div className="mt-3 text-xs text-emerald-600 bg-emerald-50 border border-emerald-200 rounded px-3 py-2">
Digest sent successfully.
</div>
)}
{sendMutation.isError && (
<div className="mt-3 text-xs text-red-600 bg-red-50 border border-red-200 rounded px-3 py-2">
Failed to send digest. Check SMTP configuration.
</div>
)}
{previewMutation.isError && (
<div className="mt-3 text-xs text-red-600 bg-red-50 border border-red-200 rounded px-3 py-2">
Digest not configured. Set CERTCTL_DIGEST_ENABLED=true and configure SMTP.
</div>
)}
</div>
{/* Preview Modal */}
{showPreview && previewHtml && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50" onClick={() => setShowPreview(false)}>
<div className="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[80vh] overflow-hidden" onClick={e => e.stopPropagation()}>
<div className="flex items-center justify-between px-5 py-3 border-b border-gray-200">
<h3 className="text-sm font-semibold text-gray-700">Digest Email Preview</h3>
<button onClick={() => setShowPreview(false)} className="text-gray-400 hover:text-gray-600">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="overflow-y-auto max-h-[calc(80vh-52px)]">
<iframe
srcDoc={previewHtml}
title="Digest Preview"
className="w-full h-[600px] border-0"
sandbox=""
/>
</div>
</div>
</div>
)}
</>
);
}
export default function DashboardPage() {
const navigate = useNavigate();
@@ -293,6 +377,9 @@ export default function DashboardPage() {
</div>
</div>
{/* Certificate Digest */}
<DigestCard />
{/* Pending Jobs Banner */}
{pendingJobs > 0 && (
<div className="bg-brand-50 border border-brand-200 rounded px-5 py-4 flex items-center justify-between">