/** * Meldungen. * * Das Regelwerk steht mit auf der Seite, und das ist die tragende Entscheidung: * **Eine leere Meldungsliste ist erst dann eine gute Nachricht, wenn man weiss, * was ueberhaupt geprueft wird.** Ohne die Liste bliebe offen, ob „keine * Meldungen" bedeutet „alles in Ordnung" oder „es wird nichts geprueft". * * „Zur Kenntnis nehmen" schliesst eine Meldung nicht. Sie bleibt offen und in * der Liste, bis ihre Ursache verschwindet — sonst verschwaende der Zustand aus * der Uebersicht, obwohl er weiterbesteht. */ import { useCallback, useState } from 'react'; import { useApiResource } from '@/api/useApiResource'; import { describeApiError, useMutation } from '@/api/useMutation'; import { PageHeader } from '@/components/layout/PageHeader'; import { Button, Callout, Card, CardContent, CardHeader, CheckboxField, EmptyState, ErrorState, LoadingState, StatusBadge, useToast, } from '@/components/ui'; import { cn, formatDateTime } from '@/lib/utils'; import { acknowledgeAlert, fetchAlertOverview, fetchAlerts, resolveAlert, } from './alertsApi'; import type { Alert, AlertOverview } from './alertsApi'; export function AlertsPage(): React.JSX.Element { const toast = useToast(); const [onlyActive, setOnlyActive] = useState(true); const [actionCounter, setActionCounter] = useState(0); const loadAlerts = useCallback( (abortSignal: AbortSignal) => fetchAlerts(onlyActive, abortSignal), [onlyActive], ); const { loadState, data, loadError, reload } = useApiResource( loadAlerts, `${String(onlyActive)}|${String(actionCounter)}`, ); const loadOverview = useCallback( (abortSignal: AbortSignal) => fetchAlertOverview(abortSignal), [], ); const overviewResource = useApiResource(loadOverview, String(actionCounter)); const acknowledgeMutation = useMutation( (alertIdentifier: string) => acknowledgeAlert(alertIdentifier, ''), { onSuccess: () => { toast.showSuccess( 'Zur Kenntnis genommen', 'Die Meldung bleibt offen, bis ihre Ursache verschwindet.', ); setActionCounter((previousCounter) => previousCounter + 1); }, onError: (apiError) => toast.showError('Fehlgeschlagen', describeApiError(apiError), apiError.requestId), }, ); const resolveMutation = useMutation( (alertIdentifier: string) => resolveAlert(alertIdentifier, 'Von Hand geschlossen.'), { onSuccess: () => { toast.showSuccess('Meldung geschlossen'); setActionCounter((previousCounter) => previousCounter + 1); }, onError: (apiError) => toast.showError('Fehlgeschlagen', describeApiError(apiError), apiError.requestId), }, ); return ( <> {overviewResource.data !== null ? ( ) : null}
{loadState === 'loading' ? : null} {loadState === 'failed' && loadError !== null ? ( ) : null} {loadState === 'loaded' && data !== null && data.length === 0 ? ( ) : null} {loadState === 'loaded' && data !== null && data.length > 0 ? (
    {data.map((alert) => ( void acknowledgeMutation.run(alert.id)} onResolve={() => void resolveMutation.run(alert.id)} /> ))}
) : null}
{overviewResource.data !== null ? : null} ); } /** Zeigt die Meldungslage in Zahlen. */ function SummaryBar({ overview }: { readonly overview: AlertOverview }): React.JSX.Element { const { summary } = overview; const figures = [ { label: 'Kritisch', value: summary.critical_count, tone: 'critical' as const }, { label: 'Ernst', value: summary.high_count, tone: 'high' as const }, { label: 'Unbearbeitet', value: summary.open_count, tone: null }, { label: 'Zur Kenntnis genommen', value: summary.acknowledged_count, tone: null }, { label: 'Erledigt (24 h)', value: summary.resolved_last_day, tone: null }, ]; return (
{figures.map((figure) => (

0 ? figure.tone === 'critical' ? 'text-critical' : 'text-high' : 'text-fg', )} > {figure.value}

{figure.label}

))}
); } /** Zeigt eine einzelne Meldung. */ function AlertRow({ alert, isBusy, onAcknowledge, onResolve, }: { readonly alert: Alert; readonly isBusy: boolean; readonly onAcknowledge: () => void; readonly onResolve: () => void; }): React.JSX.Element { const isResolved = alert.status === 'resolved'; return (
  • {alert.title}

    {/* Die Wiederholungszahl steht oben: Einmal ist ein Zwischenfall, zwanzigmal ein Zustand. */} {alert.occurrence_count > 1 ? ( {alert.occurrence_count}× aufgetreten ) : null}

    {alert.message}

    Seit {formatDateTime(alert.first_seen_at)} · zuletzt{' '} {formatDateTime(alert.last_seen_at)} · Regel {alert.rule_name}

    {alert.status === 'acknowledged' ? (

    Zur Kenntnis genommen am {formatDateTime(alert.acknowledged_at)}. Die Meldung bleibt offen, bis ihre Ursache verschwindet.

    ) : null} {isResolved ? (

    Erledigt am {formatDateTime(alert.resolved_at)} {alert.resolution_note !== undefined && alert.resolution_note !== '' ? ` — ${alert.resolution_note}` : ''}

    ) : null}
    {!isResolved ? (
    {alert.status === 'open' ? ( ) : null}
    ) : null}
  • ); } /** * Zeigt, worauf geachtet wird. * * Eine Regel, die dauerhaft schweigt, ist gefaehrlicher als keine — sie erweckt * den Eindruck, es werde geprueft. Deshalb steht bei jeder nicht ausgewerteten * Regel ausdruecklich, dass sie nicht greift, und warum. */ function RuleList({ overview }: { readonly overview: AlertOverview }): React.JSX.Element { return (
      {overview.rules.map((rule) => (
    • {rule.title} {rule.available ? ( ) : ( )}

      {rule.available ? rule.description : rule.unavailable_reason}

    • ))}
    {overview.available_rule_count < overview.rules.length ? ( {overview.rules.length - overview.available_rule_count} Regeln werden nicht ausgewertet. Rechnen Sie sie nicht als „in Ordnung" — sie schweigen, weil ihnen die Datengrundlage fehlt. ) : null}
    ); }