Files
MacMonitor/Sources/Intelligence/AlertModels.swift
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> a7f13542ae feat(alerts): implement threshold alert engine and Notification Center integration (fixes #24)
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>
2026-09-08 13:53:48 +01:00

126 lines
4.2 KiB
Swift

import Foundation
// MARK: - Alert Models
/// Categories of threshold conditions evaluated by `AlertEngine`.
public enum AlertKind: String, Codable, CaseIterable, Sendable {
case cpuTemperature
case fanStall
case memoryPressure
case lowStorage
case lowBattery
}
/// Visual and audible emphasis level for a raised alert.
public enum AlertSeverity: String, Codable, Sendable {
case warning
case critical
}
/// A single breached threshold condition.
public struct SystemAlert: Identifiable, Equatable, Sendable {
/// Stable identifier of the form `"<kind>"` or `"<kind>:<subject>"`
/// (e.g. `"lowStorage:/"`, `"fanStall:0"`) used for cooldown bookkeeping.
public let id: String
public let kind: AlertKind
public let severity: AlertSeverity
public let title: String
public let message: String
public let timestamp: Date
public init(id: String, kind: AlertKind, severity: AlertSeverity, title: String, message: String, timestamp: Date) {
self.id = id
self.kind = kind
self.severity = severity
self.title = title
self.message = message
self.timestamp = timestamp
}
}
/// What happened to an alert condition during an evaluation pass.
public enum AlertEventOutcome: String, Codable, Sendable {
case triggered
case resolved
case suppressedCooldown
}
/// An entry in the in-memory alert history log.
public struct AlertEvent: Identifiable, Equatable, Sendable {
public let id: UUID
public let timestamp: Date
public let alertID: String
public let kind: AlertKind
public let outcome: AlertEventOutcome
public let message: String
public init(timestamp: Date, alertID: String, kind: AlertKind, outcome: AlertEventOutcome, message: String) {
self.id = UUID()
self.timestamp = timestamp
self.alertID = alertID
self.kind = kind
self.outcome = outcome
self.message = message
}
}
/// Per-volume storage context consumed by the low-storage rule.
public struct StorageVolumeContext: Equatable, Sendable {
public let mountPoint: String
public let volumeName: String
public let freeBytes: UInt64
public let freePercent: Double
public init(mountPoint: String, volumeName: String, freeBytes: UInt64, freePercent: Double) {
self.mountPoint = mountPoint
self.volumeName = volumeName
self.freeBytes = freeBytes
self.freePercent = freePercent
}
}
/// Flat, immutable snapshot of the metrics the alert rules inspect.
/// Assembled by `SystemTelemetryStore` after each decode pass so the engine
/// stays decoupled from provider dictionary schemas and testable in isolation.
public struct AlertEvaluationContext: Sendable {
public var peakCoreTemperature: Double = 0
public var packageTemperature: Double = 0
/// Fan index -> current RPM. Empty when no fan telemetry is available.
public var fanRPMs: [Int: Double] = [:]
public var memoryFreeBytes: UInt64 = 0
public var memoryPressureLevel: String = "Normal"
public var volumes: [StorageVolumeContext] = []
/// Charge level 0-100 reported by the power provider.
public var batteryLevel: Double = 100
public var hasBattery: Bool = false
public var onExternalPower: Bool = true
public var isCharging: Bool = false
public var timestamp: Date = Date()
public init(
peakCoreTemperature: Double = 0,
packageTemperature: Double = 0,
fanRPMs: [Int: Double] = [:],
memoryFreeBytes: UInt64 = 0,
memoryPressureLevel: String = "Normal",
volumes: [StorageVolumeContext] = [],
batteryLevel: Double = 100,
hasBattery: Bool = false,
onExternalPower: Bool = true,
isCharging: Bool = false,
timestamp: Date = Date()
) {
self.peakCoreTemperature = peakCoreTemperature
self.packageTemperature = packageTemperature
self.fanRPMs = fanRPMs
self.memoryFreeBytes = memoryFreeBytes
self.memoryPressureLevel = memoryPressureLevel
self.volumes = volumes
self.batteryLevel = batteryLevel
self.hasBattery = hasBattery
self.onExternalPower = onExternalPower
self.isCharging = isCharging
self.timestamp = timestamp
}
}