updates
This commit is contained in:
@@ -1,132 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct MonitorMenuView: View {
|
|
||||||
@ObservedObject var monitor: ServerMonitor
|
|
||||||
@Environment(\.openSettings) private var openSettings
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
Image(systemName: "server.rack")
|
|
||||||
.font(.system(size: 26, weight: .semibold))
|
|
||||||
.foregroundStyle(.tint)
|
|
||||||
VStack(alignment: .leading, spacing: 3) {
|
|
||||||
Text("PEGASUS MONITOR")
|
|
||||||
.font(.headline)
|
|
||||||
Text("192.168.1.2 · Remote server health")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
if monitor.isRefreshing { ProgressView().controlSize(.small) }
|
|
||||||
}
|
|
||||||
.padding(18)
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
if let report = monitor.report {
|
|
||||||
ScrollView {
|
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
|
||||||
ForEach(report.groups) { group in
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
Image(systemName: group.status.symbol)
|
|
||||||
.foregroundStyle(group.status.color)
|
|
||||||
.frame(width: 22)
|
|
||||||
Text(group.name)
|
|
||||||
.font(.system(size: 13))
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.help(group.missing.isEmpty ? group.status.title : "Missing checks: \(group.missing.joined(separator: ", "))")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 18)
|
|
||||||
.padding(.vertical, 14)
|
|
||||||
}
|
|
||||||
.frame(maxHeight: 375)
|
|
||||||
Divider()
|
|
||||||
HStack {
|
|
||||||
Image(systemName: monitor.status.symbol)
|
|
||||||
.foregroundStyle(monitor.status.color)
|
|
||||||
Text("OVERALL: \(monitor.status.title.uppercased())")
|
|
||||||
.font(.system(size: 13, weight: .bold))
|
|
||||||
Spacer()
|
|
||||||
Text("\(report.counts.ok) OK · \(report.counts.warn) WARN · \(report.counts.fail) FAIL")
|
|
||||||
.font(.caption2.monospacedDigit())
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 18)
|
|
||||||
.padding(.vertical, 12)
|
|
||||||
} else {
|
|
||||||
ContentUnavailableView("No health report", systemImage: "network", description: Text("Check SSH configuration and refresh."))
|
|
||||||
.frame(height: 220)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let error = monitor.lastError {
|
|
||||||
Text(error)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.red)
|
|
||||||
.textSelection(.enabled)
|
|
||||||
.padding(.horizontal, 18)
|
|
||||||
.padding(.bottom, 10)
|
|
||||||
}
|
|
||||||
Divider()
|
|
||||||
HStack {
|
|
||||||
Text(monitor.lastChecked.map { "Checked \($0.formatted(date: .omitted, time: .shortened))" } ?? "Not checked yet")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
Spacer()
|
|
||||||
Button { Task { await monitor.refresh() } } label: {
|
|
||||||
Label("Refresh", systemImage: "arrow.clockwise")
|
|
||||||
}
|
|
||||||
.disabled(monitor.isRefreshing)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 18)
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Button("Full Diagnostics") { monitor.showDiagnostics = true }
|
|
||||||
Spacer()
|
|
||||||
Button("Settings") { openSettings() }
|
|
||||||
Button("Quit") { NSApplication.shared.terminate(nil) }
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 18)
|
|
||||||
.padding(.bottom, 12)
|
|
||||||
}
|
|
||||||
.sheet(isPresented: $monitor.showDiagnostics) {
|
|
||||||
DiagnosticsView(monitor: monitor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DiagnosticsView: View {
|
|
||||||
@ObservedObject var monitor: ServerMonitor
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
HStack {
|
|
||||||
Text("Pegasus Diagnostics").font(.title2.bold())
|
|
||||||
Spacer()
|
|
||||||
Button("Close") { dismiss() }
|
|
||||||
}
|
|
||||||
Text("Latest remote report — refresh from the menu to update")
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
ScrollView {
|
|
||||||
LazyVStack(alignment: .leading, spacing: 10) {
|
|
||||||
ForEach(monitor.report?.checks ?? []) { check in
|
|
||||||
HStack(alignment: .top) {
|
|
||||||
Image(systemName: check.status.symbol).foregroundStyle(check.status.color)
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
Text(check.name).fontWeight(.medium)
|
|
||||||
Text(check.detail).font(.caption).foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.textSelection(.enabled)
|
|
||||||
}
|
|
||||||
.padding(20)
|
|
||||||
.frame(width: 650, height: 560)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct MonitorMenuView: View {
|
|
||||||
@ObservedObject var monitor: ServerMonitor
|
|
||||||
@Environment(\.openSettings) private var openSettings
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
Image(systemName: "server.rack")
|
|
||||||
.font(.system(size: 26, weight: .semibold))
|
|
||||||
.foregroundStyle(.tint)
|
|
||||||
VStack(alignment: .leading, spacing: 3) {
|
|
||||||
Text("PEGASUS MONITOR")
|
|
||||||
.font(.headline)
|
|
||||||
Text("192.168.1.2 · Remote server health")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
if monitor.isRefreshing { ProgressView().controlSize(.small) }
|
|
||||||
}
|
|
||||||
.padding(18)
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
if let report = monitor.report {
|
|
||||||
ScrollView {
|
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
|
||||||
ForEach(report.groups) { group in
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
Image(systemName: group.status.symbol)
|
|
||||||
.foregroundStyle(group.status.color)
|
|
||||||
.frame(width: 22)
|
|
||||||
Text(group.name)
|
|
||||||
.font(.system(size: 13))
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.help(group.missing.isEmpty ? group.status.title : "Missing checks: \(group.missing.joined(separator: ", "))")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 18)
|
|
||||||
.padding(.vertical, 14)
|
|
||||||
}
|
|
||||||
.frame(maxHeight: 375)
|
|
||||||
Divider()
|
|
||||||
HStack {
|
|
||||||
Image(systemName: monitor.status.symbol)
|
|
||||||
.foregroundStyle(monitor.status.color)
|
|
||||||
Text("OVERALL: \(monitor.status.title.uppercased())")
|
|
||||||
.font(.system(size: 13, weight: .bold))
|
|
||||||
Spacer()
|
|
||||||
Text("\(report.counts.ok) OK · \(report.counts.warn) WARN · \(report.counts.fail) FAIL")
|
|
||||||
.font(.caption2.monospacedDigit())
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 18)
|
|
||||||
.padding(.vertical, 12)
|
|
||||||
} else {
|
|
||||||
ContentUnavailableView("No health report", systemImage: "network", description: Text("Check SSH configuration and refresh."))
|
|
||||||
.frame(height: 220)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let error = monitor.lastError {
|
|
||||||
Text(error)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.red)
|
|
||||||
.textSelection(.enabled)
|
|
||||||
.padding(.horizontal, 18)
|
|
||||||
.padding(.bottom, 10)
|
|
||||||
}
|
|
||||||
Divider()
|
|
||||||
HStack {
|
|
||||||
Text(monitor.lastChecked.map { "Checked \($0.formatted(date: .omitted, time: .shortened))" } ?? "Not checked yet")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
Spacer()
|
|
||||||
Button { Task { await monitor.refresh() } } label: {
|
|
||||||
Label("Refresh", systemImage: "arrow.clockwise")
|
|
||||||
}
|
|
||||||
.disabled(monitor.isRefreshing)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 18)
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Button("Full Diagnostics") { monitor.showDiagnostics = true }
|
|
||||||
Spacer()
|
|
||||||
Button("Settings") { monitor.showSettings = true }
|
|
||||||
Button("Quit") { NSApplication.shared.terminate(nil) }
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 18)
|
|
||||||
.padding(.bottom, 12)
|
|
||||||
}
|
|
||||||
.sheet(isPresented: $monitor.showDiagnostics) {
|
|
||||||
DiagnosticsView(monitor: monitor)
|
|
||||||
}
|
|
||||||
.sheet(isPresented: $monitor.showSettings) {
|
|
||||||
SettingsView(monitor: monitor)
|
|
||||||
.frame(width: 480)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DiagnosticsView: View {
|
|
||||||
@ObservedObject var monitor: ServerMonitor
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
HStack {
|
|
||||||
Text("Pegasus Diagnostics").font(.title2.bold())
|
|
||||||
Spacer()
|
|
||||||
Button("Close") { dismiss() }
|
|
||||||
}
|
|
||||||
Text("Latest remote report — refresh from the menu to update")
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
ScrollView {
|
|
||||||
LazyVStack(alignment: .leading, spacing: 10) {
|
|
||||||
ForEach(monitor.report?.checks ?? []) { check in
|
|
||||||
HStack(alignment: .top) {
|
|
||||||
Image(systemName: check.status.symbol).foregroundStyle(check.status.color)
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
Text(check.name).fontWeight(.medium)
|
|
||||||
Text(check.detail).font(.caption).foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.textSelection(.enabled)
|
|
||||||
}
|
|
||||||
.padding(20)
|
|
||||||
.frame(width: 650, height: 560)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct MonitorMenuView: View {
|
|
||||||
@ObservedObject var monitor: ServerMonitor
|
|
||||||
@Environment(\.openSettings) private var openSettings
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
Image(systemName: "server.rack")
|
|
||||||
.font(.system(size: 26, weight: .semibold))
|
|
||||||
.foregroundStyle(.tint)
|
|
||||||
VStack(alignment: .leading, spacing: 3) {
|
|
||||||
Text("PEGASUS MONITOR")
|
|
||||||
.font(.headline)
|
|
||||||
HStack(spacing: 6) {
|
|
||||||
Text("192.168.1.2")
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
|
|
||||||
Image(systemName: monitor.status.symbol)
|
|
||||||
.foregroundStyle(monitor.status.color)
|
|
||||||
|
|
||||||
Text(monitor.status.title.uppercased())
|
|
||||||
.foregroundStyle(monitor.status.color)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
}
|
|
||||||
.font(.caption)
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
if monitor.isRefreshing { ProgressView().controlSize(.small) }
|
|
||||||
}
|
|
||||||
.padding(18)
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
if let report = monitor.report {
|
|
||||||
ScrollView {
|
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
|
||||||
// Main system status table
|
|
||||||
HStack {
|
|
||||||
Text("SYSTEM")
|
|
||||||
Spacer()
|
|
||||||
Text("STATUS")
|
|
||||||
}
|
|
||||||
.font(.system(size: 10, weight: .semibold))
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
.padding(.bottom, 5)
|
|
||||||
|
|
||||||
ForEach(report.groups) { group in
|
|
||||||
HStack(spacing: 10) {
|
|
||||||
|
|
||||||
Text(group.name)
|
|
||||||
.font(.system(size: 12))
|
|
||||||
.lineLimit(1)
|
|
||||||
.minimumScaleFactor(0.85)
|
|
||||||
|
|
||||||
Spacer(minLength: 8)
|
|
||||||
|
|
||||||
HStack(spacing: 5) {
|
|
||||||
Image(systemName: group.status.symbol)
|
|
||||||
.font(.system(size: 11))
|
|
||||||
|
|
||||||
Text(group.status.title)
|
|
||||||
.font(.system(size: 11, weight: .medium))
|
|
||||||
}
|
|
||||||
.foregroundStyle(group.status.color)
|
|
||||||
.frame(width: 86, alignment: .trailing)
|
|
||||||
}
|
|
||||||
.padding(.vertical, 3)
|
|
||||||
.help(
|
|
||||||
group.missing.isEmpty
|
|
||||||
? group.status.title
|
|
||||||
: "Missing checks: \(group.missing.joined(separator: ", "))"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 18)
|
|
||||||
.padding(.vertical, 14)
|
|
||||||
}
|
|
||||||
.frame(maxHeight: 375)
|
|
||||||
Divider()
|
|
||||||
HStack {
|
|
||||||
Image(systemName: monitor.status.symbol)
|
|
||||||
.foregroundStyle(monitor.status.color)
|
|
||||||
Text("OVERALL: \(monitor.status.title.uppercased())")
|
|
||||||
.font(.system(size: 13, weight: .bold))
|
|
||||||
Spacer()
|
|
||||||
Text("\(report.counts.ok) OK · \(report.counts.warn) WARN · \(report.counts.fail) FAIL")
|
|
||||||
.font(.caption2.monospacedDigit())
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 18)
|
|
||||||
.padding(.vertical, 12)
|
|
||||||
} else {
|
|
||||||
ContentUnavailableView("No health report", systemImage: "network", description: Text("Check SSH configuration and refresh."))
|
|
||||||
.frame(height: 220)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let error = monitor.lastError {
|
|
||||||
Text(error)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.red)
|
|
||||||
.textSelection(.enabled)
|
|
||||||
.padding(.horizontal, 18)
|
|
||||||
.padding(.bottom, 10)
|
|
||||||
}
|
|
||||||
Divider()
|
|
||||||
HStack {
|
|
||||||
Text(monitor.lastChecked.map { "Checked \($0.formatted(date: .omitted, time: .shortened))" } ?? "Not checked yet")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
Spacer()
|
|
||||||
Button { Task { await monitor.refresh() } } label: {
|
|
||||||
Label("Refresh", systemImage: "arrow.clockwise")
|
|
||||||
}
|
|
||||||
.disabled(monitor.isRefreshing)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 18)
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Button("Full Diagnostics") { monitor.showDiagnostics = true }
|
|
||||||
Spacer()
|
|
||||||
Button("Settings") { monitor.showSettings = true }
|
|
||||||
Button("Quit") { NSApplication.shared.terminate(nil) }
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 18)
|
|
||||||
.padding(.bottom, 12)
|
|
||||||
}
|
|
||||||
.sheet(isPresented: $monitor.showDiagnostics) {
|
|
||||||
DiagnosticsView(monitor: monitor)
|
|
||||||
}
|
|
||||||
.sheet(isPresented: $monitor.showSettings) {
|
|
||||||
SettingsView(monitor: monitor)
|
|
||||||
.frame(width: 480)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DiagnosticsView: View {
|
|
||||||
@ObservedObject var monitor: ServerMonitor
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
HStack {
|
|
||||||
Text("Pegasus Diagnostics").font(.title2.bold())
|
|
||||||
Spacer()
|
|
||||||
Button("Close") { dismiss() }
|
|
||||||
}
|
|
||||||
Text("Latest remote report — refresh from the menu to update")
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
ScrollView {
|
|
||||||
LazyVStack(alignment: .leading, spacing: 10) {
|
|
||||||
ForEach(monitor.report?.checks ?? []) { check in
|
|
||||||
HStack(alignment: .top) {
|
|
||||||
Image(systemName: check.status.symbol).foregroundStyle(check.status.color)
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
Text(check.name).fontWeight(.medium)
|
|
||||||
Text(check.detail).font(.caption).foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.textSelection(.enabled)
|
|
||||||
}
|
|
||||||
.padding(20)
|
|
||||||
.frame(width: 650, height: 560)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,256 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
|
|
||||||
struct MonitorMenuView: View {
|
|
||||||
|
|
||||||
@ObservedObject var monitor: ServerMonitor
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
|
||||||
|
|
||||||
// MARK: Header
|
|
||||||
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
|
|
||||||
Image(systemName: "server.rack")
|
|
||||||
.font(.system(size: 26))
|
|
||||||
.foregroundStyle(monitor.status.color)
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
|
|
||||||
Text("PEGASUS MONITOR")
|
|
||||||
.font(.headline)
|
|
||||||
|
|
||||||
Text("192.168.1.2")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
VStack(alignment: .trailing, spacing: 4) {
|
|
||||||
|
|
||||||
Image(systemName: monitor.status.symbol)
|
|
||||||
.foregroundStyle(monitor.status.color)
|
|
||||||
|
|
||||||
Text(monitor.status.title)
|
|
||||||
.font(.caption.bold())
|
|
||||||
.foregroundStyle(monitor.status.color)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(18)
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
// MARK: System Status
|
|
||||||
|
|
||||||
if let report = monitor.report {
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Text("SYSTEM")
|
|
||||||
Spacer()
|
|
||||||
Text("STATUS")
|
|
||||||
}
|
|
||||||
.font(.system(size: 10, weight: .bold))
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
.padding(.horizontal, 18)
|
|
||||||
.padding(.top, 14)
|
|
||||||
.padding(.bottom, 8)
|
|
||||||
|
|
||||||
ScrollView {
|
|
||||||
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
|
|
||||||
ForEach(report.groups) { group in
|
|
||||||
|
|
||||||
HStack(spacing: 10) {
|
|
||||||
|
|
||||||
Text(group.name)
|
|
||||||
.font(.system(size: 12))
|
|
||||||
.lineLimit(1)
|
|
||||||
|
|
||||||
Spacer(minLength: 8)
|
|
||||||
|
|
||||||
HStack(spacing: 5) {
|
|
||||||
|
|
||||||
Image(systemName: group.status.symbol)
|
|
||||||
.font(.system(size: 12))
|
|
||||||
|
|
||||||
Text(group.status.title)
|
|
||||||
.font(.system(size: 11, weight: .medium))
|
|
||||||
}
|
|
||||||
.foregroundStyle(group.status.color)
|
|
||||||
.frame(width: 90, alignment: .trailing)
|
|
||||||
}
|
|
||||||
.padding(.vertical, 7)
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
.opacity(0.35)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 18)
|
|
||||||
}
|
|
||||||
.frame(maxHeight: 460)
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
// MARK: Overall Status
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
|
|
||||||
Text("OVERALL STATUS")
|
|
||||||
.font(.system(size: 12, weight: .bold))
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Image(systemName: monitor.status.symbol)
|
|
||||||
.foregroundStyle(monitor.status.color)
|
|
||||||
|
|
||||||
Text(monitor.status.title.uppercased())
|
|
||||||
.font(.system(size: 12, weight: .bold))
|
|
||||||
.foregroundStyle(monitor.status.color)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 18)
|
|
||||||
.padding(.vertical, 12)
|
|
||||||
|
|
||||||
HStack(spacing: 16) {
|
|
||||||
|
|
||||||
Label(
|
|
||||||
"\(report.counts.ok) OK",
|
|
||||||
systemImage: "checkmark.circle.fill"
|
|
||||||
)
|
|
||||||
.foregroundStyle(.green)
|
|
||||||
|
|
||||||
Label(
|
|
||||||
"\(report.counts.warn) WARN",
|
|
||||||
systemImage: "exclamationmark.triangle.fill"
|
|
||||||
)
|
|
||||||
.foregroundStyle(.orange)
|
|
||||||
|
|
||||||
Label(
|
|
||||||
"\(report.counts.fail) FAIL",
|
|
||||||
systemImage: "xmark.circle.fill"
|
|
||||||
)
|
|
||||||
.foregroundStyle(.red)
|
|
||||||
}
|
|
||||||
.font(.caption)
|
|
||||||
.padding(.horizontal, 18)
|
|
||||||
.padding(.bottom, 12)
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
ContentUnavailableView(
|
|
||||||
"No Health Report",
|
|
||||||
systemImage: "network",
|
|
||||||
description: Text(
|
|
||||||
"Check your SSH connection and refresh."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.frame(height: 260)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let error = monitor.lastError {
|
|
||||||
|
|
||||||
Text(error)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.red)
|
|
||||||
.textSelection(.enabled)
|
|
||||||
.padding(12)
|
|
||||||
}
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
// MARK: Footer
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
|
|
||||||
Text(
|
|
||||||
monitor.lastChecked.map {
|
|
||||||
"Checked \($0.formatted(date: .omitted, time: .shortened))"
|
|
||||||
} ?? "Not checked"
|
|
||||||
)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
if monitor.isRefreshing {
|
|
||||||
ProgressView()
|
|
||||||
.controlSize(.small)
|
|
||||||
}
|
|
||||||
|
|
||||||
Button("Refresh") {
|
|
||||||
Task {
|
|
||||||
await monitor.refresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.disabled(monitor.isRefreshing)
|
|
||||||
}
|
|
||||||
.padding(14)
|
|
||||||
|
|
||||||
HStack(spacing: 10) {
|
|
||||||
|
|
||||||
Button("Diagnostics") {
|
|
||||||
monitor.showDiagnostics = true
|
|
||||||
}
|
|
||||||
|
|
||||||
Button("Settings") {
|
|
||||||
monitor.showSettings = true
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Button("Quit") {
|
|
||||||
NSApplication.shared.terminate(nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 14)
|
|
||||||
.padding(.bottom, 14)
|
|
||||||
}
|
|
||||||
.frame(width: 430)
|
|
||||||
|
|
||||||
.sheet(isPresented: $monitor.showDiagnostics) {
|
|
||||||
DiagnosticsView(monitor: monitor)
|
|
||||||
}
|
|
||||||
|
|
||||||
.sheet(isPresented: $monitor.showSettings) {
|
|
||||||
SettingsView(monitor: monitor)
|
|
||||||
.frame(width: 480)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DiagnosticsView: View {
|
|
||||||
@ObservedObject var monitor: ServerMonitor
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
HStack {
|
|
||||||
Text("Pegasus Diagnostics").font(.title2.bold())
|
|
||||||
Spacer()
|
|
||||||
Button("Close") { dismiss() }
|
|
||||||
}
|
|
||||||
Text("Latest remote report — refresh from the menu to update")
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
ScrollView {
|
|
||||||
LazyVStack(alignment: .leading, spacing: 10) {
|
|
||||||
ForEach(monitor.report?.checks ?? []) { check in
|
|
||||||
HStack(alignment: .top) {
|
|
||||||
Image(systemName: check.status.symbol).foregroundStyle(check.status.color)
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
Text(check.name).fontWeight(.medium)
|
|
||||||
Text(check.detail).font(.caption).foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.textSelection(.enabled)
|
|
||||||
}
|
|
||||||
.padding(20)
|
|
||||||
.frame(width: 650, height: 560)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct SettingsView: View {
|
|
||||||
@ObservedObject var monitor: ServerMonitor
|
|
||||||
@AppStorage("server.host") private var host = "192.168.1.2"
|
|
||||||
@AppStorage("server.user") private var user = "mag"
|
|
||||||
@AppStorage("server.port") private var port = "22"
|
|
||||||
@AppStorage("server.keyFile") private var keyFile = ""
|
|
||||||
@AppStorage("monitor.intervalMinutes") private var intervalMinutes = 5
|
|
||||||
@AppStorage("monitor.notifications") private var notifications = true
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Form {
|
|
||||||
TextField("Host", text: $host)
|
|
||||||
TextField("SSH user", text: $user)
|
|
||||||
TextField("SSH port", text: $port)
|
|
||||||
TextField("SSH key path (optional)", text: $keyFile)
|
|
||||||
.help("Leave blank to use your SSH agent or ~/.ssh/config")
|
|
||||||
Picker("Refresh interval", selection: $intervalMinutes) {
|
|
||||||
Text("1 minute").tag(1)
|
|
||||||
Text("5 minutes").tag(5)
|
|
||||||
Text("10 minutes").tag(10)
|
|
||||||
Text("15 minutes").tag(15)
|
|
||||||
Text("30 minutes").tag(30)
|
|
||||||
}
|
|
||||||
Toggle("Notify when server becomes unhealthy", isOn: $notifications)
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
Button("Apply and Refresh") {
|
|
||||||
monitor.configureTimer()
|
|
||||||
Task { await monitor.refresh() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Text("SSH must authenticate without prompts. Server requires /usr/local/bin/pegasus-health-json accessible via sudo -n. Host keys are verified using your existing SSH known_hosts file.")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
.padding(20)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user