// Minimal client-side CSV builder + browser download helper. // Detectability results come from the live backend (with an offline mock // fallback when it is unreachable); the CSV downloads are real files. export type CsvValue = string | number | boolean | null | undefined function escapeCell(value: unknown): string { if (value === null || value === undefined) return '' const s = String(value) if (/[",\n]/.test(s)) { return '"' + s.replace(/"/g, '""') + '"' } return s } /** Build a CSV string from an array of row objects, using `columns` order. */ export function toCSV( rows: T[], columns?: { key: keyof T; header: string }[], ): string { if (rows.length === 0 && !columns) return '' const cols = columns ?? (Object.keys(rows[0] as object) as (keyof T)[]).map((k) => ({ key: k, header: String(k), })) const head = cols.map((c) => escapeCell(c.header)).join(',') const body = rows .map((row) => cols.map((c) => escapeCell(row[c.key])).join(',')) .join('\n') return head + '\n' + body } /** Trigger a browser download of `text` as `filename`. */ export function download( filename: string, text: string, mime = 'text/csv;charset=utf-8', ): void { const blob = new Blob([text], { type: mime }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = filename document.body.appendChild(a) a.click() document.body.removeChild(a) URL.revokeObjectURL(url) }