Files
TopManager/TopManager/Models/HealthModel.swift
hariel1985 bc494af363 Increment 4: alerts engine, notifications & System Health score
- HealthModel (pure): 0-100 score + plain-language diagnosis + rating
- SystemAlert/AlertKind/AlertThresholds + pure AlertEvaluator (sustained-breach)
- AlertCenter (@MainActor): per-cycle health + alert evaluation, de-dup by kind,
  inbox (capped), native UserNotifications on transition into a condition
- Covers: sustained high CPU, critical memory pressure, disk full, thermal,
  runaway process, low battery
- Wire evaluate() into SystemMonitor; request notif auth on launch
- HealthCardView (score ring + diagnosis) + AlertsInboxView in Power & Storage;
  health score + alert badge in menu bar
- Renamed model Alert->SystemAlert to avoid SwiftUI.Alert shadowing
- 10 new tests incl. AlertCenter de-dup integration (52 total green)
- Visual smoke: Energy + Disk I/O columns render with live data under load

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 20:20:24 +02:00

83 sor
2.7 KiB
Swift
Nyers Blame Előzmények

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
/// Architecture-neutral thermal level so the pure health model doesn't depend on
/// Foundation's `ProcessInfo.ThermalState` (keeps it trivially unit-testable).
enum ThermalLevel {
case nominal, fair, serious, critical
}
/// Everything the health score needs, as plain values.
struct HealthInput {
let cpuUsage: Double // global CPU %
let memoryPressure: MemoryPressure
let swapUsedBytes: UInt64
let thermal: ThermalLevel
let diskUsedFraction: Double // 0…1 for the fullest volume
}
/// Computes a 0–100 "system health" score and a plain-language diagnosis.
/// This is what turns raw numbers into "is my Mac OK, and if not, why?".
enum HealthModel {
static func score(_ input: HealthInput) -> Int {
var score = 100.0
// Sustained high CPU: lose up to ~20 points as usage climbs 60→100%.
if input.cpuUsage > 60 {
score -= min(20, (input.cpuUsage - 60) * 0.5)
}
switch input.memoryPressure {
case .warning: score -= 15
case .critical: score -= 30
default: break
}
if input.swapUsedBytes > 2_000_000_000 { score -= 10 }
else if input.swapUsedBytes > 500_000_000 { score -= 5 }
switch input.thermal {
case .fair: score -= 5
case .serious: score -= 20
case .critical: score -= 35
case .nominal: break
}
if input.diskUsedFraction > 0.95 { score -= 15 }
else if input.diskUsedFraction > 0.90 { score -= 8 }
return max(0, min(100, Int(score.rounded())))
}
static func diagnosis(_ input: HealthInput) -> [String] {
var issues: [String] = []
if input.cpuUsage > 85 {
issues.append("CPU is heavily loaded (\(Int(input.cpuUsage))%)")
}
switch input.memoryPressure {
case .critical: issues.append("Memory pressure is critical — the system is low on RAM")
case .warning: issues.append("Memory pressure is elevated")
default: break
}
if input.swapUsedBytes > 2_000_000_000 {
issues.append("High swap usage — closing apps may help")
}
switch input.thermal {
case .serious, .critical: issues.append("System is running hot and may be throttling")
default: break
}
if input.diskUsedFraction > 0.90 {
issues.append("Startup disk is nearly full (\(Int(input.diskUsedFraction * 100))%)")
}
return issues
}
static func rating(_ score: Int) -> String {
switch score {
case 85...: return "Excellent"
case 70..<85: return "Good"
case 50..<70: return "Fair"
default: return "Poor"
}
}
}