This commit is contained in:
Mark Gregory
2026-10-22 09:27:17 +01:00
parent ed5bf665b9
commit 0a6f6ac058
22 changed files with 791 additions and 68 deletions
+284 -4
View File
@@ -3,6 +3,27 @@ import SwiftUI
struct MonitorMenuView: View {
@ObservedObject var monitor: ServerMonitor
@Environment(\.openWindow) private var openWindow
@State private var detailItem: MonitorDetailItem?
private func showDetails(
title: String,
status: String,
details: String
) {
detailItem = MonitorDetailItem(
title: title,
status: status,
details: details
)
}
private func copyDetails(_ details: String) {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(
details,
forType: .string
)
}
var body: some View {
VStack(alignment: .leading, spacing: 0) {
@@ -70,7 +91,72 @@ struct MonitorMenuView: View {
if server.id == monitor.selectedID { Image(systemName: "checkmark") }
}.padding(.horizontal, 14).padding(.vertical, 8)
.contentShape(Rectangle())
}.buttonStyle(.plain)
}
.buttonStyle(.plain)
.contextMenu {
Button("Show Details") {
let result = monitor.results[server.id]
let report = result?.report
let failures = report?.checks.filter {
$0.status != .ok
} ?? []
let checkDetails = failures.map {
"\($0.section) / \($0.name) [\($0.status.title)]\\n\($0.detail)"
}.joined(separator: "\\n\\n")
let missingGroups = report?.groups
.filter { $0.status != .ok }
.map {
"\($0.name): \($0.missing.joined(separator: ", "))"
}
.joined(separator: "\\n") ?? ""
let details = [
"Server: \(server.name)",
"Host: \(server.host)",
"Status: \(result?.status.title ?? "Unknown")",
result?.error ?? "",
missingGroups,
checkDetails,
report?.diagnostics ?? ""
]
.filter { !$0.isEmpty }
.joined(separator: "\\n\\n")
showDetails(
title: server.name,
status: result?.status.title ?? "Unknown",
details: details
)
}
Button("Copy Error Details") {
let result = monitor.results[server.id]
let failures = result?.report?.checks
.filter { $0.status != .ok }
.map {
"\($0.name): \($0.detail)"
}
.joined(separator: "\\n") ?? ""
copyDetails(
result?.error
?? (failures.isEmpty
? "No errors reported."
: failures)
)
}
Divider()
Button("Refresh Server") {
Task {
await monitor.refresh(id: server.id)
}
}
}
}
}
}
@@ -112,6 +198,20 @@ struct MonitorMenuView: View {
Text(service.title).font(.caption).foregroundStyle(.secondary)
}
.padding(.horizontal, 14).padding(.vertical, 5)
.contentShape(Rectangle())
.contextMenu {
Button("Show Details") {
showDetails(
title: service.name,
status: service.title,
details: service.detail
)
}
Button("Copy Details") {
copyDetails(service.detail)
}
}
}
Divider()
Text("HEALTH GROUPS")
@@ -124,7 +224,57 @@ struct MonitorMenuView: View {
Spacer()
Label(group.status.title, systemImage: group.status.symbol)
.foregroundStyle(group.status.color).font(.caption)
}.padding(.horizontal, 14).padding(.vertical, 8)
}
.padding(.horizontal, 14)
.padding(.vertical, 8)
.contentShape(Rectangle())
.contextMenu {
Button("Show Details") {
let related = report.checks.filter {
group.missing.contains($0.name)
|| (
$0.status != .ok
&& $0.section == group.name
)
}
let messages = related.map {
"\($0.name): \($0.detail)"
}.joined(separator: "\\n\\n")
let missing = group.missing.isEmpty
? ""
: "Missing: "
+ group.missing.joined(separator: ", ")
let details = [
"Group: \(group.name)",
"Status: \(group.status.title)",
missing,
messages.isEmpty
? (group.status == .ok
? "All required checks passed."
: "No matching individual check details were returned.")
: messages
]
.filter { !$0.isEmpty }
.joined(separator: "\\n\\n")
showDetails(
title: group.name,
status: group.status.title,
details: details
)
}
Button("Copy Details") {
copyDetails(
"\(group.name): \(group.status.title)\\n"
+ group.missing.joined(separator: "\\n")
)
}
}
Divider().padding(.leading, 14)
}
} else {
@@ -150,12 +300,18 @@ struct MonitorMenuView: View {
Button("Settings") { openWindow(id: "monitor-settings") }
Button("Quit") { NSApp.terminate(nil) }
}.buttonStyle(.borderless).padding(14)
}.frame(width: 410)
}
.frame(width: 410)
.sheet(item: $detailItem) { item in
MonitorDetailSheet(item: item)
}
}
}
struct DiagnosticsView: View {
@ObservedObject var monitor: ServerMonitor
@State private var detailItem: MonitorDetailItem?
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("\(monitor.selected?.name ?? "Server") — Diagnostics").font(.title2.bold())
@@ -171,9 +327,133 @@ struct DiagnosticsView: View {
}
Spacer()
}
.contentShape(Rectangle())
.contextMenu {
Button("Show Details") {
detailItem = MonitorDetailItem(
title: check.name,
status: check.status.title,
details: """
Section: \(check.section)
\(check.detail)
"""
)
}
Button("Copy Details") {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(
check.detail,
forType: .string
)
}
}
}
}
}.textSelection(.enabled)
}.padding(20).frame(minWidth: 650, minHeight: 450)
}
.padding(20)
.frame(minWidth: 650, minHeight: 450)
.sheet(item: $detailItem) { item in
MonitorDetailSheet(item: item)
}
}
}
// MARK: - Monitoring Details
struct MonitorDetailItem: Identifiable {
let id = UUID()
let title: String
let status: String
let details: String
}
struct MonitorDetailSheet: View {
let item: MonitorDetailItem
@Environment(\.dismiss) private var dismiss
var body: some View {
VStack(alignment: .leading, spacing: 16) {
HStack {
Image(systemName: icon)
.font(.title2)
.foregroundStyle(statusColor)
VStack(alignment: .leading, spacing: 4) {
Text(item.title)
.font(.title2.bold())
Text(item.status)
.foregroundStyle(statusColor)
}
Spacer()
}
Divider()
ScrollView {
Text(item.details.isEmpty
? "No additional diagnostic information available."
: item.details)
.font(.system(
size: 12,
design: .monospaced
))
.frame(
maxWidth: .infinity,
alignment: .leading
)
.textSelection(.enabled)
.padding(12)
}
.background(.quaternary.opacity(0.3))
.clipShape(RoundedRectangle(cornerRadius: 8))
HStack {
Button("Copy Details") {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(
item.details,
forType: .string
)
}
Spacer()
Button("Close") {
dismiss()
}
.keyboardShortcut(.defaultAction)
}
}
.padding(20)
.frame(minWidth: 540, minHeight: 350)
}
private var icon: String {
switch item.status.lowercased() {
case "critical", "failed":
return "xmark.circle.fill"
case "warning", "stopped":
return "exclamationmark.triangle.fill"
default:
return "checkmark.circle.fill"
}
}
private var statusColor: Color {
switch item.status.lowercased() {
case "critical", "failed":
return .red
case "warning", "stopped":
return .orange
default:
return .green
}
}
}