Compare commits

...
Author SHA1 Message Date
gronod 9f2d0c58d8 Merge pull request 'release: v0.0.2 — M6 bugfix release' (#40) from develop into main
MacMonitor CI/CD Pipeline / Build & Test (Intel x86_64) (push) Successful in 49s
MacMonitor CI/CD Pipeline / Package Release Artifact (push) Successful in 1m4s
2026-09-08 15:19:51 +01:00
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 60f4039013 chore(release): bump version to 0.0.2 (build 2)
M6 bugfix release: restores all telemetry sections (snapshot contract,
SMC service+struct fixes), alert false-positive filtering, and sidebar
selection.

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

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-08 15:19:34 +01:00
gronod 125c489881 Merge pull request 'M6: Bugfix & Stability — milestone integration' (#39) from milestone/m6-bugfix-stability into develop 2026-09-08 15:18:52 +01:00
gronod d75873fa30 Merge pull request 'fix(ui): restore Alerts sidebar row selection (Issue #34)' (#38) from fix/34-alerts-sidebar-badge into milestone/m6-bugfix-stability 2026-09-08 15:18:25 +01:00
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 3095bd106a fix(ui): restore Alerts sidebar row selection by replacing .badge (fixes #34)
Applying .badge after .tag on a List(selection:) row wrapped the tagged
label in a view that no longer carried the tag, making the "Alerts &
Notifications" row unselectable on macOS 14. Replaced with an explicit
Label + HStack row showing the active-alert count as a styled capsule —
tag stays on the row, count still renders when alerts are active.

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

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-08 15:18:07 +01:00
gronod d0c286e7c8 Merge pull request 'fix(alerts): skip read-only/small volumes in low-storage rule (Issue #33)' (#37) from fix/33-readonly-storage-alerts into milestone/m6-bugfix-stability 2026-09-08 15:17:04 +01:00
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
gronod 0e5af9d70d Merge pull request 'fix(smc): correct IOKit service name + SMCKeyData_t layout (Issue #32)' (#36) from fix/32-smc-service-name into milestone/m6-bugfix-stability 2026-09-08 15:15:08 +01:00
7 changed files with 65 additions and 15 deletions
+4 -4
View File
@@ -765,7 +765,7 @@
CODE_SIGNING_ALLOWED = NO; CODE_SIGNING_ALLOWED = NO;
CODE_SIGN_IDENTITY = ""; CODE_SIGN_IDENTITY = "";
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 2;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO; ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -779,7 +779,7 @@
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 0.0.1; MARKETING_VERSION = 0.0.2;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = NO; ONLY_ACTIVE_ARCH = NO;
@@ -852,7 +852,7 @@
CODE_SIGNING_ALLOWED = NO; CODE_SIGNING_ALLOWED = NO;
CODE_SIGN_IDENTITY = ""; CODE_SIGN_IDENTITY = "";
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 2;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES; ENABLE_TESTABILITY = YES;
@@ -872,7 +872,7 @@
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 0.0.1; MARKETING_VERSION = 0.0.2;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = NO; ONLY_ACTIVE_ARCH = NO;
+3 -1
View File
@@ -702,7 +702,9 @@ public final class SystemTelemetryStore {
freeBytes: $0.freeBytes, freeBytes: $0.freeBytes,
freePercent: $0.totalBytes > 0 freePercent: $0.totalBytes > 0
? Double($0.freeBytes) / Double($0.totalBytes) * 100.0 ? Double($0.freeBytes) / Double($0.totalBytes) * 100.0
: 0 : 0,
totalBytes: $0.totalBytes,
isReadOnly: $0.isReadOnly
) )
} }
context.batteryLevel = self.batteryHealth.maxCapacity > 0 context.batteryLevel = self.batteryHealth.maxCapacity > 0
+4 -1
View File
@@ -163,7 +163,10 @@ public final class AlertEngine {
// 4. Low storage space (per mounted volume) // 4. Low storage space (per mounted volume)
if cfg.storageEnabled { if cfg.storageEnabled {
for volume in ctx.volumes { // Skip read-only mounts (DMGs, snapshots) and trivially small
// volumes where free-space thresholds are meaningless.
let minTotalBytes: UInt64 = 1_073_741_824 // 1 GB
for volume in ctx.volumes where !volume.isReadOnly && volume.totalBytes >= minTotalBytes {
let freeGB = Double(volume.freeBytes) / (1024 * 1024 * 1024) let freeGB = Double(volume.freeBytes) / (1024 * 1024 * 1024)
if freeGB < cfg.storageFreeGBThreshold || volume.freePercent < cfg.storageFreePercentThreshold { if freeGB < cfg.storageFreeGBThreshold || volume.freePercent < cfg.storageFreePercentThreshold {
alerts.append(SystemAlert( alerts.append(SystemAlert(
+7 -1
View File
@@ -70,12 +70,18 @@ public struct StorageVolumeContext: Equatable, Sendable {
public let volumeName: String public let volumeName: String
public let freeBytes: UInt64 public let freeBytes: UInt64
public let freePercent: Double 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) { public init(mountPoint: String, volumeName: String, freeBytes: UInt64, freePercent: Double,
totalBytes: UInt64 = 0, isReadOnly: Bool = false) {
self.mountPoint = mountPoint self.mountPoint = mountPoint
self.volumeName = volumeName self.volumeName = volumeName
self.freeBytes = freeBytes self.freeBytes = freeBytes
self.freePercent = freePercent self.freePercent = freePercent
self.totalBytes = totalBytes
self.isReadOnly = isReadOnly
} }
} }
+19 -3
View File
@@ -87,9 +87,25 @@ public struct ContentView: View {
} }
Section("Intelligence") { Section("Intelligence") {
Label("Alerts & Notifications", systemImage: "bell.badge") // Custom row instead of .badge() a badge applied after .tag
.tag("Alerts") // detaches the tag from the list row and makes it unselectable.
.badge(store.alertEngine.activeAlerts.isEmpty ? nil : Text("\(store.alertEngine.activeAlerts.count)")) Label {
HStack {
Text("Alerts & Notifications")
Spacer()
if !store.alertEngine.activeAlerts.isEmpty {
Text("\(store.alertEngine.activeAlerts.count)")
.font(.caption2.weight(.semibold))
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.red.opacity(0.85), in: Capsule())
.foregroundStyle(.white)
}
}
} icon: {
Image(systemName: "bell.badge")
}
.tag("Alerts")
} }
} }
.listStyle(.sidebar) .listStyle(.sidebar)
+26 -3
View File
@@ -128,8 +128,10 @@ final class MMAlertsTests: XCTestCase {
func testStorageRuleTriggersPerVolume() { func testStorageRuleTriggersPerVolume() {
let volumes = [ let volumes = [
StorageVolumeContext(mountPoint: "/", volumeName: "Macintosh HD", freeBytes: 5 * 1_073_741_824, freePercent: 5), StorageVolumeContext(mountPoint: "/", volumeName: "Macintosh HD", freeBytes: 5 * 1_073_741_824,
StorageVolumeContext(mountPoint: "/Volumes/Data", volumeName: "Data", freeBytes: 500 * 1_073_741_824, freePercent: 50) freePercent: 5, totalBytes: 500 * 1_073_741_824),
StorageVolumeContext(mountPoint: "/Volumes/Data", volumeName: "Data", freeBytes: 500 * 1_073_741_824,
freePercent: 50, totalBytes: 1_000_000_000_000)
] ]
engine.evaluate(context(volumes: volumes)) engine.evaluate(context(volumes: volumes))
XCTAssertEqual(engine.activeAlerts.count, 1) XCTAssertEqual(engine.activeAlerts.count, 1)
@@ -137,11 +139,32 @@ final class MMAlertsTests: XCTestCase {
} }
func testStorageRuleTriggersOnLowPercent() { func testStorageRuleTriggersOnLowPercent() {
let volumes = [StorageVolumeContext(mountPoint: "/Volumes/Big", volumeName: "Big", freeBytes: 200 * 1_073_741_824, freePercent: 4)] let volumes = [StorageVolumeContext(mountPoint: "/Volumes/Big", volumeName: "Big",
freeBytes: 200 * 1_073_741_824, freePercent: 4,
totalBytes: 2_000_000_000_000)]
engine.evaluate(context(volumes: volumes)) engine.evaluate(context(volumes: volumes))
XCTAssertEqual(engine.activeAlerts.count, 1) XCTAssertEqual(engine.activeAlerts.count, 1)
} }
func testStorageRuleSkipsReadOnlyVolumes() {
// A read-only DMG mount (e.g. an app running from a mounted image)
// with ~0% free must not alert issue #33 false positive.
let volumes = [StorageVolumeContext(mountPoint: "/Volumes/Ghostty", volumeName: "Ghostty",
freeBytes: 1_048_576, freePercent: 0.1,
totalBytes: 500_000_000_000, isReadOnly: true)]
engine.evaluate(context(volumes: volumes))
XCTAssertTrue(engine.activeAlerts.isEmpty)
XCTAssertTrue(engine.history.isEmpty)
}
func testStorageRuleSkipsTinyVolumes() {
let volumes = [StorageVolumeContext(mountPoint: "/Volumes/Small", volumeName: "Small",
freeBytes: 1_048_576, freePercent: 0.5,
totalBytes: 200_000_000, isReadOnly: false)]
engine.evaluate(context(volumes: volumes))
XCTAssertTrue(engine.activeAlerts.isEmpty)
}
// MARK: Battery // MARK: Battery
func testBatteryRuleTriggersOnlyWhileDischarging() { func testBatteryRuleTriggersOnlyWhileDischarging() {
+2 -2
View File
@@ -16,8 +16,8 @@ settings:
CLANG_ENABLE_OBJC_ARC: YES CLANG_ENABLE_OBJC_ARC: YES
CLANG_ENABLE_MODULES: YES CLANG_ENABLE_MODULES: YES
GENERATE_INFOPLIST_FILE: YES GENERATE_INFOPLIST_FILE: YES
MARKETING_VERSION: "0.0.1" MARKETING_VERSION: "0.0.2"
CURRENT_PROJECT_VERSION: "1" CURRENT_PROJECT_VERSION: "2"
CODE_SIGNING_ALLOWED: NO CODE_SIGNING_ALLOWED: NO
CODE_SIGN_IDENTITY: "" CODE_SIGN_IDENTITY: ""
GCC_C_LANGUAGE_STANDARD: "gnu17" GCC_C_LANGUAGE_STANDARD: "gnu17"