import React, { useEffect, useState, useMemo, useCallback } from 'react'; import { toast } from 'react-toastify'; import { classNames } from '~/utils/classNames'; import { logStore, type LogEntry } from '~/lib/stores/logs'; import { useStore } from '@nanostores/react'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '~/components/ui/Collapsible'; import { Progress } from '~/components/ui/Progress'; import { ScrollArea } from '~/components/ui/ScrollArea'; import { Badge } from '~/components/ui/Badge'; import { Dialog, DialogRoot, DialogTitle } from '~/components/ui/Dialog'; import { jsPDF } from 'jspdf'; import { useSettings } from '~/lib/hooks/useSettings'; interface SystemInfo { os: string; arch: string; platform: string; cpus: string; memory: { total: string; free: string; used: string; percentage: number; }; node: string; browser: { name: string; version: string; language: string; userAgent: string; cookiesEnabled: boolean; online: boolean; platform: string; cores: number; }; screen: { width: number; height: number; colorDepth: number; pixelRatio: number; }; time: { timezone: string; offset: number; locale: string; }; performance: { memory: { jsHeapSizeLimit: number; totalJSHeapSize: number; usedJSHeapSize: number; usagePercentage: number; }; timing: { loadTime: number; domReadyTime: number; readyStart: number; redirectTime: number; appcacheTime: number; unloadEventTime: number; lookupDomainTime: number; connectTime: number; requestTime: number; initDomTreeTime: number; loadEventTime: number; }; navigation: { type: number; redirectCount: number; }; }; network: { downlink: number; effectiveType: string; rtt: number; saveData: boolean; type: string; }; battery?: { charging: boolean; chargingTime: number; dischargingTime: number; level: number; }; storage: { quota: number; usage: number; persistent: boolean; temporary: boolean; }; } interface GitHubRepoInfo { fullName: string; defaultBranch: string; stars: number; forks: number; openIssues?: number; } interface GitInfo { local: { commitHash: string; branch: string; commitTime: string; author: string; email: string; remoteUrl: string; repoName: string; }; github?: { currentRepo: GitHubRepoInfo; upstream?: GitHubRepoInfo; }; isForked?: boolean; } interface WebAppInfo { name: string; version: string; description: string; license: string; environment: string; timestamp: string; runtimeInfo: { nodeVersion: string; }; dependencies: { production: Array<{ name: string; version: string; type: string }>; development: Array<{ name: string; version: string; type: string }>; peer: Array<{ name: string; version: string; type: string }>; optional: Array<{ name: string; version: string; type: string }>; }; gitInfo: GitInfo; } // Add Ollama service status interface interface OllamaServiceStatus { isRunning: boolean; lastChecked: Date; error?: string; models?: Array<{ name: string; size: string; quantization: string; }>; } interface ExportFormat { id: string; label: string; icon: string; handler: () => void; } const DependencySection = ({ title, deps, }: { title: string; deps: Array<{ name: string; version: string; type: string }>; }) => { const [isOpen, setIsOpen] = useState(false); if (deps.length === 0) { return null; } return (
{title} Dependencies ({deps.length})
{isOpen ? 'Hide' : 'Show'}
{deps.map((dep) => (
{dep.name} {dep.version}
))}
); }; export default function DebugTab() { const [systemInfo, setSystemInfo] = useState(null); const [webAppInfo, setWebAppInfo] = useState(null); const [ollamaStatus, setOllamaStatus] = useState({ isRunning: false, lastChecked: new Date(), }); const [loading, setLoading] = useState({ systemInfo: false, webAppInfo: false, errors: false, performance: false, }); const [openSections, setOpenSections] = useState({ system: false, webapp: false, errors: false, performance: false, }); const { providers } = useSettings(); // Subscribe to logStore updates const logs = useStore(logStore.logs); const errorLogs = useMemo(() => { return Object.values(logs).filter( (log): log is LogEntry => typeof log === 'object' && log !== null && 'level' in log && log.level === 'error', ); }, [logs]); // Set up error listeners when component mounts useEffect(() => { const handleError = (event: ErrorEvent) => { logStore.logError(event.message, event.error, { filename: event.filename, lineNumber: event.lineno, columnNumber: event.colno, }); }; const handleRejection = (event: PromiseRejectionEvent) => { logStore.logError('Unhandled Promise Rejection', event.reason); }; window.addEventListener('error', handleError); window.addEventListener('unhandledrejection', handleRejection); return () => { window.removeEventListener('error', handleError); window.removeEventListener('unhandledrejection', handleRejection); }; }, []); // Check for errors when the errors section is opened useEffect(() => { if (openSections.errors) { checkErrors(); } }, [openSections.errors]); // Load initial data when component mounts useEffect(() => { const loadInitialData = async () => { await Promise.all([getSystemInfo(), getWebAppInfo()]); }; loadInitialData(); }, []); // Refresh data when sections are opened useEffect(() => { if (openSections.system) { getSystemInfo(); } if (openSections.webapp) { getWebAppInfo(); } }, [openSections.system, openSections.webapp]); // Add periodic refresh of git info useEffect(() => { if (!openSections.webapp) { return undefined; } // Initial fetch const fetchGitInfo = async () => { try { const response = await fetch('/api/system/git-info'); const updatedGitInfo = (await response.json()) as GitInfo; setWebAppInfo((prev) => { if (!prev) { return null; } // Only update if the data has changed if (JSON.stringify(prev.gitInfo) === JSON.stringify(updatedGitInfo)) { return prev; } return { ...prev, gitInfo: updatedGitInfo, }; }); } catch (error) { console.error('Failed to fetch git info:', error); } }; fetchGitInfo(); // Refresh every 5 minutes instead of every second const interval = setInterval(fetchGitInfo, 5 * 60 * 1000); return () => clearInterval(interval); }, [openSections.webapp]); const getSystemInfo = async () => { try { setLoading((prev) => ({ ...prev, systemInfo: true })); // Get browser info const ua = navigator.userAgent; const browserName = ua.includes('Firefox') ? 'Firefox' : ua.includes('Chrome') ? 'Chrome' : ua.includes('Safari') ? 'Safari' : ua.includes('Edge') ? 'Edge' : 'Unknown'; const browserVersion = ua.match(/(Firefox|Chrome|Safari|Edge)\/([0-9.]+)/)?.[2] || 'Unknown'; // Get performance metrics const memory = (performance as any).memory || {}; const timing = performance.timing; const navigation = performance.navigation; const connection = (navigator as any).connection; // Get battery info let batteryInfo; try { const battery = await (navigator as any).getBattery(); batteryInfo = { charging: battery.charging, chargingTime: battery.chargingTime, dischargingTime: battery.dischargingTime, level: battery.level * 100, }; } catch { console.log('Battery API not supported'); } // Get storage info let storageInfo = { quota: 0, usage: 0, persistent: false, temporary: false, }; try { const storage = await navigator.storage.estimate(); const persistent = await navigator.storage.persist(); storageInfo = { quota: storage.quota || 0, usage: storage.usage || 0, persistent, temporary: !persistent, }; } catch { console.log('Storage API not supported'); } // Get memory info from browser performance API const performanceMemory = (performance as any).memory || {}; const totalMemory = performanceMemory.jsHeapSizeLimit || 0; const usedMemory = performanceMemory.usedJSHeapSize || 0; const freeMemory = totalMemory - usedMemory; const memoryPercentage = totalMemory ? (usedMemory / totalMemory) * 100 : 0; const systemInfo: SystemInfo = { os: navigator.platform, arch: navigator.userAgent.includes('x64') ? 'x64' : navigator.userAgent.includes('arm') ? 'arm' : 'unknown', platform: navigator.platform, cpus: navigator.hardwareConcurrency + ' cores', memory: { total: formatBytes(totalMemory), free: formatBytes(freeMemory), used: formatBytes(usedMemory), percentage: Math.round(memoryPercentage), }, node: 'browser', browser: { name: browserName, version: browserVersion, language: navigator.language, userAgent: navigator.userAgent, cookiesEnabled: navigator.cookieEnabled, online: navigator.onLine, platform: navigator.platform, cores: navigator.hardwareConcurrency, }, screen: { width: window.screen.width, height: window.screen.height, colorDepth: window.screen.colorDepth, pixelRatio: window.devicePixelRatio, }, time: { timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, offset: new Date().getTimezoneOffset(), locale: navigator.language, }, performance: { memory: { jsHeapSizeLimit: memory.jsHeapSizeLimit || 0, totalJSHeapSize: memory.totalJSHeapSize || 0, usedJSHeapSize: memory.usedJSHeapSize || 0, usagePercentage: memory.totalJSHeapSize ? (memory.usedJSHeapSize / memory.totalJSHeapSize) * 100 : 0, }, timing: { loadTime: timing.loadEventEnd - timing.navigationStart, domReadyTime: timing.domContentLoadedEventEnd - timing.navigationStart, readyStart: timing.fetchStart - timing.navigationStart, redirectTime: timing.redirectEnd - timing.redirectStart, appcacheTime: timing.domainLookupStart - timing.fetchStart, unloadEventTime: timing.unloadEventEnd - timing.unloadEventStart, lookupDomainTime: timing.domainLookupEnd - timing.domainLookupStart, connectTime: timing.connectEnd - timing.connectStart, requestTime: timing.responseEnd - timing.requestStart, initDomTreeTime: timing.domInteractive - timing.responseEnd, loadEventTime: timing.loadEventEnd - timing.loadEventStart, }, navigation: { type: navigation.type, redirectCount: navigation.redirectCount, }, }, network: { downlink: connection?.downlink || 0, effectiveType: connection?.effectiveType || 'unknown', rtt: connection?.rtt || 0, saveData: connection?.saveData || false, type: connection?.type || 'unknown', }, battery: batteryInfo, storage: storageInfo, }; setSystemInfo(systemInfo); toast.success('System information updated'); } catch (error) { toast.error('Failed to get system information'); console.error('Failed to get system information:', error); } finally { setLoading((prev) => ({ ...prev, systemInfo: false })); } }; const getWebAppInfo = async () => { try { setLoading((prev) => ({ ...prev, webAppInfo: true })); const [appResponse, gitResponse] = await Promise.all([ fetch('/api/system/app-info'), fetch('/api/system/git-info'), ]); if (!appResponse.ok || !gitResponse.ok) { throw new Error('Failed to fetch webapp info'); } const appData = (await appResponse.json()) as Omit; const gitData = (await gitResponse.json()) as GitInfo; console.log('Git Info Response:', gitData); // Add logging to debug setWebAppInfo({ ...appData, gitInfo: gitData, }); toast.success('WebApp information updated'); return true; } catch (error) { console.error('Failed to fetch webapp info:', error); toast.error('Failed to fetch webapp information'); setWebAppInfo(null); return false; } finally { setLoading((prev) => ({ ...prev, webAppInfo: false })); } }; // Helper function to format bytes to human readable format const formatBytes = (bytes: number) => { const units = ['B', 'KB', 'MB', 'GB']; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return `${Math.round(size)} ${units[unitIndex]}`; }; const handleLogPerformance = () => { try { setLoading((prev) => ({ ...prev, performance: true })); // Get performance metrics using modern Performance API const performanceEntries = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming; const memory = (performance as any).memory; // Calculate timing metrics const timingMetrics = { loadTime: performanceEntries.loadEventEnd - performanceEntries.startTime, domReadyTime: performanceEntries.domContentLoadedEventEnd - performanceEntries.startTime, fetchTime: performanceEntries.responseEnd - performanceEntries.fetchStart, redirectTime: performanceEntries.redirectEnd - performanceEntries.redirectStart, dnsTime: performanceEntries.domainLookupEnd - performanceEntries.domainLookupStart, tcpTime: performanceEntries.connectEnd - performanceEntries.connectStart, ttfb: performanceEntries.responseStart - performanceEntries.requestStart, processingTime: performanceEntries.loadEventEnd - performanceEntries.responseEnd, }; // Get resource timing data const resourceEntries = performance.getEntriesByType('resource'); const resourceStats = { totalResources: resourceEntries.length, totalSize: resourceEntries.reduce((total, entry) => total + ((entry as any).transferSize || 0), 0), totalTime: Math.max(...resourceEntries.map((entry) => entry.duration)), }; // Get memory metrics const memoryMetrics = memory ? { jsHeapSizeLimit: memory.jsHeapSizeLimit, totalJSHeapSize: memory.totalJSHeapSize, usedJSHeapSize: memory.usedJSHeapSize, heapUtilization: (memory.usedJSHeapSize / memory.totalJSHeapSize) * 100, } : null; // Get frame rate metrics let fps = 0; if ('requestAnimationFrame' in window) { const times: number[] = []; function calculateFPS(now: number) { times.push(now); if (times.length > 10) { const fps = Math.round((1000 * 10) / (now - times[0])); times.shift(); return fps; } requestAnimationFrame(calculateFPS); return 0; } fps = calculateFPS(performance.now()); } // Log all performance metrics logStore.logSystem('Performance Metrics', { timing: timingMetrics, resources: resourceStats, memory: memoryMetrics, fps, timestamp: new Date().toISOString(), navigationEntry: { type: performanceEntries.type, redirectCount: performanceEntries.redirectCount, }, }); toast.success('Performance metrics logged'); } catch (error) { toast.error('Failed to log performance metrics'); console.error('Failed to log performance metrics:', error); } finally { setLoading((prev) => ({ ...prev, performance: false })); } }; const checkErrors = async () => { try { setLoading((prev) => ({ ...prev, errors: true })); // Get errors from log store const storedErrors = errorLogs; if (storedErrors.length === 0) { toast.success('No errors found'); } else { toast.warning(`Found ${storedErrors.length} error(s)`); } } catch (error) { toast.error('Failed to check errors'); console.error('Failed to check errors:', error); } finally { setLoading((prev) => ({ ...prev, errors: false })); } }; const exportDebugInfo = () => { try { const debugData = { timestamp: new Date().toISOString(), system: systemInfo, webApp: webAppInfo, errors: logStore.getLogs().filter((log: LogEntry) => log.level === 'error'), performance: { memory: (performance as any).memory || {}, timing: performance.timing, navigation: performance.navigation, }, }; const blob = new Blob([JSON.stringify(debugData, null, 2)], { type: 'application/json' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `bolt-debug-info-${new Date().toISOString()}.json`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); toast.success('Debug information exported successfully'); } catch (error) { console.error('Failed to export debug info:', error); toast.error('Failed to export debug information'); } }; const exportAsCSV = () => { try { const debugData = { system: systemInfo, webApp: webAppInfo, errors: logStore.getLogs().filter((log: LogEntry) => log.level === 'error'), performance: { memory: (performance as any).memory || {}, timing: performance.timing, navigation: performance.navigation, }, }; // Convert the data to CSV format const csvData = [ ['Category', 'Key', 'Value'], ...Object.entries(debugData).flatMap(([category, data]) => Object.entries(data || {}).map(([key, value]) => [ category, key, typeof value === 'object' ? JSON.stringify(value) : String(value), ]), ), ]; // Create CSV content const csvContent = csvData.map((row) => row.join(',')).join('\n'); const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `bolt-debug-info-${new Date().toISOString()}.csv`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); toast.success('Debug information exported as CSV'); } catch (error) { console.error('Failed to export CSV:', error); toast.error('Failed to export debug information as CSV'); } }; const exportAsPDF = () => { try { const debugData = { system: systemInfo, webApp: webAppInfo, errors: logStore.getLogs().filter((log: LogEntry) => log.level === 'error'), performance: { memory: (performance as any).memory || {}, timing: performance.timing, navigation: performance.navigation, }, }; // Create new PDF document const doc = new jsPDF(); const lineHeight = 7; let yPos = 20; const margin = 20; const pageWidth = doc.internal.pageSize.getWidth(); const maxLineWidth = pageWidth - 2 * margin; // Add key-value pair with better formatting const addKeyValue = (key: string, value: any, indent = 0) => { // Check if we need a new page if (yPos > doc.internal.pageSize.getHeight() - 20) { doc.addPage(); yPos = margin; } doc.setFontSize(10); doc.setTextColor('#374151'); doc.setFont('helvetica', 'bold'); // Format the key with proper spacing const formattedKey = key.replace(/([A-Z])/g, ' $1').trim(); doc.text(formattedKey + ':', margin + indent, yPos); doc.setFont('helvetica', 'normal'); doc.setTextColor('#6B7280'); let valueText; if (typeof value === 'object' && value !== null) { // Skip rendering if value is empty object if (Object.keys(value).length === 0) { return; } yPos += lineHeight; Object.entries(value).forEach(([subKey, subValue]) => { // Check for page break before each sub-item if (yPos > doc.internal.pageSize.getHeight() - 20) { doc.addPage(); yPos = margin; } const formattedSubKey = subKey.replace(/([A-Z])/g, ' $1').trim(); addKeyValue(formattedSubKey, subValue, indent + 10); }); return; } else { valueText = String(value); } const valueX = margin + indent + doc.getTextWidth(formattedKey + ': '); const maxValueWidth = maxLineWidth - indent - doc.getTextWidth(formattedKey + ': '); const lines = doc.splitTextToSize(valueText, maxValueWidth); // Check if we need a new page for the value if (yPos + lines.length * lineHeight > doc.internal.pageSize.getHeight() - 20) { doc.addPage(); yPos = margin; } doc.text(lines, valueX, yPos); yPos += lines.length * lineHeight; }; // Add section header with page break check const addSectionHeader = (title: string) => { // Check if we need a new page if (yPos + 20 > doc.internal.pageSize.getHeight() - 20) { doc.addPage(); yPos = margin; } yPos += lineHeight; doc.setFillColor('#F3F4F6'); doc.rect(margin - 2, yPos - 5, pageWidth - 2 * (margin - 2), lineHeight + 6, 'F'); doc.setFont('helvetica', 'bold'); doc.setTextColor('#111827'); doc.setFontSize(12); doc.text(title.toUpperCase(), margin, yPos); doc.setFont('helvetica', 'normal'); yPos += lineHeight * 1.5; }; // Add horizontal line with page break check const addHorizontalLine = () => { // Check if we need a new page if (yPos + 10 > doc.internal.pageSize.getHeight() - 20) { doc.addPage(); yPos = margin; return; // Skip drawing line if we just started a new page } doc.setDrawColor('#E5E5E5'); doc.line(margin, yPos, pageWidth - margin, yPos); yPos += lineHeight; }; // Helper function to add footer to all pages const addFooters = () => { const totalPages = doc.internal.pages.length - 1; for (let i = 1; i <= totalPages; i++) { doc.setPage(i); doc.setFontSize(8); doc.setTextColor('#9CA3AF'); doc.text(`Page ${i} of ${totalPages}`, pageWidth / 2, doc.internal.pageSize.getHeight() - 10, { align: 'center', }); } }; // Title and Header (first page only) doc.setFillColor('#6366F1'); doc.rect(0, 0, pageWidth, 40, 'F'); doc.setTextColor('#FFFFFF'); doc.setFontSize(24); doc.setFont('helvetica', 'bold'); doc.text('Debug Information Report', margin, 25); yPos = 50; // Timestamp and metadata doc.setTextColor('#6B7280'); doc.setFontSize(10); doc.setFont('helvetica', 'normal'); const timestamp = new Date().toLocaleString(undefined, { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', }); doc.text(`Generated: ${timestamp}`, margin, yPos); yPos += lineHeight * 2; // System Information Section if (debugData.system) { addSectionHeader('System Information'); // OS and Architecture addKeyValue('Operating System', debugData.system.os); addKeyValue('Architecture', debugData.system.arch); addKeyValue('Platform', debugData.system.platform); addKeyValue('CPU Cores', debugData.system.cpus); // Memory const memory = debugData.system.memory; addKeyValue('Memory', { 'Total Memory': memory.total, 'Used Memory': memory.used, 'Free Memory': memory.free, Usage: memory.percentage + '%', }); // Browser Information const browser = debugData.system.browser; addKeyValue('Browser', { Name: browser.name, Version: browser.version, Language: browser.language, Platform: browser.platform, 'Cookies Enabled': browser.cookiesEnabled ? 'Yes' : 'No', 'Online Status': browser.online ? 'Online' : 'Offline', }); // Screen Information const screen = debugData.system.screen; addKeyValue('Screen', { Resolution: `${screen.width}x${screen.height}`, 'Color Depth': screen.colorDepth + ' bit', 'Pixel Ratio': screen.pixelRatio + 'x', }); // Time Information const time = debugData.system.time; addKeyValue('Time Settings', { Timezone: time.timezone, 'UTC Offset': time.offset / 60 + ' hours', Locale: time.locale, }); addHorizontalLine(); } // Web App Information Section if (debugData.webApp) { addSectionHeader('Web App Information'); // Basic Info addKeyValue('Application', { Name: debugData.webApp.name, Version: debugData.webApp.version, Environment: debugData.webApp.environment, 'Node Version': debugData.webApp.runtimeInfo.nodeVersion, }); // Git Information if (debugData.webApp.gitInfo) { const gitInfo = debugData.webApp.gitInfo.local; addKeyValue('Git Information', { Branch: gitInfo.branch, Commit: gitInfo.commitHash, Author: gitInfo.author, 'Commit Time': gitInfo.commitTime, Repository: gitInfo.repoName, }); if (debugData.webApp.gitInfo.github) { const githubInfo = debugData.webApp.gitInfo.github.currentRepo; addKeyValue('GitHub Information', { Repository: githubInfo.fullName, 'Default Branch': githubInfo.defaultBranch, Stars: githubInfo.stars, Forks: githubInfo.forks, 'Open Issues': githubInfo.openIssues || 0, }); } } addHorizontalLine(); } // Performance Section if (debugData.performance) { addSectionHeader('Performance Metrics'); // Memory Usage const memory = debugData.performance.memory || {}; const totalHeap = memory.totalJSHeapSize || 0; const usedHeap = memory.usedJSHeapSize || 0; const usagePercentage = memory.usagePercentage || 0; addKeyValue('Memory Usage', { 'Total Heap Size': formatBytes(totalHeap), 'Used Heap Size': formatBytes(usedHeap), Usage: usagePercentage.toFixed(1) + '%', }); // Timing Metrics const timing = debugData.performance.timing || {}; const navigationStart = timing.navigationStart || 0; const loadEventEnd = timing.loadEventEnd || 0; const domContentLoadedEventEnd = timing.domContentLoadedEventEnd || 0; const responseEnd = timing.responseEnd || 0; const requestStart = timing.requestStart || 0; const loadTime = loadEventEnd > navigationStart ? loadEventEnd - navigationStart : 0; const domReadyTime = domContentLoadedEventEnd > navigationStart ? domContentLoadedEventEnd - navigationStart : 0; const requestTime = responseEnd > requestStart ? responseEnd - requestStart : 0; addKeyValue('Page Load Metrics', { 'Total Load Time': (loadTime / 1000).toFixed(2) + ' seconds', 'DOM Ready Time': (domReadyTime / 1000).toFixed(2) + ' seconds', 'Request Time': (requestTime / 1000).toFixed(2) + ' seconds', }); // Network Information if (debugData.system?.network) { const network = debugData.system.network; addKeyValue('Network Information', { 'Connection Type': network.type || 'Unknown', 'Effective Type': network.effectiveType || 'Unknown', 'Download Speed': (network.downlink || 0) + ' Mbps', 'Latency (RTT)': (network.rtt || 0) + ' ms', 'Data Saver': network.saveData ? 'Enabled' : 'Disabled', }); } addHorizontalLine(); } // Errors Section if (debugData.errors && debugData.errors.length > 0) { addSectionHeader('Error Log'); debugData.errors.forEach((error: LogEntry, index: number) => { doc.setTextColor('#DC2626'); doc.setFontSize(10); doc.setFont('helvetica', 'bold'); doc.text(`Error ${index + 1}:`, margin, yPos); yPos += lineHeight; doc.setFont('helvetica', 'normal'); doc.setTextColor('#6B7280'); addKeyValue('Message', error.message, 10); if (error.stack) { addKeyValue('Stack', error.stack, 10); } if (error.source) { addKeyValue('Source', error.source, 10); } yPos += lineHeight; }); } // Add footers to all pages at the end addFooters(); // Save the PDF doc.save(`bolt-debug-info-${new Date().toISOString()}.pdf`); toast.success('Debug information exported as PDF'); } catch (error) { console.error('Failed to export PDF:', error); toast.error('Failed to export debug information as PDF'); } }; const exportAsText = () => { try { const debugData = { system: systemInfo, webApp: webAppInfo, errors: logStore.getLogs().filter((log: LogEntry) => log.level === 'error'), performance: { memory: (performance as any).memory || {}, timing: performance.timing, navigation: performance.navigation, }, }; const textContent = Object.entries(debugData) .map(([category, data]) => { return `${category.toUpperCase()}\n${'-'.repeat(30)}\n${JSON.stringify(data, null, 2)}\n\n`; }) .join('\n'); const blob = new Blob([textContent], { type: 'text/plain' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `bolt-debug-info-${new Date().toISOString()}.txt`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); toast.success('Debug information exported as text file'); } catch (error) { console.error('Failed to export text file:', error); toast.error('Failed to export debug information as text file'); } }; const exportFormats: ExportFormat[] = [ { id: 'json', label: 'Export as JSON', icon: 'i-ph:file-json', handler: exportDebugInfo, }, { id: 'csv', label: 'Export as CSV', icon: 'i-ph:file-csv', handler: exportAsCSV, }, { id: 'pdf', label: 'Export as PDF', icon: 'i-ph:file-pdf', handler: exportAsPDF, }, { id: 'txt', label: 'Export as Text', icon: 'i-ph:file-text', handler: exportAsText, }, ]; // Add Ollama health check function const checkOllamaStatus = useCallback(async () => { try { const ollamaProvider = providers?.Ollama; const baseUrl = ollamaProvider?.settings?.baseUrl || 'http://127.0.0.1:11434'; // First check if service is running const versionResponse = await fetch(`${baseUrl}/api/version`); if (!versionResponse.ok) { throw new Error('Service not running'); } // Then fetch installed models const modelsResponse = await fetch(`${baseUrl}/api/tags`); const modelsData = (await modelsResponse.json()) as { models: Array<{ name: string; size: string; quantization: string }>; }; setOllamaStatus({ isRunning: true, lastChecked: new Date(), models: modelsData.models, }); } catch { setOllamaStatus({ isRunning: false, error: 'Connection failed', lastChecked: new Date(), models: undefined, }); } }, [providers]); // Monitor Ollama provider status and check periodically useEffect(() => { const ollamaProvider = providers?.Ollama; if (ollamaProvider?.settings?.enabled) { // Check immediately when provider is enabled checkOllamaStatus(); // Set up periodic checks every 10 seconds const intervalId = setInterval(checkOllamaStatus, 10000); return () => clearInterval(intervalId); } return undefined; }, [providers, checkOllamaStatus]); // Replace the existing export button with this new component const ExportButton = () => { const [isOpen, setIsOpen] = useState(false); const handleOpenChange = useCallback((open: boolean) => { setIsOpen(open); }, []); const handleFormatClick = useCallback((handler: () => void) => { handler(); setIsOpen(false); }, []); return (
Export Debug Information
{exportFormats.map((format) => ( ))}
); }; // Add helper function to get Ollama status text and color const getOllamaStatus = () => { const ollamaProvider = providers?.Ollama; const isOllamaEnabled = ollamaProvider?.settings?.enabled; if (!isOllamaEnabled) { return { status: 'Disabled', color: 'text-red-500', bgColor: 'bg-red-500', message: 'Ollama provider is disabled in settings', }; } if (!ollamaStatus.isRunning) { return { status: 'Not Running', color: 'text-red-500', bgColor: 'bg-red-500', message: ollamaStatus.error || 'Ollama service is not running', }; } const modelCount = ollamaStatus.models?.length ?? 0; return { status: 'Running', color: 'text-green-500', bgColor: 'bg-green-500', message: `Ollama service is running with ${modelCount} installed models (Provider: Enabled)`, }; }; // Add type for status result type StatusResult = { status: string; color: string; bgColor: string; message: string; }; const status = getOllamaStatus() as StatusResult; return (
{/* Quick Stats Banner */}
{/* Errors Card */}
Errors
0 ? 'text-red-500' : 'text-green-500')} > {errorLogs.length}
0 ? 'i-ph:warning text-red-500' : 'i-ph:check-circle text-green-500', )} /> {errorLogs.length > 0 ? 'Errors detected' : 'No errors detected'}
{/* Memory Usage Card */}
Memory Usage
80 ? 'text-red-500' : (systemInfo?.memory?.percentage ?? 0) > 60 ? 'text-yellow-500' : 'text-green-500', )} > {systemInfo?.memory?.percentage ?? 0}%
80 ? '[&>div]:bg-red-500' : (systemInfo?.memory?.percentage ?? 0) > 60 ? '[&>div]:bg-yellow-500' : '[&>div]:bg-green-500', )} />
Used: {systemInfo?.memory.used ?? '0 GB'} / {systemInfo?.memory.total ?? '0 GB'}
{/* Page Load Time Card */}
Page Load Time
2000 ? 'text-red-500' : (systemInfo?.performance.timing.loadTime ?? 0) > 1000 ? 'text-yellow-500' : 'text-green-500', )} > {systemInfo ? (systemInfo.performance.timing.loadTime / 1000).toFixed(2) : '-'}s
DOM Ready: {systemInfo ? (systemInfo.performance.timing.domReadyTime / 1000).toFixed(2) : '-'}s
{/* Network Speed Card */}
Network Speed
{systemInfo?.network.downlink ?? '-'} Mbps
RTT: {systemInfo?.network.rtt ?? '-'} ms
{/* Ollama Service Card - Now spans all 4 columns */}
Ollama Service
{status.message}
{status.status}
{ollamaStatus.lastChecked.toLocaleTimeString()}
{status.status === 'Running' && ollamaStatus.models && ollamaStatus.models.length > 0 ? ( <>
Installed Models {ollamaStatus.models.length}
{ollamaStatus.models.map((model) => (
{model.name}
{Math.round(parseInt(model.size) / 1024 / 1024)}MB
))}
) : (
{status.message}
)}
{/* Action Buttons */}
{/* System Information */} setOpenSections((prev) => ({ ...prev, system: open }))} className="w-full" >

