diff --git a/src/App.css b/src/App.css index 4faacb1..cd07da7 100644 --- a/src/App.css +++ b/src/App.css @@ -450,6 +450,113 @@ body { .col-time { width: 60px; color: var(--color-text-dim); } .col-command { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +/* Alert Bar */ +.alert-bar { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 16px; + border-radius: 12px; + border: 1px solid var(--border-color); + flex-shrink: 0; + transition: opacity 0.2s ease; +} + +.alert-bar.alert-flash-off { + opacity: 0.4; +} + +.alert-bar.alert-info { + background: linear-gradient(145deg, rgba(52, 211, 153, 0.1) 0%, rgba(13, 18, 25, 0.8) 100%); + border-color: rgba(52, 211, 153, 0.3); +} + +.alert-bar.alert-warn { + background: linear-gradient(145deg, rgba(251, 191, 36, 0.1) 0%, rgba(13, 18, 25, 0.8) 100%); + border-color: rgba(251, 191, 36, 0.3); +} + +.alert-bar.alert-critical { + background: linear-gradient(145deg, rgba(248, 113, 113, 0.15) 0%, rgba(13, 18, 25, 0.8) 100%); + border-color: rgba(248, 113, 113, 0.4); +} + +.alert-icon { + font-weight: 900; + font-size: 12px; + width: 22px; + height: 22px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; +} + +.alert-info .alert-icon { + color: var(--color-green); + background: rgba(52, 211, 153, 0.15); +} + +.alert-warn .alert-icon { + color: var(--color-yellow); + background: rgba(251, 191, 36, 0.15); +} + +.alert-critical .alert-icon { + color: var(--color-red); + background: rgba(248, 113, 113, 0.15); +} + +.alert-messages { + display: flex; + gap: 16px; + flex: 1; +} + +.alert-msg { + font-size: 11px; + font-weight: 600; + letter-spacing: 0.5px; +} + +.alert-msg-info { color: var(--color-green); } +.alert-msg-warn { color: var(--color-yellow); } +.alert-msg-critical { color: var(--color-red); } + +.alert-timestamp { + color: var(--color-text-dim); + font-size: 10px; + margin-left: auto; +} + +/* Status Indicator */ +.status-indicator { + display: flex; + align-items: center; + gap: 6px; +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--color-green); + opacity: 0.3; + transition: opacity 0.3s ease; +} + +.status-dot.active { + opacity: 1; + box-shadow: 0 0 8px var(--color-green), 0 0 16px rgba(52, 211, 153, 0.4); +} + +.status-text { + color: var(--color-green); + font-size: 10px; + font-weight: 700; + letter-spacing: 2px; +} + /* Status Bar */ .status-bar { display: flex; diff --git a/src/App.tsx b/src/App.tsx index 60c8dc0..7421865 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,7 @@ import { CpuGraph } from './components/CpuGraph'; import { MemoryGraph } from './components/MemoryGraph'; import { ProcessTable } from './components/ProcessTable'; import { StatusBar } from './components/StatusBar'; +import { AlertBar } from './components/AlertBar'; import { EnvironmentPanel } from './components/EnvironmentPanel'; import { useSystemMetrics } from './hooks/useSystemMetrics'; import './App.css'; @@ -71,6 +72,8 @@ function App() { + + a + b, 0) / cpuUsage.length; + + if (avgCpu > 90) { + alerts.push({ message: 'CPU usage critical', level: 'critical' }); + } else if (avgCpu > 70) { + alerts.push({ message: 'CPU usage high', level: 'warn' }); + } + + if (memPercent > 90) { + alerts.push({ message: 'Memory usage critical', level: 'critical' }); + } else if (memPercent > 70) { + alerts.push({ message: 'Memory usage high', level: 'warn' }); + } + + if (alerts.length === 0) { + alerts.push({ message: 'All systems nominal', level: 'info' }); + } + + return alerts; +} + +export function AlertBar({ cpuUsage, memPercent }: AlertBarProps) { + const [visible, setVisible] = useState(true); + const alerts = getAlerts(cpuUsage, memPercent); + const hasWarnings = alerts.some((a) => a.level !== 'info'); + + useEffect(() => { + if (!hasWarnings) return; + const interval = setInterval(() => { + setVisible((prev) => !prev); + }, 800); + return () => clearInterval(interval); + }, [hasWarnings]); + + const highestLevel = alerts.reduce<'info' | 'warn' | 'critical'>((max, a) => { + const order = { info: 0, warn: 1, critical: 2 }; + return order[a.level] > order[max] ? a.level : max; + }, 'info'); + + return ( +
+ + {highestLevel === 'critical' ? '!!' : highestLevel === 'warn' ? '!' : '~'} + +
+ {alerts.map((alert, i) => ( + + {alert.message} + + ))} +
+ + {new Date().toLocaleTimeString()} + +
+ ); +} diff --git a/src/components/MemoryBar.tsx b/src/components/MemoryBar.tsx index 870bfa4..390ccfb 100644 --- a/src/components/MemoryBar.tsx +++ b/src/components/MemoryBar.tsx @@ -8,9 +8,9 @@ interface MemoryBarProps { function formatBytes(bytes: number): string { if (bytes === 0) return '0 B'; const k = 1024; - const sizes = ['B', 'K', 'M', 'G', 'T']; + 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(1)) + sizes[i]; + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; } export function MemoryBar({ label, used, total, percent }: MemoryBarProps) { diff --git a/src/components/MemoryGraph.tsx b/src/components/MemoryGraph.tsx index 995fe06..0ec6343 100644 --- a/src/components/MemoryGraph.tsx +++ b/src/components/MemoryGraph.tsx @@ -63,7 +63,7 @@ export function MemoryGraph({ used, total, percent }: MemoryGraphProps) {
Memory - {percent}% + {formatBytes(used)} / {formatBytes(total)} ({percent}%)
@@ -113,7 +113,7 @@ export function MemoryGraph({ used, total, percent }: MemoryGraphProps) {
Used - {formatBytes(used)} + {formatBytes(used)} ({percent}%) Total @@ -121,7 +121,7 @@ export function MemoryGraph({ used, total, percent }: MemoryGraphProps) { Free - {formatBytes(total - used)} + {formatBytes(total - used)} ({total > 0 ? Math.round(((total - used) / total) * 100) : 0}%)
diff --git a/src/components/StatusBar.tsx b/src/components/StatusBar.tsx index 0180316..19abb56 100644 --- a/src/components/StatusBar.tsx +++ b/src/components/StatusBar.tsx @@ -1,3 +1,5 @@ +import { useState, useEffect } from 'react'; + interface StatusBarProps { filter: string; onFilterChange: (filter: string) => void; @@ -6,6 +8,15 @@ interface StatusBarProps { } export function StatusBar({ filter, onFilterChange, refreshRate, onRefreshRateChange }: StatusBarProps) { + const [statusActive, setStatusActive] = useState(true); + + useEffect(() => { + const interval = setInterval(() => { + setStatusActive((prev) => !prev); + }, 1000); + return () => clearInterval(interval); + }, []); + const shortcuts = [ { key: 'F1', label: 'Help' }, { key: 'F2', label: 'Setup' }, @@ -19,6 +30,10 @@ export function StatusBar({ filter, onFilterChange, refreshRate, onRefreshRateCh return (
+
+ + LIVE +
Filter: