This commit is contained in:
Mark Gregory
2026-10-21 14:50:05 +01:00
parent 90987bd898
commit 9e501ca176
44 changed files with 461 additions and 222 deletions
@@ -15,7 +15,7 @@
"key": "ALL SERVERS: %lld OK · %lld WARN · %lld FAIL",
"location": {
"startingColumn": 18,
"startingLine": 76
"startingLine": 97
}
},
{
@@ -42,12 +42,36 @@
"startingLine": 38
}
},
{
"comment": "",
"key": "SERVICES",
"location": {
"startingColumn": 38,
"startingLine": 59
}
},
{
"comment": "",
"key": "HEALTH GROUPS",
"location": {
"startingColumn": 38,
"startingLine": 75
}
},
{
"comment": "",
"key": "REQUIRED",
"location": {
"startingColumn": 50,
"startingLine": 68
}
},
{
"comment": "",
"key": "Refresh All",
"location": {
"startingColumn": 24,
"startingLine": 84
"startingLine": 105
}
},
{
@@ -55,7 +79,7 @@
"key": "Diagnostics",
"location": {
"startingColumn": 24,
"startingLine": 86
"startingLine": 107
}
},
{
@@ -63,7 +87,7 @@
"key": "Settings",
"location": {
"startingColumn": 24,
"startingLine": 87
"startingLine": 108
}
},
{
@@ -71,7 +95,7 @@
"key": "Quit",
"location": {
"startingColumn": 24,
"startingLine": 88
"startingLine": 109
}
},
{
@@ -79,7 +103,7 @@
"key": "%@ — Diagnostics",
"location": {
"startingColumn": 18,
"startingLine": 98
"startingLine": 119
}
},
{
@@ -87,7 +111,7 @@
"key": "Select a server in the menu bar to change this report.",
"location": {
"startingColumn": 18,
"startingLine": 99
"startingLine": 120
}
}
]
@@ -7,7 +7,7 @@
"key": "Servers",
"location": {
"startingColumn": 22,
"startingLine": 14
"startingLine": 16
}
},
{
@@ -15,7 +15,7 @@
"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": 22,
"startingLine": 48
"startingLine": 52
}
},
{
@@ -23,7 +23,7 @@
"key": "Add a server",
"location": {
"startingColumn": 41,
"startingLine": 37
"startingLine": 39
}
},
{
@@ -31,7 +31,7 @@
"key": "Connection details",
"location": {
"startingColumn": 58,
"startingLine": 37
"startingLine": 39
}
},
{
@@ -39,7 +39,7 @@
"key": "Display name",
"location": {
"startingColumn": 31,
"startingLine": 40
"startingLine": 42
}
},
{
@@ -47,7 +47,7 @@
"key": "Host / SSH alias",
"location": {
"startingColumn": 31,
"startingLine": 41
"startingLine": 43
}
},
{
@@ -55,7 +55,7 @@
"key": "SSH username",
"location": {
"startingColumn": 31,
"startingLine": 42
"startingLine": 44
}
},
{
@@ -63,7 +63,7 @@
"key": "SSH port",
"location": {
"startingColumn": 31,
"startingLine": 43
"startingLine": 45
}
},
{
@@ -71,7 +71,7 @@
"key": "SSH key path (optional)",
"location": {
"startingColumn": 31,
"startingLine": 44
"startingLine": 46
}
},
{
@@ -79,7 +79,7 @@
"key": "Remote health command",
"location": {
"startingColumn": 31,
"startingLine": 45
"startingLine": 47
}
},
{
@@ -87,7 +87,15 @@
"key": "Monitoring enabled",
"location": {
"startingColumn": 28,
"startingLine": 46
"startingLine": 48
}
},
{
"comment": "",
"key": "Automatically discover services",
"location": {
"startingColumn": 28,
"startingLine": 49
}
},
{
@@ -95,7 +103,7 @@
"key": "New Server",
"location": {
"startingColumn": 28,
"startingLine": 52
"startingLine": 56
}
},
{
@@ -103,7 +111,7 @@
"key": "Save and Refresh",
"location": {
"startingColumn": 28,
"startingLine": 54
"startingLine": 58
}
},
{
@@ -111,7 +119,7 @@
"key": "Refresh every",
"location": {
"startingColumn": 28,
"startingLine": 59
"startingLine": 63
}
},
{
@@ -119,7 +127,7 @@
"key": "Notifications",
"location": {
"startingColumn": 28,
"startingLine": 64
"startingLine": 68
}
},
{
@@ -127,7 +135,87 @@
"key": "%lld min",
"location": {
"startingColumn": 34,
"startingLine": 61
"startingLine": 65
}
},
{
"comment": "",
"key": "Mark critical services as Required. Missing or non-running required services count as failures. Optional services not installed do not affect health.",
"location": {
"startingColumn": 18,
"startingLine": 98
}
},
{
"comment": "",
"key": "No service discovery data yet. Refresh the server or add a required service below.",
"location": {
"startingColumn": 22,
"startingLine": 105
}
},
{
"comment": "",
"key": "Required: %@",
"location": {
"startingColumn": 22,
"startingLine": 132
}
},
{
"comment": "",
"key": "Show not installed services",
"location": {
"startingColumn": 20,
"startingLine": 100
}
},
{
"comment": "",
"key": "Service monitoring",
"location": {
"startingColumn": 22,
"startingLine": 92
}
},
{
"comment": "",
"key": "Refresh services",
"location": {
"startingColumn": 24,
"startingLine": 94
}
},
{
"comment": "",
"key": "Required",
"location": {
"startingColumn": 40,
"startingLine": 115
}
},
{
"comment": "",
"key": "Require %@ to be running",
"location": {
"startingColumn": 56,
"startingLine": 118
}
},
{
"comment": "",
"key": "Add required service (e.g. mysql)",
"location": {
"startingColumn": 27,
"startingLine": 126
}
},
{
"comment": "",
"key": "Add Required",
"location": {
"startingColumn": 24,
"startingLine": 128
}
}
]
@@ -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"
}
}
@@ -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>
@@ -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
@@ -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 +0,0 @@
["AnyResolverProviding","AppEntity","AppEnum","AppExtension","AppIntent","AppIntentsPackage","AppShortcutProviding","AppShortcutsProvider","AppUnionValue","AppUnionValueCasesProviding","DynamicOptionsProvider","EntityQuery","ExtensionPointDefining","IntentValueQuery","Resolver","TransientEntity","_AssistantIntentsProvider","_GenerativeFunctionExtractable","_IntentValueRepresentable"]
@@ -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
@@ -1,68 +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",
"COLOR_DIAGNOSTICS" : "YES",
"diagnostic_message_length" : "162",
"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
}
@@ -1,2 +0,0 @@
Target dependency graph (1 target)
Target 'PegasusMonitor' in project 'PegasusMonitor' (no dependencies)
@@ -5,3 +5,4 @@ f917cf82bd848fc97291bd609bd414d6
ae0397526b5e3c6e8848cdfd6a483288
4366908824f51bff885c44198535c243
4f303a1f6ed436c288335f2c2511f2f6
7e38a5d8e664e612ef350f00745dc212
@@ -6,6 +6,48 @@
<integer>13</integer>
<key>logs</key>
<dict>
<key>0775894F-A03B-46F0-8D09-D39D1E301BA4</key>
<dict>
<key>className</key>
<string>IDECommandLineBuildLog</string>
<key>documentTypeString</key>
<string>&lt;nil&gt;</string>
<key>domainType</key>
<string>Xcode.IDEActivityLogDomainType.BuildLog</string>
<key>fileName</key>
<string>0775894F-A03B-46F0-8D09-D39D1E301BA4.xcactivitylog</string>
<key>hasPrimaryLog</key>
<true/>
<key>primaryObservable</key>
<dict>
<key>highLevelStatus</key>
<string>W</string>
<key>totalNumberOfAnalyzerIssues</key>
<integer>0</integer>
<key>totalNumberOfErrors</key>
<integer>0</integer>
<key>totalNumberOfTestFailures</key>
<integer>0</integer>
<key>totalNumberOfWarnings</key>
<integer>2</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>814283389.12080395</real>
<key>timeStoppedRecording</key>
<real>814283396.01673496</real>
<key>title</key>
<string>Building project PegasusMonitor with scheme PegasusMonitor and configuration Release</string>
<key>uniqueIdentifier</key>
<string>0775894F-A03B-46F0-8D09-D39D1E301BA4</string>
</dict>
<key>CC8DC4F3-DD53-4B42-8573-B118D88EBAB2</key>
<dict>
<key>className</key>
@@ -48,48 +90,6 @@
<key>uniqueIdentifier</key>
<string>CC8DC4F3-DD53-4B42-8573-B118D88EBAB2</string>
</dict>
<key>E1DE6BCB-D5CB-4252-BF49-D92793CE0E02</key>
<dict>
<key>className</key>
<string>IDECommandLineBuildLog</string>
<key>documentTypeString</key>
<string>&lt;nil&gt;</string>
<key>domainType</key>
<string>Xcode.IDEActivityLogDomainType.BuildLog</string>
<key>fileName</key>
<string>E1DE6BCB-D5CB-4252-BF49-D92793CE0E02.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>814281866.56225002</real>
<key>timeStoppedRecording</key>
<real>814281867.39492798</real>
<key>title</key>
<string>Building project PegasusMonitor with scheme PegasusMonitor and configuration Release</string>
<key>uniqueIdentifier</key>
<string>E1DE6BCB-D5CB-4252-BF49-D92793CE0E02</string>
</dict>
</dict>
</dict>
</plist>
@@ -1 +1 @@
1792589935.5735621: Module build session file for module cache at Path(str: "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/ModuleCache.noindex")
1792590590.976388: Module build session file for module cache at Path(str: "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/ModuleCache.noindex")
+1 -1
View File
@@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>LastAccessedDate</key>
<date>2026-10-21T13:38:54Z</date>
<date>2026-10-21T13:49:49Z</date>
<key>WorkspacePath</key>
<string>/Volumes/Developer/Applications/PegasusMonitor/PegasusMonitor.xcodeproj</string>
</dict>
+4 -4
View File
@@ -166,7 +166,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 2;
CURRENT_PROJECT_VERSION = 3;
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.1.0;
MARKETING_VERSION = 1.2.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 = 2;
CURRENT_PROJECT_VERSION = 3;
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.1.0;
MARKETING_VERSION = 1.2.0;
PRODUCT_BUNDLE_IDENTIFIER = net.absdev.PegasusMonitor;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
+21
View File
@@ -55,6 +55,27 @@ struct MonitorMenuView: View {
ScrollView {
LazyVStack(alignment: .leading, spacing: 0) {
if let report = monitor.report {
if !monitor.selectedServices.isEmpty {
Text("SERVICES")
.font(.caption.bold()).foregroundStyle(.secondary)
.padding(.horizontal, 14).padding(.vertical, 8)
ForEach(monitor.selectedServices) { service in
HStack(spacing: 8) {
Image(systemName: service.symbol).foregroundStyle(service.color)
Text(service.name).font(.system(size: 12))
Spacer()
if selected.requiredServices.contains(service.name) {
Text("REQUIRED").font(.system(size: 9, weight: .bold)).foregroundStyle(.orange)
}
Text(service.title).font(.caption).foregroundStyle(.secondary)
}
.padding(.horizontal, 14).padding(.vertical, 5)
}
Divider()
Text("HEALTH GROUPS")
.font(.caption.bold()).foregroundStyle(.secondary)
.padding(.horizontal, 14).padding(.vertical, 8)
}
ForEach(report.groups) { group in
HStack {
Text(group.name).font(.system(size: 12, weight: .medium))
+2 -2
View File
@@ -18,9 +18,9 @@ struct PegasusMonitorApp: App {
// Real independent windows: editing cannot be dismissed by the menu-bar popover.
Window("Pegasus Monitor Settings", id: "monitor-settings") {
SettingsView(monitor: monitor)
.frame(minWidth: 630, minHeight: 470)
.frame(minWidth: 780, minHeight: 640)
}
.defaultSize(width: 750, height: 570)
.defaultSize(width: 880, height: 750)
Window("Server Diagnostics", id: "monitor-diagnostics") {
DiagnosticsView(monitor: monitor)
+112 -1
View File
@@ -20,6 +20,47 @@ struct HealthReport: Codable {
let groups: [HealthGroup]
let checks: [HealthCheck]
let diagnostics: String?
let services: [DiscoveredService]?
}
// Service discovery is optional so legacy Pegasus health JSON stays compatible.
struct DiscoveredService: Codable, Identifiable, Equatable {
let name: String
let state: String
let detail: String
var id: String { name }
var isInstalled: Bool { state != "not_installed" }
var isHealthy: Bool { state == "running" }
var symbol: String {
switch state {
case "running": "checkmark.circle.fill"
case "failed", "required_missing": "xmark.circle.fill"
case "stopped": "exclamationmark.triangle.fill"
case "disabled": "pause.circle"
case "not_installed": "minus.circle"
default: "questionmark.circle"
}
}
var color: Color {
switch state {
case "running": .green
case "failed", "required_missing": .red
case "stopped": .orange
default: .secondary
}
}
var title: String {
switch state {
case "running": "Running"
case "failed": "Failed"
case "stopped": "Stopped"
case "disabled": "Disabled"
case "not_installed": "Not installed"
case "required_missing": "Required — missing"
default: "Unknown"
}
}
}
struct ServerProfile: Identifiable, Codable, Equatable {
@@ -31,6 +72,33 @@ struct ServerProfile: Identifiable, Codable, Equatable {
var keyFile: String = ""
var remoteCommand: String = "/usr/local/bin/pegasus-monitor-health-json"
var enabled: Bool = true
var requiredServices: [String] = []
var discoverServices: Bool = true
// Preserve profiles saved by 1.1, which did not include service preferences.
enum CodingKeys: String, CodingKey {
case id, name, host, user, port, keyFile, remoteCommand, enabled, requiredServices, discoverServices
}
init(id: UUID = UUID(), name: String = "New Server", host: String = "", user: String = "", port: String = "22",
keyFile: String = "", remoteCommand: String = "/usr/local/bin/pegasus-monitor-health-json",
enabled: Bool = true, requiredServices: [String] = [], discoverServices: Bool = true) {
self.id = id; self.name = name; self.host = host; self.user = user; self.port = port
self.keyFile = keyFile; self.remoteCommand = remoteCommand; self.enabled = enabled
self.requiredServices = requiredServices; self.discoverServices = discoverServices
}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
id = try c.decode(UUID.self, forKey: .id)
name = try c.decode(String.self, forKey: .name)
host = try c.decode(String.self, forKey: .host)
user = try c.decode(String.self, forKey: .user)
port = try c.decode(String.self, forKey: .port)
keyFile = try c.decode(String.self, forKey: .keyFile)
remoteCommand = try c.decode(String.self, forKey: .remoteCommand)
enabled = try c.decode(Bool.self, forKey: .enabled)
requiredServices = try c.decodeIfPresent([String].self, forKey: .requiredServices) ?? []
discoverServices = try c.decodeIfPresent(Bool.self, forKey: .discoverServices) ?? true
}
static func legacy() -> ServerProfile {
let defaults = UserDefaults.standard
@@ -84,6 +152,47 @@ final class ServerMonitor: ObservableObject {
var selected: ServerProfile? { servers.first(where: { $0.id == selectedID }) }
var selectedResult: ServerResult? { selectedID.flatMap { results[$0] } }
var report: HealthReport? { selectedResult?.report }
var selectedServices: [DiscoveredService] {
guard let profile = selected else { return [] }
return discoveredServices(for: profile.id).filter { profile.requiredServices.contains($0.name) || (profile.discoverServices && $0.state != "not_installed") }
}
func discoveredServices(for id: UUID) -> [DiscoveredService] {
guard let profile = servers.first(where: { $0.id == id }) else { return [] }
let discovered = results[id]?.report?.services ?? []
var byName = Dictionary(uniqueKeysWithValues: discovered.map { ($0.name, $0) })
for name in profile.requiredServices where byName[name] == nil {
byName[name] = DiscoveredService(name: name, state: "not_installed", detail: "Not reported by agent")
}
return byName.values.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }
}
func serviceStatus(_ item: DiscoveredService, required: Bool) -> HealthStatus? {
if required && !item.isHealthy { return .fail }
if item.state == "failed" { return .fail }
if item.state == "stopped" { return .warn }
return nil
}
func applyServicePolicy(_ report: HealthReport, for profile: ServerProfile) -> HealthReport {
let services = report.services ?? []
// If an older agent has no service discovery, do not invent a healthy state.
// Explicit required services with no reported data are a failure.
let known = Dictionary(uniqueKeysWithValues: services.map { ($0.name, $0) })
var extra: [HealthCheck] = []
for required in Set(profile.requiredServices) {
let item = known[required]
if item?.isHealthy != true {
let description = item?.title ?? "Not reported by monitoring agent"
extra.append(HealthCheck(name: "Required: \(required)", status: .fail, detail: description, section: "Required services"))
}
}
let existing = Set(report.checks.map { $0.id })
extra.removeAll { existing.contains($0.id) }
guard !extra.isEmpty else { return report }
let checks = report.checks + extra
let counts = HealthCounts(ok: report.counts.ok, warn: report.counts.warn, fail: report.counts.fail + extra.count)
let groups = report.groups + [HealthGroup(name: "Required services", status: .fail, missing: extra.map(\.name))]
return HealthReport(schema: report.schema, host: report.host, generatedAt: report.generatedAt, overall: .fail,
counts: counts, groups: groups, checks: checks, diagnostics: report.diagnostics, services: report.services)
}
var status: HealthStatus {
let statuses = servers.filter(\.enabled).map { results[$0.id]?.status ?? .warn }
if statuses.contains(.fail) { return .fail }
@@ -106,6 +215,7 @@ final class ServerMonitor: ObservableObject {
func save(_ profile: ServerProfile) {
if let index = servers.firstIndex(where: { $0.id == profile.id }) { servers[index] = profile }
else { servers.append(profile) }
results.removeValue(forKey: profile.id)
persist()
select(profile.id)
Task { await refresh(id: profile.id) }
@@ -151,7 +261,8 @@ final class ServerMonitor: ObservableObject {
do {
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)
let decoded = try JSONDecoder().decode(HealthReport.self, from: data)
let report = applyServicePolicy(decoded, for: server)
guard report.schema == 1 else { throw SSHError.invalidResponse }
state.report = report
state.error = nil
+70
View File
@@ -7,6 +7,8 @@ struct SettingsView: View {
@State private var validation = ""
@State private var interval = 5
@State private var notifications = true
@State private var newRequiredService = ""
@State private var showUninstalled = false
var body: some View {
HStack(spacing: 0) {
@@ -44,7 +46,9 @@ struct SettingsView: View {
TextField("SSH key path (optional)", text: $draft.keyFile)
TextField("Remote health command", text: $draft.remoteCommand)
Toggle("Monitoring enabled", isOn: $draft.enabled)
Toggle("Automatically discover services", isOn: $draft.discoverServices)
}
serviceConfiguration
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) }
@@ -65,6 +69,7 @@ struct SettingsView: View {
}
}
.padding(20)
.frame(minWidth: 460)
}
.onAppear {
interval = monitor.intervalMinutes
@@ -75,15 +80,80 @@ struct SettingsView: View {
.onChange(of: editingID) { _, newValue in
if let id = newValue, let server = monitor.servers.first(where: { $0.id == id }) { draft = server }
validation = ""
newRequiredService = ""
}
.onChange(of: interval) { _, value in monitor.intervalMinutes = value }
.onChange(of: notifications) { _, value in monitor.notifications = value }
}
private var serviceConfiguration: some View {
VStack(alignment: .leading, spacing: 10) {
HStack {
Text("Service monitoring").font(.headline)
Spacer()
Button("Refresh services") {
if let id = editingID { Task { await monitor.refresh(id: id) } }
}.disabled(editingID == nil)
}
Text("Mark critical services as Required. Missing or non-running required services count as failures. Optional services not installed do not affect health.")
.font(.caption).foregroundStyle(.secondary)
Toggle("Show not installed services", isOn: $showUninstalled)
.font(.caption)
let discovered = editingID.map { monitor.discoveredServices(for: $0) } ?? []
let display = discovered.filter { showUninstalled || $0.isInstalled || draft.requiredServices.contains($0.name) }
if display.isEmpty {
Text("No service discovery data yet. Refresh the server or add a required service below.")
.font(.caption).foregroundStyle(.secondary)
} else {
ScrollView {
LazyVStack(spacing: 6) {
ForEach(display) { service in
HStack(spacing: 8) {
Image(systemName: service.symbol).foregroundStyle(service.color).frame(width: 20)
Text(service.name).frame(maxWidth: .infinity, alignment: .leading)
Text(service.title).font(.caption).foregroundStyle(.secondary)
Toggle("Required", isOn: Binding(
get: { draft.requiredServices.contains(service.name) },
set: { required in setRequired(service.name, required: required) }
)).labelsHidden().help("Require \(service.name) to be running")
}
.font(.system(size: 12))
}
}
}.frame(maxHeight: 190)
}
HStack {
TextField("Add required service (e.g. mysql)", text: $newRequiredService)
.onSubmit { addRequiredService() }
Button("Add Required") { addRequiredService() }
.disabled(newRequiredService.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
}
if !draft.requiredServices.isEmpty {
Text("Required: \(draft.requiredServices.sorted().joined(separator: ", "))")
.font(.caption).foregroundStyle(.secondary)
}
}
}
private func setRequired(_ name: String, required: Bool) {
if required && !draft.requiredServices.contains(name) { draft.requiredServices.append(name) }
if !required { draft.requiredServices.removeAll { $0 == name } }
}
private func addRequiredService() {
let name = newRequiredService.trimmingCharacters(in: .whitespacesAndNewlines)
guard !name.isEmpty, name.range(of: #"^[a-zA-Z0-9@_.-]+$"#, options: .regularExpression) != nil else {
validation = "Use a valid systemd service name (letters, digits, @, _, -, or .)."
return
}
setRequired(name, required: true)
newRequiredService = ""
validation = ""
}
private func newProfile() {
editingID = nil
draft = ServerProfile()
validation = ""
newRequiredService = ""
}
private func save() {
guard !draft.name.trimmingCharacters(in: .whitespaces).isEmpty,
+28
View File
@@ -0,0 +1,28 @@
# Pegasus Monitor 1.2.0 (build 3)
## Changes
- Generic Linux agent reports discovered systemd service states: running, stopped, failed, disabled, not installed, and unknown.
- Optional services not installed do not count as failures.
- Per-server **Required** service toggles; any required service not reported as running is a failure.
- Service list in the menu bar, and discovery controls in the independent Settings window.
- Required services persist in existing server profiles; profiles from 1.1 migrate without losing connections.
- Manually add required units if no agent discovery has been performed.
- Existing Pegasus specialised agent and its health checks remain supported (service discovery requires updated generic agent).
## Deploy updated Linux agent
From the macOS project folder, replace `mag@REMOTE_HOST` with the SSH login for the target server:
```sh
scp server/remote-health-json mag@REMOTE_HOST:/tmp/pegasus-monitor-health-json
ssh mag@REMOTE_HOST 'sudo install -m 755 /tmp/pegasus-monitor-health-json /usr/local/bin/pegasus-monitor-health-json'
ssh mag@REMOTE_HOST '/usr/local/bin/pegasus-monitor-health-json'
```
In the Settings window, point the server's remote command to `/usr/local/bin/pegasus-monitor-health-json`, refresh services and save required services. Keep Pegasus's original `sudo -n /usr/local/bin/pegasus-health-json` for its 72 specialised checks; it does not report the new per-service inventory.
## Safety and expected behaviour
The Linux health agent is read-only; it does not install, start or stop services. Service discovery uses `systemctl show` on an allowlisted collection of commonly-used units. `unknown` indicates discovery could not establish a state and is **not** treated as installed or running. Required services that are missing, disabled, stopped, failed, unknown or unreported count as failures.
Apple notarization is only attempted by the release script on macOS with a valid configured `orion-notary` profile. No notarized DMG is included here.
+44 -9
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Generic read-only Linux health adapter for Pegasus Monitor 1.1.
"""Generic read-only Linux health adapter for Pegasus Monitor 1.2.
Install as /usr/local/bin/pegasus-monitor-health-json and run as an ordinary user.
No third-party Python packages required.
"""
@@ -64,13 +64,48 @@ if shutil.which('docker'):
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')
# Discover common services without assuming they are installed or enabled.
# Each state is explicit so optional missing software never counts as a failure.
SERVICES = (
('nginx', 'nginx'), ('mysql', 'mysql'), ('mariadb', 'mariadb'),
('postgresql', 'postgresql'), ('redis-server', 'redis-server'),
('gitea', 'gitea'), ('docker', 'docker'), ('containerd', 'containerd'),
('php8.3-fpm', 'php8.3-fpm'), ('php8.4-fpm', 'php8.4-fpm'),
('php8.5-fpm', 'php8.5-fpm'), ('ssh', 'ssh'), ('cloudflared', 'cloudflared'),
('cockpit.socket', 'cockpit.socket'), ('libvirtd', 'libvirtd'),
)
services = []
for display, unit in SERVICES:
full_unit = unit if unit.endswith(('.service', '.socket')) else unit + '.service'
result = run(['systemctl', 'show', full_unit, '--no-pager',
'--property=LoadState', '--property=ActiveState',
'--property=UnitFileState'], timeout=5)
if result is None or not result.stdout.strip():
state, detail = 'unknown', 'systemctl unavailable'
else:
properties = dict(line.split('=', 1) for line in result.stdout.splitlines() if '=' in line)
load = properties.get('LoadState', 'not-found')
active = properties.get('ActiveState', 'unknown')
unit_state = properties.get('UnitFileState', 'unknown')
detail = f'Load: {load}; active: {active}; enablement: {unit_state}'
if load == 'not-found':
state = 'not_installed'
elif active == 'active':
state = 'running'
elif active == 'failed':
state = 'failed'
elif unit_state in ('disabled', 'masked'):
state = 'disabled'
elif active in ('inactive', 'deactivating'):
state = 'stopped'
else:
state = 'unknown'
services.append(dict(name=display, state=state, detail=detail))
# Uninstalled/disabled services are informational, not warnings or failures.
if state == 'failed':
check('Services', display, 'fail', detail)
elif state == 'stopped':
check('Services', display, 'warn', detail)
groups = []
for section in dict.fromkeys(c['section'] for c in checks):
@@ -80,4 +115,4 @@ for section in dict.fromkeys(c['section'] for c in checks):
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)))
overall=overall, counts=counts, groups=groups, checks=checks, diagnostics=None, services=services)))