/** * Uebersicht der Anlage. * * Der Plan (§14) nennt zehn Kennzahlen. Jede erscheint — auch die ohne * Datengrundlage, dann mit der Angabe, was fehlt. Ein weggelassenes Feld sieht * aus wie ein vergessenes; ein mit einer Null gefuelltes waere eine erfundene * Statistik (PROMPT.md §139). * * Der Schweregrad faerbt nur die Randlinie, nicht die Kachel. Zehn farbige * Flaechen nebeneinander ergeben ein Mosaik, in dem die eine kritische Zahl * untergeht — und genau die soll auffallen. */ import { useCallback } from 'react'; import { useApiResource } from '@/api/useApiResource'; import { describeApiError } from '@/api/useMutation'; import { PageHeader } from '@/components/layout/PageHeader'; import { Card, ErrorState, LoadingState } from '@/components/ui'; import { cn } from '@/lib/utils'; import { fetchDashboard } from './dashboardApi'; import type { Dashboard, DashboardWidget } from './dashboardApi'; export function DashboardPage(): React.JSX.Element { const loadDashboard = useCallback( (abortSignal: AbortSignal) => fetchDashboard(abortSignal), [], ); const { loadState, data, loadError, reload } = useApiResource(loadDashboard); return ( <> {loadState === 'loading' ? ( ) : null} {loadState === 'failed' && loadError !== null ? ( ) : null} {loadState === 'loaded' && data !== null ? (
{data.widgets.map((dashboardWidget) => ( ))}
) : null} ); } /** Randfarbe je Schweregrad. Nur die Linie, nicht die Flaeche. */ const severityBorderClasses: Readonly> = { healthy: 'border-l-healthy', info: 'border-l-info', warning: 'border-l-warning', high: 'border-l-high', critical: 'border-l-critical', }; /** Zeigt eine einzelne Kennzahl. */ function WidgetCard({ widget }: { readonly widget: DashboardWidget }): React.JSX.Element { if (!widget.available) { return (

{widget.title}

noch nicht verfuegbar

{widget.unavailable_reason}

); } return (

{widget.title}

{widget.value === undefined ? ( // Kein Wert heisst nicht null. Eine Kennzahl, die sich nicht bilden // laesst, sagt das — der Grund steht im Detailtext darunter. nicht bezifferbar ) : ( <> {formatWidgetValue(widget.value)} {widget.unit !== undefined ? ( {widget.unit} ) : null} )}

{widget.detail !== undefined ? (

{widget.detail}

) : null} {widget.breakdown !== undefined ? (
{Object.entries(widget.breakdown).map(([breakdownKey, breakdownValue]) => (
{formatBreakdownLabel(breakdownKey)}
{breakdownValue.toLocaleString('de-DE')}
))}
) : null}
); } /** Formatiert die Hauptzahl einer Kennzahl. */ function formatWidgetValue(widgetValue: number): string { // Ein kleiner Wert wird nicht auf null gerundet: „0 %" belegter Speicher liest // sich wie „nichts abgelegt", obwohl Daten da sind. Dieselbe Ueberlegung wie // bei der Recovery Assurance — eine Zahl darf nicht mehr behaupten, als sie // weiss, und auch nicht weniger. if (widgetValue > 0 && widgetValue < 0.1) { return '< 0,1'; } // Ganze Zahlen ohne Nachkommastellen, gebrochene mit einer: „99,7 %" sagt // mehr als „100 %", wenn drei von tausend Laeufen scheiterten. if (Number.isInteger(widgetValue)) { return widgetValue.toLocaleString('de-DE'); } return widgetValue.toLocaleString('de-DE', { maximumFractionDigits: 1 }); } /** Macht aus einem Schluessel der Aufschluesselung eine Beschriftung. */ function formatBreakdownLabel(breakdownKey: string): string { const readableLabel = breakdownKey.replace(/_/g, ' '); return readableLabel.charAt(0).toUpperCase() + readableLabel.slice(1); }