Files
MacMonitor/Sources/Intelligence/AlertModels.swift
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 807fcae6b2 fix(alerts): skip read-only and trivially small volumes in low-storage rule (fixes #33)
Read-only mounts (DMG images like /Volumes/Ghostty, update snapshots) with
~0% free produced false-positive low-storage alerts. StorageVolumeContext
now carries isReadOnly and totalBytes; the rule skips read-only volumes and
volumes under 1 GB where thresholds are meaningless. Regression tests cover
both exclusions plus the normal trigger path.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-08 15:16:45 +01:00

132 lines
4.5 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 let totalBytes: UInt64
/// Read-only mounts (DMG images, snapshots) are excluded from alerting.
public let isReadOnly: Bool
public init(mountPoint: String, volumeName: String, freeBytes: UInt64, freePercent: Double,
totalBytes: UInt64 = 0, isReadOnly: Bool = false) {
self.mountPoint = mountPoint
self.volumeName = volumeName
self.freeBytes = freeBytes
self.freePercent = freePercent
self.totalBytes = totalBytes
self.isReadOnly = isReadOnly
}
}
/// 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
}
}