/** Kleine Helfer, die quer durch die Oberflaeche gebraucht werden. */ import { clsx, type ClassValue } from 'clsx'; import { twMerge } from 'tailwind-merge'; /** * Fuehrt Klassennamen zusammen und loest Tailwind-Konflikte auf. * * Ohne twMerge gewaenne bei "p-2 p-4" die Reihenfolge im Stylesheet, nicht die * im Aufruf — eine Komponente liesse sich dann von aussen nicht mehr anpassen, * und das faellt erst auf, wenn jemand es versucht. */ export function cn(...classValues: ClassValue[]): string { return twMerge(clsx(classValues)); } /** * Formatiert eine Byte-Zahl fuer die Anzeige. * * Bewusst mit binaeren Einheiten (KiB, MiB): Ein Backup-Produkt, das GB und GiB * vermischt, erzeugt Rueckfragen bei jeder Kapazitaetsplanung. */ export function formatBytes(byteCount: number | null | undefined): string { // Unbekannt ist nicht null. Eine 0 laese sich wie "nichts abgelegt" — // derselbe Grundsatz wie bei den Kennzahlen aus Phase 13. if (byteCount === null || byteCount === undefined || Number.isNaN(byteCount)) { return '—'; } if (byteCount === 0) { return '0 B'; } const unitNames = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB']; const unitIndex = Math.min( Math.floor(Math.log(Math.abs(byteCount)) / Math.log(1024)), unitNames.length - 1, ); const scaledValue = byteCount / Math.pow(1024, unitIndex); // Unter 10 eine Nachkommastelle, darueber keine: "1,4 GiB" ist nuetzlich, // "847,3 GiB" tut nur so genau. const decimalPlaces = unitIndex === 0 ? 0 : scaledValue < 10 ? 1 : 0; return `${scaledValue.toLocaleString('de-DE', { minimumFractionDigits: decimalPlaces, maximumFractionDigits: decimalPlaces, })} ${unitNames[unitIndex]}`; } /** Formatiert einen Zeitstempel als deutsches Datum mit Uhrzeit. */ export function formatDateTime(isoTimestamp: string | null | undefined): string { if (!isoTimestamp) { return '—'; } const parsedDate = new Date(isoTimestamp); if (Number.isNaN(parsedDate.getTime())) { return '—'; } return parsedDate.toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', }); } /** * Formatiert einen Zeitpunkt als Abstand zu jetzt ("vor 3 Stunden"). * * Fuer eine Betriebskonsole ist der Abstand oft die wichtigere Angabe: Ob die * letzte Sicherung um 02:15 lief, sagt wenig — dass sie vor drei Tagen lief, * sagt alles. */ export function formatRelativeTime(isoTimestamp: string | null | undefined): string { if (!isoTimestamp) { return '—'; } const parsedDate = new Date(isoTimestamp); if (Number.isNaN(parsedDate.getTime())) { return '—'; } const elapsedSeconds = (Date.now() - parsedDate.getTime()) / 1000; const isInFuture = elapsedSeconds < 0; const absoluteSeconds = Math.abs(elapsedSeconds); const relativeFormatter = new Intl.RelativeTimeFormat('de-DE', { numeric: 'auto' }); if (absoluteSeconds < 45) { return isInFuture ? 'gleich' : 'gerade eben'; } // Schwelle, Teiler und Einheit gehoeren zusammen und stehen deshalb in einem // Eintrag. Drei parallele Arrays waeren die Sorte Code, bei der ein // eingeschobener Eintrag still die Zuordnung verschiebt. const timeScales = [ { limit: 3600, divisor: 60, unit: 'minute' }, { limit: 86400, divisor: 3600, unit: 'hour' }, { limit: 2592000, divisor: 86400, unit: 'day' }, { limit: 31536000, divisor: 2592000, unit: 'month' }, { limit: Number.POSITIVE_INFINITY, divisor: 31536000, unit: 'year' }, ] as const satisfies ReadonlyArray<{ limit: number; divisor: number; unit: Intl.RelativeTimeFormatUnit; }>; for (const scale of timeScales) { if (absoluteSeconds < scale.limit) { const unitValue = Math.round(absoluteSeconds / scale.divisor); return relativeFormatter.format(isInFuture ? unitValue : -unitValue, scale.unit); } } return formatDateTime(isoTimestamp); } /** Formatiert eine Dauer in Sekunden als lesbare Angabe. */ export function formatDuration(totalSeconds: number | null | undefined): string { if (totalSeconds === null || totalSeconds === undefined || Number.isNaN(totalSeconds)) { return '—'; } if (totalSeconds < 1) { return `${Math.round(totalSeconds * 1000)} ms`; } if (totalSeconds < 60) { return `${totalSeconds.toFixed(totalSeconds < 10 ? 1 : 0)} s`; } const wholeMinutes = Math.floor(totalSeconds / 60); const remainingSeconds = Math.round(totalSeconds % 60); if (wholeMinutes < 60) { return `${wholeMinutes} min ${remainingSeconds} s`; } const wholeHours = Math.floor(wholeMinutes / 60); const remainingMinutes = wholeMinutes % 60; return `${wholeHours} h ${remainingMinutes} min`; } /** * Formatiert einen Durchsatz in Byte je Sekunde. */ export function formatThroughput(bytesPerSecond: number | null | undefined): string { if (bytesPerSecond === null || bytesPerSecond === undefined) { return '—'; } return `${formatBytes(bytesPerSecond)}/s`; }