bin/status.ps1
|
# WinMole - Live System Health Dashboard # Real-time CPU, memory, disk I/O, network, and process stats param( [switch]$Json, [double]$ProcCpuThreshold = 50.0, [int]$ProcCpuWindow = 10, [switch]$NoProcCpuAlerts ) $ErrorActionPreference = 'SilentlyContinue' . "$PSScriptRoot\..\lib\core.ps1" . "$PSScriptRoot\..\lib\ui.ps1" # ── Data collection helpers ─────────────────────────────────────────────────── function Get-CpuUsage { try { $cpu = Get-CimInstance -ClassName Win32_Processor -ErrorAction SilentlyContinue $total = ($cpu | Measure-Object -Property LoadPercentage -Average).Average $cores = $cpu | ForEach-Object { $_.LoadPercentage } return [pscustomobject]@{ Total = $total; Cores = @($cores) } } catch { return [pscustomobject]@{ Total = 0; Cores = @() } } } function Get-MemoryInfo { try { $os = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction SilentlyContinue $cs = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction SilentlyContinue $total = $cs.TotalPhysicalMemory $free = $os.FreePhysicalMemory * 1KB $used = $total - $free return [pscustomobject]@{ TotalGB = [Math]::Round($total / 1GB, 1) UsedGB = [Math]::Round($used / 1GB, 1) FreeGB = [Math]::Round($free / 1GB, 1) UsedPct = [Math]::Round($used / $total * 100, 1) } } catch { return [pscustomobject]@{ TotalGB = 0; UsedGB = 0; FreeGB = 0; UsedPct = 0 } } } function Get-DiskInfo { try { $disk = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DriveType=3" ` -ErrorAction SilentlyContinue | Select-Object -First 1 $total = $disk.Size $free = $disk.FreeSpace $used = $total - $free return [pscustomobject]@{ Drive = $disk.DeviceID TotalGB = [Math]::Round($total / 1GB, 1) UsedGB = [Math]::Round($used / 1GB, 1) FreeGB = [Math]::Round($free / 1GB, 1) UsedPct = [Math]::Round($used / $total * 100, 1) } } catch { return [pscustomobject]@{ Drive='C:'; TotalGB=0; UsedGB=0; FreeGB=0; UsedPct=0 } } } function Get-NetworkIO { param([hashtable]$Previous) try { $adapters = Get-CimInstance -ClassName Win32_PerfFormattedData_Tcpip_NetworkInterface ` -ErrorAction SilentlyContinue $rx = [long]0; $tx = [long]0 foreach ($a in $adapters) { $rx += $a.BytesReceivedPersec $tx += $a.BytesSentPersec } return [pscustomobject]@{ RxBps = $rx; TxBps = $tx } } catch { return [pscustomobject]@{ RxBps = 0; TxBps = 0 } } } function Get-DiskIO { try { $perf = Get-CimInstance -ClassName Win32_PerfFormattedData_PerfDisk_PhysicalDisk ` -Filter "Name='_Total'" -ErrorAction SilentlyContinue return [pscustomobject]@{ ReadBps = if ($perf) { $perf.DiskReadBytesPersec } else { 0 } WriteBps = if ($perf) { $perf.DiskWriteBytesPersec } else { 0 } } } catch { return [pscustomobject]@{ ReadBps = 0; WriteBps = 0 } } } function Get-TopProcesses { param([int]$Count = 5) try { return Get-Process -ErrorAction SilentlyContinue | Sort-Object CPU -Descending | Select-Object -First $Count | ForEach-Object { [pscustomobject]@{ Name = $_.Name CpuPct = [Math]::Min(100, [Math]::Round($_.CPU / ([Environment]::ProcessorCount * 10), 1)) } } } catch { return @() } } function Get-HealthScore { param($Cpu, $Mem, $Disk) $score = 100 if ($Cpu.Total -gt 90) { $score -= 30 } elseif ($Cpu.Total -gt 70) { $score -= 15 } elseif ($Cpu.Total -gt 50) { $score -= 5 } if ($Mem.UsedPct -gt 90) { $score -= 25 } elseif ($Mem.UsedPct -gt 75) { $score -= 10 } elseif ($Mem.UsedPct -gt 60) { $score -= 5 } if ($Disk.UsedPct -gt 95) { $score -= 20 } elseif ($Disk.UsedPct -gt 85) { $score -= 10 } elseif ($Disk.UsedPct -gt 75) { $score -= 5 } return [Math]::Max(0, $score) } function Get-HealthColor { param([int]$Score) if ($Score -ge 80) { return 'green' } if ($Score -ge 60) { return 'gold1' } return 'red1' } function Format-NetSpeed { param([long]$Bps) if ($Bps -ge 1MB) { return '{0:F1} MB/s' -f ($Bps / 1MB) } if ($Bps -ge 1KB) { return '{0:F0} KB/s' -f ($Bps / 1KB) } return "${Bps} B/s" } # Helper: clear current line then write Spectre markup function CL { Write-Host "`e[2K" -NoNewline } # ── JSON mode ───────────────────────────────────────────────────────────────── if ($Json -or [Console]::IsOutputRedirected) { $cpu = Get-CpuUsage $mem = Get-MemoryInfo $disk = Get-DiskInfo $net = Get-NetworkIO $io = Get-DiskIO $procs = Get-TopProcesses $score = Get-HealthScore $cpu $mem $disk $os = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction SilentlyContinue $uptime = if ($os) { Format-Uptime -Seconds ([long](New-TimeSpan -Start $os.LastBootUpTime -End (Get-Date)).TotalSeconds) } else { 'Unknown' } [pscustomobject]@{ host = $env:COMPUTERNAME health_score = $score cpu = [pscustomobject]@{ usage = $cpu.Total; logical_cpu = [Environment]::ProcessorCount } memory = [pscustomobject]@{ total_gb = $mem.TotalGB; used_gb = $mem.UsedGB; used_percent = $mem.UsedPct } disk = [pscustomobject]@{ drive = $disk.Drive; total_gb = $disk.TotalGB; free_gb = $disk.FreeGB; used_percent = $disk.UsedPct } network = [pscustomobject]@{ rx_bps = $net.RxBps; tx_bps = $net.TxBps } uptime = $uptime top_processes = $procs } | ConvertTo-Json -Depth 3 return } # ── Live TUI dashboard ───────────────────────────────────────────────────────── $info = Get-SysInfoSummary $refresh = 2 # seconds between refreshes Hide-Cursor [Console]::TreatControlCAsInput = $true $quitRequested = $false $cancelHandler = [System.ConsoleCancelEventHandler]{ param($sender, $eventArgs) $eventArgs.Cancel = $true $script:quitRequested = $true } [Console]::add_CancelKeyPress($cancelHandler) $hwStr = "$($info.MachineName) · $($info.CPU.Split(',')[0]) · $($info.RamTotalGB)GB · $($info.OSCaption -replace 'Microsoft ','')" # Track high-CPU processes for alerts $highCpuTracker = @{} try { while ($true) { if ($quitRequested) { break } # Check for keypress without blocking $quit = $false while (Test-KeyAvailable) { if ((Read-Key) -in @('Escape', 'Quit')) { $quit = $true } } if ($quit) { break } # Collect data $cpu = Get-CpuUsage $mem = Get-MemoryInfo $disk = Get-DiskInfo $net = Get-NetworkIO $io = Get-DiskIO $procs = Get-TopProcesses -Count 5 $score = Get-HealthScore $cpu $mem $disk $hColor= Get-HealthColor $score # CPU alert check if (-not $NoProcCpuAlerts) { $currentProcNames = [System.Collections.Generic.HashSet[string]]::new( [string[]]@($procs | ForEach-Object { $_.Name })) # Evict processes no longer in top list @($highCpuTracker.Keys) | Where-Object { -not $currentProcNames.Contains($_) } | ForEach-Object { $highCpuTracker.Remove($_) } foreach ($p in $procs) { if ($p.CpuPct -ge $ProcCpuThreshold) { if (-not $highCpuTracker.ContainsKey($p.Name)) { $highCpuTracker[$p.Name] = 0 } $highCpuTracker[$p.Name]++ } else { $highCpuTracker.Remove($p.Name) } } } # ── Render ──────────────────────────────────────────────────────────── [Console]::SetCursorPosition(0, 0) # Header CL; Write-SpectreHost " [bold deepskyblue1]WinMole Status[/] Health [$hColor]● $score[/] [grey]$(Esc $hwStr)[/]" CL; Write-Host "" # CPU $cpuColor = if ($cpu.Total -gt 80) { 'red' } elseif ($cpu.Total -gt 60) { 'yellow' } else { 'green' } $cpuBar = Get-ProgressBar -Pct $cpu.Total -Width 18 -Color $cpuColor CL; Write-SpectreHost " [bold]⚙ CPU[/]" CL; Write-SpectreHost (" Total $cpuBar [gold1]{0,5:F1}%[/] [grey]{1} logical cores[/]" -f $cpu.Total, [Environment]::ProcessorCount) CL; Write-Host "" # Memory $memColor = if ($mem.UsedPct -gt 80) { 'red' } elseif ($mem.UsedPct -gt 60) { 'yellow' } else { 'green' } $memBar = Get-ProgressBar -Pct $mem.UsedPct -Width 18 -Color $memColor CL; Write-SpectreHost " [bold]▦ Memory[/]" CL; Write-SpectreHost (" Used $memBar [gold1]{0,5:F1}%[/] [grey]{1:F1} / {2:F1} GB[/]" -f $mem.UsedPct, $mem.UsedGB, $mem.TotalGB) CL; Write-SpectreHost (" Avail [grey]{0:F1} GB[/]" -f $mem.FreeGB) CL; Write-Host "" # Disk $diskColor = if ($disk.UsedPct -gt 90) { 'red' } elseif ($disk.UsedPct -gt 75) { 'yellow' } else { 'green' } $diskBar = Get-ProgressBar -Pct $disk.UsedPct -Width 18 -Color $diskColor $rdBar = Get-SparkBar -Pct ([Math]::Min(100, $io.ReadBps / 50MB * 100)) $wrBar = Get-SparkBar -Pct ([Math]::Min(100, $io.WriteBps / 50MB * 100)) CL; Write-SpectreHost " [bold]▤ Disk ($($disk.Drive))[/]" CL; Write-SpectreHost (" Used $diskBar [gold1]{0,5:F1}%[/] Free [grey]{1:F1} GB[/]" -f $disk.UsedPct, $disk.FreeGB) CL; Write-SpectreHost " Read [teal]$rdBar[/] [grey]$(Format-NetSpeed $io.ReadBps)[/] Write [fuchsia]$wrBar[/] [grey]$(Format-NetSpeed $io.WriteBps)[/]" CL; Write-Host "" # Network $dlBar = Get-SparkBar -Pct ([Math]::Min(100, $net.RxBps / 10MB * 100)) $ulBar = Get-SparkBar -Pct ([Math]::Min(100, $net.TxBps / 10MB * 100)) CL; Write-SpectreHost " [bold]⇅ Network[/]" CL; Write-SpectreHost " Down [green]$dlBar[/] [grey]$(Format-NetSpeed $net.RxBps)[/] Up [dodgerblue1]$ulBar[/] [grey]$(Format-NetSpeed $net.TxBps)[/]" CL; Write-Host "" # Top Processes CL; Write-SpectreHost " [bold]▶ Top Processes[/]" $procLines = @($procs) for ($i = 0; $i -lt 5; $i++) { if ($i -lt $procLines.Count) { $pName = $procLines[$i].Name if ($pName.Length -gt 14) { $pName = $pName.Substring(0, 11) + '...' } $pName = $pName.PadRight(14) $pBar = Get-ProgressBar -Pct $procLines[$i].CpuPct -Width 5 CL; Write-SpectreHost " [grey]$(Esc $pName)[/] $pBar [gold1]$($procLines[$i].CpuPct)%[/]" } else { CL; Write-Host "" } } CL; Write-Host "" # CPU alerts $alerts = @($highCpuTracker.GetEnumerator() | Where-Object { $_.Value -ge $ProcCpuWindow }) if ($alerts -and -not $NoProcCpuAlerts) { $alertMsg = ($alerts | ForEach-Object { "$(Esc $_.Key) sustained $($_.Value)× above ${ProcCpuThreshold}%" }) -join ', ' CL; Write-SpectreHost " [red1]⚠ High CPU Alert:[/] $alertMsg" CL; Write-Host "" } CL; Write-SpectreHost " [grey]Refreshes every ${refresh}s | Q Quit[/]" # Clear anything remaining below Write-Host "`e[J" -NoNewline Start-Sleep -Seconds $refresh } } finally { Show-Cursor [Console]::remove_CancelKeyPress($cancelHandler) [Console]::TreatControlCAsInput = $false } Write-Host "" |