|
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 { useUpdateCheck } from '~/lib/hooks/useUpdateCheck'; |
|
import { tabConfigurationStore, type TabConfig } from '~/lib/stores/tabConfigurationStore'; |
|
import { useStore } from 'zustand'; |
|
|
|
|
|
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; |
|
} |
|
|
|
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; |
|
}; |
|
} |
|
} |
|
|
|
|
|
const UPDATE_INTERVALS = { |
|
normal: { |
|
metrics: 1000, |
|
animation: 16, |
|
}, |
|
energySaver: { |
|
metrics: 5000, |
|
animation: 32, |
|
}, |
|
}; |
|
|
|
|
|
const PERFORMANCE_THRESHOLDS = { |
|
cpu: { |
|
warning: 70, |
|
critical: 90, |
|
}, |
|
memory: { |
|
warning: 80, |
|
critical: 95, |
|
}, |
|
fps: { |
|
warning: 30, |
|
critical: 15, |
|
}, |
|
}; |
|
|
|
|
|
const ENERGY_COSTS = { |
|
update: 0.1, |
|
}; |
|
|
|
|
|
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, |
|
}, |
|
}, |
|
]; |
|
|
|
|
|
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: [], |
|
}, |
|
}; |
|
|
|
|
|
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), |
|
}; |
|
|
|
|
|
const BATTERY_THRESHOLD = 20; |
|
|
|
|
|
const MAX_HISTORY_POINTS = 10; |
|
|
|
const TaskManagerTab: React.FC = () => { |
|
|
|
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); |
|
|
|
|
|
const { hasUpdate } = useUpdateCheck(); |
|
const tabConfig = useStore(tabConfigurationStore); |
|
|
|
const resetTabConfiguration = useCallback(() => { |
|
tabConfig.reset(); |
|
return tabConfig.get(); |
|
}, [tabConfig]); |
|
|
|
|
|
useEffect(() => { |
|
const handleTabVisibility = () => { |
|
const currentConfig = tabConfig.get(); |
|
const controlledTabs = ['debug', 'update']; |
|
|
|
|
|
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]); |
|
|
|
|
|
useEffect(() => { |
|
const resetToDefaults = () => { |
|
console.log('TaskManagerTab: Resetting to defaults'); |
|
|
|
|
|
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; |
|
|
|
|
|
const defaultConfig = resetTabConfiguration(); |
|
console.log('TaskManagerTab: Reset tab configuration:', defaultConfig); |
|
}; |
|
|
|
|
|
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(); |
|
} |
|
}; |
|
|
|
|
|
const initializeTab = async () => { |
|
try { |
|
|
|
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); |
|
}; |
|
}, []); |
|
|
|
|
|
const getPerformanceMetrics = async (): Promise<Partial<SystemMetrics['performance']>> => { |
|
try { |
|
|
|
const fps = await measureFrameRate(); |
|
|
|
|
|
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming; |
|
const pageLoad = navigation.loadEventEnd - navigation.startTime; |
|
const domReady = navigation.domContentLoadedEventEnd - navigation.startTime; |
|
|
|
|
|
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)), |
|
}; |
|
|
|
|
|
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 {}; |
|
} |
|
}; |
|
|
|
|
|
useEffect(() => { |
|
let isComponentMounted = true; |
|
|
|
const updateMetricsWrapper = async () => { |
|
if (!isComponentMounted) { |
|
return; |
|
} |
|
|
|
try { |
|
await updateMetrics(); |
|
} catch (error) { |
|
console.error('Failed to update metrics:', error); |
|
} |
|
}; |
|
|
|
|
|
updateMetricsWrapper(); |
|
|
|
|
|
const metricsInterval = setInterval( |
|
updateMetricsWrapper, |
|
energySaverMode ? UPDATE_INTERVALS.energySaver.metrics : UPDATE_INTERVALS.normal.metrics, |
|
); |
|
|
|
|
|
return () => { |
|
isComponentMounted = false; |
|
clearInterval(metricsInterval); |
|
}; |
|
}, [energySaverMode]); |
|
|
|
|
|
const handleEnergySaverChange = (checked: boolean) => { |
|
setEnergySaverMode(checked); |
|
localStorage.setItem('energySaverMode', JSON.stringify(checked)); |
|
toast.success(checked ? 'Energy Saver mode enabled' : 'Energy Saver mode disabled'); |
|
}; |
|
|
|
|
|
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) { |
|
|
|
setEnergySaverMode(false); |
|
localStorage.setItem('energySaverMode', 'false'); |
|
} |
|
}; |
|
|
|
|
|
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]); |
|
|
|
|
|
useEffect(() => { |
|
const interval = setInterval(updateEnergySavings, 1000); |
|
return () => clearInterval(interval); |
|
}, [updateEnergySavings]); |
|
|
|
|
|
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); |
|
}); |
|
}; |
|
|
|
|
|
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'] }); |
|
|
|
|
|
setTimeout(() => resolve(undefined), 3000); |
|
}); |
|
}; |
|
|
|
|
|
const analyzeSystemHealth = (currentMetrics: SystemMetrics): SystemMetrics['health'] => { |
|
const issues: string[] = []; |
|
const suggestions: string[] = []; |
|
let score = 100; |
|
|
|
|
|
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'); |
|
} |
|
|
|
|
|
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'); |
|
} |
|
|
|
|
|
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'); |
|
} |
|
|
|
|
|
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, |
|
}; |
|
}; |
|
|
|
|
|
const updateMetrics = async () => { |
|
try { |
|
|
|
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; |
|
|
|
|
|
const cpuUsage = await getCPUUsage(); |
|
|
|
|
|
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'); |
|
} |
|
|
|
|
|
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, |
|
}; |
|
|
|
|
|
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: [] }, |
|
}; |
|
|
|
|
|
metrics.health = analyzeSystemHealth(metrics); |
|
|
|
|
|
checkPerformanceAlerts(metrics); |
|
|
|
setMetrics(metrics); |
|
|
|
|
|
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); |
|
} |
|
}; |
|
|
|
|
|
const getCPUUsage = async (): Promise<number> => { |
|
try { |
|
const t0 = performance.now(); |
|
|
|
|
|
let result = 0; |
|
|
|
for (let i = 0; i < 10000; i++) { |
|
result += Math.random(); |
|
} |
|
|
|
|
|
if (result < 0) { |
|
console.log('Unexpected negative result'); |
|
} |
|
|
|
const t1 = performance.now(); |
|
const timeTaken = t1 - t0; |
|
|
|
|
|
|
|
|
|
|
|
const maxExpectedTime = 50; |
|
const cpuAvailability = Math.max(0, Math.min(100, ((maxExpectedTime - timeTaken) / maxExpectedTime) * 100)); |
|
|
|
return 100 - cpuAvailability; |
|
} catch (error) { |
|
console.error('Failed to get CPU usage:', error); |
|
return 0; |
|
} |
|
}; |
|
|
|
|
|
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); |
|
|
|
|
|
return () => connection.removeEventListener('change', updateNetworkInfo); |
|
}, []); |
|
|
|
|
|
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) { |
|
|
|
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]); |
|
|
|
|
|
const checkPerformanceAlerts = (currentMetrics: SystemMetrics) => { |
|
const newAlerts: PerformanceAlert[] = []; |
|
|
|
|
|
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, |
|
}); |
|
} |
|
|
|
|
|
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, |
|
}); |
|
} |
|
|
|
|
|
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); |
|
|
|
|
|
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]}`; |
|
}; |
|
|
|
|
|
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`; |
|
}; |
|
|