/** * Formatting utilities for display purposes */ /** * Safely convert a value to a valid number, returning default if invalid */ function safeNumber(value: unknown, defaultValue = 0): number { if (value === null || value === undefined) return defaultValue; const num = Number(value); if (!Number.isFinite(num) || Number.isNaN(num) || num < 0) return defaultValue; return num; } /** * Convert a memory value to GB for display. * * The GPU API returns values in bytes. Other APIs may return MiB. * We detect based on magnitude: * > 1 million → bytes (even 1MB = 1M bytes, smallest realistic GPU memory query) * > 1,000 → MiB (1000 MiB = ~1GB, smallest realistic GPU) * ≤ 1,000 → already GB */ function toGB(value: number | null | undefined): number { const safe = safeNumber(value, 0); if (safe === 0) return 0; // 1 million+ is definitely bytes (API returns bytes for GPU memory) if (safe > 1_000_000) return Math.round((safe / (1024 * 1024 * 1024)) * 100) / 100; // 1000-1M range is MiB (no GPU has less than 1GB) if (safe > 1_000) return Math.round((safe / 1024) * 100) / 100; // Small values assumed to already be in GB return Math.round(safe * 100) / 100; } function toGBFromMB(value: number | null | undefined): number { const safe = safeNumber(value, 0); if (safe === 0) return 0; return Math.round((safe / 1024) * 100) / 100; } function formatNumber(value: number | null | undefined): string { const n = safeNumber(value, 0); if (n >= 1_000_000_000) return (n / 1_000_000_000).toFixed(2) + "B"; if (n >= 1_000_000) return (n / 1_000_000).toFixed(2) + "M"; if (n >= 1_000) return (n / 1_000).toFixed(1) + "K"; return n.toLocaleString(); } /** * Format bytes into a human-readable string (B, KB, MB, GB, TB). */ function formatBytes(bytes: number | null): string { if (!bytes || bytes <= 0) return "-"; const units = ["B", "KB", "MB", "GB", "TB"]; let value = bytes; let unitIndex = 0; while (value >= 1024 && unitIndex < units.length - 1) { value /= 1024; unitIndex += 1; } return `${value.toFixed(value >= 10 ? 0 : 1)} ${units[unitIndex]}`; } /** * A duration, in the unit a reader can hold in their head. * * Sub-second latencies are read as integers ("412 ms"); anything longer stops * being a number you compare digit-by-digit and becomes a magnitude ("1.24 s"). * Null is "—", never 0 — a metric the controller did not report is not a fast * one. */ function formatMs(value: number | null | undefined): string { if (value === null || value === undefined || !Number.isFinite(value)) return "—"; if (value < 1000) return `${Math.round(value)} ms`; return `${(value / 1000).toFixed(2)} s`; } /** * Token counts at table density: lowercase suffixes, one decimal, no padding. * * Distinct from formatNumber's "1.2K / 3.40M" on purpose — a column of token * counts is scanned, not read, and the shorter glyphs keep the column narrow * enough to sit beside six other numeric columns. */ function formatCompactTokens(value: number | null | undefined): string { const n = safeNumber(value, 0); if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(1)}b`; if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}m`; if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`; return String(Math.round(n)); } const RELATIVE_UNITS: Array<[seconds: number, label: string]> = [ [31_536_000, "y"], [2_592_000, "mo"], [604_800, "w"], [86_400, "d"], [3600, "h"], [60, "min"], ]; /** * "3 min ago" with a non-breaking space, so a timestamp never wraps onto two * lines inside a table cell and doubles the row height. */ function formatRelativeTime(iso: string | null | undefined): string { if (!iso) return "—"; const at = Date.parse(iso); if (Number.isNaN(at)) return "—"; const seconds = Math.max(0, Math.round((Date.now() - at) / 1000)); if (seconds < 45) return "just now"; for (const [size, label] of RELATIVE_UNITS) { if (seconds >= size) return `${Math.floor(seconds / size)} ${label} ago`; } return `${seconds} s ago`; } export { toGB, toGBFromMB, formatNumber, formatBytes, formatMs, formatCompactTokens, formatRelativeTime, };