'use client' import { useCallback, useEffect, useState } from 'react' import { parseCertList, type CertInfo } from '@/lib/parse-caddy' import { apiFetch } from '@/lib/csrf' async function fetchCerts(): Promise { const res = await apiFetch('/api/agent/exec', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ command_key: 'caddy_list_certs', args: [] }), }) if (!res.ok) { const text = await res.text() throw new Error(`agent ${res.status}: ${text}`) } const reader = res.body?.getReader() if (!reader) throw new Error('no response body') const decoder = new TextDecoder() let buffer = '' let output = '' while (true) { const { done, value } = await reader.read() if (done) break buffer += decoder.decode(value, { stream: true }) const lines = buffer.split('\n') buffer = lines.pop() ?? '' for (const line of lines) { if (line.startsWith('data:')) { try { const parsed = JSON.parse(line.slice(5).trim()) as { data?: string } if (parsed.data !== undefined) output += parsed.data } catch { // ignore malformed } } } } return parseCertList(output) } type Props = { initialCerts: CertInfo[] certsError: string | null } export default function CaddyView({ initialCerts, certsError }: Props) { const [certs, setCerts] = useState(initialCerts) const [error, setError] = useState(certsError) const [refreshing, setRefreshing] = useState(false) const [lastUpdated, setLastUpdated] = useState(new Date()) const refresh = useCallback(async () => { setRefreshing(true) try { const updated = await fetchCerts() setCerts(updated) setError(null) setLastUpdated(new Date()) } catch (err) { setError(err instanceof Error ? err.message : 'failed') } finally { setRefreshing(false) } }, []) useEffect(() => { const id = setInterval(refresh, 60000) return () => clearInterval(id) }, [refresh]) return (

TLS Certificates

{refreshing && ( refreshing… )} updated {lastUpdated.toLocaleTimeString()}
{error ? (
{error}
) : certs.length === 0 ? (
No certificates found in /data/caddy/certificates/.
) : (
{certs.map((cert) => ( ))}
Domain Issuer Valid from Expires Status
{cert.domain} {cert.issuerCN} {cert.notBefore || '—'} {cert.notAfter || '—'}
)}
) } function CertStatusBadge({ cert }: { cert: CertInfo }) { if (cert.expired) { return ( Expired ) } if (cert.expiringWarning) { return ( Expiring soon ) } return ( Valid ) }