export function delay(ms: number): Promise { return new Promise((resolve) => { window.setTimeout(resolve, ms); }); } function looksLikeHTML(value: string): boolean { const sample = value.trim().slice(0, 512).toLowerCase(); return sample.startsWith('\s*([^<]+)\s*<\/title>/i); return match?.[1]?.trim() || ''; } function extractRequestID(value: string): string { const match = value.match(/Request ID:\s*([0-9a-f-]+)/i); return match?.[1]?.trim() || ''; } export async function fetchJSON(input: string, init?: RequestInit): Promise { const response = await fetch(input, init); const text = await response.text(); let payload: unknown = {}; if (text.trim() !== '') { try { payload = JSON.parse(text); } catch { payload = { error: text }; } } if (!response.ok) { let message = typeof payload === 'object' && payload !== null && 'error' in payload ? String((payload as { error: unknown }).error) : `${response.status} ${response.statusText}`; if (looksLikeHTML(text)) { const title = extractHTMLTitle(text); const requestID = extractRequestID(text); message = `upstream gateway error (${response.status}${title ? ` ${title}` : ''})`; if (requestID) { message = `${message}; request id ${requestID}`; } } throw new Error(message); } return payload as T; } export function formatBytes(value?: number): string { if (value === undefined || value === null || Number.isNaN(value)) { return 'n/a'; } if (value <= 0) { return '0 B'; } const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB']; let size = value; let unit = 0; while (size >= 1024 && unit < units.length - 1) { size /= 1024; unit += 1; } return `${size.toFixed(unit === 0 ? 0 : 2)} ${units[unit]}`; } export function formatTimestamp(value?: string): string { if (!value) { return 'n/a'; } const parsed = new Date(value); if (Number.isNaN(parsed.valueOf())) { return value; } return parsed.toLocaleString(); } export function formatLabel(value?: string): string { if (!value) { return 'Needs attention'; } return value.replace(/_/g, ' '); } export function progressChipClass(state?: string): 'good' | 'warn' | 'bad' { switch ((state || '').toLowerCase()) { case 'completed': return 'good'; case 'failed': return 'bad'; default: return 'warn'; } } export function suggestTargetPVCName(sourcePVC: string): string { const now = new Date(); const pad = (item: number) => String(item).padStart(2, '0'); const stamp = [ now.getUTCFullYear(), pad(now.getUTCMonth() + 1), pad(now.getUTCDate()), pad(now.getUTCHours()), pad(now.getUTCMinutes()) ].join(''); return (`restore-${sourcePVC}-${stamp}`) .toLowerCase() .replace(/[^a-z0-9-]/g, '-') .replace(/-+/g, '-') .replace(/^-|-$/g, '') .slice(0, 63) .replace(/-+$/g, ''); } export function suggestNamespacePrefix(): string { const now = new Date(); const pad = (item: number) => String(item).padStart(2, '0'); const stamp = [ now.getUTCFullYear(), pad(now.getUTCMonth() + 1), pad(now.getUTCDate()), pad(now.getUTCHours()), pad(now.getUTCMinutes()) ].join(''); return `restore-${stamp}-`; }