mirror of
https://github.com/stackblitz-labs/bolt.diy.git
synced 2025-06-18 01:49:07 +01:00
Enhanced System Metrics: Detailed CPU metrics including temperature and frequency Comprehensive memory breakdown with heap usage Advanced performance metrics (FPS, page load times, web vitals) Detailed network statistics Storage monitoring with visual indicators Battery health and detailed status Power Management: Multiple power profiles (Performance, Balanced, Power Saver) Enhanced energy saver mode Automatic power management based on system state Detailed energy savings statistics System Health: Overall system health score Real-time issue detection and alerts Performance optimization suggestions Historical metrics tracking UI Improvements: Interactive graphs for all metrics Color-coded status indicators Detailed tooltips and explanations Collapsible sections for better organization Alert system for critical events Performance Monitoring: Frame rate monitoring Resource usage tracking Network performance analysis Web vitals monitoring Detailed timing metrics To use the enhanced task manager: Monitor system health in the new health score section Choose a power profile based on your needs Enable auto energy saver for automatic power management Monitor real-time alerts for system issues 5. View detailed metrics in each category Check optimization suggestions when performance issues arise
1245 lines
52 KiB
TypeScript
1245 lines
52 KiB
TypeScript
import React, { useEffect, useState, useMemo } 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';
|
|
|
|
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;
|
|
}
|
|
|
|
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 (
|
|
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
|
<CollapsibleTrigger className="flex w-full items-center justify-between p-4 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg">
|
|
<div className="flex items-center gap-3">
|
|
<div className="i-ph:package text-bolt-elements-textSecondary w-4 h-4" />
|
|
<span className="text-base text-bolt-elements-textPrimary">
|
|
{title} Dependencies ({deps.length})
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm text-bolt-elements-textSecondary">{isOpen ? 'Hide' : 'Show'}</span>
|
|
<div
|
|
className={classNames(
|
|
'i-ph:caret-down w-4 h-4 transform transition-transform duration-200',
|
|
isOpen ? 'rotate-180' : '',
|
|
)}
|
|
/>
|
|
</div>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent>
|
|
<ScrollArea className="h-[200px] w-full p-4">
|
|
<div className="space-y-2 pl-7">
|
|
{deps.map((dep) => (
|
|
<div key={dep.name} className="flex items-center justify-between text-sm">
|
|
<span className="text-bolt-elements-textPrimary">{dep.name}</span>
|
|
<span className="text-bolt-elements-textSecondary">{dep.version}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</ScrollArea>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
);
|
|
};
|
|
|
|
export default function DebugTab() {
|
|
const [systemInfo, setSystemInfo] = useState<SystemInfo | null>(null);
|
|
const [webAppInfo, setWebAppInfo] = useState<WebAppInfo | null>(null);
|
|
const [loading, setLoading] = useState({
|
|
systemInfo: false,
|
|
webAppInfo: false,
|
|
errors: false,
|
|
performance: false,
|
|
});
|
|
const [openSections, setOpenSections] = useState({
|
|
system: false,
|
|
webapp: false,
|
|
errors: false,
|
|
performance: false,
|
|
});
|
|
|
|
// 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;
|
|
}
|
|
|
|
const interval = setInterval(async () => {
|
|
try {
|
|
const response = await fetch('/api/system/git-info');
|
|
const updatedGitInfo = (await response.json()) as GitInfo;
|
|
|
|
setWebAppInfo((prev) => {
|
|
if (!prev) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
...prev,
|
|
gitInfo: updatedGitInfo,
|
|
};
|
|
});
|
|
} catch (error) {
|
|
console.error('Failed to refresh git info:', error);
|
|
}
|
|
}, 5000);
|
|
|
|
const cleanup = () => {
|
|
clearInterval(interval);
|
|
};
|
|
|
|
return cleanup;
|
|
}, [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<WebAppInfo, 'gitInfo'>;
|
|
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');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col gap-6 max-w-7xl mx-auto p-4">
|
|
{/* Quick Stats Banner */}
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<div className="p-4 rounded-xl bg-gradient-to-br from-purple-500/10 to-purple-500/5 border border-purple-500/20">
|
|
<div className="text-sm text-bolt-elements-textSecondary">Memory Usage</div>
|
|
<div className="text-2xl font-semibold text-bolt-elements-textPrimary mt-1">
|
|
{systemInfo?.memory.percentage}%
|
|
</div>
|
|
<Progress value={systemInfo?.memory.percentage || 0} className="mt-2" />
|
|
</div>
|
|
|
|
<div className="p-4 rounded-xl bg-gradient-to-br from-blue-500/10 to-blue-500/5 border border-blue-500/20">
|
|
<div className="text-sm text-bolt-elements-textSecondary">Page Load Time</div>
|
|
<div className="text-2xl font-semibold text-bolt-elements-textPrimary mt-1">
|
|
{systemInfo ? (systemInfo.performance.timing.loadTime / 1000).toFixed(2) + 's' : '-'}
|
|
</div>
|
|
<div className="text-xs text-bolt-elements-textSecondary mt-2">
|
|
DOM Ready: {systemInfo ? (systemInfo.performance.timing.domReadyTime / 1000).toFixed(2) + 's' : '-'}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-4 rounded-xl bg-gradient-to-br from-green-500/10 to-green-500/5 border border-green-500/20">
|
|
<div className="text-sm text-bolt-elements-textSecondary">Network Speed</div>
|
|
<div className="text-2xl font-semibold text-bolt-elements-textPrimary mt-1">
|
|
{systemInfo?.network.downlink || '-'} Mbps
|
|
</div>
|
|
<div className="text-xs text-bolt-elements-textSecondary mt-2">RTT: {systemInfo?.network.rtt || '-'} ms</div>
|
|
</div>
|
|
|
|
<div className="p-4 rounded-xl bg-gradient-to-br from-red-500/10 to-red-500/5 border border-red-500/20">
|
|
<div className="text-sm text-bolt-elements-textSecondary">Errors</div>
|
|
<div className="text-2xl font-semibold text-bolt-elements-textPrimary mt-1">{errorLogs.length}</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Action Buttons */}
|
|
<div className="flex flex-wrap gap-4">
|
|
<button
|
|
onClick={getSystemInfo}
|
|
disabled={loading.systemInfo}
|
|
className={classNames(
|
|
'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
|
|
'bg-[#F5F5F5] hover:bg-purple-500/10 hover:text-purple-500',
|
|
'dark:bg-[#1A1A1A] dark:hover:bg-purple-500/20',
|
|
'text-bolt-elements-textPrimary dark:hover:text-purple-500',
|
|
'focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 dark:focus:ring-offset-[#0A0A0A]',
|
|
{ 'opacity-50 cursor-not-allowed': loading.systemInfo },
|
|
)}
|
|
>
|
|
{loading.systemInfo ? (
|
|
<div className="i-ph:spinner-gap w-4 h-4 animate-spin" />
|
|
) : (
|
|
<div className="i-ph:gear w-4 h-4" />
|
|
)}
|
|
Update System Info
|
|
</button>
|
|
|
|
<button
|
|
onClick={handleLogPerformance}
|
|
disabled={loading.performance}
|
|
className={classNames(
|
|
'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
|
|
'bg-[#F5F5F5] hover:bg-purple-500/10 hover:text-purple-500',
|
|
'dark:bg-[#1A1A1A] dark:hover:bg-purple-500/20',
|
|
'text-bolt-elements-textPrimary dark:hover:text-purple-500',
|
|
'focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 dark:focus:ring-offset-[#0A0A0A]',
|
|
{ 'opacity-50 cursor-not-allowed': loading.performance },
|
|
)}
|
|
>
|
|
{loading.performance ? (
|
|
<div className="i-ph:spinner-gap w-4 h-4 animate-spin" />
|
|
) : (
|
|
<div className="i-ph:chart-bar w-4 h-4" />
|
|
)}
|
|
Log Performance
|
|
</button>
|
|
|
|
<button
|
|
onClick={checkErrors}
|
|
disabled={loading.errors}
|
|
className={classNames(
|
|
'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
|
|
'bg-[#F5F5F5] hover:bg-purple-500/10 hover:text-purple-500',
|
|
'dark:bg-[#1A1A1A] dark:hover:bg-purple-500/20',
|
|
'text-bolt-elements-textPrimary dark:hover:text-purple-500',
|
|
'focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 dark:focus:ring-offset-[#0A0A0A]',
|
|
{ 'opacity-50 cursor-not-allowed': loading.errors },
|
|
)}
|
|
>
|
|
{loading.errors ? (
|
|
<div className="i-ph:spinner-gap w-4 h-4 animate-spin" />
|
|
) : (
|
|
<div className="i-ph:warning w-4 h-4" />
|
|
)}
|
|
Check Errors
|
|
</button>
|
|
|
|
<button
|
|
onClick={getWebAppInfo}
|
|
disabled={loading.webAppInfo}
|
|
className={classNames(
|
|
'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
|
|
'bg-[#F5F5F5] hover:bg-purple-500/10 hover:text-purple-500',
|
|
'dark:bg-[#1A1A1A] dark:hover:bg-purple-500/20',
|
|
'text-bolt-elements-textPrimary dark:hover:text-purple-500',
|
|
'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-[#0A0A0A]',
|
|
{ 'opacity-50 cursor-not-allowed': loading.webAppInfo },
|
|
)}
|
|
>
|
|
{loading.webAppInfo ? (
|
|
<div className="i-ph:spinner-gap w-4 h-4 animate-spin" />
|
|
) : (
|
|
<div className="i-ph:info w-4 h-4" />
|
|
)}
|
|
Fetch WebApp Info
|
|
</button>
|
|
|
|
<button
|
|
onClick={exportDebugInfo}
|
|
className={classNames(
|
|
'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
|
|
'bg-[#F5F5F5] hover:bg-purple-500/10 hover:text-purple-500',
|
|
'dark:bg-[#1A1A1A] dark:hover:bg-purple-500/20',
|
|
'text-bolt-elements-textPrimary dark:hover:text-purple-500',
|
|
'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-[#0A0A0A]',
|
|
)}
|
|
>
|
|
<div className="i-ph:download w-4 h-4" />
|
|
Export Debug Info
|
|
</button>
|
|
</div>
|
|
|
|
{/* System Information */}
|
|
<Collapsible
|
|
open={openSections.system}
|
|
onOpenChange={(open: boolean) => setOpenSections((prev) => ({ ...prev, system: open }))}
|
|
className="w-full"
|
|
>
|
|
<CollapsibleTrigger className="w-full">
|
|
<div className="flex items-center justify-between p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
|
|
<div className="flex items-center gap-3">
|
|
<div className="i-ph:cpu text-purple-500 w-5 h-5" />
|
|
<h3 className="text-base font-medium text-bolt-elements-textPrimary">System Information</h3>
|
|
</div>
|
|
<div
|
|
className={classNames(
|
|
'i-ph:caret-down w-4 h-4 transform transition-transform duration-200',
|
|
openSections.system ? 'rotate-180' : '',
|
|
)}
|
|
/>
|
|
</div>
|
|
</CollapsibleTrigger>
|
|
|
|
<CollapsibleContent>
|
|
<div className="p-6 mt-2 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
|
|
{systemInfo ? (
|
|
<div className="grid grid-cols-2 gap-6">
|
|
<div className="space-y-2">
|
|
<div className="text-sm flex items-center gap-2">
|
|
<div className="i-ph:desktop text-bolt-elements-textSecondary w-4 h-4" />
|
|
<span className="text-bolt-elements-textSecondary">OS: </span>
|
|
<span className="text-bolt-elements-textPrimary">{systemInfo.os}</span>
|
|
</div>
|
|
<div className="text-sm flex items-center gap-2">
|
|
<div className="i-ph:device-mobile text-bolt-elements-textSecondary w-4 h-4" />
|
|
<span className="text-bolt-elements-textSecondary">Platform: </span>
|
|
<span className="text-bolt-elements-textPrimary">{systemInfo.platform}</span>
|
|
</div>
|
|
<div className="text-sm flex items-center gap-2">
|
|
<div className="i-ph:microchip text-bolt-elements-textSecondary w-4 h-4" />
|
|
<span className="text-bolt-elements-textSecondary">Architecture: </span>
|
|
<span className="text-bolt-elements-textPrimary">{systemInfo.arch}</span>
|
|
</div>
|
|
<div className="text-sm flex items-center gap-2">
|
|
<div className="i-ph:cpu text-bolt-elements-textSecondary w-4 h-4" />
|
|
<span className="text-bolt-elements-textSecondary">CPU Cores: </span>
|
|
<span className="text-bolt-elements-textPrimary">{systemInfo.cpus}</span>
|
|
</div>
|
|
<div className="text-sm flex items-center gap-2">
|
|
<div className="i-ph:node text-bolt-elements-textSecondary w-4 h-4" />
|
|
<span className="text-bolt-elements-textSecondary">Node Version: </span>
|
|
<span className="text-bolt-elements-textPrimary">{systemInfo.node}</span>
|
|
</div>
|
|
<div className="text-sm flex items-center gap-2">
|
|
<div className="i-ph:wifi-high text-bolt-elements-textSecondary w-4 h-4" />
|
|
<span className="text-bolt-elements-textSecondary">Network Type: </span>
|
|
<span className="text-bolt-elements-textPrimary">
|
|
{systemInfo.network.type} ({systemInfo.network.effectiveType})
|
|
</span>
|
|
</div>
|
|
<div className="text-sm flex items-center gap-2">
|
|
<div className="i-ph:gauge text-bolt-elements-textSecondary w-4 h-4" />
|
|
<span className="text-bolt-elements-textSecondary">Network Speed: </span>
|
|
<span className="text-bolt-elements-textPrimary">
|
|
{systemInfo.network.downlink}Mbps (RTT: {systemInfo.network.rtt}ms)
|
|
</span>
|
|
</div>
|
|
{systemInfo.battery && (
|
|
<div className="text-sm flex items-center gap-2">
|
|
<div className="i-ph:battery-charging text-bolt-elements-textSecondary w-4 h-4" />
|
|
<span className="text-bolt-elements-textSecondary">Battery: </span>
|
|
<span className="text-bolt-elements-textPrimary">
|
|
{systemInfo.battery.level.toFixed(1)}% {systemInfo.battery.charging ? '(Charging)' : ''}
|
|
</span>
|
|
</div>
|
|
)}
|
|
<div className="text-sm flex items-center gap-2">
|
|
<div className="i-ph:hard-drive text-bolt-elements-textSecondary w-4 h-4" />
|
|
<span className="text-bolt-elements-textSecondary">Storage: </span>
|
|
<span className="text-bolt-elements-textPrimary">
|
|
{(systemInfo.storage.usage / (1024 * 1024 * 1024)).toFixed(2)}GB /{' '}
|
|
{(systemInfo.storage.quota / (1024 * 1024 * 1024)).toFixed(2)}GB
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<div className="text-sm flex items-center gap-2">
|
|
<div className="i-ph:database text-bolt-elements-textSecondary w-4 h-4" />
|
|
<span className="text-bolt-elements-textSecondary">Memory Usage: </span>
|
|
<span className="text-bolt-elements-textPrimary">
|
|
{systemInfo.memory.used} / {systemInfo.memory.total} ({systemInfo.memory.percentage}%)
|
|
</span>
|
|
</div>
|
|
<div className="text-sm flex items-center gap-2">
|
|
<div className="i-ph:browser text-bolt-elements-textSecondary w-4 h-4" />
|
|
<span className="text-bolt-elements-textSecondary">Browser: </span>
|
|
<span className="text-bolt-elements-textPrimary">
|
|
{systemInfo.browser.name} {systemInfo.browser.version}
|
|
</span>
|
|
</div>
|
|
<div className="text-sm flex items-center gap-2">
|
|
<div className="i-ph:monitor text-bolt-elements-textSecondary w-4 h-4" />
|
|
<span className="text-bolt-elements-textSecondary">Screen: </span>
|
|
<span className="text-bolt-elements-textPrimary">
|
|
{systemInfo.screen.width}x{systemInfo.screen.height} ({systemInfo.screen.pixelRatio}x)
|
|
</span>
|
|
</div>
|
|
<div className="text-sm flex items-center gap-2">
|
|
<div className="i-ph:clock text-bolt-elements-textSecondary w-4 h-4" />
|
|
<span className="text-bolt-elements-textSecondary">Timezone: </span>
|
|
<span className="text-bolt-elements-textPrimary">{systemInfo.time.timezone}</span>
|
|
</div>
|
|
<div className="text-sm flex items-center gap-2">
|
|
<div className="i-ph:translate text-bolt-elements-textSecondary w-4 h-4" />
|
|
<span className="text-bolt-elements-textSecondary">Language: </span>
|
|
<span className="text-bolt-elements-textPrimary">{systemInfo.browser.language}</span>
|
|
</div>
|
|
<div className="text-sm flex items-center gap-2">
|
|
<div className="i-ph:chart-pie text-bolt-elements-textSecondary w-4 h-4" />
|
|
<span className="text-bolt-elements-textSecondary">JS Heap: </span>
|
|
<span className="text-bolt-elements-textPrimary">
|
|
{(systemInfo.performance.memory.usedJSHeapSize / (1024 * 1024)).toFixed(1)}MB /{' '}
|
|
{(systemInfo.performance.memory.totalJSHeapSize / (1024 * 1024)).toFixed(1)}MB (
|
|
{systemInfo.performance.memory.usagePercentage.toFixed(1)}%)
|
|
</span>
|
|
</div>
|
|
<div className="text-sm flex items-center gap-2">
|
|
<div className="i-ph:timer text-bolt-elements-textSecondary w-4 h-4" />
|
|
<span className="text-bolt-elements-textSecondary">Page Load: </span>
|
|
<span className="text-bolt-elements-textPrimary">
|
|
{(systemInfo.performance.timing.loadTime / 1000).toFixed(2)}s
|
|
</span>
|
|
</div>
|
|
<div className="text-sm flex items-center gap-2">
|
|
<div className="i-ph:code text-bolt-elements-textSecondary w-4 h-4" />
|
|
<span className="text-bolt-elements-textSecondary">DOM Ready: </span>
|
|
<span className="text-bolt-elements-textPrimary">
|
|
{(systemInfo.performance.timing.domReadyTime / 1000).toFixed(2)}s
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="text-sm text-bolt-elements-textSecondary">Loading system information...</div>
|
|
)}
|
|
</div>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
|
|
{/* Performance Metrics */}
|
|
<Collapsible
|
|
open={openSections.performance}
|
|
onOpenChange={(open: boolean) => setOpenSections((prev) => ({ ...prev, performance: open }))}
|
|
className="w-full"
|
|
>
|
|
<CollapsibleTrigger className="w-full">
|
|
<div className="flex items-center justify-between p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
|
|
<div className="flex items-center gap-3">
|
|
<div className="i-ph:chart-line text-purple-500 w-5 h-5" />
|
|
<h3 className="text-base font-medium text-bolt-elements-textPrimary">Performance Metrics</h3>
|
|
</div>
|
|
<div
|
|
className={classNames(
|
|
'i-ph:caret-down w-4 h-4 transform transition-transform duration-200',
|
|
openSections.performance ? 'rotate-180' : '',
|
|
)}
|
|
/>
|
|
</div>
|
|
</CollapsibleTrigger>
|
|
|
|
<CollapsibleContent>
|
|
<div className="p-6 mt-2 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
|
|
{systemInfo && (
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<div className="text-sm">
|
|
<span className="text-bolt-elements-textSecondary">Page Load Time: </span>
|
|
<span className="text-bolt-elements-textPrimary">
|
|
{(systemInfo.performance.timing.loadTime / 1000).toFixed(2)}s
|
|
</span>
|
|
</div>
|
|
<div className="text-sm">
|
|
<span className="text-bolt-elements-textSecondary">DOM Ready Time: </span>
|
|
<span className="text-bolt-elements-textPrimary">
|
|
{(systemInfo.performance.timing.domReadyTime / 1000).toFixed(2)}s
|
|
</span>
|
|
</div>
|
|
<div className="text-sm">
|
|
<span className="text-bolt-elements-textSecondary">Request Time: </span>
|
|
<span className="text-bolt-elements-textPrimary">
|
|
{(systemInfo.performance.timing.requestTime / 1000).toFixed(2)}s
|
|
</span>
|
|
</div>
|
|
<div className="text-sm">
|
|
<span className="text-bolt-elements-textSecondary">Redirect Time: </span>
|
|
<span className="text-bolt-elements-textPrimary">
|
|
{(systemInfo.performance.timing.redirectTime / 1000).toFixed(2)}s
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<div className="text-sm">
|
|
<span className="text-bolt-elements-textSecondary">JS Heap Usage: </span>
|
|
<span className="text-bolt-elements-textPrimary">
|
|
{(systemInfo.performance.memory.usedJSHeapSize / (1024 * 1024)).toFixed(1)}MB /{' '}
|
|
{(systemInfo.performance.memory.totalJSHeapSize / (1024 * 1024)).toFixed(1)}MB
|
|
</span>
|
|
</div>
|
|
<div className="text-sm">
|
|
<span className="text-bolt-elements-textSecondary">Heap Utilization: </span>
|
|
<span className="text-bolt-elements-textPrimary">
|
|
{systemInfo.performance.memory.usagePercentage.toFixed(1)}%
|
|
</span>
|
|
</div>
|
|
<div className="text-sm">
|
|
<span className="text-bolt-elements-textSecondary">Navigation Type: </span>
|
|
<span className="text-bolt-elements-textPrimary">
|
|
{systemInfo.performance.navigation.type === 0
|
|
? 'Navigate'
|
|
: systemInfo.performance.navigation.type === 1
|
|
? 'Reload'
|
|
: systemInfo.performance.navigation.type === 2
|
|
? 'Back/Forward'
|
|
: 'Other'}
|
|
</span>
|
|
</div>
|
|
<div className="text-sm">
|
|
<span className="text-bolt-elements-textSecondary">Redirects: </span>
|
|
<span className="text-bolt-elements-textPrimary">
|
|
{systemInfo.performance.navigation.redirectCount}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
|
|
{/* WebApp Information */}
|
|
<Collapsible
|
|
open={openSections.webapp}
|
|
onOpenChange={(open) => setOpenSections((prev) => ({ ...prev, webapp: open }))}
|
|
className="w-full"
|
|
>
|
|
<CollapsibleTrigger className="w-full">
|
|
<div className="flex items-center justify-between p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
|
|
<div className="flex items-center gap-3">
|
|
<div className="i-ph:info text-blue-500 w-5 h-5" />
|
|
<h3 className="text-base font-medium text-bolt-elements-textPrimary">WebApp Information</h3>
|
|
{loading.webAppInfo && <span className="loading loading-spinner loading-sm" />}
|
|
</div>
|
|
<div
|
|
className={classNames(
|
|
'i-ph:caret-down w-4 h-4 transform transition-transform duration-200',
|
|
openSections.webapp ? 'rotate-180' : '',
|
|
)}
|
|
/>
|
|
</div>
|
|
</CollapsibleTrigger>
|
|
|
|
<CollapsibleContent>
|
|
<div className="p-6 mt-2 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
|
|
{loading.webAppInfo ? (
|
|
<div className="flex items-center justify-center p-8">
|
|
<span className="loading loading-spinner loading-lg" />
|
|
</div>
|
|
) : !webAppInfo ? (
|
|
<div className="flex flex-col items-center justify-center p-8 text-bolt-elements-textSecondary">
|
|
<div className="i-ph:warning-circle w-8 h-8 mb-2" />
|
|
<p>Failed to load WebApp information</p>
|
|
<button
|
|
onClick={() => getWebAppInfo()}
|
|
className="mt-4 px-4 py-2 text-sm bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
|
|
>
|
|
Retry
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-2 gap-6">
|
|
<div>
|
|
<h3 className="mb-4 text-base font-medium text-bolt-elements-textPrimary">Basic Information</h3>
|
|
<div className="space-y-3">
|
|
<div className="text-sm flex items-center gap-2">
|
|
<div className="i-ph:app-window text-bolt-elements-textSecondary w-4 h-4" />
|
|
<span className="text-bolt-elements-textSecondary">Name:</span>
|
|
<span className="text-bolt-elements-textPrimary">{webAppInfo.name}</span>
|
|
</div>
|
|
<div className="text-sm flex items-center gap-2">
|
|
<div className="i-ph:tag text-bolt-elements-textSecondary w-4 h-4" />
|
|
<span className="text-bolt-elements-textSecondary">Version:</span>
|
|
<span className="text-bolt-elements-textPrimary">{webAppInfo.version}</span>
|
|
</div>
|
|
<div className="text-sm flex items-center gap-2">
|
|
<div className="i-ph:certificate text-bolt-elements-textSecondary w-4 h-4" />
|
|
<span className="text-bolt-elements-textSecondary">License:</span>
|
|
<span className="text-bolt-elements-textPrimary">{webAppInfo.license}</span>
|
|
</div>
|
|
<div className="text-sm flex items-center gap-2">
|
|
<div className="i-ph:cloud text-bolt-elements-textSecondary w-4 h-4" />
|
|
<span className="text-bolt-elements-textSecondary">Environment:</span>
|
|
<span className="text-bolt-elements-textPrimary">{webAppInfo.environment}</span>
|
|
</div>
|
|
<div className="text-sm flex items-center gap-2">
|
|
<div className="i-ph:node text-bolt-elements-textSecondary w-4 h-4" />
|
|
<span className="text-bolt-elements-textSecondary">Node Version:</span>
|
|
<span className="text-bolt-elements-textPrimary">{webAppInfo.runtimeInfo.nodeVersion}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h3 className="mb-4 text-base font-medium text-bolt-elements-textPrimary">Git Information</h3>
|
|
<div className="space-y-3">
|
|
<div className="text-sm flex items-center gap-2">
|
|
<div className="i-ph:git-branch text-bolt-elements-textSecondary w-4 h-4" />
|
|
<span className="text-bolt-elements-textSecondary">Branch:</span>
|
|
<span className="text-bolt-elements-textPrimary">{webAppInfo.gitInfo.local.branch}</span>
|
|
</div>
|
|
<div className="text-sm flex items-center gap-2">
|
|
<div className="i-ph:git-commit text-bolt-elements-textSecondary w-4 h-4" />
|
|
<span className="text-bolt-elements-textSecondary">Commit:</span>
|
|
<span className="text-bolt-elements-textPrimary">{webAppInfo.gitInfo.local.commitHash}</span>
|
|
</div>
|
|
<div className="text-sm flex items-center gap-2">
|
|
<div className="i-ph:user text-bolt-elements-textSecondary w-4 h-4" />
|
|
<span className="text-bolt-elements-textSecondary">Author:</span>
|
|
<span className="text-bolt-elements-textPrimary">{webAppInfo.gitInfo.local.author}</span>
|
|
</div>
|
|
<div className="text-sm flex items-center gap-2">
|
|
<div className="i-ph:clock text-bolt-elements-textSecondary w-4 h-4" />
|
|
<span className="text-bolt-elements-textSecondary">Commit Time:</span>
|
|
<span className="text-bolt-elements-textPrimary">{webAppInfo.gitInfo.local.commitTime}</span>
|
|
</div>
|
|
|
|
{webAppInfo.gitInfo.github && (
|
|
<>
|
|
<div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-800">
|
|
<div className="text-sm flex items-center gap-2">
|
|
<div className="i-ph:git-repository text-bolt-elements-textSecondary w-4 h-4" />
|
|
<span className="text-bolt-elements-textSecondary">Repository:</span>
|
|
<span className="text-bolt-elements-textPrimary">
|
|
{webAppInfo.gitInfo.github.currentRepo.fullName}
|
|
{webAppInfo.gitInfo.isForked && ' (fork)'}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="mt-2 flex items-center gap-4 text-sm">
|
|
<div className="flex items-center gap-1">
|
|
<div className="i-ph:star text-yellow-500 w-4 h-4" />
|
|
<span className="text-bolt-elements-textSecondary">
|
|
{webAppInfo.gitInfo.github.currentRepo.stars}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<div className="i-ph:git-fork text-blue-500 w-4 h-4" />
|
|
<span className="text-bolt-elements-textSecondary">
|
|
{webAppInfo.gitInfo.github.currentRepo.forks}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<div className="i-ph:warning-circle text-red-500 w-4 h-4" />
|
|
<span className="text-bolt-elements-textSecondary">
|
|
{webAppInfo.gitInfo.github.currentRepo.openIssues}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{webAppInfo.gitInfo.github.upstream && (
|
|
<div className="mt-2">
|
|
<div className="text-sm flex items-center gap-2">
|
|
<div className="i-ph:git-fork text-bolt-elements-textSecondary w-4 h-4" />
|
|
<span className="text-bolt-elements-textSecondary">Upstream:</span>
|
|
<span className="text-bolt-elements-textPrimary">
|
|
{webAppInfo.gitInfo.github.upstream.fullName}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="mt-2 flex items-center gap-4 text-sm">
|
|
<div className="flex items-center gap-1">
|
|
<div className="i-ph:star text-yellow-500 w-4 h-4" />
|
|
<span className="text-bolt-elements-textSecondary">
|
|
{webAppInfo.gitInfo.github.upstream.stars}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<div className="i-ph:git-fork text-blue-500 w-4 h-4" />
|
|
<span className="text-bolt-elements-textSecondary">
|
|
{webAppInfo.gitInfo.github.upstream.forks}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{webAppInfo && (
|
|
<div className="mt-6">
|
|
<h3 className="mb-4 text-base font-medium text-bolt-elements-textPrimary">Dependencies</h3>
|
|
<div className="space-y-2 bg-gray-50 dark:bg-[#1A1A1A] rounded-lg">
|
|
<DependencySection title="Production" deps={webAppInfo.dependencies.production} />
|
|
<DependencySection title="Development" deps={webAppInfo.dependencies.development} />
|
|
<DependencySection title="Peer" deps={webAppInfo.dependencies.peer} />
|
|
<DependencySection title="Optional" deps={webAppInfo.dependencies.optional} />
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
|
|
{/* Error Check */}
|
|
<Collapsible
|
|
open={openSections.errors}
|
|
onOpenChange={(open) => setOpenSections((prev) => ({ ...prev, errors: open }))}
|
|
className="w-full"
|
|
>
|
|
<CollapsibleTrigger className="w-full">
|
|
<div className="flex items-center justify-between p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
|
|
<div className="flex items-center gap-3">
|
|
<div className="i-ph:warning text-red-500 w-5 h-5" />
|
|
<h3 className="text-base font-medium text-bolt-elements-textPrimary">Error Check</h3>
|
|
{errorLogs.length > 0 && (
|
|
<Badge variant="destructive" className="ml-2">
|
|
{errorLogs.length} Errors
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
<div
|
|
className={classNames(
|
|
'i-ph:caret-down w-4 h-4 transform transition-transform duration-200',
|
|
openSections.errors ? 'rotate-180' : '',
|
|
)}
|
|
/>
|
|
</div>
|
|
</CollapsibleTrigger>
|
|
|
|
<CollapsibleContent>
|
|
<div className="p-6 mt-2 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
|
|
<ScrollArea className="h-[300px]">
|
|
<div className="space-y-4">
|
|
<div className="text-sm text-bolt-elements-textSecondary">
|
|
Checks for:
|
|
<ul className="list-disc list-inside mt-2 space-y-1">
|
|
<li>Unhandled JavaScript errors</li>
|
|
<li>Unhandled Promise rejections</li>
|
|
<li>Runtime exceptions</li>
|
|
<li>Network errors</li>
|
|
</ul>
|
|
</div>
|
|
<div className="text-sm">
|
|
<span className="text-bolt-elements-textSecondary">Status: </span>
|
|
<span className="text-bolt-elements-textPrimary">
|
|
{loading.errors
|
|
? 'Checking...'
|
|
: errorLogs.length > 0
|
|
? `${errorLogs.length} errors found`
|
|
: 'No errors found'}
|
|
</span>
|
|
</div>
|
|
{errorLogs.length > 0 && (
|
|
<div className="mt-4">
|
|
<div className="text-sm font-medium text-bolt-elements-textPrimary mb-2">Recent Errors:</div>
|
|
<div className="space-y-2">
|
|
{errorLogs.map((error) => (
|
|
<div key={error.id} className="text-sm text-red-500 dark:text-red-400 p-2 rounded bg-red-500/5">
|
|
<div className="font-medium">{error.message}</div>
|
|
{error.source && (
|
|
<div className="text-xs mt-1 text-red-400">
|
|
Source: {error.source}
|
|
{error.details?.lineNumber && `:${error.details.lineNumber}`}
|
|
</div>
|
|
)}
|
|
{error.stack && (
|
|
<div className="text-xs mt-1 text-red-400 font-mono whitespace-pre-wrap">{error.stack}</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</ScrollArea>
|
|
</div>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
</div>
|
|
);
|
|
}
|