Introduce Sources/Intelligence with an @Observable AlertEngine that evaluates five configurable rules (CPU temperature, fan stall, low memory, low storage, low battery) against a Sendable AlertEvaluationContext built after each telemetry decode pass. Breached conditions dispatch through a mockable AlertNotificationDispatching protocol to UNUserNotificationCenter with per-alert cooldowns, edge-triggered resolve detection, and a capped in-memory history log. Add a macOS Settings scene (AlertsSettingsView) for thresholds, cooldown, permission status, and test notifications; a new Alerts and Notifications sidebar tab with active-alert cards and the history log; header and menu bar warning indicators; and an AppNavigationBus so notification actions (Open MacMonitor, View Processes) steer the main window. Also fix the memory snapshot decode to read memoryPressureStatus so pressureLevel is populated correctly. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
68 lines
2.5 KiB
Swift
68 lines
2.5 KiB
Swift
import Foundation
|
|
|
|
// MARK: - Alert Configuration
|
|
|
|
/// User-tunable threshold and cooldown settings for `AlertEngine`.
|
|
/// Persisted as a single JSON blob in `UserDefaults` so the engine can read
|
|
/// configuration outside of a SwiftUI context.
|
|
public struct AlertConfiguration: Codable, Equatable, Sendable {
|
|
/// Global switch — when false, no rules are evaluated.
|
|
public var alertsEnabled: Bool = true
|
|
|
|
/// Minimum minutes between two notifications for the same alert identifier
|
|
/// while a condition remains breached.
|
|
public var cooldownMinutes: Double = 15
|
|
|
|
// MARK: CPU Temperature
|
|
public var cpuTempEnabled: Bool = true
|
|
/// Degrees Celsius; breach when the highest core/package reading reaches this.
|
|
public var cpuTempThresholdC: Double = 95
|
|
|
|
// MARK: Fan Stall
|
|
public var fanStallEnabled: Bool = true
|
|
/// Degrees Celsius; a fan reading 0 RPM only alerts while the CPU is at or
|
|
/// above this temperature (distinguishes genuine stalls from idle spin-down).
|
|
public var fanStallTempThresholdC: Double = 75
|
|
|
|
// MARK: Memory Pressure
|
|
public var memoryEnabled: Bool = true
|
|
/// Free physical RAM threshold in megabytes.
|
|
public var memoryFreeMBThreshold: Double = 500
|
|
/// Also alert whenever the kernel pressure tier reports "Critical".
|
|
public var memoryAlertOnCriticalPressure: Bool = true
|
|
|
|
// MARK: Storage
|
|
public var storageEnabled: Bool = true
|
|
/// Free-space threshold per volume in gigabytes.
|
|
public var storageFreeGBThreshold: Double = 10
|
|
/// Free-space threshold per volume as a percentage of capacity.
|
|
public var storageFreePercentThreshold: Double = 10
|
|
|
|
// MARK: Battery
|
|
public var batteryEnabled: Bool = true
|
|
/// Charge percentage; only evaluated while discharging on battery power.
|
|
public var batteryPercentThreshold: Double = 10
|
|
|
|
public init() {}
|
|
}
|
|
|
|
// MARK: - UserDefaults Persistence
|
|
|
|
public extension AlertConfiguration {
|
|
static let storageKey = "com.i3omb.macmonitor.alertConfiguration"
|
|
|
|
static func load(from defaults: UserDefaults = .standard) -> AlertConfiguration {
|
|
guard let data = defaults.data(forKey: storageKey),
|
|
let config = try? JSONDecoder().decode(AlertConfiguration.self, from: data) else {
|
|
return AlertConfiguration()
|
|
}
|
|
return config
|
|
}
|
|
|
|
func save(to defaults: UserDefaults = .standard) {
|
|
if let data = try? JSONEncoder().encode(self) {
|
|
defaults.set(data, forKey: AlertConfiguration.storageKey)
|
|
}
|
|
}
|
|
}
|