feat(dashboard): vervang SECTIONS-grid door 5 live status-widgets
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>
This commit is contained in:
parent
ae63876f21
commit
faa1463cd7
8 changed files with 523 additions and 22 deletions
54
app/_components/DockerWidget.tsx
Normal file
54
app/_components/DockerWidget.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { parseDockerPs } from '@/lib/parse-docker'
|
||||
import { fetchAgentOutput } from '@/lib/agent-fetch'
|
||||
|
||||
type DockerData = { running: number; total: number }
|
||||
export type DockerInitial = { data: DockerData; error: null } | { data: null; error: string }
|
||||
|
||||
async function refreshDocker(): Promise<DockerData> {
|
||||
const output = await fetchAgentOutput('docker_ps')
|
||||
const containers = parseDockerPs(output)
|
||||
return {
|
||||
running: containers.filter((c) => c.status.toLowerCase().startsWith('up')).length,
|
||||
total: containers.length,
|
||||
}
|
||||
}
|
||||
|
||||
export default function DockerWidget({ initial }: { initial: DockerInitial }) {
|
||||
const [data, setData] = useState<DockerData | null>(initial.data)
|
||||
const [error, setError] = useState<string | null>(initial.error)
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
try {
|
||||
const d = await refreshDocker()
|
||||
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="/docker" className="block rounded-lg border bg-card p-5 transition-colors hover:bg-accent">
|
||||
<h2 className="text-sm font-medium text-muted-foreground">Docker</h2>
|
||||
{error ? (
|
||||
<p className="mt-2 text-sm text-destructive truncate">{error}</p>
|
||||
) : data ? (
|
||||
<p className="mt-2 text-2xl font-semibold">
|
||||
{data.running}
|
||||
<span className="text-sm font-normal text-muted-foreground"> / {data.total} running</span>
|
||||
</p>
|
||||
) : (
|
||||
<p className="mt-2 text-sm text-muted-foreground">—</p>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue