// The diagnostics-bundle export — the Settings "Export diagnostics" flow. // // Carved verbatim out of static/app.js (R3a). A LEAF module: imports nothing. // It snapshots the browser-only state (console ring buffer, hardware probe, // localStorage, ua) via window.feedBack.diagnostics, POSTs it to // /api/diagnostics/export with the user's include/redact toggles, and streams the // returned zip to disk. Bundle layout + schemas: docs/diagnostics-bundle-spec.md. // // Everything except the two entry points is module-private — the preview // renderer, the file-label table, and the byte/HTML formatters are used nowhere // else in core. // // Companion to Settings export but for troubleshooting bug reports. // Bundle layout + schemas: docs/diagnostics-bundle-spec.md. // // Frontend's job is to: // 1. Snapshot the browser-only state (console ring buffer, hardware // probe, localStorage, ua) via window.feedBack.diagnostics. // 2. POST it to /api/diagnostics/export with the user's include / // redact toggles. // 3. Stream the returned zip to disk. import { downloadBlob } from './blob-io.js'; function _diagIncludeFromUI() { const v = (id) => document.getElementById(id)?.checked !== false; return { system: v('diag-incl-system'), hardware: v('diag-incl-hardware'), logs: v('diag-incl-logs'), console: v('diag-incl-console'), plugins: v('diag-incl-plugins'), }; } function _diagRedactFromUI() { const el = document.getElementById('diag-redact'); return el ? !!el.checked : true; } // Map raw file paths inside the bundle to plain-English labels + // descriptions for the preview UI. Only paths that show up in // previews need entries — unknown paths fall back to the path itself. const _DIAG_FILE_LABELS = { 'system/version.json': { label: 'App version', desc: 'FeedBack version, Python, OS' }, 'system/env.json': { label: 'Environment', desc: 'Allowlisted env vars (LOG_LEVEL, etc.). No secrets.' }, 'system/hardware.json': { label: 'Hardware (server-side)', desc: 'CPU, RAM, GPU. In Docker this reflects the container, not the host.' }, 'system/plugins.json': { label: 'Plugins', desc: 'Loaded plugins + git commit + orphan detection.' }, 'logs/server.log': { label: 'Server log', desc: 'Tail of LOG_FILE (last ~5 MB).' }, 'logs/server.log.meta.json': { label: 'Log metadata', desc: 'Log file path, size, rotation info.' }, 'client/console.json': { label: 'Browser console', desc: 'console.log/warn/error transcript + window errors.' }, 'client/hardware.json': { label: 'Hardware (browser)', desc: 'WebGL/WebGPU adapter, host OS via userAgent.' }, 'client/local_storage.json': { label: 'Browser storage', desc: 'localStorage contents (preferences).' }, 'client/ua.json': { label: 'User agent', desc: 'Browser, screen, page URL.' }, }; function _formatBytes(n) { if (!n || n < 1024) return (n || 0) + ' B'; if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB'; return (n / (1024 * 1024)).toFixed(1) + ' MB'; } function _escapeHtml(s) { return String(s || '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', }[c])); } function _renderDiagPreview(data) { const m = data.manifest || {}; const files = m.files || []; const groups = { system: [], logs: [], client: [], plugins: [], other: [] }; for (const f of files) { const top = (f.path || '').split('/')[0]; (groups[top] || groups.other).push(f); } const totalBytes = files.reduce((s, f) => s + (f.size || 0), 0); const include = _diagIncludeFromUI(); const redact = _diagRedactFromUI(); const sections = []; // Per-file `summary` (server-derived) → human one-liner. function _summaryLine(path, summary) { if (!summary || typeof summary !== 'object') return ''; if (path === 'system/plugins.json') { const loaded = summary.loaded_count || 0; const orphans = summary.orphan_count || 0; const orphPart = orphans ? ` · ${orphans} orphan${orphans === 1 ? '' : 's'}` : ''; return `${loaded} plugin${loaded === 1 ? '' : 's'} loaded${orphPart}`; } if (path === 'client/console.json') { const total = summary.entry_count || 0; const lvl = summary.by_level || {}; const parts = []; for (const k of ['error','warn','info','log','debug']) { if (lvl[k]) parts.push(`${lvl[k]} ${k}`); } return `${total} entries${parts.length ? ' (' + parts.join(', ') + ')' : ''}`; } if (path === 'system/hardware.json') { const bits = []; if (summary.cpu_brand) bits.push(summary.cpu_brand); if (summary.cores_logical) bits.push(`${summary.cores_logical} cores`); if (summary.gpu_count) bits.push(`${summary.gpu_count} GPU`); if (summary.runtime) bits.push(`runtime: ${summary.runtime}`); return bits.join(' · '); } if (path === 'client/hardware.json') { const bits = []; if (summary.runtime) bits.push(summary.runtime); if (summary.webgl_renderer) bits.push(summary.webgl_renderer); return bits.join(' · '); } if (path === 'client/local_storage.json') { return `${summary.key_count || 0} keys`; } if (path === 'system/version.json') { const bits = []; if (summary.feedBack) bits.push(`feedBack ${summary.feedBack}`); if (summary.python) bits.push(`python ${summary.python}`); if (summary.os) bits.push(summary.os); return bits.join(' · '); } return ''; } function pushSection(title, list, emptyHint) { if (!list.length) { if (emptyHint) { sections.push(`