Files
MacMonitor/Sources/UI/ContentView.swift
gronod f2dc6bc7fb
MacMonitor CI/CD Pipeline / Build & Test (Intel x86_64) (push) Canceled after 0s
feat(ui): bind all primary telemetry providers to SystemTelemetryStore and ContentView dashboard
2026-09-08 12:27:02 +01:00

641 lines
27 KiB
Swift

import SwiftUI
public struct ContentView: View {
@State private var store = SystemTelemetryStore.shared
@State private var selectedTab: String = "Dashboard"
public init() {}
public var body: some View {
NavigationSplitView {
sidebarContent
} detail: {
detailContent
}
.frame(minWidth: 900, minHeight: 650)
.onAppear {
store.start()
}
}
// MARK: - Sidebar
private var sidebarContent: some View {
List(selection: $selectedTab) {
Section("Overview") {
Label("Dashboard", systemImage: "gauge.with.needle")
.tag("Dashboard")
Label("Hardware Architecture", systemImage: "cpu")
.tag("Architecture")
}
Section("Primary Telemetry") {
Label("CPU Load & Cores", systemImage: "chart.bar.xaxis")
.tag("CPU")
Label("Thermal Matrix", systemImage: "flame")
.tag("Thermals")
Label("Fans & Cooling", systemImage: "fanblades")
.tag("Fans")
Label("Memory & Swap", systemImage: "memorychip")
.tag("Memory")
Label("Storage Volumes", systemImage: "internaldrive")
.tag("Storage")
Label("Power & Voltage", systemImage: "bolt.fill")
.tag("Power")
}
}
.listStyle(.sidebar)
.navigationSplitViewColumnWidth(min: 200, ideal: 230, max: 280)
}
// MARK: - Detail Content Router
@ViewBuilder
private var detailContent: some View {
ScrollView {
VStack(alignment: .leading, spacing: 20) {
headerCard
engineControlCard
switch selectedTab {
case "Dashboard":
dashboardView
case "Architecture":
specsCard
providersCard
case "CPU":
cpuLoadCard
specsCard
case "Thermals":
cpuThermalCard
componentThermalCard
case "Fans":
fansCard
case "Memory":
memoryCard
case "Storage":
storageCard
case "Power":
powerCard
default:
dashboardView
}
}
.padding(24)
}
.navigationTitle("MacMonitor — \(selectedTab)")
}
// MARK: - Dashboard Composite View
private var dashboardView: some View {
VStack(alignment: .leading, spacing: 20) {
// Top Row: CPU & Memory Summary Cards
HStack(alignment: .top, spacing: 16) {
cpuLoadCard
memoryCard
}
// Middle Row: Thermals & Fans Summary Cards
HStack(alignment: .top, spacing: 16) {
cpuThermalCard
fansCard
}
// Power & Storage Summary
HStack(alignment: .top, spacing: 16) {
powerCard
storageCard
}
// Detailed Component Thermals Matrix
componentThermalCard
}
}
// MARK: - Header Banner
private var headerCard: some View {
HStack(alignment: .center, spacing: 16) {
Image(systemName: "macpro.gen3.fill")
.resizable()
.scaledToFit()
.frame(width: 44, height: 44)
.foregroundStyle(.blue)
VStack(alignment: .leading, spacing: 4) {
Text(store.hostModel)
.font(.title2.bold())
Text("Intel x86_64 • \(store.osVersion)")
.font(.subheadline)
.foregroundStyle(.secondary)
}
Spacer()
HStack(spacing: 8) {
Circle()
.fill(store.isRunning ? Color.green : Color.red)
.frame(width: 10, height: 10)
Text(store.isRunning ? "ACTIVE" : "PAUSED")
.font(.caption.bold())
.foregroundStyle(store.isRunning ? .green : .secondary)
}
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(.quaternary, in: Capsule())
}
.padding(16)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
}
// MARK: - Telemetry Coordinator Controls
private var engineControlCard: some View {
HStack(spacing: 16) {
Button(action: {
if store.isRunning { store.stop() } else { store.start() }
}) {
Label(store.isRunning ? "Pause Engine" : "Start Engine",
systemImage: store.isRunning ? "pause.fill" : "play.fill")
}
.buttonStyle(.borderedProminent)
.tint(store.isRunning ? .orange : .green)
Button(action: { store.triggerManualSample() }) {
Label("Sample Now", systemImage: "arrow.clockwise")
}
.buttonStyle(.bordered)
Spacer()
Picker("Interval:", selection: Binding(
get: { store.sampleInterval },
set: { store.setSampleInterval($0) }
)) {
Text("0.5s").tag(0.5)
Text("1.0s").tag(1.0)
Text("2.0s").tag(2.0)
Text("5.0s").tag(5.0)
}
.pickerStyle(.segmented)
.frame(width: 220)
Text("Updated: \(store.lastUpdateTimestamp.formatted(date: .omitted, time: .standard))")
.font(.caption)
.foregroundStyle(.secondary)
}
.padding(14)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
}
// MARK: - CPU Load Card
private var cpuLoadCard: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Label("CPU Activity", systemImage: "cpu")
.font(.headline)
Spacer()
if store.cpuLoad.isThrottled {
Text("THROTTLED")
.font(.caption.bold())
.foregroundStyle(.red)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.red.opacity(0.15), in: Capsule())
}
Text(String(format: "%.1f%%", store.cpuLoad.totalLoad))
.font(.title3.bold().monospacedDigit())
}
ProgressView(value: min(store.cpuLoad.totalLoad / 100.0, 1.0))
.tint(store.cpuLoad.totalLoad > 85 ? .red : (store.cpuLoad.totalLoad > 60 ? .orange : .blue))
HStack {
Text("User: \(String(format: "%.1f%%", store.cpuLoad.userLoad))")
Spacer()
Text("System: \(String(format: "%.1f%%", store.cpuLoad.systemLoad))")
Spacer()
Text("Idle: \(String(format: "%.1f%%", store.cpuLoad.idleLoad))")
}
.font(.caption)
.foregroundStyle(.secondary)
if !store.cpuLoad.perCoreLoad.isEmpty {
Divider()
Text("Per-Core Utilization (\(store.cpuLoad.perCoreLoad.count) Cores)")
.font(.caption.bold())
.foregroundStyle(.secondary)
LazyVGrid(columns: [GridItem(.adaptive(minimum: 70))], spacing: 8) {
ForEach(Array(store.cpuLoad.perCoreLoad.enumerated()), id: \.offset) { idx, load in
VStack(spacing: 4) {
Text("Core \(idx)")
.font(.system(size: 10))
.foregroundStyle(.secondary)
ProgressView(value: min(load / 100.0, 1.0))
.tint(load > 85 ? .red : .blue)
Text(String(format: "%.0f%%", load))
.font(.system(size: 10, weight: .bold, design: .monospaced))
}
}
}
}
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .topLeading)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
}
// MARK: - Memory & Swap Card
private var memoryCard: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Label("Memory & Swap", systemImage: "memorychip")
.font(.headline)
Spacer()
Text(store.memory.pressureLevel)
.font(.caption.bold())
.foregroundStyle(pressureColor(store.memory.pressureLevel))
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(pressureColor(store.memory.pressureLevel).opacity(0.15), in: Capsule())
Text(String(format: "%.1f%%", store.memory.utilizationPercentage))
.font(.title3.bold().monospacedDigit())
}
ProgressView(value: min(store.memory.utilizationPercentage / 100.0, 1.0))
.tint(pressureColor(store.memory.pressureLevel))
Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 6) {
GridRow {
Text("Physical RAM:").foregroundStyle(.secondary)
Text(formatBytes(store.memory.totalBytes)).bold()
Text("Used:").foregroundStyle(.secondary)
Text(formatBytes(store.memory.usedBytes)).bold()
}
GridRow {
Text("Wired:").foregroundStyle(.secondary)
Text(formatBytes(store.memory.wiredBytes))
Text("Compressed:").foregroundStyle(.secondary)
Text(formatBytes(store.memory.compressedBytes))
}
GridRow {
Text("Active:").foregroundStyle(.secondary)
Text(formatBytes(store.memory.activeBytes))
Text("Free / Inactive:").foregroundStyle(.secondary)
Text(formatBytes(store.memory.freeBytes + store.memory.inactiveBytes))
}
if store.memory.swapTotalBytes > 0 {
GridRow {
Text("Swap Total:").foregroundStyle(.secondary)
Text(formatBytes(store.memory.swapTotalBytes))
Text("Swap Used:").foregroundStyle(.secondary)
Text(formatBytes(store.memory.swapUsedBytes))
}
}
}
.font(.caption)
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .topLeading)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
}
// MARK: - CPU Thermal Card
private var cpuThermalCard: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Label("CPU Thermals", systemImage: "flame.fill")
.font(.headline)
Spacer()
Text(store.cpuThermal.thermalPressureState)
.font(.caption.bold())
.foregroundStyle(store.cpuThermal.isThrottling ? .red : .green)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background((store.cpuThermal.isThrottling ? Color.red : Color.green).opacity(0.15), in: Capsule())
Text(String(format: "%.1f°C", store.cpuThermal.packageTemperature))
.font(.title3.bold().monospacedDigit())
}
HStack(spacing: 24) {
VStack(alignment: .leading, spacing: 2) {
Text("Package Temp").font(.caption).foregroundStyle(.secondary)
Text(String(format: "%.1f°C", store.cpuThermal.packageTemperature)).bold()
}
VStack(alignment: .leading, spacing: 2) {
Text("Peak Core").font(.caption).foregroundStyle(.secondary)
Text(String(format: "%.1f°C", store.cpuThermal.peakCoreTemperature)).bold()
}
VStack(alignment: .leading, spacing: 2) {
Text("Average Core").font(.caption).foregroundStyle(.secondary)
Text(String(format: "%.1f°C", store.cpuThermal.averageCoreTemperature)).bold()
}
}
if !store.cpuThermal.coreTemperatures.isEmpty {
Divider()
Text("Core Temperature Breakdown")
.font(.caption.bold())
.foregroundStyle(.secondary)
LazyVGrid(columns: [GridItem(.adaptive(minimum: 80))], spacing: 8) {
ForEach(store.cpuThermal.coreTemperatures, id: \.key) { core in
VStack(spacing: 2) {
Text("Core \(core.index)")
.font(.system(size: 10))
.foregroundStyle(.secondary)
Text(String(format: "%.1f°C", core.temperature))
.font(.system(size: 11, weight: .bold, design: .monospaced))
.foregroundStyle(core.temperature > 85 ? .red : (core.temperature > 70 ? .orange : .primary))
}
.padding(6)
.background(.quaternary, in: RoundedRectangle(cornerRadius: 6))
}
}
}
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .topLeading)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
}
// MARK: - Fans & Tachometers Card
private var fansCard: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Label("Cooling Fans", systemImage: "fanblades.fill")
.font(.headline)
Spacer()
Text("\(store.fans.count) Detected")
.font(.caption.bold())
.foregroundStyle(.secondary)
}
if store.fans.isEmpty {
Text("No active AppleSMC fans detected or passive cooling chassis.")
.font(.caption)
.foregroundStyle(.secondary)
.padding(.vertical, 8)
} else {
ForEach(store.fans) { fan in
VStack(alignment: .leading, spacing: 6) {
HStack {
Text(fan.name).bold()
Spacer()
Text(String(format: "%.0f RPM (%.0f%%)", fan.currentRPM, fan.utilization))
.font(.subheadline.bold().monospacedDigit())
}
ProgressView(value: min(fan.utilization / 100.0, 1.0))
.tint(fan.utilization > 80 ? .red : .blue)
HStack {
Text("Min: \(Int(fan.minRPM)) RPM")
Spacer()
Text("Target: \(Int(fan.targetRPM)) RPM")
Spacer()
Text("Max: \(Int(fan.maxRPM)) RPM")
}
.font(.system(size: 10))
.foregroundStyle(.secondary)
}
.padding(8)
.background(.quaternary, in: RoundedRectangle(cornerRadius: 8))
}
}
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .topLeading)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
}
// MARK: - Component Thermals Matrix Card
private var componentThermalCard: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Label("Chassis & Component Thermals", systemImage: "thermometer.medium")
.font(.headline)
Spacer()
Text("\(store.componentTemps.count) Sensors")
.font(.caption.bold())
.foregroundStyle(.secondary)
}
if store.componentTemps.isEmpty {
Text("Awaiting component thermal probe...")
.font(.caption)
.foregroundStyle(.secondary)
} else {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 160))], spacing: 10) {
ForEach(store.componentTemps) { item in
VStack(alignment: .leading, spacing: 4) {
Text(item.name)
.font(.system(size: 11, weight: .semibold))
.lineLimit(1)
HStack {
Text(item.category)
.font(.system(size: 9))
.foregroundStyle(.secondary)
Spacer()
Text(String(format: "%.1f°C", item.temperature))
.font(.system(size: 11, weight: .bold, design: .monospaced))
.foregroundStyle(item.temperature > 75 ? .red : .primary)
}
}
.padding(8)
.background(.quaternary, in: RoundedRectangle(cornerRadius: 8))
}
}
}
}
.padding(16)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
}
// MARK: - Storage Volumes Card
private var storageCard: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Label("Storage Volumes", systemImage: "internaldrive")
.font(.headline)
Spacer()
Text("\(store.storageVolumes.count) Mounted")
.font(.caption.bold())
.foregroundStyle(.secondary)
}
if store.storageVolumes.isEmpty {
Text("No mounted APFS/HFS volumes discovered.")
.font(.caption)
.foregroundStyle(.secondary)
} else {
ForEach(store.storageVolumes) { vol in
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(vol.volumeName).bold()
Text("(\(vol.fileSystem))").font(.caption).foregroundStyle(.secondary)
Spacer()
Text("\(formatBytes(vol.usedBytes)) / \(formatBytes(vol.totalBytes))")
.font(.caption.monospacedDigit())
}
ProgressView(value: min(vol.usedPercentage / 100.0, 1.0))
.tint(vol.usedPercentage > 90 ? .red : .blue)
HStack {
Text("Mount: \(vol.mountPoint)").font(.system(size: 10)).foregroundStyle(.secondary)
Spacer()
Text(String(format: "%.1f%% Used", vol.usedPercentage)).font(.system(size: 10, weight: .bold))
}
}
.padding(8)
.background(.quaternary, in: RoundedRectangle(cornerRadius: 8))
}
}
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .topLeading)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
}
// MARK: - Power & Voltage Card
private var powerCard: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Label("Power & Voltage", systemImage: "bolt.fill")
.font(.headline)
Spacer()
HStack(spacing: 4) {
Image(systemName: store.power.powerSource.contains("Battery") ? "battery.75" : "powerplug.fill")
Text(store.power.powerSource)
}
.font(.caption.bold())
.foregroundStyle(.secondary)
Text(String(format: "%.1f W", store.power.systemTotalWatts))
.font(.title3.bold().monospacedDigit())
}
Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 6) {
GridRow {
Text("Total Draw:").foregroundStyle(.secondary)
Text(String(format: "%.1f W", store.power.systemTotalWatts)).bold()
Text("CPU Package:").foregroundStyle(.secondary)
Text(String(format: "%.1f W", store.power.cpuWatts)).bold()
}
GridRow {
Text("CPU Core Voltage:").foregroundStyle(.secondary)
Text(store.power.cpuVoltage > 0 ? String(format: "%.3f V", store.power.cpuVoltage) : "N/A")
Text("CPU Current:").foregroundStyle(.secondary)
Text(store.power.cpuCurrent > 0 ? String(format: "%.2f A", store.power.cpuCurrent) : "N/A")
}
if store.power.gpuWatts > 0 || store.power.memoryWatts > 0 {
GridRow {
Text("GPU Draw:").foregroundStyle(.secondary)
Text(String(format: "%.1f W", store.power.gpuWatts))
Text("Memory Subsystem:").foregroundStyle(.secondary)
Text(String(format: "%.1f W", store.power.memoryWatts))
}
}
if store.power.powerSource.contains("Battery") {
GridRow {
Text("Battery Level:").foregroundStyle(.secondary)
Text(String(format: "%.0f%% %@", store.power.batteryLevel, store.power.isCharging ? "(Charging)" : ""))
Text("Status:").foregroundStyle(.secondary)
Text(store.power.isCharging ? "Charging" : "Discharging")
}
}
}
.font(.caption)
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .topLeading)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
}
// MARK: - Hardware Specs Card
private var specsCard: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Host Architecture & Kernel")
.font(.headline)
Grid(alignment: .leading, horizontalSpacing: 24, verticalSpacing: 10) {
GridRow {
Text("Hardware Model:").foregroundStyle(.secondary)
Text(store.hostModel).bold()
Text("Physical Cores:").foregroundStyle(.secondary)
Text("\(store.physicalCpuCount)").bold()
}
GridRow {
Text("Darwin Kernel:").foregroundStyle(.secondary)
Text(store.kernelVersion).bold()
Text("Logical Cores:").foregroundStyle(.secondary)
Text("\(store.logicalCpuCount)").bold()
}
GridRow {
Text("Target Architecture:").foregroundStyle(.secondary)
Text("Intel x86_64").bold()
Text("Deployment Target:").foregroundStyle(.secondary)
Text("macOS 14.0 (Sonoma)").bold()
}
}
}
.padding(16)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
}
// MARK: - Providers Card
private var providersCard: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("Registered Telemetry Providers")
.font(.headline)
Spacer()
Text("\(store.registeredProviderCount) Active")
.font(.caption.bold())
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(Color.blue.opacity(0.15))
.foregroundStyle(.blue)
.clipShape(Capsule())
}
ForEach(Array(store.latestSnapshot.keys.sorted()), id: \.self) { key in
HStack {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(.green)
Text(key)
.font(.system(.body, design: .monospaced))
Spacer()
Text("\(store.latestSnapshot[key]?.count ?? 0) metrics")
.font(.caption)
.foregroundStyle(.secondary)
}
.padding(.vertical, 4)
}
}
.padding(16)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
}
// MARK: - Formatting Helpers
private func formatBytes(_ bytes: UInt64) -> String {
let formatter = ByteCountFormatter()
formatter.allowedUnits = [.useGB, .useMB]
formatter.countStyle = .memory
return formatter.string(fromByteCount: Int64(bytes))
}
private func pressureColor(_ level: String) -> Color {
switch level {
case "Critical": return .red
case "Warning": return .orange
default: return .green
}
}
}