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>
141 lines
5.3 KiB
TypeScript
141 lines
5.3 KiB
TypeScript
import { redirect } from 'next/navigation'
|
|
import { getCurrentUser } from '@/lib/session'
|
|
import { execAgent } from '@/lib/agent-client'
|
|
import { prisma } from '@/lib/prisma'
|
|
import { parseDockerPs } from '@/lib/parse-docker'
|
|
import { parseCertList } from '@/lib/parse-caddy'
|
|
import { parseSystemctlStatus } from '@/lib/parse-systemd'
|
|
import { parseGitStatus } from '@/lib/parse-git'
|
|
import DockerWidget, { type DockerInitial } from './_components/DockerWidget'
|
|
import CaddyWidget, { type CaddyInitial } from './_components/CaddyWidget'
|
|
import SystemdWidget, { type SystemdInitial } from './_components/SystemdWidget'
|
|
import GitWidget, { type GitInitial } from './_components/GitWidget'
|
|
import AuditWidget, { type AuditInitial } from './_components/AuditWidget'
|
|
|
|
export const dynamic = 'force-dynamic'
|
|
|
|
export default async function Home() {
|
|
const user = await getCurrentUser()
|
|
if (!user) redirect('/login')
|
|
|
|
const systemdUnits = (process.env.SYSTEMD_UNITS ?? '')
|
|
.split(',')
|
|
.map((u) => u.trim())
|
|
.filter(Boolean)
|
|
|
|
const repoPaths = (process.env.REPO_PATHS ?? '')
|
|
.split(',')
|
|
.map((p) => p.trim())
|
|
.filter(Boolean)
|
|
|
|
const [[dockerResult, caddyResult, auditResult], unitResults, repoResults] = await Promise.all([
|
|
Promise.allSettled([
|
|
execAgent('docker_ps'),
|
|
execAgent('caddy_list_certs'),
|
|
prisma.flowRun.findFirst({
|
|
where: { user_id: user.id },
|
|
orderBy: { started_at: 'desc' },
|
|
select: { id: true, flow_key: true, status: true, started_at: true },
|
|
}),
|
|
]),
|
|
Promise.allSettled(systemdUnits.map((unit) => execAgent('systemctl_status', [unit]))),
|
|
Promise.allSettled(repoPaths.map((path) => execAgent('git_status', [path]))),
|
|
])
|
|
|
|
// Docker widget initial state
|
|
const dockerInitial: DockerInitial =
|
|
dockerResult.status === 'rejected'
|
|
? { data: null, error: dockerResult.reason instanceof Error ? dockerResult.reason.message : 'failed' }
|
|
: (() => {
|
|
const containers = parseDockerPs(dockerResult.value)
|
|
return {
|
|
data: {
|
|
running: containers.filter((c) => c.status.toLowerCase().startsWith('up')).length,
|
|
total: containers.length,
|
|
},
|
|
error: null,
|
|
}
|
|
})()
|
|
|
|
// Caddy widget initial state
|
|
const caddyInitial: CaddyInitial =
|
|
caddyResult.status === 'rejected'
|
|
? { data: null, error: caddyResult.reason instanceof Error ? caddyResult.reason.message : 'failed' }
|
|
: (() => {
|
|
const certs = parseCertList(caddyResult.value)
|
|
const expiryTimes = certs
|
|
.filter((c) => c.notAfter)
|
|
.map((c) => new Date(c.notAfter).getTime())
|
|
return {
|
|
data: {
|
|
soonestExpiryMs: expiryTimes.length > 0 ? Math.min(...expiryTimes) : null,
|
|
count: certs.length,
|
|
},
|
|
error: null,
|
|
}
|
|
})()
|
|
|
|
// Systemd widget initial state
|
|
let systemdInitial: SystemdInitial
|
|
if (systemdUnits.length === 0) {
|
|
systemdInitial = { configured: false }
|
|
} else if (unitResults.every((r) => r.status === 'rejected')) {
|
|
const first = unitResults[0]
|
|
systemdInitial = {
|
|
data: null,
|
|
error: first.status === 'rejected' && first.reason instanceof Error ? first.reason.message : 'all units failed',
|
|
}
|
|
} else {
|
|
const healthy = unitResults.reduce((count, r, i) => {
|
|
if (r.status !== 'fulfilled') return count
|
|
return parseSystemctlStatus(r.value, systemdUnits[i]).activeState === 'active' ? count + 1 : count
|
|
}, 0)
|
|
systemdInitial = { data: { healthy, total: systemdUnits.length }, error: null }
|
|
}
|
|
|
|
// Git widget initial state
|
|
let gitInitial: GitInitial
|
|
if (repoPaths.length === 0) {
|
|
gitInitial = { configured: false }
|
|
} else if (repoResults.every((r) => r.status === 'rejected')) {
|
|
const first = repoResults[0]
|
|
gitInitial = {
|
|
data: null,
|
|
error: first.status === 'rejected' && first.reason instanceof Error ? first.reason.message : 'all repos failed',
|
|
}
|
|
} else {
|
|
const dirty = repoResults.filter(
|
|
(r) => r.status === 'fulfilled' && parseGitStatus(r.value).dirty,
|
|
).length
|
|
gitInitial = { data: { dirty, total: repoPaths.length }, error: null }
|
|
}
|
|
|
|
// Audit widget initial state
|
|
const auditInitial: AuditInitial =
|
|
auditResult.status === 'rejected'
|
|
? { data: null, error: auditResult.reason instanceof Error ? auditResult.reason.message : 'failed' }
|
|
: {
|
|
data: auditResult.value
|
|
? { ...auditResult.value, started_at: auditResult.value.started_at.toISOString() }
|
|
: null,
|
|
error: null,
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-background p-6">
|
|
<div className="mx-auto max-w-6xl space-y-6">
|
|
<div>
|
|
<h1 className="text-2xl font-semibold tracking-tight">Ops Dashboard</h1>
|
|
<p className="text-sm text-muted-foreground">Welkom {user.email}</p>
|
|
</div>
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
<DockerWidget initial={dockerInitial} />
|
|
<CaddyWidget initial={caddyInitial} />
|
|
<SystemdWidget initial={systemdInitial} units={systemdUnits} />
|
|
<GitWidget initial={gitInitial} repos={repoPaths} />
|
|
<AuditWidget initial={auditInitial} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|