Ops-dashboard/app/_components/SystemdWidget.tsx
Scrum4Me Agent f6d0807a81 feat(widgets): voeg kleurbadges toe aan SystemdWidget en GitWidget
- SystemdWidget: groen als N=M healthy, oranje als 0<N<M, rood als N=0
- GitWidget: groen als 0 dirty repos, oranje als >0; toon K/M formaat

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

79 lines
2.6 KiB
TypeScript

'use client'
import { useCallback, useEffect, useState } from 'react'
import Link from 'next/link'
import { parseSystemctlStatus } from '@/lib/parse-systemd'
import { fetchAgentOutput } from '@/lib/agent-fetch'
type SystemdData = { healthy: number; total: number }
export type SystemdInitial =
| { configured: false }
| { data: SystemdData; error: null }
| { data: null; error: string }
async function refreshSystemd(units: string[]): Promise<SystemdData> {
const results = await Promise.allSettled(
units.map(async (unit) => {
const output = await fetchAgentOutput('systemctl_status', [unit])
return parseSystemctlStatus(output, unit)
}),
)
const healthy = results.filter(
(r) => r.status === 'fulfilled' && r.value.activeState === 'active',
).length
return { healthy, total: units.length }
}
export default function SystemdWidget({ initial, units }: { initial: SystemdInitial; units: string[] }) {
const notConfigured = 'configured' in initial && initial.configured === false
const [data, setData] = useState<SystemdData | null>(
!notConfigured && 'data' in initial ? initial.data : null,
)
const [error, setError] = useState<string | null>(
!notConfigured && 'error' in initial ? initial.error : null,
)
const refresh = useCallback(async () => {
if (notConfigured || units.length === 0) return
try {
const d = await refreshSystemd(units)
setData(d)
setError(null)
} catch (err) {
setError(err instanceof Error ? err.message : 'refresh failed')
}
}, [notConfigured, units])
useEffect(() => {
if (notConfigured) return
const id = setInterval(refresh, 30_000)
return () => clearInterval(id)
}, [refresh, notConfigured])
return (
<Link href="/systemd" className="block rounded-lg border bg-card p-5 transition-colors hover:bg-accent">
<h2 className="text-sm font-medium text-muted-foreground">systemd</h2>
{notConfigured ? (
<p className="mt-2 text-sm text-muted-foreground">niet geconfigureerd</p>
) : error ? (
<p className="mt-2 text-sm text-destructive truncate">{error}</p>
) : data ? (
<p
className={[
'mt-2 text-2xl font-semibold',
data.total > 0 && data.healthy === data.total
? 'text-green-600'
: data.healthy > 0
? 'text-orange-500'
: 'text-destructive',
].join(' ')}
>
{data.healthy}/{data.total}
<span className="text-sm font-normal text-muted-foreground"> healthy</span>
</p>
) : (
<p className="mt-2 text-sm text-muted-foreground"></p>
)}
</Link>
)
}