updates
This commit is contained in:
+1
-1
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
+34
-58
@@ -4,66 +4,50 @@
|
||||
"Localizable": [
|
||||
{
|
||||
"comment": "",
|
||||
"key": "192.168.1.2",
|
||||
"key": "SERVERS",
|
||||
"location": {
|
||||
"startingColumn": 22,
|
||||
"startingLine": 23
|
||||
"startingColumn": 18,
|
||||
"startingLine": 21
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "ALL SERVERS: %lld OK · %lld WARN · %lld FAIL",
|
||||
"location": {
|
||||
"startingColumn": 18,
|
||||
"startingLine": 76
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "%lld offline",
|
||||
"location": {
|
||||
"startingColumn": 26,
|
||||
"startingLine": 17
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "PEGASUS MONITOR",
|
||||
"location": {
|
||||
"startingColumn": 23,
|
||||
"startingLine": 13
|
||||
"startingColumn": 27,
|
||||
"startingLine": 11
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "Waiting for Pegasus health data…",
|
||||
"key": "%lld OK",
|
||||
"location": {
|
||||
"startingColumn": 30,
|
||||
"startingLine": 62
|
||||
"startingColumn": 42,
|
||||
"startingLine": 38
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "No system groups returned by Pegasus.",
|
||||
"location": {
|
||||
"startingColumn": 34,
|
||||
"startingLine": 36
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "Not checked",
|
||||
"location": {
|
||||
"startingColumn": 25,
|
||||
"startingLine": 83
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "Last checked: %@",
|
||||
"location": {
|
||||
"startingColumn": 29,
|
||||
"startingLine": 84
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "Not checked",
|
||||
"location": {
|
||||
"startingColumn": 30,
|
||||
"startingLine": 85
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "Refresh",
|
||||
"key": "Refresh All",
|
||||
"location": {
|
||||
"startingColumn": 24,
|
||||
"startingLine": 106
|
||||
"startingLine": 84
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -71,7 +55,7 @@
|
||||
"key": "Diagnostics",
|
||||
"location": {
|
||||
"startingColumn": 24,
|
||||
"startingLine": 111
|
||||
"startingLine": 86
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -79,7 +63,7 @@
|
||||
"key": "Settings",
|
||||
"location": {
|
||||
"startingColumn": 24,
|
||||
"startingLine": 115
|
||||
"startingLine": 87
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -87,31 +71,23 @@
|
||||
"key": "Quit",
|
||||
"location": {
|
||||
"startingColumn": 24,
|
||||
"startingLine": 119
|
||||
"startingLine": 88
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "Latest remote report — refresh from the menu to update",
|
||||
"key": "%@ — Diagnostics",
|
||||
"location": {
|
||||
"startingColumn": 18,
|
||||
"startingLine": 149
|
||||
"startingLine": 98
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "Pegasus Diagnostics",
|
||||
"key": "Select a server in the menu bar to change this report.",
|
||||
"location": {
|
||||
"startingColumn": 22,
|
||||
"startingLine": 145
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "Close",
|
||||
"location": {
|
||||
"startingColumn": 24,
|
||||
"startingLine": 147
|
||||
"startingColumn": 18,
|
||||
"startingLine": 99
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
BIN
Binary file not shown.
+4
-4
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+1
-1
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
+17
-1
@@ -2,12 +2,28 @@
|
||||
"source": "/Volumes/Developer/Applications/PegasusMonitor/PegasusMonitor/PegasusMonitorApp.swift",
|
||||
"tables": {
|
||||
"Localizable": [
|
||||
{
|
||||
"comment": "",
|
||||
"key": "Pegasus Monitor Settings",
|
||||
"location": {
|
||||
"startingColumn": 16,
|
||||
"startingLine": 19
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "Server Diagnostics",
|
||||
"location": {
|
||||
"startingColumn": 16,
|
||||
"startingLine": 25
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "Pegasus Monitor — %@",
|
||||
"location": {
|
||||
"startingColumn": 23,
|
||||
"startingLine": 16
|
||||
"startingLine": 14
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
BIN
Binary file not shown.
+1
-1
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+1
-1
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
+66
-58
@@ -4,122 +4,130 @@
|
||||
"Localizable": [
|
||||
{
|
||||
"comment": "",
|
||||
"key": "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.",
|
||||
"key": "Servers",
|
||||
"location": {
|
||||
"startingColumn": 18,
|
||||
"startingLine": 38
|
||||
"startingColumn": 22,
|
||||
"startingLine": 14
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "Host",
|
||||
"key": "Use the Pegasus adapter for Pegasus, or install server/remote-health-json on other Linux servers. SSH must work without interactive prompts. SSH known_hosts is enforced.",
|
||||
"location": {
|
||||
"startingColumn": 23,
|
||||
"startingLine": 15
|
||||
"startingColumn": 22,
|
||||
"startingLine": 48
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "SSH user",
|
||||
"key": "Add a server",
|
||||
"location": {
|
||||
"startingColumn": 23,
|
||||
"startingLine": 16
|
||||
"startingColumn": 41,
|
||||
"startingLine": 37
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "Connection details",
|
||||
"location": {
|
||||
"startingColumn": 58,
|
||||
"startingLine": 37
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "Display name",
|
||||
"location": {
|
||||
"startingColumn": 31,
|
||||
"startingLine": 40
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "Host / SSH alias",
|
||||
"location": {
|
||||
"startingColumn": 31,
|
||||
"startingLine": 41
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "SSH username",
|
||||
"location": {
|
||||
"startingColumn": 31,
|
||||
"startingLine": 42
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "SSH port",
|
||||
"location": {
|
||||
"startingColumn": 23,
|
||||
"startingLine": 17
|
||||
"startingColumn": 31,
|
||||
"startingLine": 43
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "SSH key path (optional)",
|
||||
"location": {
|
||||
"startingColumn": 23,
|
||||
"startingLine": 18
|
||||
"startingColumn": 31,
|
||||
"startingLine": 44
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "Leave blank to use your SSH agent or ~/.ssh/config",
|
||||
"key": "Remote health command",
|
||||
"location": {
|
||||
"startingColumn": 23,
|
||||
"startingLine": 19
|
||||
"startingColumn": 31,
|
||||
"startingLine": 45
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "Refresh interval",
|
||||
"key": "Monitoring enabled",
|
||||
"location": {
|
||||
"startingColumn": 20,
|
||||
"startingLine": 20
|
||||
"startingColumn": 28,
|
||||
"startingLine": 46
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "Notify when server becomes unhealthy",
|
||||
"key": "New Server",
|
||||
"location": {
|
||||
"startingColumn": 20,
|
||||
"startingLine": 27
|
||||
"startingColumn": 28,
|
||||
"startingLine": 52
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "1 minute",
|
||||
"key": "Save and Refresh",
|
||||
"location": {
|
||||
"startingColumn": 22,
|
||||
"startingLine": 21
|
||||
"startingColumn": 28,
|
||||
"startingLine": 54
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "5 minutes",
|
||||
"key": "Refresh every",
|
||||
"location": {
|
||||
"startingColumn": 22,
|
||||
"startingLine": 22
|
||||
"startingColumn": 28,
|
||||
"startingLine": 59
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "10 minutes",
|
||||
"key": "Notifications",
|
||||
"location": {
|
||||
"startingColumn": 22,
|
||||
"startingLine": 23
|
||||
"startingColumn": 28,
|
||||
"startingLine": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "15 minutes",
|
||||
"key": "%lld min",
|
||||
"location": {
|
||||
"startingColumn": 22,
|
||||
"startingLine": 24
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "30 minutes",
|
||||
"location": {
|
||||
"startingColumn": 22,
|
||||
"startingLine": 25
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "Close",
|
||||
"location": {
|
||||
"startingColumn": 24,
|
||||
"startingLine": 29
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "Apply and Refresh",
|
||||
"location": {
|
||||
"startingColumn": 24,
|
||||
"startingLine": 33
|
||||
"startingColumn": 34,
|
||||
"startingLine": 61
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
-49
@@ -1,49 +0,0 @@
|
||||
{
|
||||
"" : {
|
||||
"diagnostics" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/PegasusMonitor-primary.dia",
|
||||
"emit-module-dependencies" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/PegasusMonitor-primary-emit-module.d",
|
||||
"emit-module-diagnostics" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/PegasusMonitor-primary-emit-module.dia",
|
||||
"pch" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/PegasusMonitor-primary-Bridging-header.pch",
|
||||
"swift-dependencies" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/PegasusMonitor-primary.swiftdeps"
|
||||
},
|
||||
"/Volumes/Developer/Applications/PegasusMonitor/PegasusMonitor/MonitorMenuView.swift" : {
|
||||
"const-values" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/MonitorMenuView.swiftconstvalues",
|
||||
"dependencies" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/MonitorMenuView.d",
|
||||
"diagnostics" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/MonitorMenuView.dia",
|
||||
"index-unit-output-path" : "/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/MonitorMenuView.o",
|
||||
"llvm-bc" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/MonitorMenuView.bc",
|
||||
"object" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/MonitorMenuView.o",
|
||||
"swift-dependencies" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/MonitorMenuView.swiftdeps",
|
||||
"swiftmodule" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/MonitorMenuView~partial.swiftmodule"
|
||||
},
|
||||
"/Volumes/Developer/Applications/PegasusMonitor/PegasusMonitor/PegasusMonitorApp.swift" : {
|
||||
"const-values" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/PegasusMonitorApp.swiftconstvalues",
|
||||
"dependencies" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/PegasusMonitorApp.d",
|
||||
"diagnostics" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/PegasusMonitorApp.dia",
|
||||
"index-unit-output-path" : "/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/PegasusMonitorApp.o",
|
||||
"llvm-bc" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/PegasusMonitorApp.bc",
|
||||
"object" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/PegasusMonitorApp.o",
|
||||
"swift-dependencies" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/PegasusMonitorApp.swiftdeps",
|
||||
"swiftmodule" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/PegasusMonitorApp~partial.swiftmodule"
|
||||
},
|
||||
"/Volumes/Developer/Applications/PegasusMonitor/PegasusMonitor/ServerMonitor.swift" : {
|
||||
"const-values" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/ServerMonitor.swiftconstvalues",
|
||||
"dependencies" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/ServerMonitor.d",
|
||||
"diagnostics" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/ServerMonitor.dia",
|
||||
"index-unit-output-path" : "/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/ServerMonitor.o",
|
||||
"llvm-bc" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/ServerMonitor.bc",
|
||||
"object" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/ServerMonitor.o",
|
||||
"swift-dependencies" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/ServerMonitor.swiftdeps",
|
||||
"swiftmodule" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/ServerMonitor~partial.swiftmodule"
|
||||
},
|
||||
"/Volumes/Developer/Applications/PegasusMonitor/PegasusMonitor/SettingsView.swift" : {
|
||||
"const-values" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/SettingsView.swiftconstvalues",
|
||||
"dependencies" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/SettingsView.d",
|
||||
"diagnostics" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/SettingsView.dia",
|
||||
"index-unit-output-path" : "/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/SettingsView.o",
|
||||
"llvm-bc" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/SettingsView.bc",
|
||||
"object" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/SettingsView.o",
|
||||
"swift-dependencies" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/SettingsView.swiftdeps",
|
||||
"swiftmodule" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/SettingsView~partial.swiftmodule"
|
||||
}
|
||||
}
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict/>
|
||||
</plist>
|
||||
BIN
Binary file not shown.
-4
@@ -1,4 +0,0 @@
|
||||
/Volumes/Developer/Applications/PegasusMonitor/PegasusMonitor/PegasusMonitorApp.swift
|
||||
/Volumes/Developer/Applications/PegasusMonitor/PegasusMonitor/ServerMonitor.swift
|
||||
/Volumes/Developer/Applications/PegasusMonitor/PegasusMonitor/MonitorMenuView.swift
|
||||
/Volumes/Developer/Applications/PegasusMonitor/PegasusMonitor/SettingsView.swift
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/PegasusMonitorApp.o
|
||||
/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/ServerMonitor.o
|
||||
/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/MonitorMenuView.o
|
||||
/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/SettingsView.o
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"case-sensitive":"false","roots":[],"version":0}
|
||||
-1
@@ -1 +0,0 @@
|
||||
["AnyResolverProviding","AppEntity","AppEnum","AppExtension","AppIntent","AppIntentsPackage","AppShortcutProviding","AppShortcutsProvider","AppUnionValue","AppUnionValueCasesProviding","DynamicOptionsProvider","EntityQuery","ExtensionPointDefining","IntentValueQuery","Resolver","TransientEntity","_AssistantIntentsProvider","_GenerativeFunctionExtractable","_IntentValueRepresentable"]
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/PegasusMonitorApp.swiftconstvalues
|
||||
/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/ServerMonitor.swiftconstvalues
|
||||
/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/MonitorMenuView.swiftconstvalues
|
||||
/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PegasusMonitor.build/Release/PegasusMonitor.build/Objects-normal/arm64/SettingsView.swiftconstvalues
|
||||
BIN
Binary file not shown.
-66
@@ -1,66 +0,0 @@
|
||||
{
|
||||
"buildCommand" : {
|
||||
"command" : "build",
|
||||
"skipDependencies" : false,
|
||||
"style" : "buildOnly"
|
||||
},
|
||||
"configuredTargets" : [
|
||||
{
|
||||
"guid" : "651e0ee2bbff102a3d1ac16211f573867d28c6b0d11cbd2d9a7f0a38de01f763"
|
||||
}
|
||||
],
|
||||
"containerPath" : "/Volumes/Developer/Applications/PegasusMonitor/PegasusMonitor.xcodeproj",
|
||||
"continueBuildingAfterErrors" : false,
|
||||
"dependencyScope" : "workspace",
|
||||
"enableIndexBuildArena" : false,
|
||||
"hideShellScriptEnvironment" : false,
|
||||
"parameters" : {
|
||||
"action" : "build",
|
||||
"activeArchitecture" : "arm64",
|
||||
"activeRunDestination" : {
|
||||
"disableOnlyActiveArch" : false,
|
||||
"hostTargetedPlatform" : null,
|
||||
"platform" : "macosx",
|
||||
"sdk" : "macosx27.0",
|
||||
"sdkVariant" : "macos",
|
||||
"supportedArchitectures" : [
|
||||
"arm64e",
|
||||
"arm64",
|
||||
"x86_64"
|
||||
],
|
||||
"targetArchitecture" : "arm64"
|
||||
},
|
||||
"arenaInfo" : {
|
||||
"buildIntermediatesPath" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex",
|
||||
"buildProductsPath" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Products",
|
||||
"derivedDataPath" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData",
|
||||
"indexDataStoreFolderPath" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Index.noindex/DataStore",
|
||||
"indexEnableDataStore" : true,
|
||||
"indexPCHPath" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Index.noindex/PrecompiledHeaders",
|
||||
"pchPath" : "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Intermediates.noindex/PrecompiledHeaders"
|
||||
},
|
||||
"configurationName" : "Release",
|
||||
"overrides" : {
|
||||
"commandLine" : {
|
||||
"table" : {
|
||||
"CODE_SIGNING_ALLOWED" : "NO"
|
||||
}
|
||||
},
|
||||
"synthesized" : {
|
||||
"table" : {
|
||||
"ACTION" : "build",
|
||||
"ENABLE_PREVIEWS" : "NO",
|
||||
"ENABLE_XOJIT_PREVIEWS" : "YES",
|
||||
"ONLY_ACTIVE_ARCH" : "YES",
|
||||
"TOOLCHAINS" : "com.apple.dt.toolchain.Metal.32023.921.5 $(inherited)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"schemeCommand" : "launch",
|
||||
"showNonLoggedProgress" : true,
|
||||
"useDryRun" : false,
|
||||
"useImplicitDependencies" : true,
|
||||
"useLegacyBuildLocations" : false,
|
||||
"useParallelTargets" : true
|
||||
}
|
||||
BIN
Binary file not shown.
-1
File diff suppressed because one or more lines are too long
-2
@@ -1,2 +0,0 @@
|
||||
Target dependency graph (1 target)
|
||||
Target 'PegasusMonitor' in project 'PegasusMonitor' (no dependencies)
|
||||
BIN
Binary file not shown.
+1
@@ -4,3 +4,4 @@ f917cf82bd848fc97291bd609bd414d6
|
||||
4366908824f51bff885c44198535c243
|
||||
ae0397526b5e3c6e8848cdfd6a483288
|
||||
4366908824f51bff885c44198535c243
|
||||
4f303a1f6ed436c288335f2c2511f2f6
|
||||
|
||||
+1
@@ -1,2 +1,3 @@
|
||||
/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Products/Release/PegasusMonitor.app
|
||||
/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Products/Release/PegasusMonitor.app
|
||||
/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/Build/Products/Release/PegasusMonitor.app
|
||||
|
||||
+2
-2
@@ -23,13 +23,13 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.0</string>
|
||||
<string>1.1.0</string>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>MacOSX</string>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<string>2</string>
|
||||
<key>DTCompiler</key>
|
||||
<string>com.apple.compilers.llvm.clang.1_0</string>
|
||||
<key>DTPlatformBuild</key>
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+4
-4
@@ -6,11 +6,11 @@
|
||||
<dict>
|
||||
<key>Resources/AppIcon.icns</key>
|
||||
<data>
|
||||
S8C6t1Z/4EVT9S8MtxX+dWOTDDI=
|
||||
vKs1dGjxnr8qvszRig1NygUnEvQ=
|
||||
</data>
|
||||
<key>Resources/Assets.car</key>
|
||||
<data>
|
||||
nXwOZPAX5QoAWKwBNXu/+rgmZas=
|
||||
0vWT7uDgnvGjO8uys5wHeLklVEA=
|
||||
</data>
|
||||
</dict>
|
||||
<key>files2</key>
|
||||
@@ -19,14 +19,14 @@
|
||||
<dict>
|
||||
<key>hash2</key>
|
||||
<data>
|
||||
969/63CS71MfGGi22Ly12tj0fLLBfSujCjuZm/Pzg3A=
|
||||
1Im+MWM/WyHdmIHqPK/QYzToAHalwBwkHPbSyKKKpKI=
|
||||
</data>
|
||||
</dict>
|
||||
<key>Resources/Assets.car</key>
|
||||
<dict>
|
||||
<key>hash2</key>
|
||||
<data>
|
||||
XL2oX1sbJ0IMB/Y+o7sObFkNbTn/DWGDRiC/RrqhQb0=
|
||||
Zu8DqsY66ElekR5oriOrFU+DwTtetDvo/TZkYknqheM=
|
||||
</data>
|
||||
</dict>
|
||||
</dict>
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -6,7 +6,7 @@
|
||||
<integer>13</integer>
|
||||
<key>logs</key>
|
||||
<dict>
|
||||
<key>0CF3E8FE-5D7C-4BFF-95CD-9A3DB378E862</key>
|
||||
<key>CC8DC4F3-DD53-4B42-8573-B118D88EBAB2</key>
|
||||
<dict>
|
||||
<key>className</key>
|
||||
<string>IDECommandLineBuildLog</string>
|
||||
@@ -15,175 +15,7 @@
|
||||
<key>domainType</key>
|
||||
<string>Xcode.IDEActivityLogDomainType.BuildLog</string>
|
||||
<key>fileName</key>
|
||||
<string>0CF3E8FE-5D7C-4BFF-95CD-9A3DB378E862.xcactivitylog</string>
|
||||
<key>hasPrimaryLog</key>
|
||||
<true/>
|
||||
<key>primaryObservable</key>
|
||||
<dict>
|
||||
<key>highLevelStatus</key>
|
||||
<string>S</string>
|
||||
<key>totalNumberOfAnalyzerIssues</key>
|
||||
<integer>0</integer>
|
||||
<key>totalNumberOfErrors</key>
|
||||
<integer>0</integer>
|
||||
<key>totalNumberOfTestFailures</key>
|
||||
<integer>0</integer>
|
||||
<key>totalNumberOfWarnings</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<key>schemeIdentifier-containerName</key>
|
||||
<string>PegasusMonitor project</string>
|
||||
<key>schemeIdentifier-schemeName</key>
|
||||
<string>PegasusMonitor</string>
|
||||
<key>schemeIdentifier-sharedScheme</key>
|
||||
<integer>1</integer>
|
||||
<key>signature</key>
|
||||
<string>Building project PegasusMonitor with scheme PegasusMonitor and configuration Release</string>
|
||||
<key>timeStartedRecording</key>
|
||||
<real>814281795.00424099</real>
|
||||
<key>timeStoppedRecording</key>
|
||||
<real>814281795.84052396</real>
|
||||
<key>title</key>
|
||||
<string>Building project PegasusMonitor with scheme PegasusMonitor and configuration Release</string>
|
||||
<key>uniqueIdentifier</key>
|
||||
<string>0CF3E8FE-5D7C-4BFF-95CD-9A3DB378E862</string>
|
||||
</dict>
|
||||
<key>128B3D00-57CF-468F-AFAF-A168A96DF72F</key>
|
||||
<dict>
|
||||
<key>className</key>
|
||||
<string>IDECommandLineBuildLog</string>
|
||||
<key>documentTypeString</key>
|
||||
<string><nil></string>
|
||||
<key>domainType</key>
|
||||
<string>Xcode.IDEActivityLogDomainType.BuildLog</string>
|
||||
<key>fileName</key>
|
||||
<string>128B3D00-57CF-468F-AFAF-A168A96DF72F.xcactivitylog</string>
|
||||
<key>hasPrimaryLog</key>
|
||||
<true/>
|
||||
<key>primaryObservable</key>
|
||||
<dict>
|
||||
<key>highLevelStatus</key>
|
||||
<string>S</string>
|
||||
<key>totalNumberOfAnalyzerIssues</key>
|
||||
<integer>0</integer>
|
||||
<key>totalNumberOfErrors</key>
|
||||
<integer>0</integer>
|
||||
<key>totalNumberOfTestFailures</key>
|
||||
<integer>0</integer>
|
||||
<key>totalNumberOfWarnings</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<key>schemeIdentifier-containerName</key>
|
||||
<string>PegasusMonitor project</string>
|
||||
<key>schemeIdentifier-schemeName</key>
|
||||
<string>PegasusMonitor</string>
|
||||
<key>schemeIdentifier-sharedScheme</key>
|
||||
<integer>1</integer>
|
||||
<key>signature</key>
|
||||
<string>Building project PegasusMonitor with scheme PegasusMonitor and configuration Release</string>
|
||||
<key>timeStartedRecording</key>
|
||||
<real>814281713.47929299</real>
|
||||
<key>timeStoppedRecording</key>
|
||||
<real>814281714.33905697</real>
|
||||
<key>title</key>
|
||||
<string>Building project PegasusMonitor with scheme PegasusMonitor and configuration Release</string>
|
||||
<key>uniqueIdentifier</key>
|
||||
<string>128B3D00-57CF-468F-AFAF-A168A96DF72F</string>
|
||||
</dict>
|
||||
<key>24D00FB5-EA46-4D8C-9DFD-013A873B2575</key>
|
||||
<dict>
|
||||
<key>className</key>
|
||||
<string>IDECommandLineBuildLog</string>
|
||||
<key>documentTypeString</key>
|
||||
<string><nil></string>
|
||||
<key>domainType</key>
|
||||
<string>Xcode.IDEActivityLogDomainType.BuildLog</string>
|
||||
<key>fileName</key>
|
||||
<string>24D00FB5-EA46-4D8C-9DFD-013A873B2575.xcactivitylog</string>
|
||||
<key>hasPrimaryLog</key>
|
||||
<true/>
|
||||
<key>primaryObservable</key>
|
||||
<dict>
|
||||
<key>highLevelStatus</key>
|
||||
<string>S</string>
|
||||
<key>totalNumberOfAnalyzerIssues</key>
|
||||
<integer>0</integer>
|
||||
<key>totalNumberOfErrors</key>
|
||||
<integer>0</integer>
|
||||
<key>totalNumberOfTestFailures</key>
|
||||
<integer>0</integer>
|
||||
<key>totalNumberOfWarnings</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<key>schemeIdentifier-containerName</key>
|
||||
<string>PegasusMonitor project</string>
|
||||
<key>schemeIdentifier-schemeName</key>
|
||||
<string>PegasusMonitor</string>
|
||||
<key>schemeIdentifier-sharedScheme</key>
|
||||
<integer>1</integer>
|
||||
<key>signature</key>
|
||||
<string>Building project PegasusMonitor with scheme PegasusMonitor and configuration Release</string>
|
||||
<key>timeStartedRecording</key>
|
||||
<real>814281613.32994998</real>
|
||||
<key>timeStoppedRecording</key>
|
||||
<real>814281614.79972601</real>
|
||||
<key>title</key>
|
||||
<string>Building project PegasusMonitor with scheme PegasusMonitor and configuration Release</string>
|
||||
<key>uniqueIdentifier</key>
|
||||
<string>24D00FB5-EA46-4D8C-9DFD-013A873B2575</string>
|
||||
</dict>
|
||||
<key>4F97B3EC-928A-4943-A881-C363BC254670</key>
|
||||
<dict>
|
||||
<key>className</key>
|
||||
<string>IDECommandLineBuildLog</string>
|
||||
<key>documentTypeString</key>
|
||||
<string><nil></string>
|
||||
<key>domainType</key>
|
||||
<string>Xcode.IDEActivityLogDomainType.BuildLog</string>
|
||||
<key>fileName</key>
|
||||
<string>4F97B3EC-928A-4943-A881-C363BC254670.xcactivitylog</string>
|
||||
<key>hasPrimaryLog</key>
|
||||
<true/>
|
||||
<key>primaryObservable</key>
|
||||
<dict>
|
||||
<key>highLevelStatus</key>
|
||||
<string>S</string>
|
||||
<key>totalNumberOfAnalyzerIssues</key>
|
||||
<integer>0</integer>
|
||||
<key>totalNumberOfErrors</key>
|
||||
<integer>0</integer>
|
||||
<key>totalNumberOfTestFailures</key>
|
||||
<integer>0</integer>
|
||||
<key>totalNumberOfWarnings</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<key>schemeIdentifier-containerName</key>
|
||||
<string>PegasusMonitor project</string>
|
||||
<key>schemeIdentifier-schemeName</key>
|
||||
<string>PegasusMonitor</string>
|
||||
<key>schemeIdentifier-sharedScheme</key>
|
||||
<integer>1</integer>
|
||||
<key>signature</key>
|
||||
<string>Building project PegasusMonitor with scheme PegasusMonitor and configuration Release</string>
|
||||
<key>timeStartedRecording</key>
|
||||
<real>814281826.88459897</real>
|
||||
<key>timeStoppedRecording</key>
|
||||
<real>814281828.74992299</real>
|
||||
<key>title</key>
|
||||
<string>Building project PegasusMonitor with scheme PegasusMonitor and configuration Release</string>
|
||||
<key>uniqueIdentifier</key>
|
||||
<string>4F97B3EC-928A-4943-A881-C363BC254670</string>
|
||||
</dict>
|
||||
<key>D32A8397-030B-41E2-A72D-ADAD409E6B98</key>
|
||||
<dict>
|
||||
<key>className</key>
|
||||
<string>IDECommandLineBuildLog</string>
|
||||
<key>documentTypeString</key>
|
||||
<string><nil></string>
|
||||
<key>domainType</key>
|
||||
<string>Xcode.IDEActivityLogDomainType.BuildLog</string>
|
||||
<key>fileName</key>
|
||||
<string>D32A8397-030B-41E2-A72D-ADAD409E6B98.xcactivitylog</string>
|
||||
<string>CC8DC4F3-DD53-4B42-8573-B118D88EBAB2.xcactivitylog</string>
|
||||
<key>hasPrimaryLog</key>
|
||||
<true/>
|
||||
<key>primaryObservable</key>
|
||||
@@ -197,7 +29,7 @@
|
||||
<key>totalNumberOfTestFailures</key>
|
||||
<integer>0</integer>
|
||||
<key>totalNumberOfWarnings</key>
|
||||
<integer>10</integer>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
<key>schemeIdentifier-containerName</key>
|
||||
<string>PegasusMonitor project</string>
|
||||
@@ -208,13 +40,13 @@
|
||||
<key>signature</key>
|
||||
<string>Building project PegasusMonitor with scheme PegasusMonitor and configuration Release</string>
|
||||
<key>timeStartedRecording</key>
|
||||
<real>814281516.31568694</real>
|
||||
<real>814282733.91194606</real>
|
||||
<key>timeStoppedRecording</key>
|
||||
<real>814281518.00763202</real>
|
||||
<real>814282739.63976002</real>
|
||||
<key>title</key>
|
||||
<string>Building project PegasusMonitor with scheme PegasusMonitor and configuration Release</string>
|
||||
<key>uniqueIdentifier</key>
|
||||
<string>D32A8397-030B-41E2-A72D-ADAD409E6B98</string>
|
||||
<string>CC8DC4F3-DD53-4B42-8573-B118D88EBAB2</string>
|
||||
</dict>
|
||||
<key>E1DE6BCB-D5CB-4252-BF49-D92793CE0E02</key>
|
||||
<dict>
|
||||
|
||||
@@ -1 +1 @@
|
||||
1792589066.837541: Module build session file for module cache at Path(str: "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/ModuleCache.noindex")
|
||||
1792589935.5735621: Module build session file for module cache at Path(str: "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/ModuleCache.noindex")
|
||||
|
||||
BIN
Binary file not shown.
@@ -3,7 +3,7 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>LastAccessedDate</key>
|
||||
<date>2026-10-21T13:24:26Z</date>
|
||||
<date>2026-10-21T13:38:54Z</date>
|
||||
<key>WorkspacePath</key>
|
||||
<string>/Volumes/Developer/Applications/PegasusMonitor/PegasusMonitor.xcodeproj</string>
|
||||
</dict>
|
||||
|
||||
@@ -166,7 +166,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
DEVELOPMENT_TEAM = 39R6U3437D;
|
||||
ENABLE_APP_SANDBOX = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -174,7 +174,7 @@
|
||||
INFOPLIST_KEY_LSUIElement = YES;
|
||||
INFOPLIST_KEY_NSPrincipalClass = NSApplication;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
MARKETING_VERSION = 1.1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.absdev.PegasusMonitor;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = macosx;
|
||||
@@ -189,7 +189,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
DEVELOPMENT_TEAM = 39R6U3437D;
|
||||
ENABLE_APP_SANDBOX = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -197,7 +197,7 @@
|
||||
INFOPLIST_KEY_LSUIElement = YES;
|
||||
INFOPLIST_KEY_NSPrincipalClass = NSApplication;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
MARKETING_VERSION = 1.1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.absdev.PegasusMonitor;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = macosx;
|
||||
|
||||
@@ -1,153 +1,102 @@
|
||||
import SwiftUI
|
||||
|
||||
|
||||
|
||||
struct MonitorMenuView: View {
|
||||
@ObservedObject var monitor: ServerMonitor
|
||||
@Environment(\.openWindow) private var openWindow
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
|
||||
// Header
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Label("PEGASUS MONITOR", systemImage: "server.rack")
|
||||
.font(.headline)
|
||||
|
||||
Label(
|
||||
monitor.status.title,
|
||||
systemImage: monitor.status.symbol
|
||||
)
|
||||
.foregroundStyle(monitor.status.color)
|
||||
.font(.subheadline.bold())
|
||||
|
||||
Text("192.168.1.2")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(16)
|
||||
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Label("PEGASUS MONITOR", systemImage: "server.rack").font(.headline)
|
||||
Label(monitor.status.title, systemImage: monitor.status.symbol)
|
||||
.foregroundStyle(monitor.status.color)
|
||||
}
|
||||
Spacer()
|
||||
if monitor.offlineCount > 0 {
|
||||
Text("\(monitor.offlineCount) offline").foregroundStyle(.red).font(.caption.bold())
|
||||
}
|
||||
}.padding(14)
|
||||
Divider()
|
||||
|
||||
// Systems: vertical stack
|
||||
ScrollView(.vertical) {
|
||||
LazyVStack(alignment: .leading, spacing: 0) {
|
||||
if let report = monitor.report {
|
||||
if report.groups.isEmpty {
|
||||
Text("No system groups returned by Pegasus.")
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(16)
|
||||
}
|
||||
|
||||
ForEach(report.groups) { group in
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text(group.name)
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Label(
|
||||
group.status.title,
|
||||
systemImage: group.status.symbol
|
||||
)
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundStyle(group.status.color)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 11)
|
||||
|
||||
Divider()
|
||||
.padding(.leading, 16)
|
||||
}
|
||||
} else {
|
||||
Text("Waiting for Pegasus health data…")
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(16)
|
||||
Text("SERVERS").font(.caption.bold()).foregroundStyle(.secondary).padding(.horizontal, 14).padding(.top, 12)
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 2) {
|
||||
ForEach(monitor.servers) { server in
|
||||
Button {
|
||||
monitor.select(server.id)
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: monitor.results[server.id]?.status.symbol ?? "circle.dotted")
|
||||
.foregroundStyle(server.enabled ? (monitor.results[server.id]?.status.color ?? .secondary) : .gray)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(server.name).fontWeight(server.id == monitor.selectedID ? .bold : .regular)
|
||||
Text(server.host).font(.caption2).foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
if monitor.results[server.id]?.refreshing == true { ProgressView().controlSize(.mini) }
|
||||
else if let report = monitor.results[server.id]?.report {
|
||||
Text("\(report.counts.ok) OK").font(.caption2).foregroundStyle(.secondary)
|
||||
}
|
||||
if server.id == monitor.selectedID { Image(systemName: "checkmark") }
|
||||
}.padding(.horizontal, 14).padding(.vertical, 8)
|
||||
.contentShape(Rectangle())
|
||||
}.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: 420)
|
||||
|
||||
}.frame(maxHeight: 175)
|
||||
Divider()
|
||||
|
||||
// Summary
|
||||
if let report = monitor.report {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(
|
||||
"\(report.counts.ok) OK · " +
|
||||
"\(report.counts.warn) WARN · " +
|
||||
"\(report.counts.fail) FAIL"
|
||||
)
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
|
||||
Text(
|
||||
monitor.lastChecked.map {
|
||||
"Last checked: \($0.formatted(date: .omitted, time: .shortened))"
|
||||
} ?? "Not checked"
|
||||
)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(16)
|
||||
if let selected = monitor.selected {
|
||||
HStack {
|
||||
Text(selected.name.uppercased()).font(.caption.bold())
|
||||
Spacer()
|
||||
Button { Task { await monitor.refreshSelected() } } label: { Image(systemName: "arrow.clockwise") }
|
||||
.buttonStyle(.plain)
|
||||
}.padding(.horizontal, 14).padding(.vertical, 10)
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading, spacing: 0) {
|
||||
if let report = monitor.report {
|
||||
ForEach(report.groups) { group in
|
||||
HStack {
|
||||
Text(group.name).font(.system(size: 12, weight: .medium))
|
||||
Spacer()
|
||||
Label(group.status.title, systemImage: group.status.symbol)
|
||||
.foregroundStyle(group.status.color).font(.caption)
|
||||
}.padding(.horizontal, 14).padding(.vertical, 8)
|
||||
Divider().padding(.leading, 14)
|
||||
}
|
||||
} else {
|
||||
Text(monitor.selectedResult?.error ?? "Waiting for health data…")
|
||||
.font(.caption).foregroundStyle(.secondary).padding(14)
|
||||
}
|
||||
}
|
||||
}.frame(height: 300)
|
||||
}
|
||||
|
||||
if let error = monitor.lastError {
|
||||
Text(error)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.red)
|
||||
.textSelection(.enabled)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
// Vertical actions
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Button("Refresh") {
|
||||
Task { await monitor.refresh() }
|
||||
}
|
||||
.disabled(monitor.isRefreshing)
|
||||
|
||||
Button("Diagnostics") {
|
||||
monitor.showDiagnostics = true
|
||||
}
|
||||
|
||||
Button("Settings") {
|
||||
monitor.showSettings = true
|
||||
}
|
||||
|
||||
Button("Quit") {
|
||||
NSApplication.shared.terminate(nil)
|
||||
}
|
||||
let totals = monitor.aggregate
|
||||
Text("ALL SERVERS: \(totals.ok) OK · \(totals.warn) WARN · \(totals.fail) FAIL")
|
||||
.font(.caption.bold()).padding(14)
|
||||
if let error = monitor.selectedResult?.error {
|
||||
Text(error).foregroundStyle(.red).font(.caption).textSelection(.enabled)
|
||||
.padding(.horizontal, 14).padding(.bottom, 8)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(16)
|
||||
}
|
||||
.frame(width: 320)
|
||||
.sheet(isPresented: $monitor.showDiagnostics) {
|
||||
DiagnosticsView(monitor: monitor)
|
||||
}
|
||||
.sheet(isPresented: $monitor.showSettings) {
|
||||
SettingsView(monitor: monitor)
|
||||
.frame(width: 480)
|
||||
}
|
||||
Divider()
|
||||
HStack {
|
||||
Button("Refresh All") { Task { await monitor.refreshAll() } }
|
||||
Spacer()
|
||||
Button("Diagnostics") { openWindow(id: "monitor-diagnostics") }
|
||||
Button("Settings") { openWindow(id: "monitor-settings") }
|
||||
Button("Quit") { NSApp.terminate(nil) }
|
||||
}.buttonStyle(.borderless).padding(14)
|
||||
}.frame(width: 410)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
Text("\(monitor.selected?.name ?? "Server") — Diagnostics").font(.title2.bold())
|
||||
Text("Select a server in the menu bar to change this report.").font(.caption).foregroundStyle(.secondary)
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading, spacing: 10) {
|
||||
ForEach(monitor.report?.checks ?? []) { check in
|
||||
@@ -161,10 +110,7 @@ struct DiagnosticsView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
.padding(20)
|
||||
.frame(width: 650, height: 560)
|
||||
}.textSelection(.enabled)
|
||||
}.padding(20).frame(minWidth: 650, minHeight: 450)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import SwiftUI
|
||||
import UserNotifications
|
||||
|
||||
@main
|
||||
struct PegasusMonitorApp: App {
|
||||
@@ -8,7 +7,6 @@ struct PegasusMonitorApp: App {
|
||||
var body: some Scene {
|
||||
MenuBarExtra {
|
||||
MonitorMenuView(monitor: monitor)
|
||||
.frame(width: 380)
|
||||
} label: {
|
||||
Image(systemName: monitor.status.symbol)
|
||||
.symbolRenderingMode(.palette)
|
||||
@@ -17,9 +15,16 @@ struct PegasusMonitorApp: App {
|
||||
}
|
||||
.menuBarExtraStyle(.window)
|
||||
|
||||
Settings {
|
||||
// Real independent windows: editing cannot be dismissed by the menu-bar popover.
|
||||
Window("Pegasus Monitor Settings", id: "monitor-settings") {
|
||||
SettingsView(monitor: monitor)
|
||||
.frame(width: 480)
|
||||
.frame(minWidth: 630, minHeight: 470)
|
||||
}
|
||||
.defaultSize(width: 750, height: 570)
|
||||
|
||||
Window("Server Diagnostics", id: "monitor-diagnostics") {
|
||||
DiagnosticsView(monitor: monitor)
|
||||
}
|
||||
.defaultSize(width: 760, height: 600)
|
||||
}
|
||||
}
|
||||
|
||||
+142
-104
@@ -4,53 +4,13 @@ import UserNotifications
|
||||
|
||||
enum HealthStatus: String, Codable {
|
||||
case ok, warn, fail
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .ok: "Healthy"
|
||||
case .warn: "Warning"
|
||||
case .fail: "Critical"
|
||||
}
|
||||
}
|
||||
|
||||
var symbol: String {
|
||||
switch self {
|
||||
case .ok: "checkmark.circle.fill"
|
||||
case .warn: "exclamationmark.triangle.fill"
|
||||
case .fail: "xmark.circle.fill"
|
||||
}
|
||||
}
|
||||
|
||||
var color: Color {
|
||||
switch self {
|
||||
case .ok: .green
|
||||
case .warn: .orange
|
||||
case .fail: .red
|
||||
}
|
||||
}
|
||||
var title: String { switch self { case .ok: "Healthy"; case .warn: "Warning"; case .fail: "Critical" } }
|
||||
var symbol: String { switch self { case .ok: "checkmark.circle.fill"; case .warn: "exclamationmark.triangle.fill"; case .fail: "xmark.circle.fill" } }
|
||||
var color: Color { switch self { case .ok: .green; case .warn: .orange; case .fail: .red } }
|
||||
}
|
||||
|
||||
struct HealthCounts: Codable {
|
||||
let ok: Int
|
||||
let warn: Int
|
||||
let fail: Int
|
||||
}
|
||||
|
||||
struct HealthGroup: Codable, Identifiable {
|
||||
let name: String
|
||||
let status: HealthStatus
|
||||
let missing: [String]
|
||||
var id: String { name }
|
||||
}
|
||||
|
||||
struct HealthCheck: Codable, Identifiable {
|
||||
let name: String
|
||||
let status: HealthStatus
|
||||
let detail: String
|
||||
let section: String
|
||||
var id: String { "\(section)/\(name)" }
|
||||
}
|
||||
|
||||
struct HealthCounts: Codable { let ok: Int; let warn: Int; let fail: Int }
|
||||
struct HealthGroup: Codable, Identifiable { let name: String; let status: HealthStatus; let missing: [String]; var id: String { name } }
|
||||
struct HealthCheck: Codable, Identifiable { let name: String; let status: HealthStatus; let detail: String; let section: String; var id: String { "\(section)/\(name)" } }
|
||||
struct HealthReport: Codable {
|
||||
let schema: Int
|
||||
let host: String?
|
||||
@@ -62,71 +22,153 @@ struct HealthReport: Codable {
|
||||
let diagnostics: String?
|
||||
}
|
||||
|
||||
struct SSHSettings {
|
||||
@AppStorage("server.host") var host = "192.168.1.2"
|
||||
@AppStorage("server.user") var user = "mag"
|
||||
@AppStorage("server.port") var port = "22"
|
||||
@AppStorage("server.keyFile") var keyFile = ""
|
||||
@AppStorage("monitor.intervalMinutes") var intervalMinutes = 5
|
||||
@AppStorage("monitor.notifications") var notifications = true
|
||||
struct ServerProfile: Identifiable, Codable, Equatable {
|
||||
var id: UUID = UUID()
|
||||
var name: String = "New Server"
|
||||
var host: String = ""
|
||||
var user: String = ""
|
||||
var port: String = "22"
|
||||
var keyFile: String = ""
|
||||
var remoteCommand: String = "/usr/local/bin/pegasus-monitor-health-json"
|
||||
var enabled: Bool = true
|
||||
|
||||
static func legacy() -> ServerProfile {
|
||||
let defaults = UserDefaults.standard
|
||||
return ServerProfile(name: "Pegasus",
|
||||
host: defaults.string(forKey: "server.host") ?? "192.168.1.2",
|
||||
user: defaults.string(forKey: "server.user") ?? "mag",
|
||||
port: defaults.string(forKey: "server.port") ?? "22",
|
||||
keyFile: defaults.string(forKey: "server.keyFile") ?? "",
|
||||
remoteCommand: "sudo -n /usr/local/bin/pegasus-health-json")
|
||||
}
|
||||
}
|
||||
|
||||
struct ServerResult {
|
||||
var report: HealthReport?
|
||||
var error: String?
|
||||
var lastChecked: Date?
|
||||
var refreshing = false
|
||||
var status: HealthStatus { error != nil ? .fail : (report?.overall ?? .warn) }
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class ServerMonitor: ObservableObject {
|
||||
@Published private(set) var report: HealthReport?
|
||||
@Published private(set) var status: HealthStatus = .warn
|
||||
@Published private(set) var isRefreshing = false
|
||||
@Published private(set) var lastChecked: Date?
|
||||
@Published private(set) var lastError: String?
|
||||
@Published var showDiagnostics = false
|
||||
@Published var showSettings = false
|
||||
|
||||
private var settings = SSHSettings()
|
||||
@Published private(set) var servers: [ServerProfile] = []
|
||||
@Published private(set) var results: [UUID: ServerResult] = [:]
|
||||
@Published var selectedID: UUID?
|
||||
@Published var intervalMinutes: Int { didSet { UserDefaults.standard.set(intervalMinutes, forKey: "monitor.intervalMinutes"); configureTimer() } }
|
||||
@Published var notifications: Bool { didSet { UserDefaults.standard.set(notifications, forKey: "monitor.notifications") } }
|
||||
private var timer: Timer?
|
||||
private var everLoaded = false
|
||||
private var visited = Set<UUID>()
|
||||
private let profilesKey = "monitor.serverProfiles.v1"
|
||||
private let selectedKey = "monitor.selectedServer.v1"
|
||||
|
||||
init() {
|
||||
let defaults = UserDefaults.standard
|
||||
intervalMinutes = max(1, defaults.object(forKey: "monitor.intervalMinutes") as? Int ?? 5)
|
||||
notifications = defaults.object(forKey: "monitor.notifications") as? Bool ?? true
|
||||
if let bytes = defaults.data(forKey: profilesKey), let loaded = try? JSONDecoder().decode([ServerProfile].self, from: bytes) {
|
||||
servers = loaded
|
||||
} else {
|
||||
servers = [.legacy()]
|
||||
}
|
||||
if let stored = defaults.string(forKey: selectedKey), let id = UUID(uuidString: stored), servers.contains(where: { $0.id == id }) {
|
||||
selectedID = id
|
||||
} else {
|
||||
selectedID = servers.first?.id
|
||||
}
|
||||
configureTimer()
|
||||
Task { await refresh() }
|
||||
Task { await refreshAll() }
|
||||
}
|
||||
|
||||
var selected: ServerProfile? { servers.first(where: { $0.id == selectedID }) }
|
||||
var selectedResult: ServerResult? { selectedID.flatMap { results[$0] } }
|
||||
var report: HealthReport? { selectedResult?.report }
|
||||
var status: HealthStatus {
|
||||
let statuses = servers.filter(\.enabled).map { results[$0.id]?.status ?? .warn }
|
||||
if statuses.contains(.fail) { return .fail }
|
||||
if statuses.contains(.warn) { return .warn }
|
||||
return statuses.isEmpty ? .warn : .ok
|
||||
}
|
||||
var aggregate: HealthCounts {
|
||||
let active = servers.filter(\.enabled)
|
||||
return HealthCounts(ok: active.reduce(0) { $0 + (results[$1.id]?.report?.counts.ok ?? 0) },
|
||||
warn: active.reduce(0) { $0 + (results[$1.id]?.report?.counts.warn ?? 0) },
|
||||
fail: active.reduce(0) { $0 + (results[$1.id]?.report?.counts.fail ?? (results[$1.id]?.error == nil ? 0 : 1)) })
|
||||
}
|
||||
var offlineCount: Int { servers.filter { $0.enabled && results[$0.id]?.error != nil }.count }
|
||||
|
||||
func select(_ id: UUID) {
|
||||
guard servers.contains(where: { $0.id == id }) else { return }
|
||||
selectedID = id
|
||||
UserDefaults.standard.set(id.uuidString, forKey: selectedKey)
|
||||
}
|
||||
func save(_ profile: ServerProfile) {
|
||||
if let index = servers.firstIndex(where: { $0.id == profile.id }) { servers[index] = profile }
|
||||
else { servers.append(profile) }
|
||||
persist()
|
||||
select(profile.id)
|
||||
Task { await refresh(id: profile.id) }
|
||||
}
|
||||
func remove(_ id: UUID) {
|
||||
servers.removeAll { $0.id == id }
|
||||
results.removeValue(forKey: id)
|
||||
visited.remove(id)
|
||||
if selectedID == id { selectedID = servers.first?.id }
|
||||
persist()
|
||||
}
|
||||
func move(from source: IndexSet, to destination: Int) {
|
||||
servers.move(fromOffsets: source, toOffset: destination)
|
||||
persist()
|
||||
}
|
||||
private func persist() {
|
||||
if let bytes = try? JSONEncoder().encode(servers) { UserDefaults.standard.set(bytes, forKey: profilesKey) }
|
||||
UserDefaults.standard.set(selectedID?.uuidString, forKey: selectedKey)
|
||||
}
|
||||
func configureTimer() {
|
||||
timer?.invalidate()
|
||||
let minutes = max(1, settings.intervalMinutes)
|
||||
timer = Timer.scheduledTimer(withTimeInterval: TimeInterval(minutes * 60), repeats: true) { [weak self] _ in
|
||||
Task { @MainActor in await self?.refresh() }
|
||||
timer = Timer.scheduledTimer(withTimeInterval: TimeInterval(max(1, intervalMinutes) * 60), repeats: true) { [weak self] _ in
|
||||
Task { @MainActor [weak self] in await self?.refreshAll() }
|
||||
}
|
||||
}
|
||||
|
||||
func refresh() async {
|
||||
guard !isRefreshing else { return }
|
||||
isRefreshing = true
|
||||
defer { isRefreshing = false; lastChecked = Date() }
|
||||
let previous = report?.overall
|
||||
func refreshAll() async {
|
||||
await withTaskGroup(of: Void.self) { group in
|
||||
for server in servers where server.enabled {
|
||||
group.addTask { await self.refresh(id: server.id) }
|
||||
}
|
||||
}
|
||||
}
|
||||
func refreshSelected() async {
|
||||
if let id = selectedID { await refresh(id: id) }
|
||||
}
|
||||
func refresh(id: UUID) async {
|
||||
guard let server = servers.first(where: { $0.id == id }), server.enabled else { return }
|
||||
if results[id]?.refreshing == true { return }
|
||||
var state = results[id] ?? ServerResult()
|
||||
state.refreshing = true
|
||||
results[id] = state
|
||||
let previous = state.status
|
||||
do {
|
||||
let data = try await SSHRunner.run(host: settings.host, user: settings.user, port: settings.port, keyFile: settings.keyFile, remoteCommand: "sudo -n /usr/local/bin/pegasus-health-json")
|
||||
let value = try JSONDecoder().decode(HealthReport.self, from: data)
|
||||
guard value.schema == 1, value.groups.count == 14 else { throw SSHError.invalidResponse }
|
||||
report = value
|
||||
status = value.overall
|
||||
lastError = nil
|
||||
if settings.notifications && everLoaded && previous == .ok && value.overall != .ok {
|
||||
notify(title: "Pegasus \(value.overall.title)", message: "\(value.counts.fail) failures, \(value.counts.warn) warnings")
|
||||
let data = try await SSHRunner.run(host: server.host, user: server.user, port: server.port,
|
||||
keyFile: server.keyFile, remoteCommand: server.remoteCommand)
|
||||
let report = try JSONDecoder().decode(HealthReport.self, from: data)
|
||||
guard report.schema == 1 else { throw SSHError.invalidResponse }
|
||||
state.report = report
|
||||
state.error = nil
|
||||
if notifications && visited.contains(id) && previous == .ok && report.overall != .ok {
|
||||
notify(title: "\(server.name): \(report.overall.title)", message: "\(report.counts.fail) failures, \(report.counts.warn) warnings")
|
||||
}
|
||||
everLoaded = true
|
||||
} catch {
|
||||
lastError = error.localizedDescription
|
||||
status = .fail
|
||||
if settings.notifications && everLoaded && previous != .fail {
|
||||
notify(title: "Pegasus Unreachable", message: error.localizedDescription)
|
||||
state.error = error.localizedDescription
|
||||
if notifications && visited.contains(id) && previous != .fail {
|
||||
notify(title: "\(server.name) unreachable", message: error.localizedDescription)
|
||||
}
|
||||
}
|
||||
state.lastChecked = Date()
|
||||
state.refreshing = false
|
||||
results[id] = state
|
||||
visited.insert(id)
|
||||
}
|
||||
|
||||
func showSystemSettings() {
|
||||
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
|
||||
}
|
||||
|
||||
private func notify(title: String, message: String) {
|
||||
let center = UNUserNotificationCenter.current()
|
||||
center.requestAuthorization(options: [.alert, .sound]) { allowed, _ in
|
||||
@@ -140,12 +182,11 @@ final class ServerMonitor: ObservableObject {
|
||||
}
|
||||
|
||||
enum SSHError: LocalizedError {
|
||||
case invalidSettings, timeout, invalidResponse, process(String)
|
||||
case invalidSettings, invalidResponse, process(String)
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .invalidSettings: "Invalid SSH settings. Check host, user and port."
|
||||
case .timeout: "SSH connection timed out."
|
||||
case .invalidResponse: "Server returned invalid health JSON. Install the server adapter."
|
||||
case .invalidSettings: "Invalid SSH settings (host, user, port or command)."
|
||||
case .invalidResponse: "Invalid health JSON. Install the remote health adapter."
|
||||
case .process(let text): text
|
||||
}
|
||||
}
|
||||
@@ -154,23 +195,21 @@ enum SSHError: LocalizedError {
|
||||
enum SSHRunner {
|
||||
static func run(host: String, user: String, port: String, keyFile: String, remoteCommand: String) async throws -> Data {
|
||||
guard !host.isEmpty, !user.isEmpty, let number = Int(port), (1...65535).contains(number),
|
||||
host.range(of: #"^[A-Za-z0-9.:-]+$"#, options: .regularExpression) != nil,
|
||||
user.range(of: #"^[A-Za-z0-9_-]+$"#, options: .regularExpression) != nil else {
|
||||
throw SSHError.invalidSettings
|
||||
}
|
||||
host.range(of: #"^[A-Za-z0-9._:-]+$"#, options: .regularExpression) != nil,
|
||||
user.range(of: #"^[A-Za-z0-9_-]+$"#, options: .regularExpression) != nil,
|
||||
!remoteCommand.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { throw SSHError.invalidSettings }
|
||||
return try await Task.detached(priority: .utility) {
|
||||
let process = Process()
|
||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/ssh")
|
||||
var args = ["-T", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=yes", "-o", "ConnectTimeout=8", "-o", "ServerAliveInterval=5", "-o", "ServerAliveCountMax=2", "-p", String(number)]
|
||||
if !keyFile.isEmpty { args += ["-i", (keyFile as NSString).expandingTildeInPath] }
|
||||
args += ["--", "\(user)@\(host)", remoteCommand]
|
||||
args += ["\(user)@\(host)", remoteCommand]
|
||||
process.arguments = args
|
||||
let output = Pipe()
|
||||
let errors = Pipe()
|
||||
process.standardOutput = output
|
||||
process.standardError = errors
|
||||
try process.run()
|
||||
// Drain both pipes concurrently so verbose output cannot block the child.
|
||||
let stdout = Task.detached { output.fileHandleForReading.readDataToEndOfFile() }
|
||||
let stderr = Task.detached { errors.fileHandleForReading.readDataToEndOfFile() }
|
||||
let timeout = Task.detached {
|
||||
@@ -181,10 +220,9 @@ enum SSHRunner {
|
||||
let errorBytes = await stderr.value
|
||||
process.waitUntilExit()
|
||||
timeout.cancel()
|
||||
// Health status exits 1/2 intentionally; JSON remains authoritative.
|
||||
if bytes.isEmpty {
|
||||
let errorMessage = String(data: errorBytes, encoding: .utf8) ?? "SSH returned no data"
|
||||
throw SSHError.process(errorMessage.isEmpty ? "SSH returned no data" : errorMessage)
|
||||
let message = String(data: errorBytes, encoding: .utf8) ?? "SSH returned no data"
|
||||
throw SSHError.process(message.isEmpty ? "SSH returned no data" : message)
|
||||
}
|
||||
return bytes
|
||||
}.value
|
||||
|
||||
@@ -2,43 +2,100 @@ import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
@ObservedObject var monitor: ServerMonitor
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@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
|
||||
@State private var draft = ServerProfile()
|
||||
@State private var editingID: UUID?
|
||||
@State private var validation = ""
|
||||
@State private var interval = 5
|
||||
@State 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 {
|
||||
Button("Close") {
|
||||
dismiss()
|
||||
HStack(spacing: 0) {
|
||||
VStack(spacing: 8) {
|
||||
Text("Servers").font(.headline).frame(maxWidth: .infinity, alignment: .leading)
|
||||
List(selection: $editingID) {
|
||||
ForEach(monitor.servers) { server in
|
||||
HStack {
|
||||
Image(systemName: "server.rack")
|
||||
Text(server.name)
|
||||
if !server.enabled { Image(systemName: "pause.circle") }
|
||||
}.tag(server.id)
|
||||
}
|
||||
.onMove { monitor.move(from: $0, to: $1) }
|
||||
}
|
||||
Spacer()
|
||||
Button("Apply and Refresh") {
|
||||
monitor.configureTimer()
|
||||
Task { await monitor.refresh() }
|
||||
HStack {
|
||||
Button { newProfile() } label: { Image(systemName: "plus") }
|
||||
Button { if let id = editingID { monitor.remove(id); editingID = monitor.servers.first?.id } } label: { Image(systemName: "minus") }
|
||||
.disabled(editingID == nil)
|
||||
Spacer()
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
}
|
||||
.frame(width: 205)
|
||||
.padding()
|
||||
Divider()
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
Text(editingID == nil ? "Add a server" : "Connection details")
|
||||
.font(.title3.bold())
|
||||
Form {
|
||||
TextField("Display name", text: $draft.name)
|
||||
TextField("Host / SSH alias", text: $draft.host)
|
||||
TextField("SSH username", text: $draft.user)
|
||||
TextField("SSH port", text: $draft.port)
|
||||
TextField("SSH key path (optional)", text: $draft.keyFile)
|
||||
TextField("Remote health command", text: $draft.remoteCommand)
|
||||
Toggle("Monitoring enabled", isOn: $draft.enabled)
|
||||
}
|
||||
Text("Use the Pegasus adapter for Pegasus, or install server/remote-health-json on other Linux servers. SSH must work without interactive prompts. SSH known_hosts is enforced.")
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
if !validation.isEmpty { Text(validation).foregroundStyle(.red).font(.caption) }
|
||||
HStack {
|
||||
Button("New Server") { newProfile() }
|
||||
Spacer()
|
||||
Button("Save and Refresh") { save() }
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
Divider()
|
||||
HStack {
|
||||
Picker("Refresh every", selection: $interval) {
|
||||
ForEach([1, 5, 10, 15, 30], id: \.self) { value in
|
||||
Text("\(value) min").tag(value)
|
||||
}
|
||||
}
|
||||
Toggle("Notifications", isOn: $notifications)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
.padding(20)
|
||||
.onAppear {
|
||||
interval = monitor.intervalMinutes
|
||||
notifications = monitor.notifications
|
||||
editingID = monitor.selectedID
|
||||
if let selected = monitor.selected { draft = selected }
|
||||
}
|
||||
.onChange(of: editingID) { _, newValue in
|
||||
if let id = newValue, let server = monitor.servers.first(where: { $0.id == id }) { draft = server }
|
||||
validation = ""
|
||||
}
|
||||
.onChange(of: interval) { _, value in monitor.intervalMinutes = value }
|
||||
.onChange(of: notifications) { _, value in monitor.notifications = value }
|
||||
}
|
||||
|
||||
private func newProfile() {
|
||||
editingID = nil
|
||||
draft = ServerProfile()
|
||||
validation = ""
|
||||
}
|
||||
private func save() {
|
||||
guard !draft.name.trimmingCharacters(in: .whitespaces).isEmpty,
|
||||
!draft.host.trimmingCharacters(in: .whitespaces).isEmpty,
|
||||
!draft.user.trimmingCharacters(in: .whitespaces).isEmpty,
|
||||
let port = Int(draft.port), (1...65535).contains(port),
|
||||
!draft.remoteCommand.trimmingCharacters(in: .whitespaces).isEmpty else {
|
||||
validation = "Enter a name, host, SSH username, valid port and remote command."
|
||||
return
|
||||
}
|
||||
monitor.save(draft)
|
||||
editingID = draft.id
|
||||
validation = ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# Pegasus Monitor 1.1.0 (build 2)
|
||||
|
||||
## What's new
|
||||
- Monitor multiple Ubuntu/Debian servers via independent SSH profiles.
|
||||
- Add, edit, remove and reorder servers; persist profiles locally in macOS UserDefaults.
|
||||
- Select a server from the menu bar; view its original health groups and diagnostics.
|
||||
- Consolidated health totals and offline detection; parallel refresh across enabled servers.
|
||||
- Remote profiles support SSH host aliases, custom key files and individual remote JSON commands.
|
||||
- New generic read-only Linux health adapter (`server/remote-health-json`).
|
||||
- Settings and Diagnostics are now **independent windows**, rather than sheets tied to the menu bar window. Editing a field will not dismiss Settings when the menu bar loses focus.
|
||||
- Retains Pegasus's existing specialised health adapter, application icon, code signing and notarization workflow.
|
||||
|
||||
## Install the adapter on another Linux server
|
||||
|
||||
Copy `server/remote-health-json` to the remote host, e.g.:
|
||||
|
||||
```sh
|
||||
scp server/remote-health-json USER@HOST:/tmp/pegasus-monitor-health-json
|
||||
ssh USER@HOST 'sudo install -m 755 /tmp/pegasus-monitor-health-json /usr/local/bin/pegasus-monitor-health-json'
|
||||
ssh USER@HOST /usr/local/bin/pegasus-monitor-health-json
|
||||
```
|
||||
|
||||
The remote adapter requires Python 3 and built-in utilities; it runs without sudo. Docker checks require appropriate Docker group access; if unavailable they report warning rather than modify permissions.
|
||||
|
||||
In Settings → New Server, set the server name, SSH host, username, port and remote command `/usr/local/bin/pegasus-monitor-health-json`. Select Save and Refresh. Configure SSH keys and known_hosts **before** adding the server; interactive password prompts are intentionally disabled.
|
||||
|
||||
For Pegasus, the migrated connection continues using `sudo -n /usr/local/bin/pegasus-health-json` and retains its existing 14-group report and 72 checks.
|
||||
|
||||
## Notes
|
||||
- Existing single-server settings migrate automatically on first launch.
|
||||
- The old preferences are left intact for rollback.
|
||||
- Profiles do not store passwords or private key contents.
|
||||
- No remote installations or privileged actions are performed by the Mac app.
|
||||
- Build and notarize on macOS with Xcode using the existing Scripts directory. The ZIP contains source, not a prebuilt signed application.
|
||||
Executable
+83
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generic read-only Linux health adapter for Pegasus Monitor 1.1.
|
||||
Install as /usr/local/bin/pegasus-monitor-health-json and run as an ordinary user.
|
||||
No third-party Python packages required.
|
||||
"""
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
|
||||
checks = []
|
||||
|
||||
def run(args, timeout=8):
|
||||
try:
|
||||
return subprocess.run(args, capture_output=True, text=True, timeout=timeout, check=False)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
return None
|
||||
|
||||
def check(section, name, status, detail):
|
||||
checks.append(dict(section=section, name=name, status=status, detail=str(detail)[:500]))
|
||||
|
||||
host = socket.gethostname()
|
||||
try:
|
||||
load = os.getloadavg()
|
||||
cpus = os.cpu_count() or 1
|
||||
check('System', 'CPU load', 'warn' if load[0] > cpus * 1.5 else 'ok',
|
||||
f'1m load {load[0]:.2f}; {cpus} CPUs')
|
||||
except OSError as exc:
|
||||
check('System', 'CPU load', 'warn', str(exc))
|
||||
|
||||
try:
|
||||
with open('/proc/meminfo', encoding='utf-8') as f:
|
||||
values = {parts[0].rstrip(':'): int(parts[1]) for line in f if (parts := line.split()) and len(parts) > 1}
|
||||
total = values['MemTotal']
|
||||
available = values.get('MemAvailable', values.get('MemFree', 0))
|
||||
pct = round(100 * (total - available) / total)
|
||||
check('System', 'Memory', 'fail' if pct >= 95 else 'warn' if pct >= 85 else 'ok', f'{pct}% used')
|
||||
except (OSError, KeyError, ZeroDivisionError, ValueError) as exc:
|
||||
check('System', 'Memory', 'warn', str(exc))
|
||||
|
||||
for directory in ('/', '/backup'):
|
||||
if directory != '/' and not os.path.ismount(directory):
|
||||
continue
|
||||
try:
|
||||
usage = shutil.disk_usage(directory)
|
||||
pct = round(100 * usage.used / usage.total)
|
||||
check('Storage', f'Disk {directory}', 'fail' if pct >= 95 else 'warn' if pct >= 85 else 'ok', f'{pct}% used')
|
||||
except (OSError, ZeroDivisionError) as exc:
|
||||
check('Storage', f'Disk {directory}', 'warn', str(exc))
|
||||
|
||||
systemctl = run(['systemctl', '--failed', '--no-legend', '--plain'])
|
||||
if systemctl is not None:
|
||||
failed = [line for line in systemctl.stdout.splitlines() if line.strip()]
|
||||
check('Services', 'Failed systemd units', 'fail' if failed else 'ok',
|
||||
', '.join(failed[:5]) if failed else 'None')
|
||||
|
||||
if shutil.which('docker'):
|
||||
docker = run(['docker', 'ps', '--format', '{{.Names}}'], timeout=12)
|
||||
if docker and docker.returncode == 0:
|
||||
names = [name for name in docker.stdout.splitlines() if name]
|
||||
check('Docker', 'Docker containers', 'ok', f'{len(names)} running: {", ".join(names[:8])}')
|
||||
else:
|
||||
check('Docker', 'Docker access', 'warn', 'Docker is installed but unavailable to this SSH user')
|
||||
|
||||
for service in ('nginx', 'mysql', 'postgresql', 'redis-server', 'gitea'):
|
||||
result = run(['systemctl', 'is-enabled', service])
|
||||
if not result or result.returncode != 0:
|
||||
continue
|
||||
active = run(['systemctl', 'is-active', service])
|
||||
working = active is not None and active.stdout.strip() == 'active'
|
||||
check('Services', service, 'ok' if working else 'fail', 'active' if working else 'inactive')
|
||||
|
||||
groups = []
|
||||
for section in dict.fromkeys(c['section'] for c in checks):
|
||||
relevant = [c for c in checks if c['section'] == section]
|
||||
status = 'fail' if any(c['status'] == 'fail' for c in relevant) else 'warn' if any(c['status'] == 'warn' for c in relevant) else 'ok'
|
||||
groups.append(dict(name=section, status=status, missing=[]))
|
||||
counts = {s: sum(c['status'] == s for c in checks) for s in ('ok', 'warn', 'fail')}
|
||||
overall = 'fail' if counts['fail'] else 'warn' if counts['warn'] else 'ok'
|
||||
print(json.dumps(dict(schema=1, host=host, generatedAt=datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||
overall=overall, counts=counts, groups=groups, checks=checks, diagnostics=None)))
|
||||
Reference in New Issue
Block a user