System Information

{systemInfo ? (
OS: {systemInfo.os}
Platform: {systemInfo.platform}
Architecture: {systemInfo.arch}
CPU Cores: {systemInfo.cpus}
Node Version: {systemInfo.node}
Network Type: {systemInfo.network.type} ({systemInfo.network.effectiveType})
Network Speed: {systemInfo.network.downlink}Mbps (RTT: {systemInfo.network.rtt}ms)
{systemInfo.battery && (
Battery: {systemInfo.battery.level.toFixed(1)}% {systemInfo.battery.charging ? '(Charging)' : ''}
)}
Storage: {(systemInfo.storage.usage / (1024 * 1024 * 1024)).toFixed(2)}GB /{' '} {(systemInfo.storage.quota / (1024 * 1024 * 1024)).toFixed(2)}GB
Memory Usage: {systemInfo.memory.used} / {systemInfo.memory.total} ({systemInfo.memory.percentage}%)
Browser: {systemInfo.browser.name} {systemInfo.browser.version}
Screen: {systemInfo.screen.width}x{systemInfo.screen.height} ({systemInfo.screen.pixelRatio}x)
Timezone: {systemInfo.time.timezone}
Language: {systemInfo.browser.language}
JS Heap: {(systemInfo.performance.memory.usedJSHeapSize / (1024 * 1024)).toFixed(1)}MB /{' '} {(systemInfo.performance.memory.totalJSHeapSize / (1024 * 1024)).toFixed(1)}MB ( {systemInfo.performance.memory.usagePercentage.toFixed(1)}%)
Page Load: {(systemInfo.performance.timing.loadTime / 1000).toFixed(2)}s
DOM Ready: {(systemInfo.performance.timing.domReadyTime / 1000).toFixed(2)}s
) : (
Loading system information...
)}
{/* Performance Metrics */} setOpenSections((prev) => ({ ...prev, performance: open }))} className="w-full" >

