Parallel server-side fetches via Promise.allSettled voor Docker, Caddy, systemd, Git en Audit. Iedere widget toont geaggregeerde status en refresht elke 30s client-side onafhankelijk van de andere widgets. - lib/agent-fetch.ts: gedeelde client-side streaming helper - app/api/audit/latest/route.ts: GET endpoint voor AuditWidget refresh - app/_components/DockerWidget.tsx: running/total containers - app/_components/CaddyWidget.tsx: soonest cert expiry in dagen - app/_components/SystemdWidget.tsx: healthy/total units (of niet geconfigureerd) - app/_components/GitWidget.tsx: dirty repo count (of niet geconfigureerd) - app/_components/AuditWidget.tsx: laatste FlowRun status + relatief tijdstip - app/page.tsx: vervangt SECTIONS-grid, doet parallel fetches, rendert widgets Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
68 lines
2.3 KiB
TypeScript
68 lines
2.3 KiB
TypeScript
'use client'
|
|
|
|
import { useCallback, useEffect, useState } from 'react'
|
|
import Link from 'next/link'
|
|
import { parseCertList } from '@/lib/parse-caddy'
|
|
import { fetchAgentOutput } from '@/lib/agent-fetch'
|
|
|
|
type CaddyData = { soonestExpiryMs: number | null; count: number }
|
|
export type CaddyInitial = { data: CaddyData; error: null } | { data: null; error: string }
|
|
|
|
async function refreshCaddy(): Promise<CaddyData> {
|
|
const output = await fetchAgentOutput('caddy_list_certs')
|
|
const certs = parseCertList(output)
|
|
const expiryTimes = certs
|
|
.filter((c) => c.notAfter)
|
|
.map((c) => new Date(c.notAfter).getTime())
|
|
const soonestExpiryMs = expiryTimes.length > 0 ? Math.min(...expiryTimes) : null
|
|
return { soonestExpiryMs, count: certs.length }
|
|
}
|
|
|
|
function daysUntil(ms: number): number {
|
|
return Math.floor((ms - Date.now()) / (1000 * 60 * 60 * 24))
|
|
}
|
|
|
|
export default function CaddyWidget({ initial }: { initial: CaddyInitial }) {
|
|
const [data, setData] = useState<CaddyData | null>(initial.data)
|
|
const [error, setError] = useState<string | null>(initial.error)
|
|
|
|
const refresh = useCallback(async () => {
|
|
try {
|
|
const d = await refreshCaddy()
|
|
setData(d)
|
|
setError(null)
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'refresh failed')
|
|
}
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
const id = setInterval(refresh, 30_000)
|
|
return () => clearInterval(id)
|
|
}, [refresh])
|
|
|
|
return (
|
|
<Link href="/caddy" className="block rounded-lg border bg-card p-5 transition-colors hover:bg-accent">
|
|
<h2 className="text-sm font-medium text-muted-foreground">Caddy / TLS</h2>
|
|
{error ? (
|
|
<p className="mt-2 text-sm text-destructive truncate">{error}</p>
|
|
) : data ? (
|
|
<div className="mt-2">
|
|
{data.soonestExpiryMs !== null ? (
|
|
<p className="text-2xl font-semibold">
|
|
{daysUntil(data.soonestExpiryMs)}
|
|
<span className="text-sm font-normal text-muted-foreground"> days to expiry</span>
|
|
</p>
|
|
) : (
|
|
<p className="text-sm text-muted-foreground">no certs</p>
|
|
)}
|
|
<p className="mt-1 text-xs text-muted-foreground">
|
|
{data.count} cert{data.count !== 1 ? 's' : ''}
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<p className="mt-2 text-sm text-muted-foreground">—</p>
|
|
)}
|
|
</Link>
|
|
)
|
|
}
|