import SwiftUI import AppKit import Charts @MainActor final class Monitor: ObservableObject { @Published var kind: ResourceKind = .monitor @Published var resources: [Resource] = [] @Published var containerResources: [Resource] = [] @Published var lastSample: Date? @Published var telemetryError = "" @Published var sampling = false @Published var history: [TelemetryPoint] = [] @Published var lastAction = "" private var rateTracker = NetworkRateTracker() @Published var telemetryAvailable = false private var telemetrySource = "" @Published var selection: String? @Published var search = "" @Published var mode = "Inspect" @Published var detail = "Select a resource to inspect." @Published var status = "Connecting…" @Published var error: String? @Published var busy = false @Published var acting = false @Published var showStopped = true @Published var showHostMounts = true @Published var showingConfirmation = false @Published var pendingAction: String? @Published var pendingResource: Resource? @Published var pendingKind: ResourceKind = .containers @Published var follow = true @AppStorage("containerBinary") var binary = ContainerService.defaultBinary @AppStorage("composeProject") var project = "" @Published var demo = CommandLine.arguments.contains("--demo") private var detailToken = UUID() private var refreshToken = UUID() private var mutationEpoch = 0 var service: ContainerService { ContainerService(binary: binary, demo: demo) } var selected: Resource? { kind == .monitor ? nil : visible.first { $0.id == selection } } var visible: [Resource] { resources.filter { r in (kind != .projects || (!r.project.isEmpty && (project.isEmpty || r.project == project))) && (showStopped || !kind.isContainer || r.state == "running") && (kind != .volumes || showHostMounts || !r.readOnlyStorage) && (search.isEmpty || (r.name + " " + r.image + " " + r.project + " " + r.source + " " + r.usedBy).localizedCaseInsensitiveContains(search)) } } func recordTelemetry(_ rows: [Resource], source: String) { if source != telemetrySource { history = []; rateTracker = NetworkRateTracker(); telemetrySource = source } let measured = rows.filter { $0.cpuFraction != nil || $0.memoryBytes != nil } telemetryAvailable = !measured.isEmpty guard !measured.isEmpty else { rateTracker = NetworkRateTracker(); return } let now = Date(), cpu = measured.reduce(0) { $0 + ($1.cpuFraction ?? 0) * 100 }, memory = measured.reduce(0) { $0 + ($1.memoryBytes ?? 0) } let rates = rateTracker.sample(measured, at: now) if demo && history.isEmpty { // Clearly labelled illustrative history makes the demo immediately explorable. history = (0..<60).map { n in let date = now.addingTimeInterval(Double(n - 60) * 3); let wave = sin(date.timeIntervalSince1970 / 13); return TelemetryPoint(date: date, cpu: 8 + 5 * wave, memory: (42 + 4 * wave) * 1_048_576, rx: (12 + 8 * wave) * 1024, tx: (7 + 4 * wave) * 1024) } } history.append(TelemetryPoint(date: now, cpu: cpu, memory: memory, rx: rates.rx, tx: rates.tx)) history = history.filter { now.timeIntervalSince($0.date) <= 360 }.suffix(360).map { $0 } } func changeKind() { resources = []; selection = nil; detail = "Select a resource to inspect."; detailToken = UUID(); refreshToken = UUID(); busy = false } func resetConnection() { changeKind(); containerResources = []; history = []; lastSample = nil; telemetryAvailable = false; telemetryError = ""; status = "Connecting…"; rateTracker = NetworkRateTracker() } func connectLive() { demo = false; resetConnection() } var connectionID: String { "\(binary)|\(demo)|\(project)" } func sampleTelemetry() async { let backend = service, connection = connectionID, epoch = mutationEpoch sampling = true defer { sampling = false } do { let listed = try await backend.list(.containers, project: project) let measured = try await backend.withStats(listed) guard !Task.isCancelled, connectionID == connection, mutationEpoch == epoch else { return } containerResources = measured; lastSample = Date(); telemetryError = "" recordTelemetry(measured, source: connection) if kind.isContainer && !acting { resources = measured if !visible.contains(where: { $0.id == selection }) { selection = visible.first?.id } } if mode == "Stats", let selected { detail = "\(selected.name)\nCPU: \(selected.cpu)\nMemory: \(selected.memory)\nNetwork RX / TX (cumulative): \(selected.network)\nDisk read / write (cumulative): \(selected.disk)\nSample: \(Date.now.formatted(date: .omitted, time: .standard))" } } catch { guard !Task.isCancelled, connectionID == connection, mutationEpoch == epoch else { return } telemetryError = error.localizedDescription; telemetryAvailable = false } } func refresh() async { guard !busy, !acting else { return }; busy = true let token = UUID(); refreshToken = token let requestedKind = kind, backend = service defer { if refreshToken == token { busy = false } } do { var rows = try await backend.list(requestedKind, project: project) guard !Task.isCancelled, refreshToken == token, kind == requestedKind else { return } if requestedKind.isContainer { rows = rows.map { row in var result = row if row.state == "running", let metrics = containerResources.first(where: { $0.id == row.id && $0.state == "running" }) { result.cpu = metrics.cpu; result.memory = metrics.memory; result.network = metrics.network; result.disk = metrics.disk } return result } } if requestedKind == .volumes { let containers = try await backend.list(.containers, project: project) guard !Task.isCancelled, refreshToken == token else { return } rows = VolumeInventory.combine(volumes: rows, containers: containers) } resources = rows if !visible.contains(where: { $0.id == selection }) { selection = visible.first?.id } status = "Connected · \(rows.count) resources · \(rows.filter { $0.state == "running" }.count) running" } catch { if refreshToken == token, !Task.isCancelled { status = "Unavailable: \(error.localizedDescription)"; resources = []; selection = nil; detail = "Check the Apple Container service and executable in Settings." } } } func loadDetail() async { guard let resource = selected else { detail = "Select a resource to inspect."; return } if kind == .volumes, resource.readOnlyStorage { detail = "\(resource.storageKind) · inspect only\nSource: \(resource.source)\nUsed by: \(resource.usedBy)\n\n\(resource.raw)"; return } let token = UUID(); detailToken = token let requestedMode = mode, requestedKind = kind, backend = service do { let result = try await backend.run(ContainerService.detailArguments(kind: requestedKind, id: resource.id, mode: requestedMode)) guard detailToken == token, !Task.isCancelled else { return }; detail = ContainerService.formatted(result) } catch { if detailToken == token, !Task.isCancelled { detail = "Unavailable for this runtime or resource:\n\(error.localizedDescription)" } } } func request(_ action: String) { guard !acting else { return }; guard action == "prune" || selected?.readOnlyStorage != true else { error = "Host mounts are inspect-only. Manage their files on the host; they are not Apple named volumes."; return }; guard !demo else { error = "Demo mode is read-only. Use Connect to live runtime to control real containers."; return }; pendingResource = selected; pendingKind = kind; pendingAction = action; showingConfirmation = true } func performPending() { guard let action = pendingAction, !acting else { return } let resource = pendingResource, requestedKind = pendingKind, backend = service pendingAction = nil; acting = true; mutationEpoch += 1; refreshToken = UUID(); busy = false lastAction = "\(action.capitalized) in progress · \(resource?.name ?? requestedKind.title)" Task { do { let result = try await backend.action(kind: requestedKind, action: action, resource: resource) if let resource, ["start", "stop", "restart"].contains(action) { let expected = action == "stop" ? "stopped" : "running" let verified = try await backend.waitForState(id: resource.id, expected: expected) if let index = resources.firstIndex(where: { $0.id == verified.id }) { resources[index] = verified } lastAction = "\(resource.name) is \(expected) · verified by runtime" } else { lastAction = "Completed \(action) · \(resource?.name ?? requestedKind.title)" } detail = result; status = lastAction } catch { self.error = error.localizedDescription; lastAction = "\(action.capitalized) failed · \(error.localizedDescription)" } acting = false; await refresh(); if mode != "Stats" { await loadDetail() } } } func exportCLI() { guard let source = Bundle.main.url(forAuxiliaryExecutable: "container-compose") else { error = "Build the release app bundle to include the command-line tool."; return } let panel = NSSavePanel(); panel.nameFieldStringValue = "container-compose"; panel.title = "Save the container-compose command-line tool" guard panel.runModal() == .OK, let url = panel.url else { return } do { if FileManager.default.fileExists(atPath: url.path) { try FileManager.default.removeItem(at: url) }; try FileManager.default.copyItem(at: source, to: url); try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: url.path) } catch { self.error = error.localizedDescription } } } struct Workspace: View { @ObservedObject var model: Monitor @State private var sidebarSelection: ResourceKind? = .monitor var body: some View { NavigationSplitView { VStack(alignment: .leading, spacing: 18) { HStack(spacing: 10) { Image(systemName: "shippingbox.fill").font(.largeTitle).foregroundStyle(.orange); VStack(alignment: .leading) { Text("Container Compose").font(.title.bold()); Text("Apple Containers").foregroundStyle(.secondary).font(.caption) } }.padding(.horizontal) List(ResourceKind.allCases, selection: $sidebarSelection) { kind in Label(kind.title, systemImage: kind.symbol).tag(kind) } VStack(alignment: .leading, spacing: 8) { Label(model.demo ? "Demo workspace" : "Local runtime", systemImage: model.demo ? "play.rectangle" : "desktopcomputer"); Text(model.demo ? "Fixture data · actions are simulated" : "Apple container CLI").font(.caption).foregroundStyle(.secondary) }.padding() }.padding(.top, 12).navigationSplitViewColumnWidth(min: 190, ideal: 210) } detail: { VStack(spacing: 0) { if model.demo { HStack { Label("Demo preview · container controls are disabled", systemImage: "eye"); Spacer(); Button("Connect to live runtime") { model.connectLive() } }.font(.callout).padding(12).background(Color.orange.opacity(0.12)) } if !model.lastAction.isEmpty { HStack { if model.acting { ProgressView().controlSize(.small) }; Text(model.lastAction).textSelection(.enabled); Spacer() }.font(.caption).padding(10).background(Color.accentColor.opacity(0.08)) } if model.kind == .monitor { MonitorDashboard(model: model) } else { HStack { VStack(alignment: .leading, spacing: 4) { Text(model.kind.title).font(.title2.bold()); Text("\(model.visible.count) visible resources").font(.caption).foregroundStyle(.secondary) }; Spacer(); if model.busy { ProgressView().controlSize(.small) }; if model.kind.isContainer { Toggle("Show stopped", isOn: $model.showStopped).toggleStyle(.checkbox) }; if model.kind == .volumes { Toggle("Include host mounts", isOn: $model.showHostMounts).toggleStyle(.checkbox) } }.padding() VSplitView { resourceTable.frame(minHeight: 140) VStack(alignment: .leading, spacing: 12) { if let r = model.selected { HStack { Circle().fill(r.state == "running" ? Color.green : Color.secondary).frame(width: 7, height: 7); Text(r.name).font(.headline).textSelection(.enabled); Spacer(); Text(r.state).foregroundStyle(.secondary) } Text(model.kind == .volumes ? "\(r.storageKind) · \(r.source) · \(r.usedBy)" : "\(r.image)\(r.project.isEmpty ? "" : " · " + r.project + " / " + r.service)").font(.caption).foregroundStyle(.secondary).textSelection(.enabled) } HStack { Picker("Details", selection: $model.mode) { Text("Inspect").tag("Inspect"); if model.kind.isContainer { Text("Logs").tag("Logs"); Text("Stats").tag("Stats"); Text("Processes").tag("Processes") } }.pickerStyle(.segmented).frame(maxWidth: 460) Spacer(); if model.mode == "Logs" { Toggle("Follow", isOn: $model.follow).toggleStyle(.checkbox) } Button { NSPasteboard.general.clearContents(); NSPasteboard.general.setString(model.detail, forType: .string) } label: { Image(systemName: "doc.on.doc") }.help("Copy details") } ScrollViewReader { proxy in ScrollView([.horizontal, .vertical]) { VStack(alignment: .leading) { Text(model.detail).font(.system(.caption, design: .monospaced)).textSelection(.enabled).frame(maxWidth: .infinity, alignment: .leading); Color.clear.frame(height: 1).id("end") }.padding(12) } .background(Color(nsColor: .textBackgroundColor), in: RoundedRectangle(cornerRadius: 8)) .onChange(of: model.detail) { _, _ in if model.follow && model.mode == "Logs" { proxy.scrollTo("end", anchor: .bottom) } } } }.padding().frame(minHeight: 240) } } Divider() HStack { Circle().fill(model.status.hasPrefix("Connected") ? .green : .orange).frame(width: 6, height: 6); Text(model.status).font(.caption).lineLimit(2).textSelection(.enabled); Spacer(); TimelineView(.periodic(from: .now, by: 1)) { context in if let sample = model.lastSample { let age = context.date.timeIntervalSince(sample); Text("Sample \(Int(max(0,age)))s ago").foregroundStyle(age > 8 ? Color.orange : Color.secondary).font(.caption).monospacedDigit() } }; Text(model.sampling ? "Sampling…" : "Swift Edition").font(.caption).foregroundStyle(.secondary) }.padding(10) }.background(Color(nsColor: .windowBackgroundColor)) .searchable(text: $model.search, prompt: "Filter names, images, projects") .toolbar { ToolbarItemGroup { Button { Task { await model.refresh() } } label: { Label("Refresh", systemImage: "arrow.clockwise") }.disabled(model.busy || model.acting) if model.kind.isContainer && model.kind != .monitor { Button { model.request("start") } label: { Label("Start", systemImage: "play.fill") }.disabled(model.selected == nil || model.acting || model.demo); Button { model.request("stop") } label: { Label("Stop", systemImage: "stop.fill") }.disabled(model.selected == nil || model.acting || model.demo) } Menu { if model.kind.isContainer { Button("Restart") { model.request("restart") }; Button("Kill…", role: .destructive) { model.request("kill") } }; Button("Delete selected…", role: .destructive) { model.request("delete") } } label: { Label("Actions", systemImage: "ellipsis.circle") }.disabled(model.selected == nil || model.selected?.readOnlyStorage == true || model.acting || model.demo) Button { model.request("prune") } label: { Label("Prune…", systemImage: "trash") }.disabled(model.acting || model.demo || model.kind == .monitor) } } } .onChange(of: model.kind) { _, kind in if sidebarSelection != kind { sidebarSelection = kind } } .onChange(of: sidebarSelection) { _, kind in if let kind { model.kind = kind; model.mode = "Inspect"; model.changeKind() } } .onChange(of: model.binary) { _, _ in model.resetConnection() } .onChange(of: model.demo) { _, _ in model.resetConnection() } .onChange(of: model.project) { _, _ in model.resetConnection() } .task(id: "\(model.kind.rawValue)|\(model.connectionID)") { while !Task.isCancelled { await model.refresh(); do { try await Task.sleep(for: .seconds(2)) } catch { break } } } .task(id: model.connectionID) { while !Task.isCancelled { await model.sampleTelemetry(); do { try await Task.sleep(for: .milliseconds(model.demo ? 1000 : 100)) } catch { break } } } .task(id: "\(model.selection ?? "")|\(model.mode)|\(model.connectionID)") { if model.mode == "Stats" { return } repeat { await model.loadDetail(); if model.mode == "Inspect" { break }; do { try await Task.sleep(for: .seconds(1)) } catch { break } } while !Task.isCancelled } .alert("Confirm \(model.pendingAction ?? "action")", isPresented: $model.showingConfirmation) { Button("Cancel", role: .cancel) { model.pendingAction = nil } Button("Confirm", role: .destructive) { model.performPending() } } message: { Text(model.pendingAction == "prune" ? "This affects ALL unused \(model.pendingKind == .projects ? "containers" : model.pendingKind.rawValue), regardless of project or search filters. Container prune removes stopped containers; image prune removes dangling images." : "\(model.pendingAction?.capitalized ?? "Change") \(model.pendingResource?.name ?? "selected resource")?") } .alert("Command failed", isPresented: Binding(get: { model.error != nil }, set: { if !$0 { model.error = nil } })) { Button("OK") { model.error = nil } } message: { Text(model.error ?? "") } } @ViewBuilder private var resourceTable: some View { if model.kind == .volumes { Table(model.visible, selection: $model.selection) { TableColumn("Name", value: \.name).width(min: 140, ideal: 190) TableColumn("Type", value: \.storageKind).width(110) TableColumn("Capacity", value: \.capacity).width(90) TableColumn("Driver", value: \.driver).width(70) TableColumn("Format", value: \.format).width(65) TableColumn("Used by", value: \.usedBy).width(min: 150, ideal: 220) TableColumn("Source", value: \.source).width(min: 160, ideal: 250) TableColumn("Created", value: \.created).width(min: 140, ideal: 180) }.overlay { if model.visible.isEmpty { ContentUnavailableView("No named volumes or host mounts", systemImage: "externaldrive", description: Text("This runtime currently reports no volumes. Use container-compose monitor volume create NAME, or mount a host directory in a container. Bind mounts are shown separately and are inspect-only.")) } } } else { Table(model.visible, selection: $model.selection) { TableColumn("Name", value: \.name).width(min: 150, ideal: 220) TableColumn("State") { r in Text(r.state).foregroundStyle(r.state == "running" ? .green : .secondary) }.width(75) TableColumn("Image / Digest", value: \.image).width(min: 130, ideal: 240) TableColumn("CPU", value: \.cpu).width(65) TableColumn("Memory", value: \.memory).width(85) TableColumn("Network RX / TX", value: \.network).width(min: 110, ideal: 140) TableColumn("Disk read / write", value: \.disk).width(min: 110, ideal: 140) TableColumn("Project", value: \.project).width(min: 80, ideal: 120) }.overlay { if model.visible.isEmpty { ContentUnavailableView(model.search.isEmpty ? "No \(model.kind.rawValue)" : "No matches", systemImage: model.kind.symbol, description: Text(model.kind == .projects ? "Set your container-compose project name in Settings, or use containers with Compose labels." : "Refresh the list or check the runtime status below.")) } } } } } struct Preferences: View { @ObservedObject var model: Monitor var body: some View { Form { TextField("Container executable", text: $model.binary) TextField("container-compose project", text: $model.project) Text("Recognizes --1 names. Compose labels are also recognized. Actions operate on individual containers.").font(.caption).foregroundStyle(.secondary) Toggle("Demo mode", isOn: $model.demo) Text("Logs show the latest 200 lines and refresh every second. Telemetry runs continuously (about two seconds per native sample). Stats, volumes and processes depend on runtime support.").font(.caption).foregroundStyle(.secondary) Button("Save Command-Line Tool…") { model.exportCLI() } Link("Apple Container installation", destination: URL(string: "https://github.com/apple/container")!) }.padding(24).frame(width: 560) } } struct TelemetryPoint: Identifiable { let id = UUID() var date: Date var cpu: Double var memory: Double var rx: Double var tx: Double } struct MonitorDashboard: View { @ObservedObject var model: Monitor private let columns = [GridItem(.flexible()), GridItem(.flexible())] private var current: TelemetryPoint? { model.telemetryAvailable ? model.history.last : nil } private var running: Int { model.containerResources.filter { $0.state == "running" }.count } var body: some View { ScrollView { VStack(alignment: .leading, spacing: 22) { if !model.telemetryError.isEmpty { Label("Telemetry unavailable: " + model.telemetryError, systemImage: "exclamationmark.triangle").font(.callout).foregroundStyle(.orange).textSelection(.enabled) } HStack(alignment: .top) { VStack(alignment: .leading, spacing: 7) { Text("Container Monitor").font(.system(size: 30, weight: .bold, design: .rounded)) Text(model.demo ? "DEMO · Illustrative history and fixture metrics" : "Live Apple Container telemetry · continuous two-second samples").font(.callout).foregroundStyle(.secondary) } Spacer() Label(model.demo ? "Demo" : "Live", systemImage: "circle.fill").font(.caption.bold()).foregroundStyle(model.demo ? .orange : .mint).padding(.horizontal, 12).padding(.vertical, 7).background((model.demo ? Color.orange : Color.mint).opacity(0.12), in: Capsule()) } HStack(spacing: 14) { metricCard("Running", value: "\(running) / \(model.containerResources.count)", subtitle: "containers", symbol: "shippingbox.fill", color: .orange) metricCard("CPU", value: current.map { String(format: "%.1f%%", $0.cpu) } ?? "—", subtitle: "of one core · all containers", symbol: "cpu", color: .mint) metricCard("Memory", value: current.map { ContainerService.bytes($0.memory) } ?? "—", subtitle: "total working usage", symbol: "memorychip", color: .purple) metricCard("Network", value: current.map { ContainerService.bytes($0.rx + $0.tx) + "/s" } ?? "—", subtitle: "combined receive + transmit", symbol: "arrow.up.arrow.down", color: .blue) } LazyVGrid(columns: columns, spacing: 16) { graphCard("CPU usage", subtitle: "Percentage of one core", color: .mint, keyPath: \.cpu, divisor: 1, unit: "%") graphCard("Memory", subtitle: "Aggregate container memory · MiB", color: .purple, keyPath: \.memory, divisor: 1_048_576, unit: "") } networkCard HStack { Text("Container activity").font(.title3.bold()); Spacer(); Button("Manage containers") { model.kind = .containers; model.changeKind() } } VStack(spacing: 0) { ForEach(model.containerResources) { r in HStack(spacing: 12) { Image(systemName: "shippingbox").foregroundStyle(.orange).frame(width: 28, height: 28).background(Color.orange.opacity(0.1), in: RoundedRectangle(cornerRadius: 8)) VStack(alignment: .leading, spacing: 3) { Text(r.name).font(.system(.body, design: .monospaced).weight(.medium)); Text(r.image).font(.caption).foregroundStyle(.secondary).lineLimit(1) } Spacer(); Label(r.state, systemImage: "circle.fill").font(.caption).foregroundStyle(r.state == "running" ? .mint : .secondary).frame(width: 90, alignment: .leading) Text(r.cpu).monospacedDigit().frame(width: 65, alignment: .trailing) Text(r.memory).monospacedDigit().frame(width: 85, alignment: .trailing) }.padding(12) if r.id != model.containerResources.last?.id { Divider().padding(.leading, 52) } } if model.containerResources.isEmpty { Text("No application containers found. Start a container to see live metrics.").foregroundStyle(.secondary).padding(24) } }.background(.background, in: RoundedRectangle(cornerRadius: 12)).overlay(RoundedRectangle(cornerRadius: 12).strokeBorder(.primary.opacity(0.08))) if !model.lastAction.isEmpty { Label(model.lastAction, systemImage: "checkmark.circle").foregroundStyle(.secondary).font(.caption) } Text("Charts retain up to six minutes of in-app history. Network rates use differences between cumulative counters; unavailable metrics are shown as —. CPU can exceed 100% across multiple cores.").font(.caption).foregroundStyle(.secondary) }.padding(24) }.background(Color(nsColor: .underPageBackgroundColor)) } private func metricCard(_ title: String, value: String, subtitle: String, symbol: String, color: Color) -> some View { VStack(alignment: .leading, spacing: 12) { HStack { Label(title, systemImage: symbol).font(.subheadline.weight(.medium)).foregroundStyle(.secondary); Spacer() }; Text(value).font(.system(size: 25, weight: .semibold, design: .rounded)).monospacedDigit().lineLimit(1).minimumScaleFactor(0.6).foregroundStyle(color); Text(subtitle).font(.caption).foregroundStyle(.secondary).lineLimit(1) }.padding(16).frame(maxWidth: .infinity, alignment: .leading).background(.background, in: RoundedRectangle(cornerRadius: 12)).overlay(RoundedRectangle(cornerRadius: 12).strokeBorder(color.opacity(0.18))) } private func graphCard(_ title: String, subtitle: String, color: Color, keyPath: KeyPath, divisor: Double, unit: String) -> some View { VStack(alignment: .leading, spacing: 14) { HStack { VStack(alignment: .leading, spacing: 4) { Text(title).font(.headline); Text(subtitle).font(.caption).foregroundStyle(.secondary) }; Spacer(); Text(current.map { String(format: "%.1f", $0[keyPath: keyPath] / divisor) + unit } ?? "—").font(.title3.bold()).monospacedDigit().foregroundStyle(color) } Chart(model.history) { p in AreaMark(x: .value("Time", p.date), y: .value(title, p[keyPath: keyPath] / divisor)).foregroundStyle(LinearGradient(colors: [color.opacity(0.26), color.opacity(0.01)], startPoint: .top, endPoint: .bottom)).interpolationMethod(.monotone) LineMark(x: .value("Time", p.date), y: .value(title, p[keyPath: keyPath] / divisor)).foregroundStyle(color).lineStyle(StrokeStyle(lineWidth: 2)).interpolationMethod(.monotone) }.chartYScale(domain: .automatic(includesZero: true)).chartXAxis { AxisMarks(values: .automatic(desiredCount: 4)) { _ in AxisGridLine(); AxisValueLabel(format: .dateTime.hour().minute().second()) } }.chartYAxis { AxisMarks(position: .leading) }.frame(height: 160).overlay { if model.history.isEmpty { Text("Waiting for supported container metrics…").font(.caption).foregroundStyle(.secondary) } } }.padding(18).background(.background, in: RoundedRectangle(cornerRadius: 12)).overlay(RoundedRectangle(cornerRadius: 12).strokeBorder(.primary.opacity(0.08))) } private var networkCard: some View { VStack(alignment: .leading, spacing: 14) { HStack { VStack(alignment: .leading, spacing: 4) { Text("Network throughput").font(.headline); Text("Receive and transmit · KiB/s").font(.caption).foregroundStyle(.secondary) }; Spacer(); Label("Receive", systemImage: "arrow.down").foregroundStyle(.blue); Label("Transmit", systemImage: "arrow.up").foregroundStyle(.orange) }.font(.caption) Chart(model.history) { p in LineMark(x: .value("Time", p.date), y: .value("KiB/s", p.rx / 1024), series: .value("Direction", "Receive")).foregroundStyle(.blue).interpolationMethod(.monotone) LineMark(x: .value("Time", p.date), y: .value("KiB/s", p.tx / 1024), series: .value("Direction", "Transmit")).foregroundStyle(.orange).interpolationMethod(.monotone) }.chartYScale(domain: .automatic(includesZero: true)).chartXAxis { AxisMarks(values: .automatic(desiredCount: 5)) { _ in AxisGridLine(); AxisValueLabel(format: .dateTime.hour().minute().second()) } }.chartYAxis { AxisMarks(position: .leading) }.frame(height: 145).overlay { if model.history.isEmpty { Text("Waiting for supported container metrics…").font(.caption).foregroundStyle(.secondary) } } }.padding(18).background(.background, in: RoundedRectangle(cornerRadius: 12)).overlay(RoundedRectangle(cornerRadius: 12).strokeBorder(.primary.opacity(0.08))) } }