/** Formatting helpers for numbers, bytes, durations and timestamps. */ export function formatInt(value: number | null | undefined): string { if (value === null || value === undefined || Number.isNaN(value)) return "—"; return value.toLocaleString("en-US"); } export function formatBytes(value: number | null | undefined): string { if (value === null || value === undefined || Number.isNaN(value)) return "—"; if (value < 1024) return `${value} B`; const units = ["KiB", "MiB", "GiB", "TiB"]; let size = value; let unit = "B"; for (const next of units) { if (size < 1024) break; size /= 1024; unit = next; } return `${size >= 100 ? size.toFixed(0) : size.toFixed(1)} ${unit}`; } export function formatDurationMs(value: number | null | undefined): string { if (value === null || value === undefined || Number.isNaN(value)) return "—"; if (value < 1000) return `${Math.round(value)} ms`; const totalSeconds = Math.floor(value / 1000); const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = totalSeconds % 60; if (hours > 0) return `${hours}h ${String(minutes).padStart(2, "0")}m`; if (minutes > 0) return `${minutes}m ${String(seconds).padStart(2, "0")}s`; const fraction = value / 1000; return `${fraction >= 10 ? fraction.toFixed(0) : fraction.toFixed(1)}s`; } /** Parse an ISO/RFC3339 timestamp; returns null when unparseable. */ export function parseTime(value: string | null | undefined): Date | null { if (!value) return null; const date = new Date(value); return Number.isNaN(date.getTime()) ? null : date; } /** "2026-07-17 01:46:55 UTC" — stable, timezone-explicit rendering. */ export function formatUtc(value: string | null | undefined): string { const date = parseTime(value); if (!date) return value ? String(value) : "—"; const pad = (n: number) => String(n).padStart(2, "0"); return ( `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())} ` + `${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())} UTC` ); } export function formatRelative(value: string | null | undefined): string { const date = parseTime(value); if (!date) return "—"; const deltaMs = Date.now() - date.getTime(); const future = deltaMs < 0; const abs = Math.abs(deltaMs); const minutes = Math.floor(abs / 60_000); const hours = Math.floor(abs / 3_600_000); const days = Math.floor(abs / 86_400_000); let text: string; if (abs < 60_000) text = "just now"; else if (minutes < 60) text = `${minutes}m`; else if (hours < 24) text = `${hours}h`; else text = `${days}d`; if (text === "just now") return text; return future ? `in ${text}` : `${text} ago`; } export function formatPercent(part: number, total: number): string { if (!total) return "—"; return `${((part / total) * 100).toFixed(1)}%`; } /** Truncate a long identifier for display, keeping head and tail. */ export function truncateMiddle(value: string, max = 32): string { if (value.length <= max) return value; const head = Math.ceil((max - 1) / 2); const tail = Math.floor((max - 1) / 2); return `${value.slice(0, head)}…${value.slice(value.length - tail)}`; } /** Human label for an object type code (accepts both report-stream and projection vocabularies). */ export function objectTypeLabel(type: string | null | undefined): string { switch ((type ?? "").toLowerCase()) { case "roa": return "ROA"; case "mft": case "manifest": return "Manifest"; case "crl": return "CRL"; case "cer": case "certificate": return "Certificate"; case "asa": case "aspa": return "ASPA"; case "gbr": return "Ghostbusters"; case "ee": return "EE Certificate"; case "other": return "Other"; default: return type ? type.toUpperCase() : "—"; } }