108 lines
4.0 KiB
TypeScript
108 lines
4.0 KiB
TypeScript
import { type ReactNode } from 'react';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { apiRequest } from '../../lib/api-client';
|
|
import { z } from 'zod';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '../../components/ui/card';
|
|
import { Badge } from '../../components/ui/badge';
|
|
import { Button } from '../../components/ui/button';
|
|
import { EmptyState, ErrorState, Spinner } from '../../components/ui/states';
|
|
|
|
/** Erweiterter Systemstatus (API-Vertrag /api/v1/system/status). */
|
|
const systemStatusSchema = z.object({
|
|
overall: z.enum(['HEALTHY', 'DEGRADED', 'UNHEALTHY']),
|
|
components: z.array(
|
|
z.object({
|
|
name: z.string(),
|
|
status: z.enum(['HEALTHY', 'DEGRADED', 'UNHEALTHY', 'STOPPED']),
|
|
detail: z.string(),
|
|
}),
|
|
),
|
|
});
|
|
type SystemStatus = z.infer<typeof systemStatusSchema>;
|
|
|
|
/** Badge-Variante je Komponenten-Status. */
|
|
function statusVariant(
|
|
status: SystemStatus['components'][number]['status'],
|
|
): 'success' | 'warning' | 'danger' | 'neutral' {
|
|
switch (status) {
|
|
case 'HEALTHY':
|
|
return 'success';
|
|
case 'DEGRADED':
|
|
return 'warning';
|
|
case 'UNHEALTHY':
|
|
return 'danger';
|
|
default:
|
|
return 'neutral';
|
|
}
|
|
}
|
|
|
|
/** Admin-Seite: detaillierter Systemstatus aller Komponenten und Module. */
|
|
export function SystemStatusPage(): ReactNode {
|
|
const statusQuery = useQuery({
|
|
queryKey: ['system-status'],
|
|
queryFn: async (): Promise<SystemStatus> =>
|
|
systemStatusSchema.parse(await apiRequest('/api/v1/system/status')),
|
|
refetchInterval: 30_000,
|
|
});
|
|
|
|
return (
|
|
<div className="mx-auto max-w-screen-2xl space-y-8 p-8">
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-slate-900">Systemstatus</h1>
|
|
<p className="mt-2 text-base text-slate-500">
|
|
Zustand aller Plattform-Komponenten und Module (aktualisiert alle 30 Sekunden).
|
|
</p>
|
|
</div>
|
|
|
|
<Card>
|
|
<CardHeader><CardTitle>Gesamtstatus</CardTitle></CardHeader>
|
|
<CardContent>
|
|
{statusQuery.isLoading && <Spinner />}
|
|
{statusQuery.isError && <ErrorState title="Systemstatus nicht erreichbar" message="Prüfe die Verbindung und versuche es erneut." action={<Button variant="secondary" onClick={() => void statusQuery.refetch()}>Erneut versuchen</Button>} />}
|
|
{statusQuery.data && statusQuery.data.components.length === 0 && <EmptyState title="Keine Komponenten gemeldet" description="Sobald Plattformdienste registriert sind, erscheinen sie hier." />}
|
|
{statusQuery.data && statusQuery.data.components.length > 0 && (
|
|
<div className="flex items-center gap-3">
|
|
<Badge
|
|
variant={
|
|
statusQuery.data.overall === 'HEALTHY'
|
|
? 'success'
|
|
: statusQuery.data.overall === 'DEGRADED'
|
|
? 'warning'
|
|
: 'danger'
|
|
}
|
|
>
|
|
{statusQuery.data.overall}
|
|
</Badge>
|
|
<span className="text-sm text-slate-500">
|
|
{statusQuery.data.components.length} Komponenten überwacht
|
|
</span>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader><CardTitle>Komponenten</CardTitle></CardHeader>
|
|
<CardContent>
|
|
{statusQuery.data && (
|
|
<div className="space-y-3">
|
|
{statusQuery.data.components.map((component) => (
|
|
<div
|
|
key={component.name}
|
|
className="flex items-center justify-between rounded-lg border border-slate-200 px-4 py-3"
|
|
>
|
|
<div>
|
|
<span className="text-sm font-medium text-slate-700">{component.name}</span>
|
|
<span className="ml-2 text-xs text-slate-500">{component.detail}</span>
|
|
</div>
|
|
<Badge variant={statusVariant(component.status)}>{component.status}</Badge>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|