feat: Phase 9 – Administration (Audit-UI, Systemeinstellungen, erweiterter Systemstatus)
This commit is contained in:
@@ -1,17 +1,46 @@
|
||||
import { type ReactNode } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { apiRequest } from '../../lib/api-client';
|
||||
import { healthSchema, type Health } from '../../lib/schemas';
|
||||
import { z } from 'zod';
|
||||
import { Card, CardBody, CardHeader } from '../../components/ui/card';
|
||||
import { Badge } from '../../components/ui/badge';
|
||||
import { ErrorState, Spinner } from '../../components/ui/states';
|
||||
|
||||
/** Admin-Seite: detaillierter Systemstatus der Plattform. */
|
||||
/** 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 healthQuery = useQuery({
|
||||
queryKey: ['health'],
|
||||
queryFn: async (): Promise<Health> =>
|
||||
healthSchema.parse(await apiRequest('/api/v1/health')),
|
||||
const statusQuery = useQuery({
|
||||
queryKey: ['system-status'],
|
||||
queryFn: async (): Promise<SystemStatus> =>
|
||||
systemStatusSchema.parse(await apiRequest('/api/v1/system/status')),
|
||||
refetchInterval: 30_000,
|
||||
});
|
||||
|
||||
@@ -20,69 +49,59 @@ export function SystemStatusPage(): ReactNode {
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900">Systemstatus</h1>
|
||||
<p className="mt-1 text-sm text-slate-500">
|
||||
Zustand aller Plattform-Komponenten (aktualisiert alle 30 Sekunden).
|
||||
Zustand aller Plattform-Komponenten und Module (aktualisiert alle 30 Sekunden).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader title="Gesamtstatus" />
|
||||
<CardBody>
|
||||
{statusQuery.isLoading && <Spinner />}
|
||||
{statusQuery.isError && (
|
||||
<ErrorState message="Der Systemstatus ist nicht erreichbar." />
|
||||
)}
|
||||
{statusQuery.data && (
|
||||
<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>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader title="Komponenten" />
|
||||
<CardBody>
|
||||
{healthQuery.isLoading && <Spinner />}
|
||||
{healthQuery.isError && (
|
||||
<ErrorState message="Der Health-Endpoint ist nicht erreichbar." />
|
||||
)}
|
||||
{healthQuery.data && (
|
||||
{statusQuery.data && (
|
||||
<div className="space-y-3">
|
||||
<StatusRow
|
||||
label="Management-Backend"
|
||||
variant={healthQuery.data.components.backend === 'healthy' ? 'success' : 'danger'}
|
||||
status={healthQuery.data.components.backend === 'healthy' ? 'Healthy' : 'Unhealthy'}
|
||||
/>
|
||||
<StatusRow
|
||||
label="PostgreSQL"
|
||||
variant={
|
||||
healthQuery.data.components.database.status === 'healthy'
|
||||
? 'success'
|
||||
: 'danger'
|
||||
}
|
||||
status={
|
||||
healthQuery.data.components.database.status === 'healthy'
|
||||
? `Healthy · ${healthQuery.data.components.database.latencyMs} ms`
|
||||
: 'Unhealthy'
|
||||
}
|
||||
/>
|
||||
<div className="flex items-center justify-between rounded-lg border border-slate-200 px-4 py-3">
|
||||
<span className="text-sm font-medium text-slate-700">Plattform-Version</span>
|
||||
<span className="text-sm text-slate-500">{healthQuery.data.version}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded-lg border border-slate-200 px-4 py-3">
|
||||
<span className="text-sm font-medium text-slate-700">Uptime</span>
|
||||
<span className="text-sm text-slate-500">
|
||||
{Math.floor(healthQuery.data.uptimeSeconds / 60)} Minuten
|
||||
</span>
|
||||
</div>
|
||||
{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>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Zeile im Systemstatus. */
|
||||
function StatusRow({
|
||||
label,
|
||||
status,
|
||||
variant,
|
||||
}: {
|
||||
label: string;
|
||||
status: string;
|
||||
variant: 'success' | 'danger';
|
||||
}): ReactNode {
|
||||
return (
|
||||
<div className="flex items-center justify-between rounded-lg border border-slate-200 px-4 py-3">
|
||||
<span className="text-sm font-medium text-slate-700">{label}</span>
|
||||
<Badge variant={variant}>{status}</Badge>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user