Files

181 lines
8.1 KiB
Swift

// SPDX-FileCopyrightText: 2026 Kaede Akino
// SPDX-License-Identifier: GPL-3.0-only
import SwiftUI
import TermindApplication
import TermindDomain
/// Independently scrollable system panels; the process table owns its own viewport.
public struct MetricsSystemPanels: View {
public init(server: ServerMetric) { self.server = server }
let server: ServerMetric
private var point: MetricsPoint? { server.point }
@State private var virtualInterfacesExpanded = false
public var body: some View {
VStack(spacing: 12) {
panel { MetricsCPUContent(point: point) }
panel {
HStack {
MetricsFigure("Free Memory", value: MetricsFormat.bytes(point?.memoryFree, style: .memory))
MetricsFigure("Used", value: MetricsFormat.bytes(point?.memoryUsed, style: .memory))
MetricsFigure("Page Cache", value: MetricsFormat.bytes(point?.memoryCached, style: .memory))
MetricsRing(title: AppStrings.Metrics.memory, value: point?.memory)
}
}
if let swapTotal = point?.swapTotal, swapTotal > 0 {
panel {
HStack {
MetricsFigure("Free Memory", value: MetricsFormat.bytes(point?.swapFree, style: .memory))
MetricsFigure("Used", value: MetricsFormat.bytes(point?.swapUsed, style: .memory))
MetricsFigure("Total", value: MetricsFormat.bytes(point?.swapTotal, style: .memory))
MetricsRing(title: "Swap", value: point?.swapPercent)
}
}
}
if server.gpuSnapshot?.devices.isEmpty == false {
MetricsGPUSection(server: server)
}
networkPanel
if server.filesystemsFailed {
Text("Filesystem metrics unavailable").font(.caption).foregroundStyle(.secondary)
}
ForEach(server.filesystems) { filesystem in
filesystemPanel(filesystem)
}
}
}
private var hardwareInterfaces: [MetricsDevicePoint] {
(point?.networkDevices ?? []).filter { $0.kind != .virtual }
}
private var virtualInterfaces: [MetricsDevicePoint] { (point?.networkDevices ?? []).filter { $0.kind == .virtual } }
private var networkPanel: some View {
panel {
HStack(spacing: 12) {
VStack(spacing: 8) {
MetricsFigure("Send Speed", value: MetricsFormat.rate(point?.networkSend))
MetricsFigure("Receive Speed", value: MetricsFormat.rate(point?.networkReceive))
}
VStack(spacing: 8) {
MetricsFigure(
"Total Sent", value: MetricsFormat.bytes(point?.networkSentTotal), color: Color.orange)
MetricsFigure(
"Total Received", value: MetricsFormat.bytes(point?.networkReceivedTotal),
color: Color.green)
}
trafficRing
}
HStack(spacing: 6) {
MetricsFigure("Retransmissions", value: MetricsFormat.percent(point?.tcpRetransmissionPercent))
MetricsFigure("Active Opens", value: point?.tcp?.active?.formatted() ?? "—")
MetricsFigure("Passive Opens", value: point?.tcp?.passive?.formatted() ?? "—")
MetricsFigure("Failed Attempts", value: point?.tcp?.failures?.formatted() ?? "—")
}
ForEach(hardwareInterfaces) { interface in
Divider()
interfaceRow(interface)
}
if !virtualInterfaces.isEmpty {
DisclosureGroup(isExpanded: $virtualInterfacesExpanded) {
VStack(spacing: 12) {
ForEach(virtualInterfaces) { interface in
Divider()
interfaceRow(interface)
}
}
} label: {
HStack {
Text("Virtual Interfaces")
Spacer()
Text(virtualInterfaces.count.formatted()).monospacedDigit()
}.font(.caption).foregroundStyle(.secondary)
}
.tint(Color.secondary)
}
}
}
private func interfaceRow(_ device: MetricsDevicePoint) -> some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Label(device.id, systemImage: "network").font(.caption.bold())
Spacer()
Text(device.addresses.joined(separator: ", ")).font(.system(size: 9)).foregroundStyle(.secondary)
.lineLimit(1).help(device.addresses.joined(separator: "\n"))
}
HStack(spacing: 6) {
MetricsFigure("Send Speed", value: MetricsFormat.rate(device.write))
MetricsFigure("Receive Speed", value: MetricsFormat.rate(device.read))
MetricsFigure("Total Sent", value: MetricsFormat.bytes(device.writeTotal))
MetricsFigure("Total Received", value: MetricsFormat.bytes(device.readTotal))
}
}
}
private var trafficRing: some View {
let sent = point?.networkSentTotal ?? 0
let received = point?.networkReceivedTotal ?? 0
let fraction = sent + received > 0 ? sent / (sent + received) : 0
return ZStack {
Circle().stroke(.quaternary, lineWidth: 4)
if sent + received > 0 {
Circle().trim(from: 0, to: fraction).stroke(Color.orange, lineWidth: 4)
Circle().trim(from: fraction, to: 1).stroke(Color.green, lineWidth: 4)
}
}
.rotationEffect(.degrees(-90)).frame(width: 34, height: 34)
.accessibilityLabel(Text("Sent / Received Traffic"))
.accessibilityValue(
MetricsFormat.capacity(point?.networkSentTotal, point?.networkReceivedTotal, style: .decimal))
}
private func filesystemPanel(_ filesystem: MetricsFilesystem) -> some View {
let io = point?.blockDevices[filesystem.blockID]
return panel {
HStack(spacing: 10) {
VStack(alignment: .leading, spacing: 4) {
Text(filesystem.mountPoint).font(.headline).lineLimit(1).help(filesystem.mountPoint)
Text(filesystem.device).font(.caption).foregroundStyle(.secondary).lineLimit(1).help(
filesystem.device)
}
Spacer(minLength: 0)
VStack(alignment: .trailing, spacing: 4) {
Text(filesystem.type.uppercased()).font(.system(size: 9)).foregroundStyle(.secondary)
Text(MetricsFormat.capacity(filesystem.used, filesystem.total))
.font(.system(size: 10, weight: .medium, design: .monospaced)).lineLimit(1).minimumScaleFactor(
0.7)
}
capacityBar(filesystem.usedPercent)
}
MetricsDiskTable(io: io)
}
}
private func capacityBar(_ percent: Double?) -> some View {
let color =
(percent ?? 0) >= 90
? Color.red : (percent ?? 0) >= 75 ? Color.orange : Color.green
return RoundedRectangle(cornerRadius: 3).fill(.quaternary)
.overlay(alignment: .bottom) {
if let percent {
RoundedRectangle(cornerRadius: 3).fill(color).frame(height: 36 * min(1, max(0, percent / 100)))
}
}
.frame(width: 8, height: 36)
.accessibilityLabel(Text("Disk Usage"))
.accessibilityValue(MetricsFormat.percent(percent))
}
private func panel<Content: View>(@ViewBuilder content: () -> Content) -> some View {
VStack(alignment: .leading, spacing: 12, content: content)
.padding(12).frame(maxWidth: .infinity, alignment: .leading)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay {
RoundedRectangle(cornerRadius: 12).strokeBorder(.quaternary, lineWidth: 1)
}
}
}