feat: Per-service certs with opt-in wildcard provisioning

Remove automatic wildcard cert assumption. Each service gets its own
cert by default. Wildcards are user-initiated via the Certificates page.

Backend:
- HAProxy L7 frontend uses cert directory (/etc/haproxy/certs/)
  instead of single wildcard file — loads all PEMs, serves by SNI
- Cert status API shows every registered service individually with
  coveredBy field when a wildcard cert covers the domain
- Reconciliation filters L7 routes to services that have a cert
- No auto-provisioning in reconciliation (just warnings)

Frontend:
- Certificates page shows per-service cert status
- "Provision" button for individual certs
- "Add Wildcard" form for opt-in wildcard provisioning
- Fixed CloudflareComponent type errors from cert API changes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-09 14:47:00 +00:00
parent 447c7e51a3
commit 7e8f660d11
6 changed files with 170 additions and 167 deletions

View File

@@ -182,7 +182,7 @@ export function CloudflareComponent() {
<div className="font-medium">Central Domain</div>
</div>
<div className="flex items-center gap-2">
{(certStatus?.certs?.some(c => c.cert.exists) || certStatus?.cert?.exists) && (
{certStatus?.certs?.some(c => c.cert.exists) && (
<Badge variant="success" className="gap-1">
<Shield className="h-3 w-3" />
TLS
@@ -257,7 +257,7 @@ export function CloudflareComponent() {
</div>
{globalConfig?.cloud?.central?.domain && (() => {
const centralCert = certStatus?.certs?.find(c => c.type === 'central') ?? (certStatus?.cert ? { domain: certStatus.domain ?? '', type: 'central' as const, cert: certStatus.cert } : undefined);
const centralCert = certStatus?.certs?.find(c => c.domain === globalConfig?.cloud?.central?.domain);
return (
<div className="ml-7 space-y-2">
{certLoading ? (
@@ -319,7 +319,7 @@ export function CloudflareComponent() {
<div key={entry.domain} className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2">
<span className="font-mono">{entry.domain}</span>
<Badge variant="outline" className="text-xs">{entry.type}</Badge>
<Badge variant="outline" className="text-xs">{entry.source}</Badge>
</div>
<div className="flex items-center gap-2">
{entry.cert.exists ? (