Spaces:
Build error
Build error
| import * as React from 'react'; | |
| import { useEffect, useState, useRef, useCallback } from 'react'; | |
| import { classNames } from '~/utils/classNames'; | |
| import { Line } from 'react-chartjs-2'; | |
| import { | |
| Chart as ChartJS, | |
| CategoryScale, | |
| LinearScale, | |
| PointElement, | |
| LineElement, | |
| Title, | |
| Tooltip, | |
| Legend, | |
| } from 'chart.js'; | |
| import { toast } from 'react-toastify'; // Import toast | |
| import { useUpdateCheck } from '~/lib/hooks/useUpdateCheck'; | |
| import { tabConfigurationStore, type TabConfig } from '~/lib/stores/tabConfigurationStore'; | |
| import { useStore } from 'zustand'; | |
| // Register ChartJS components | |
| ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend); | |
| interface BatteryManager extends EventTarget { | |
| charging: boolean; | |
| chargingTime: number; | |
| dischargingTime: number; | |
| level: number; | |
| } | |
| interface SystemMetrics { | |
| cpu: { | |
| usage: number; | |
| cores: number[]; | |
| temperature?: number; | |
| frequency?: number; | |
| }; | |
| memory: { | |
| used: number; | |
| total: number; | |
| percentage: number; | |
| heap: { | |
| used: number; | |
| total: number; | |
| limit: number; | |
| }; | |
| cache?: number; | |
| }; | |
| uptime: number; | |
| battery?: { | |
| level: number; | |
| charging: boolean; | |
| timeRemaining?: number; | |
| temperature?: number; | |
| cycles?: number; | |
| health?: number; | |
| }; | |
| network: { | |
| downlink: number; | |
| uplink?: number; | |
| latency: number; | |
| type: string; | |
| activeConnections?: number; | |
| bytesReceived: number; | |
| bytesSent: number; | |
| }; | |
| performance: { | |
| fps: number; | |
| pageLoad: number; | |
| domReady: number; | |
| resources: { | |
| total: number; | |
| size: number; | |
| loadTime: number; | |
| }; | |
| timing: { | |
| ttfb: number; | |
| fcp: number; | |
| lcp: number; | |
| }; | |
| }; | |
| health: { | |
| score: number; | |
| issues: string[]; | |
| suggestions: string[]; | |
| }; | |
| } | |
| interface MetricsHistory { | |
| timestamps: string[]; | |
| cpu: number[]; | |
| memory: number[]; | |
| battery: number[]; | |
| network: number[]; | |
| } | |
| interface EnergySavings { | |
| updatesReduced: number; | |
| timeInSaverMode: number; | |
| estimatedEnergySaved: number; // in mWh (milliwatt-hours) | |
| } | |
| interface PowerProfile { | |
| name: string; | |
| description: string; | |
| settings: { | |
| updateInterval: number; | |
| enableAnimations: boolean; | |
| backgroundProcessing: boolean; | |
| networkThrottling: boolean; | |
| }; | |
| } | |
| interface PerformanceAlert { | |
| type: 'warning' | 'error' | 'info'; | |
| message: string; | |
| timestamp: number; | |
| metric: string; | |
| threshold: number; | |
| value: number; | |
| } | |
| declare global { | |
| interface Navigator { | |
| getBattery(): Promise<BatteryManager>; | |
| } | |
| interface Performance { | |
| memory?: { | |
| jsHeapSizeLimit: number; | |
| totalJSHeapSize: number; | |
| usedJSHeapSize: number; | |
| }; | |
| } | |
| } | |
| // Constants for update intervals | |
| const UPDATE_INTERVALS = { | |
| normal: { | |
| metrics: 1000, // 1 second | |
| animation: 16, // ~60fps | |
| }, | |
| energySaver: { | |
| metrics: 5000, // 5 seconds | |
| animation: 32, // ~30fps | |
| }, | |
| }; | |
| // Constants for performance thresholds | |
| const PERFORMANCE_THRESHOLDS = { | |
| cpu: { | |
| warning: 70, | |
| critical: 90, | |
| }, | |
| memory: { | |
| warning: 80, | |
| critical: 95, | |
| }, | |
| fps: { | |
| warning: 30, | |
| critical: 15, | |
| }, | |
| }; | |
| // Constants for energy calculations | |
| const ENERGY_COSTS = { | |
| update: 0.1, // mWh per update | |
| }; | |
| // Default power profiles | |
| const POWER_PROFILES: PowerProfile[] = [ | |
| { | |
| name: 'Performance', | |
| description: 'Maximum performance with frequent updates', | |
| settings: { | |
| updateInterval: UPDATE_INTERVALS.normal.metrics, | |
| enableAnimations: true, | |
| backgroundProcessing: true, | |
| networkThrottling: false, | |
| }, | |
| }, | |
| { | |
| name: 'Balanced', | |
| description: 'Optimal balance between performance and energy efficiency', | |
| settings: { | |
| updateInterval: 2000, | |
| enableAnimations: true, | |
| backgroundProcessing: true, | |
| networkThrottling: false, | |
| }, | |
| }, | |
| { | |
| name: 'Energy Saver', | |
| description: 'Maximum energy efficiency with reduced updates', | |
| settings: { | |
| updateInterval: UPDATE_INTERVALS.energySaver.metrics, | |
| enableAnimations: false, | |
| backgroundProcessing: false, | |
| networkThrottling: true, | |
| }, | |
| }, | |
| ]; | |
| // Default metrics state | |
| const DEFAULT_METRICS_STATE: SystemMetrics = { | |
| cpu: { | |
| usage: 0, | |
| cores: [], | |
| }, | |
| memory: { | |
| used: 0, | |
| total: 0, | |
| percentage: 0, | |
| heap: { | |
| used: 0, | |
| total: 0, | |
| limit: 0, | |
| }, | |
| }, | |
| uptime: 0, | |
| network: { | |
| downlink: 0, | |
| latency: 0, | |
| type: 'unknown', | |
| bytesReceived: 0, | |
| bytesSent: 0, | |
| }, | |
| performance: { | |
| fps: 0, | |
| pageLoad: 0, | |
| domReady: 0, | |
| resources: { | |
| total: 0, | |
| size: 0, | |
| loadTime: 0, | |
| }, | |
| timing: { | |
| ttfb: 0, | |
| fcp: 0, | |
| lcp: 0, | |
| }, | |
| }, | |
| health: { | |
| score: 0, | |
| issues: [], | |
| suggestions: [], | |
| }, | |
| }; | |
| // Default metrics history | |
| const DEFAULT_METRICS_HISTORY: MetricsHistory = { | |
| timestamps: Array(10).fill(new Date().toLocaleTimeString()), | |
| cpu: Array(10).fill(0), | |
| memory: Array(10).fill(0), | |
| battery: Array(10).fill(0), | |
| network: Array(10).fill(0), | |
| }; | |
| // Battery threshold for auto energy saver mode | |
| const BATTERY_THRESHOLD = 20; // percentage | |
| // Maximum number of history points to keep | |
| const MAX_HISTORY_POINTS = 10; | |
| const TaskManagerTab: React.FC = () => { | |
| // Initialize metrics state with defaults | |
| const [metrics, setMetrics] = useState<SystemMetrics>(() => DEFAULT_METRICS_STATE); | |
| const [metricsHistory, setMetricsHistory] = useState<MetricsHistory>(() => DEFAULT_METRICS_HISTORY); | |
| const [energySaverMode, setEnergySaverMode] = useState<boolean>(false); | |
| const [autoEnergySaver, setAutoEnergySaver] = useState<boolean>(false); | |
| const [energySavings, setEnergySavings] = useState<EnergySavings>(() => ({ | |
| updatesReduced: 0, | |
| timeInSaverMode: 0, | |
| estimatedEnergySaved: 0, | |
| })); | |
| const [selectedProfile, setSelectedProfile] = useState<PowerProfile>(() => POWER_PROFILES[1]); | |
| const [alerts, setAlerts] = useState<PerformanceAlert[]>([]); | |
| const saverModeStartTime = useRef<number | null>(null); | |
| // Get update status and tab configuration | |
| const { hasUpdate } = useUpdateCheck(); | |
| const tabConfig = useStore(tabConfigurationStore); | |
| const resetTabConfiguration = useCallback(() => { | |
| tabConfig.reset(); | |
| return tabConfig.get(); | |
| }, [tabConfig]); | |
| // Effect to handle tab visibility | |
| useEffect(() => { | |
| const handleTabVisibility = () => { | |
| const currentConfig = tabConfig.get(); | |
| const controlledTabs = ['debug', 'update']; | |
| // Update visibility based on conditions | |
| const updatedTabs = currentConfig.userTabs.map((tab: TabConfig) => { | |
| if (controlledTabs.includes(tab.id)) { | |
| return { | |
| ...tab, | |
| visible: tab.id === 'debug' ? metrics.cpu.usage > 80 : hasUpdate, | |
| }; | |
| } | |
| return tab; | |
| }); | |
| tabConfig.set({ | |
| ...currentConfig, | |
| userTabs: updatedTabs, | |
| }); | |
| }; | |
| const checkInterval = setInterval(handleTabVisibility, 5000); | |
| return () => { | |
| clearInterval(checkInterval); | |
| }; | |
| }, [metrics.cpu.usage, hasUpdate, tabConfig]); | |
| // Effect to handle reset and initialization | |
| useEffect(() => { | |
| const resetToDefaults = () => { | |
| console.log('TaskManagerTab: Resetting to defaults'); | |
| // Reset metrics and local state | |
| setMetrics(DEFAULT_METRICS_STATE); | |
| setMetricsHistory(DEFAULT_METRICS_HISTORY); | |
| setEnergySaverMode(false); | |
| setAutoEnergySaver(false); | |
| setEnergySavings({ | |
| updatesReduced: 0, | |
| timeInSaverMode: 0, | |
| estimatedEnergySaved: 0, | |
| }); | |
| setSelectedProfile(POWER_PROFILES[1]); | |
| setAlerts([]); | |
| saverModeStartTime.current = null; | |
| // Reset tab configuration to ensure proper visibility | |
| const defaultConfig = resetTabConfiguration(); | |
| console.log('TaskManagerTab: Reset tab configuration:', defaultConfig); | |
| }; | |
| // Listen for both storage changes and custom reset event | |
| const handleReset = (event: Event | StorageEvent) => { | |
| if (event instanceof StorageEvent) { | |
| if (event.key === 'tabConfiguration' && event.newValue === null) { | |
| resetToDefaults(); | |
| } | |
| } else if (event instanceof CustomEvent && event.type === 'tabConfigReset') { | |
| resetToDefaults(); | |
| } | |
| }; | |
| // Initial setup | |
| const initializeTab = async () => { | |
| try { | |
| // Load saved preferences | |
| const savedEnergySaver = localStorage.getItem('energySaverMode'); | |
| const savedAutoSaver = localStorage.getItem('autoEnergySaver'); | |
| const savedProfile = localStorage.getItem('selectedProfile'); | |
| if (savedEnergySaver) { | |
| setEnergySaverMode(JSON.parse(savedEnergySaver)); | |
| } | |
| if (savedAutoSaver) { | |
| setAutoEnergySaver(JSON.parse(savedAutoSaver)); | |
| } | |
| if (savedProfile) { | |
| const profile = POWER_PROFILES.find((p) => p.name === savedProfile); | |
| if (profile) { | |
| setSelectedProfile(profile); | |
| } | |
| } | |
| await updateMetrics(); | |
| } catch (error) { | |
| console.error('Failed to initialize TaskManagerTab:', error); | |
| resetToDefaults(); | |
| } | |
| }; | |
| window.addEventListener('storage', handleReset); | |
| window.addEventListener('tabConfigReset', handleReset); | |
| initializeTab(); | |
| return () => { | |
| window.removeEventListener('storage', handleReset); | |
| window.removeEventListener('tabConfigReset', handleReset); | |
| }; | |
| }, []); | |
| // Get detailed performance metrics | |
| const getPerformanceMetrics = async (): Promise<Partial<SystemMetrics['performance']>> => { | |
| try { | |
| // Get FPS | |
| const fps = await measureFrameRate(); | |
| // Get page load metrics | |
| const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming; | |
| const pageLoad = navigation.loadEventEnd - navigation.startTime; | |
| const domReady = navigation.domContentLoadedEventEnd - navigation.startTime; | |
| // Get resource metrics | |
| const resources = performance.getEntriesByType('resource') as PerformanceResourceTiming[]; | |
| const resourceMetrics = { | |
| total: resources.length, | |
| size: resources.reduce((total, r) => total + (r.transferSize || 0), 0), | |
| loadTime: Math.max(0, ...resources.map((r) => r.duration)), | |
| }; | |
| // Get Web Vitals | |
| const ttfb = navigation.responseStart - navigation.requestStart; | |
| const paintEntries = performance.getEntriesByType('paint'); | |
| const fcp = paintEntries.find((entry) => entry.name === 'first-contentful-paint')?.startTime || 0; | |
| const lcpEntry = await getLargestContentfulPaint(); | |
| return { | |
| fps, | |
| pageLoad, | |
| domReady, | |
| resources: resourceMetrics, | |
| timing: { | |
| ttfb, | |
| fcp, | |
| lcp: lcpEntry?.startTime || 0, | |
| }, | |
| }; | |
| } catch (error) { | |
| console.error('Failed to get performance metrics:', error); | |
| return {}; | |
| } | |
| }; | |
| // Single useEffect for metrics updates | |
| useEffect(() => { | |
| let isComponentMounted = true; | |
| const updateMetricsWrapper = async () => { | |
| if (!isComponentMounted) { | |
| return; | |
| } | |
| try { | |
| await updateMetrics(); | |
| } catch (error) { | |
| console.error('Failed to update metrics:', error); | |
| } | |
| }; | |
| // Initial update | |
| updateMetricsWrapper(); | |
| // Set up interval with immediate assignment | |
| const metricsInterval = setInterval( | |
| updateMetricsWrapper, | |
| energySaverMode ? UPDATE_INTERVALS.energySaver.metrics : UPDATE_INTERVALS.normal.metrics, | |
| ); | |
| // Cleanup function | |
| return () => { | |
| isComponentMounted = false; | |
| clearInterval(metricsInterval); | |
| }; | |
| }, [energySaverMode]); // Only depend on energySaverMode | |
| // Handle energy saver mode changes | |
| const handleEnergySaverChange = (checked: boolean) => { | |
| setEnergySaverMode(checked); | |
| localStorage.setItem('energySaverMode', JSON.stringify(checked)); | |
| toast.success(checked ? 'Energy Saver mode enabled' : 'Energy Saver mode disabled'); | |
| }; | |
| // Handle auto energy saver changes | |
| const handleAutoEnergySaverChange = (checked: boolean) => { | |
| setAutoEnergySaver(checked); | |
| localStorage.setItem('autoEnergySaver', JSON.stringify(checked)); | |
| toast.success(checked ? 'Auto Energy Saver enabled' : 'Auto Energy Saver disabled'); | |
| if (!checked) { | |
| // When disabling auto mode, also disable energy saver mode | |
| setEnergySaverMode(false); | |
| localStorage.setItem('energySaverMode', 'false'); | |
| } | |
| }; | |
| // Update energy savings calculation | |
| const updateEnergySavings = useCallback(() => { | |
| if (!energySaverMode) { | |
| saverModeStartTime.current = null; | |
| setEnergySavings({ | |
| updatesReduced: 0, | |
| timeInSaverMode: 0, | |
| estimatedEnergySaved: 0, | |
| }); | |
| return; | |
| } | |
| if (!saverModeStartTime.current) { | |
| saverModeStartTime.current = Date.now(); | |
| } | |
| const timeInSaverMode = Math.max(0, (Date.now() - (saverModeStartTime.current || Date.now())) / 1000); | |
| const normalUpdatesPerMinute = 60 / (UPDATE_INTERVALS.normal.metrics / 1000); | |
| const saverUpdatesPerMinute = 60 / (UPDATE_INTERVALS.energySaver.metrics / 1000); | |
| const updatesReduced = Math.floor((normalUpdatesPerMinute - saverUpdatesPerMinute) * (timeInSaverMode / 60)); | |
| const energyPerUpdate = ENERGY_COSTS.update; | |
| const energySaved = (updatesReduced * energyPerUpdate) / 3600; | |
| setEnergySavings({ | |
| updatesReduced, | |
| timeInSaverMode, | |
| estimatedEnergySaved: energySaved, | |
| }); | |
| }, [energySaverMode]); | |
| // Add interval for energy savings updates | |
| useEffect(() => { | |
| const interval = setInterval(updateEnergySavings, 1000); | |
| return () => clearInterval(interval); | |
| }, [updateEnergySavings]); | |
| // Measure frame rate | |
| const measureFrameRate = async (): Promise<number> => { | |
| return new Promise((resolve) => { | |
| const frameCount = { value: 0 }; | |
| const startTime = performance.now(); | |
| const countFrame = (time: number) => { | |
| frameCount.value++; | |
| if (time - startTime >= 1000) { | |
| resolve(Math.round((frameCount.value * 1000) / (time - startTime))); | |
| } else { | |
| requestAnimationFrame(countFrame); | |
| } | |
| }; | |
| requestAnimationFrame(countFrame); | |
| }); | |
| }; | |
| // Get Largest Contentful Paint | |
| const getLargestContentfulPaint = async (): Promise<PerformanceEntry | undefined> => { | |
| return new Promise((resolve) => { | |
| new PerformanceObserver((list) => { | |
| const entries = list.getEntries(); | |
| resolve(entries[entries.length - 1]); | |
| }).observe({ entryTypes: ['largest-contentful-paint'] }); | |
| // Resolve after 3 seconds if no LCP entry is found | |
| setTimeout(() => resolve(undefined), 3000); | |
| }); | |
| }; | |
| // Analyze system health | |
| const analyzeSystemHealth = (currentMetrics: SystemMetrics): SystemMetrics['health'] => { | |
| const issues: string[] = []; | |
| const suggestions: string[] = []; | |
| let score = 100; | |
| // CPU analysis | |
| if (currentMetrics.cpu.usage > PERFORMANCE_THRESHOLDS.cpu.critical) { | |
| score -= 30; | |
| issues.push('Critical CPU usage'); | |
| suggestions.push('Consider closing resource-intensive applications'); | |
| } else if (currentMetrics.cpu.usage > PERFORMANCE_THRESHOLDS.cpu.warning) { | |
| score -= 15; | |
| issues.push('High CPU usage'); | |
| suggestions.push('Monitor system processes for unusual activity'); | |
| } | |
| // Memory analysis | |
| if (currentMetrics.memory.percentage > PERFORMANCE_THRESHOLDS.memory.critical) { | |
| score -= 30; | |
| issues.push('Critical memory usage'); | |
| suggestions.push('Close unused applications to free up memory'); | |
| } else if (currentMetrics.memory.percentage > PERFORMANCE_THRESHOLDS.memory.warning) { | |
| score -= 15; | |
| issues.push('High memory usage'); | |
| suggestions.push('Consider freeing up memory by closing background applications'); | |
| } | |
| // Performance analysis | |
| if (currentMetrics.performance.fps < PERFORMANCE_THRESHOLDS.fps.critical) { | |
| score -= 20; | |
| issues.push('Very low frame rate'); | |
| suggestions.push('Disable animations or switch to power saver mode'); | |
| } else if (currentMetrics.performance.fps < PERFORMANCE_THRESHOLDS.fps.warning) { | |
| score -= 10; | |
| issues.push('Low frame rate'); | |
| suggestions.push('Consider reducing visual effects'); | |
| } | |
| // Battery analysis | |
| if (currentMetrics.battery && !currentMetrics.battery.charging && currentMetrics.battery.level < 20) { | |
| score -= 10; | |
| issues.push('Low battery'); | |
| suggestions.push('Connect to power source or enable power saver mode'); | |
| } | |
| return { | |
| score: Math.max(0, score), | |
| issues, | |
| suggestions, | |
| }; | |
| }; | |
| // Update metrics with enhanced data | |
| const updateMetrics = async () => { | |
| try { | |
| // Get memory info using Performance API | |
| const memory = performance.memory || { | |
| jsHeapSizeLimit: 0, | |
| totalJSHeapSize: 0, | |
| usedJSHeapSize: 0, | |
| }; | |
| const totalMem = memory.totalJSHeapSize / (1024 * 1024); | |
| const usedMem = memory.usedJSHeapSize / (1024 * 1024); | |
| const memPercentage = (usedMem / totalMem) * 100; | |
| // Get CPU usage using Performance API | |
| const cpuUsage = await getCPUUsage(); | |
| // Get battery info | |
| let batteryInfo: SystemMetrics['battery'] | undefined; | |
| try { | |
| const battery = await navigator.getBattery(); | |
| batteryInfo = { | |
| level: battery.level * 100, | |
| charging: battery.charging, | |
| timeRemaining: battery.charging ? battery.chargingTime : battery.dischargingTime, | |
| }; | |
| } catch { | |
| console.log('Battery API not available'); | |
| } | |
| // Get network info using Network Information API | |
| const connection = | |
| (navigator as any).connection || (navigator as any).mozConnection || (navigator as any).webkitConnection; | |
| const networkInfo = { | |
| downlink: connection?.downlink || 0, | |
| uplink: connection?.uplink, | |
| latency: connection?.rtt || 0, | |
| type: connection?.type || 'unknown', | |
| activeConnections: connection?.activeConnections, | |
| bytesReceived: connection?.bytesReceived || 0, | |
| bytesSent: connection?.bytesSent || 0, | |
| }; | |
| // Get enhanced performance metrics | |
| const performanceMetrics = await getPerformanceMetrics(); | |
| const metrics: SystemMetrics = { | |
| cpu: { usage: cpuUsage, cores: [], temperature: undefined, frequency: undefined }, | |
| memory: { | |
| used: Math.round(usedMem), | |
| total: Math.round(totalMem), | |
| percentage: Math.round(memPercentage), | |
| heap: { | |
| used: Math.round(usedMem), | |
| total: Math.round(totalMem), | |
| limit: Math.round(totalMem), | |
| }, | |
| }, | |
| uptime: performance.now() / 1000, | |
| battery: batteryInfo, | |
| network: networkInfo, | |
| performance: performanceMetrics as SystemMetrics['performance'], | |
| health: { score: 0, issues: [], suggestions: [] }, | |
| }; | |
| // Analyze system health | |
| metrics.health = analyzeSystemHealth(metrics); | |
| // Check for alerts | |
| checkPerformanceAlerts(metrics); | |
| setMetrics(metrics); | |
| // Update metrics history | |
| const now = new Date().toLocaleTimeString(); | |
| setMetricsHistory((prev) => { | |
| const timestamps = [...prev.timestamps, now].slice(-MAX_HISTORY_POINTS); | |
| const cpu = [...prev.cpu, metrics.cpu.usage].slice(-MAX_HISTORY_POINTS); | |
| const memory = [...prev.memory, metrics.memory.percentage].slice(-MAX_HISTORY_POINTS); | |
| const battery = [...prev.battery, batteryInfo?.level || 0].slice(-MAX_HISTORY_POINTS); | |
| const network = [...prev.network, networkInfo.downlink].slice(-MAX_HISTORY_POINTS); | |
| return { timestamps, cpu, memory, battery, network }; | |
| }); | |
| } catch (error) { | |
| console.error('Failed to update system metrics:', error); | |
| } | |
| }; | |
| // Get real CPU usage using Performance API | |
| const getCPUUsage = async (): Promise<number> => { | |
| try { | |
| const t0 = performance.now(); | |
| // Create some actual work to measure and use the result | |
| let result = 0; | |
| for (let i = 0; i < 10000; i++) { | |
| result += Math.random(); | |
| } | |
| // Use result to prevent optimization | |
| if (result < 0) { | |
| console.log('Unexpected negative result'); | |
| } | |
| const t1 = performance.now(); | |
| const timeTaken = t1 - t0; | |
| /* | |
| * Normalize to percentage (0-100) | |
| * Lower time = higher CPU availability | |
| */ | |
| const maxExpectedTime = 50; // baseline in ms | |
| const cpuAvailability = Math.max(0, Math.min(100, ((maxExpectedTime - timeTaken) / maxExpectedTime) * 100)); | |
| return 100 - cpuAvailability; // Convert availability to usage | |
| } catch (error) { | |
| console.error('Failed to get CPU usage:', error); | |
| return 0; | |
| } | |
| }; | |
| // Add network change listener | |
| useEffect(() => { | |
| const connection = | |
| (navigator as any).connection || (navigator as any).mozConnection || (navigator as any).webkitConnection; | |
| if (!connection) { | |
| return; | |
| } | |
| const updateNetworkInfo = () => { | |
| setMetrics((prev) => ({ | |
| ...prev, | |
| network: { | |
| downlink: connection.downlink || 0, | |
| latency: connection.rtt || 0, | |
| type: connection.type || 'unknown', | |
| bytesReceived: connection.bytesReceived || 0, | |
| bytesSent: connection.bytesSent || 0, | |
| }, | |
| })); | |
| }; | |
| connection.addEventListener('change', updateNetworkInfo); | |
| // eslint-disable-next-line consistent-return | |
| return () => connection.removeEventListener('change', updateNetworkInfo); | |
| }, []); | |
| // Remove all animation and process monitoring | |
| useEffect(() => { | |
| const metricsInterval = setInterval( | |
| () => { | |
| if (!energySaverMode) { | |
| updateMetrics(); | |
| } | |
| }, | |
| energySaverMode ? UPDATE_INTERVALS.energySaver.metrics : UPDATE_INTERVALS.normal.metrics, | |
| ); | |
| return () => { | |
| clearInterval(metricsInterval); | |
| }; | |
| }, [energySaverMode]); | |
| const getUsageColor = (usage: number): string => { | |
| if (usage > 80) { | |
| return 'text-red-500'; | |
| } | |
| if (usage > 50) { | |
| return 'text-yellow-500'; | |
| } | |
| return 'text-gray-500'; | |
| }; | |
| const renderUsageGraph = (data: number[], label: string, color: string) => { | |
| const chartData = { | |
| labels: metricsHistory.timestamps, | |
| datasets: [ | |
| { | |
| label, | |
| data, | |
| borderColor: color, | |
| fill: false, | |
| tension: 0.4, | |
| }, | |
| ], | |
| }; | |
| const options = { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| scales: { | |
| y: { | |
| beginAtZero: true, | |
| max: 100, | |
| grid: { | |
| color: 'rgba(255, 255, 255, 0.1)', | |
| }, | |
| }, | |
| x: { | |
| grid: { | |
| display: false, | |
| }, | |
| }, | |
| }, | |
| plugins: { | |
| legend: { | |
| display: false, | |
| }, | |
| }, | |
| animation: { | |
| duration: 0, | |
| } as const, | |
| }; | |
| return ( | |
| <div className="h-32"> | |
| <Line data={chartData} options={options} /> | |
| </div> | |
| ); | |
| }; | |
| useEffect((): (() => void) | undefined => { | |
| if (!autoEnergySaver) { | |
| // If auto mode is disabled, clear any forced energy saver state | |
| setEnergySaverMode(false); | |
| return undefined; | |
| } | |
| const checkBatteryStatus = async () => { | |
| try { | |
| const battery = await navigator.getBattery(); | |
| const shouldEnableSaver = !battery.charging && battery.level * 100 <= BATTERY_THRESHOLD; | |
| setEnergySaverMode(shouldEnableSaver); | |
| } catch { | |
| console.log('Battery API not available'); | |
| } | |
| }; | |
| checkBatteryStatus(); | |
| const batteryCheckInterval = setInterval(checkBatteryStatus, 60000); | |
| return () => clearInterval(batteryCheckInterval); | |
| }, [autoEnergySaver]); | |
| // Check for performance alerts | |
| const checkPerformanceAlerts = (currentMetrics: SystemMetrics) => { | |
| const newAlerts: PerformanceAlert[] = []; | |
| // CPU alert | |
| if (currentMetrics.cpu.usage > PERFORMANCE_THRESHOLDS.cpu.critical) { | |
| newAlerts.push({ | |
| type: 'error', | |
| message: 'Critical CPU usage detected', | |
| timestamp: Date.now(), | |
| metric: 'cpu', | |
| threshold: PERFORMANCE_THRESHOLDS.cpu.critical, | |
| value: currentMetrics.cpu.usage, | |
| }); | |
| } | |
| // Memory alert | |
| if (currentMetrics.memory.percentage > PERFORMANCE_THRESHOLDS.memory.critical) { | |
| newAlerts.push({ | |
| type: 'error', | |
| message: 'Critical memory usage detected', | |
| timestamp: Date.now(), | |
| metric: 'memory', | |
| threshold: PERFORMANCE_THRESHOLDS.memory.critical, | |
| value: currentMetrics.memory.percentage, | |
| }); | |
| } | |
| // Performance alert | |
| if (currentMetrics.performance.fps < PERFORMANCE_THRESHOLDS.fps.critical) { | |
| newAlerts.push({ | |
| type: 'warning', | |
| message: 'Very low frame rate detected', | |
| timestamp: Date.now(), | |
| metric: 'fps', | |
| threshold: PERFORMANCE_THRESHOLDS.fps.critical, | |
| value: currentMetrics.performance.fps, | |
| }); | |
| } | |
| if (newAlerts.length > 0) { | |
| setAlerts((prev) => [...prev, ...newAlerts]); | |
| newAlerts.forEach((alert) => { | |
| toast.warning(alert.message); | |
| }); | |
| } | |
| }; | |
| return ( | |
| <div className="flex flex-col gap-6"> | |
| {/* Power Profile Selection */} | |
| <div className="flex flex-col gap-4"> | |
| <div className="flex items-center justify-between"> | |
| <h3 className="text-base font-medium text-bolt-elements-textPrimary">Power Management</h3> | |
| <div className="flex items-center gap-4"> | |
| <div className="flex items-center gap-2"> | |
| <input | |
| type="checkbox" | |
| id="autoEnergySaver" | |
| checked={autoEnergySaver} | |
| onChange={(e) => handleAutoEnergySaverChange(e.target.checked)} | |
| className="form-checkbox h-4 w-4 text-purple-600 rounded border-gray-300 dark:border-gray-700" | |
| /> | |
| <div className="i-ph:gauge-duotone w-4 h-4 text-bolt-elements-textSecondary" /> | |
| <label htmlFor="autoEnergySaver" className="text-sm text-bolt-elements-textSecondary"> | |
| Auto Energy Saver | |
| </label> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <input | |
| type="checkbox" | |
| id="energySaver" | |
| checked={energySaverMode} | |
| onChange={(e) => !autoEnergySaver && handleEnergySaverChange(e.target.checked)} | |
| disabled={autoEnergySaver} | |
| className="form-checkbox h-4 w-4 text-purple-600 rounded border-gray-300 dark:border-gray-700 disabled:opacity-50" | |
| /> | |
| <div className="i-ph:leaf-duotone w-4 h-4 text-bolt-elements-textSecondary" /> | |
| <label | |
| htmlFor="energySaver" | |
| className={classNames('text-sm text-bolt-elements-textSecondary', { 'opacity-50': autoEnergySaver })} | |
| > | |
| Energy Saver | |
| {energySaverMode && <span className="ml-2 text-xs text-bolt-elements-textSecondary">Active</span>} | |
| </label> | |
| </div> | |
| <div className="relative"> | |
| <select | |
| value={selectedProfile.name} | |
| onChange={(e) => { | |
| const profile = POWER_PROFILES.find((p) => p.name === e.target.value); | |
| if (profile) { | |
| setSelectedProfile(profile); | |
| toast.success(`Switched to ${profile.name} power profile`); | |
| } | |
| }} | |
| className="pl-8 pr-8 py-1.5 rounded-md bg-bolt-background-secondary dark:bg-[#1E1E1E] border border-bolt-border dark:border-bolt-borderDark text-sm text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimaryDark hover:border-bolt-action-primary dark:hover:border-bolt-action-primary focus:outline-none focus:ring-1 focus:ring-bolt-action-primary appearance-none min-w-[160px] cursor-pointer transition-colors duration-150" | |
| style={{ WebkitAppearance: 'none', MozAppearance: 'none' }} | |
| > | |
| {POWER_PROFILES.map((profile) => ( | |
| <option | |
| key={profile.name} | |
| value={profile.name} | |
| className="py-2 px-3 bg-bolt-background-secondary dark:bg-[#1E1E1E] text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimaryDark hover:bg-bolt-background-tertiary dark:hover:bg-bolt-backgroundDark-tertiary cursor-pointer" | |
| > | |
| {profile.name} | |
| </option> | |
| ))} | |
| </select> | |
| <div className="absolute left-2 top-1/2 -translate-y-1/2 pointer-events-none"> | |
| <div | |
| className={classNames('w-4 h-4 text-bolt-elements-textSecondary', { | |
| 'i-ph:lightning-fill text-yellow-500': selectedProfile.name === 'Performance', | |
| 'i-ph:scales-fill text-blue-500': selectedProfile.name === 'Balanced', | |
| 'i-ph:leaf-fill text-green-500': selectedProfile.name === 'Energy Saver', | |
| })} | |
| /> | |
| </div> | |
| <div className="absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none"> | |
| <div className="i-ph:caret-down w-4 h-4 text-bolt-elements-textSecondary opacity-75" /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="text-sm text-bolt-elements-textSecondary">{selectedProfile.description}</div> | |
| </div> | |
| {/* System Health Score */} | |
| <div className="flex flex-col gap-4"> | |
| <h3 className="text-base font-medium text-bolt-elements-textPrimary">System Health</h3> | |
| <div className="grid grid-cols-1 gap-4"> | |
| <div className="flex flex-col gap-2 rounded-lg bg-[#F8F8F8] dark:bg-[#141414] p-4"> | |
| <div className="flex items-center justify-between"> | |
| <span className="text-sm text-bolt-elements-textSecondary">Health Score</span> | |
| <span | |
| className={classNames('text-lg font-medium', { | |
| 'text-green-500': metrics.health.score >= 80, | |
| 'text-yellow-500': metrics.health.score >= 60 && metrics.health.score < 80, | |
| 'text-red-500': metrics.health.score < 60, | |
| })} | |
| > | |
| {metrics.health.score}% | |
| </span> | |
| </div> | |
| {metrics.health.issues.length > 0 && ( | |
| <div className="mt-2"> | |
| <div className="text-sm font-medium text-bolt-elements-textSecondary mb-1">Issues:</div> | |
| <ul className="text-sm text-bolt-elements-textSecondary space-y-1"> | |
| {metrics.health.issues.map((issue, index) => ( | |
| <li key={index} className="flex items-center gap-2"> | |
| <div className="i-ph:warning-circle-fill text-yellow-500 w-4 h-4" /> | |
| {issue} | |
| </li> | |
| ))} | |
| </ul> | |
| </div> | |
| )} | |
| {metrics.health.suggestions.length > 0 && ( | |
| <div className="mt-2"> | |
| <div className="text-sm font-medium text-bolt-elements-textSecondary mb-1">Suggestions:</div> | |
| <ul className="text-sm text-bolt-elements-textSecondary space-y-1"> | |
| {metrics.health.suggestions.map((suggestion, index) => ( | |
| <li key={index} className="flex items-center gap-2"> | |
| <div className="i-ph:lightbulb-fill text-purple-500 w-4 h-4" /> | |
| {suggestion} | |
| </li> | |
| ))} | |
| </ul> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| {/* System Metrics */} | |
| <div className="flex flex-col gap-4"> | |
| <h3 className="text-base font-medium text-bolt-elements-textPrimary">System Metrics</h3> | |
| <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> | |
| {/* CPU Usage */} | |
| <div className="flex flex-col gap-2 rounded-lg bg-[#F8F8F8] dark:bg-[#141414] p-4"> | |
| <div className="flex items-center justify-between"> | |
| <span className="text-sm text-bolt-elements-textSecondary">CPU Usage</span> | |
| <span className={classNames('text-sm font-medium', getUsageColor(metrics.cpu.usage))}> | |
| {Math.round(metrics.cpu.usage)}% | |
| </span> | |
| </div> | |
| {renderUsageGraph(metricsHistory.cpu, 'CPU', '#9333ea')} | |
| {metrics.cpu.temperature && ( | |
| <div className="text-xs text-bolt-elements-textSecondary mt-2"> | |
| Temperature: {metrics.cpu.temperature}°C | |
| </div> | |
| )} | |
| {metrics.cpu.frequency && ( | |
| <div className="text-xs text-bolt-elements-textSecondary"> | |
| Frequency: {(metrics.cpu.frequency / 1000).toFixed(1)} GHz | |
| </div> | |
| )} | |
| </div> | |
| {/* Memory Usage */} | |
| <div className="flex flex-col gap-2 rounded-lg bg-[#F8F8F8] dark:bg-[#141414] p-4"> | |
| <div className="flex items-center justify-between"> | |
| <span className="text-sm text-bolt-elements-textSecondary">Memory Usage</span> | |
| <span className={classNames('text-sm font-medium', getUsageColor(metrics.memory.percentage))}> | |
| {Math.round(metrics.memory.percentage)}% | |
| </span> | |
| </div> | |
| {renderUsageGraph(metricsHistory.memory, 'Memory', '#2563eb')} | |
| <div className="text-xs text-bolt-elements-textSecondary mt-2"> | |
| Used: {formatBytes(metrics.memory.used)} | |
| </div> | |
| <div className="text-xs text-bolt-elements-textSecondary">Total: {formatBytes(metrics.memory.total)}</div> | |
| <div className="text-xs text-bolt-elements-textSecondary"> | |
| Heap: {formatBytes(metrics.memory.heap.used)} / {formatBytes(metrics.memory.heap.total)} | |
| </div> | |
| </div> | |
| {/* Performance */} | |
| <div className="flex flex-col gap-2 rounded-lg bg-[#F8F8F8] dark:bg-[#141414] p-4"> | |
| <div className="flex items-center justify-between"> | |
| <span className="text-sm text-bolt-elements-textSecondary">Performance</span> | |
| <span | |
| className={classNames('text-sm font-medium', { | |
| 'text-red-500': metrics.performance.fps < PERFORMANCE_THRESHOLDS.fps.critical, | |
| 'text-yellow-500': metrics.performance.fps < PERFORMANCE_THRESHOLDS.fps.warning, | |
| 'text-green-500': metrics.performance.fps >= PERFORMANCE_THRESHOLDS.fps.warning, | |
| })} | |
| > | |
| {Math.round(metrics.performance.fps)} FPS | |
| </span> | |
| </div> | |
| <div className="text-xs text-bolt-elements-textSecondary mt-2"> | |
| Page Load: {(metrics.performance.pageLoad / 1000).toFixed(2)}s | |
| </div> | |
| <div className="text-xs text-bolt-elements-textSecondary"> | |
| DOM Ready: {(metrics.performance.domReady / 1000).toFixed(2)}s | |
| </div> | |
| <div className="text-xs text-bolt-elements-textSecondary"> | |
| TTFB: {(metrics.performance.timing.ttfb / 1000).toFixed(2)}s | |
| </div> | |
| <div className="text-xs text-bolt-elements-textSecondary"> | |
| Resources: {metrics.performance.resources.total} ({formatBytes(metrics.performance.resources.size)}) | |
| </div> | |
| </div> | |
| {/* Network */} | |
| <div className="flex flex-col gap-2 rounded-lg bg-[#F8F8F8] dark:bg-[#141414] p-4"> | |
| <div className="flex items-center justify-between"> | |
| <span className="text-sm text-bolt-elements-textSecondary">Network</span> | |
| <span className="text-sm font-medium text-bolt-elements-textPrimary"> | |
| {metrics.network.downlink.toFixed(1)} Mbps | |
| </span> | |
| </div> | |
| {renderUsageGraph(metricsHistory.network, 'Network', '#f59e0b')} | |
| <div className="text-xs text-bolt-elements-textSecondary mt-2">Type: {metrics.network.type}</div> | |
| <div className="text-xs text-bolt-elements-textSecondary">Latency: {metrics.network.latency}ms</div> | |
| <div className="text-xs text-bolt-elements-textSecondary"> | |
| Received: {formatBytes(metrics.network.bytesReceived)} | |
| </div> | |
| <div className="text-xs text-bolt-elements-textSecondary"> | |
| Sent: {formatBytes(metrics.network.bytesSent)} | |
| </div> | |
| </div> | |
| </div> | |
| {/* Battery Section */} | |
| {metrics.battery && ( | |
| <div className="flex flex-col gap-2 rounded-lg bg-[#F8F8F8] dark:bg-[#141414] p-4"> | |
| <div className="flex items-center justify-between"> | |
| <span className="text-sm text-bolt-elements-textSecondary">Battery</span> | |
| <div className="flex items-center gap-2"> | |
| {metrics.battery.charging && <div className="i-ph:lightning-fill w-4 h-4 text-bolt-action-primary" />} | |
| <span | |
| className={classNames( | |
| 'text-sm font-medium', | |
| metrics.battery.level > 20 ? 'text-bolt-elements-textPrimary' : 'text-red-500', | |
| )} | |
| > | |
| {Math.round(metrics.battery.level)}% | |
| </span> | |
| </div> | |
| </div> | |
| {renderUsageGraph(metricsHistory.battery, 'Battery', '#22c55e')} | |
| {metrics.battery.timeRemaining && ( | |
| <div className="text-xs text-bolt-elements-textSecondary mt-2"> | |
| {metrics.battery.charging ? 'Time to full: ' : 'Time remaining: '} | |
| {formatTime(metrics.battery.timeRemaining)} | |
| </div> | |
| )} | |
| {metrics.battery.temperature && ( | |
| <div className="text-xs text-bolt-elements-textSecondary"> | |
| Temperature: {metrics.battery.temperature}°C | |
| </div> | |
| )} | |
| {metrics.battery.cycles && ( | |
| <div className="text-xs text-bolt-elements-textSecondary">Charge cycles: {metrics.battery.cycles}</div> | |
| )} | |
| {metrics.battery.health && ( | |
| <div className="text-xs text-bolt-elements-textSecondary">Battery health: {metrics.battery.health}%</div> | |
| )} | |
| </div> | |
| )} | |
| {/* Performance Alerts */} | |
| {alerts.length > 0 && ( | |
| <div className="flex flex-col gap-2 rounded-lg bg-[#F8F8F8] dark:bg-[#141414] p-4"> | |
| <div className="flex items-center justify-between"> | |
| <span className="text-sm font-medium text-bolt-elements-textPrimary">Recent Alerts</span> | |
| <button | |
| onClick={() => setAlerts([])} | |
| className="text-xs text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary" | |
| > | |
| Clear All | |
| </button> | |
| </div> | |
| <div className="space-y-2"> | |
| {alerts.slice(-5).map((alert, index) => ( | |
| <div | |
| key={index} | |
| className={classNames('flex items-center gap-2 text-sm', { | |
| 'text-red-500': alert.type === 'error', | |
| 'text-yellow-500': alert.type === 'warning', | |
| 'text-blue-500': alert.type === 'info', | |
| })} | |
| > | |
| <div | |
| className={classNames('w-4 h-4', { | |
| 'i-ph:warning-circle-fill': alert.type === 'warning', | |
| 'i-ph:x-circle-fill': alert.type === 'error', | |
| 'i-ph:info-fill': alert.type === 'info', | |
| })} | |
| /> | |
| <span>{alert.message}</span> | |
| <span className="text-xs text-bolt-elements-textSecondary ml-auto"> | |
| {new Date(alert.timestamp).toLocaleTimeString()} | |
| </span> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| {/* Energy Savings */} | |
| {energySaverMode && ( | |
| <div className="flex flex-col gap-2 rounded-lg bg-[#F8F8F8] dark:bg-[#141414] p-4"> | |
| <h4 className="text-sm font-medium text-bolt-elements-textPrimary">Energy Savings</h4> | |
| <div className="grid grid-cols-3 gap-4"> | |
| <div> | |
| <span className="text-sm text-bolt-elements-textSecondary">Updates Reduced</span> | |
| <p className="text-lg font-medium text-bolt-elements-textPrimary">{energySavings.updatesReduced}</p> | |
| </div> | |
| <div> | |
| <span className="text-sm text-bolt-elements-textSecondary">Time in Saver Mode</span> | |
| <p className="text-lg font-medium text-bolt-elements-textPrimary"> | |
| {Math.floor(energySavings.timeInSaverMode / 60)}m {Math.floor(energySavings.timeInSaverMode % 60)}s | |
| </p> | |
| </div> | |
| <div> | |
| <span className="text-sm text-bolt-elements-textSecondary">Energy Saved</span> | |
| <p className="text-lg font-medium text-bolt-elements-textPrimary"> | |
| {energySavings.estimatedEnergySaved.toFixed(2)} mWh | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default React.memo(TaskManagerTab); | |
| // Helper function to format bytes | |
| const formatBytes = (bytes: number): string => { | |
| if (bytes === 0) { | |
| return '0 B'; | |
| } | |
| const k = 1024; | |
| const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; | |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
| return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; | |
| }; | |
| // Helper function to format time | |
| const formatTime = (seconds: number): string => { | |
| if (!isFinite(seconds) || seconds === 0) { | |
| return 'Unknown'; | |
| } | |
| const hours = Math.floor(seconds / 3600); | |
| const minutes = Math.floor((seconds % 3600) / 60); | |
| if (hours > 0) { | |
| return `${hours}h ${minutes}m`; | |
| } | |
| return `${minutes}m`; | |
| }; | |