Performance Metrics

{systemInfo && (
Page Load Time: {(systemInfo.performance.timing.loadTime / 1000).toFixed(2)}s
DOM Ready Time: {(systemInfo.performance.timing.domReadyTime / 1000).toFixed(2)}s
Request Time: {(systemInfo.performance.timing.requestTime / 1000).toFixed(2)}s
Redirect Time: {(systemInfo.performance.timing.redirectTime / 1000).toFixed(2)}s
JS Heap Usage: {(systemInfo.performance.memory.usedJSHeapSize / (1024 * 1024)).toFixed(1)}MB /{' '} {(systemInfo.performance.memory.totalJSHeapSize / (1024 * 1024)).toFixed(1)}MB
Heap Utilization: {systemInfo.performance.memory.usagePercentage.toFixed(1)}%
Navigation Type: {systemInfo.performance.navigation.type === 0 ? 'Navigate' : systemInfo.performance.navigation.type === 1 ? 'Reload' : systemInfo.performance.navigation.type === 2 ? 'Back/Forward' : 'Other'}
Redirects: {systemInfo.performance.navigation.redirectCount}
)}
{/* WebApp Information */} setOpenSections((prev) => ({ ...prev, webapp: open }))} className="w-full" >

WebApp Information

{loading.webAppInfo && }
{loading.webAppInfo ? (
) : !webAppInfo ? (

Failed to load WebApp information

) : (

Basic Information

Name: {webAppInfo.name}
Version: {webAppInfo.version}
License: {webAppInfo.license}
Environment: {webAppInfo.environment}
Node Version: {webAppInfo.runtimeInfo.nodeVersion}

Git Information

Branch: {webAppInfo.gitInfo.local.branch}
Commit: {webAppInfo.gitInfo.local.commitHash}
Author: {webAppInfo.gitInfo.local.author}
Commit Time: {webAppInfo.gitInfo.local.commitTime}
{webAppInfo.gitInfo.github && ( <>
Repository: {webAppInfo.gitInfo.github.currentRepo.fullName} {webAppInfo.gitInfo.isForked && ' (fork)'}
{webAppInfo.gitInfo.github.currentRepo.stars}
{webAppInfo.gitInfo.github.currentRepo.forks}
{webAppInfo.gitInfo.github.currentRepo.openIssues}
{webAppInfo.gitInfo.github.upstream && (
Upstream: {webAppInfo.gitInfo.github.upstream.fullName}
{webAppInfo.gitInfo.github.upstream.stars}
{webAppInfo.gitInfo.github.upstream.forks}
)} )}
)} {webAppInfo && (

Dependencies

)}
{/* Error Check */} setOpenSections((prev) => ({ ...prev, errors: open }))} className="w-full" >

Error Check

{errorLogs.length > 0 && ( {errorLogs.length} Errors )}
Checks for:
  • Unhandled JavaScript errors
  • Unhandled Promise rejections
  • Runtime exceptions
  • Network errors
Status: {loading.errors ? 'Checking...' : errorLogs.length > 0 ? `${errorLogs.length} errors found` : 'No errors found'}
{errorLogs.length > 0 && (
Recent Errors:
{errorLogs.map((error) => (
{error.message}
{error.source && (
Source: {error.source} {error.details?.lineNumber && `:${error.details.lineNumber}`}
)} {error.stack && (
{error.stack}
)}
))}
)}
); }