Ops-dashboard/app/_components/CaddyWidget.tsx
Scrum4Me Agent 08d4b48190 feat(widgets): voeg relativeTime toe in lib/utils, expiringWarning-badge in CaddyWidget
- relativeTime(date: Date) helper toegevoegd aan lib/utils.ts
- AuditWidget gebruikt nu gedeelde relativeTime in plaats van inline functie
- CaddyWidget toont rode badge als soonest cert-expiry <30 dagen
- app/page.tsx berekent expiringWarning voor CaddyInitial

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 22:10:51 +02:00

76 lines
2.8 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; expiringWarning: boolean }
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
const expiringWarning = certs.some((c) => c.expiringWarning)
return { soonestExpiryMs, count: certs.length, expiringWarning }
}
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 ? (
<div className="flex items-center gap-2">
<p className="text-2xl font-semibold">
{daysUntil(data.soonestExpiryMs)}
<span className="text-sm font-normal text-muted-foreground"> dagen tot expiry</span>
</p>
{data.expiringWarning && (
<span className="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400">
&lt;30d
</span>
)}
</div>
) : (
<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>
)
}