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", "key": "ALL SERVERS: %lld OK · %lld WARN · %lld FAIL",
"location": { "location": {
"startingColumn": 18, "startingColumn": 18,
"startingLine": 76 "startingLine": 97
} }
}, },
{ {
@@ -42,12 +42,36 @@
"startingLine": 38 "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": "", "comment": "",
"key": "Refresh All", "key": "Refresh All",
"location": { "location": {
"startingColumn": 24, "startingColumn": 24,
"startingLine": 84 "startingLine": 105
} }
}, },
{ {
@@ -55,7 +79,7 @@
"key": "Diagnostics", "key": "Diagnostics",
"location": { "location": {
"startingColumn": 24, "startingColumn": 24,
"startingLine": 86 "startingLine": 107
} }
}, },
{ {
@@ -63,7 +87,7 @@
"key": "Settings", "key": "Settings",
"location": { "location": {
"startingColumn": 24, "startingColumn": 24,
"startingLine": 87 "startingLine": 108
} }
}, },
{ {
@@ -71,7 +95,7 @@
"key": "Quit", "key": "Quit",
"location": { "location": {
"startingColumn": 24, "startingColumn": 24,
"startingLine": 88 "startingLine": 109
} }
}, },
{ {
@@ -79,7 +103,7 @@
"key": "%@ — Diagnostics", "key": "%@ — Diagnostics",
"location": { "location": {
"startingColumn": 18, "startingColumn": 18,
"startingLine": 98 "startingLine": 119
} }
}, },
{ {
@@ -87,7 +111,7 @@
"key": "Select a server in the menu bar to change this report.", "key": "Select a server in the menu bar to change this report.",
"location": { "location": {
"startingColumn": 18, "startingColumn": 18,
"startingLine": 99 "startingLine": 120
} }
} }
] ]
@@ -7,7 +7,7 @@
"key": "Servers", "key": "Servers",
"location": { "location": {
"startingColumn": 22, "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.", "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": { "location": {
"startingColumn": 22, "startingColumn": 22,
"startingLine": 48 "startingLine": 52
} }
}, },
{ {
@@ -23,7 +23,7 @@
"key": "Add a server", "key": "Add a server",
"location": { "location": {
"startingColumn": 41, "startingColumn": 41,
"startingLine": 37 "startingLine": 39
} }
}, },
{ {
@@ -31,7 +31,7 @@
"key": "Connection details", "key": "Connection details",
"location": { "location": {
"startingColumn": 58, "startingColumn": 58,
"startingLine": 37 "startingLine": 39
} }
}, },
{ {
@@ -39,7 +39,7 @@
"key": "Display name", "key": "Display name",
"location": { "location": {
"startingColumn": 31, "startingColumn": 31,
"startingLine": 40 "startingLine": 42
} }
}, },
{ {
@@ -47,7 +47,7 @@
"key": "Host / SSH alias", "key": "Host / SSH alias",
"location": { "location": {
"startingColumn": 31, "startingColumn": 31,
"startingLine": 41 "startingLine": 43
} }
}, },
{ {
@@ -55,7 +55,7 @@
"key": "SSH username", "key": "SSH username",
"location": { "location": {
"startingColumn": 31, "startingColumn": 31,
"startingLine": 42 "startingLine": 44
} }
}, },
{ {
@@ -63,7 +63,7 @@
"key": "SSH port", "key": "SSH port",
"location": { "location": {
"startingColumn": 31, "startingColumn": 31,
"startingLine": 43 "startingLine": 45
} }
}, },
{ {
@@ -71,7 +71,7 @@
"key": "SSH key path (optional)", "key": "SSH key path (optional)",
"location": { "location": {
"startingColumn": 31, "startingColumn": 31,
"startingLine": 44 "startingLine": 46
} }
}, },
{ {
@@ -79,7 +79,7 @@
"key": "Remote health command", "key": "Remote health command",
"location": { "location": {
"startingColumn": 31, "startingColumn": 31,
"startingLine": 45 "startingLine": 47
} }
}, },
{ {
@@ -87,7 +87,15 @@
"key": "Monitoring enabled", "key": "Monitoring enabled",
"location": { "location": {
"startingColumn": 28, "startingColumn": 28,
"startingLine": 46 "startingLine": 48
}
},
{
"comment": "",
"key": "Automatically discover services",
"location": {
"startingColumn": 28,
"startingLine": 49
} }
}, },
{ {
@@ -95,7 +103,7 @@
"key": "New Server", "key": "New Server",
"location": { "location": {
"startingColumn": 28, "startingColumn": 28,
"startingLine": 52 "startingLine": 56
} }
}, },
{ {
@@ -103,7 +111,7 @@
"key": "Save and Refresh", "key": "Save and Refresh",
"location": { "location": {
"startingColumn": 28, "startingColumn": 28,
"startingLine": 54 "startingLine": 58
} }
}, },
{ {
@@ -111,7 +119,7 @@
"key": "Refresh every", "key": "Refresh every",
"location": { "location": {
"startingColumn": 28, "startingColumn": 28,
"startingLine": 59 "startingLine": 63
} }
}, },
{ {
@@ -119,7 +127,7 @@
"key": "Notifications", "key": "Notifications",
"location": { "location": {
"startingColumn": 28, "startingColumn": 28,
"startingLine": 64 "startingLine": 68
} }
}, },
{ {
@@ -127,7 +135,87 @@
"key": "%lld min", "key": "%lld min",
"location": { "location": {
"startingColumn": 34, "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 ae0397526b5e3c6e8848cdfd6a483288
4366908824f51bff885c44198535c243 4366908824f51bff885c44198535c243
4f303a1f6ed436c288335f2c2511f2f6 4f303a1f6ed436c288335f2c2511f2f6
7e38a5d8e664e612ef350f00745dc212
@@ -6,6 +6,48 @@
<integer>13</integer> <integer>13</integer>
<key>logs</key> <key>logs</key>
<dict> <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> <key>CC8DC4F3-DD53-4B42-8573-B118D88EBAB2</key>
<dict> <dict>
<key>className</key> <key>className</key>
@@ -48,48 +90,6 @@
<key>uniqueIdentifier</key> <key>uniqueIdentifier</key>
<string>CC8DC4F3-DD53-4B42-8573-B118D88EBAB2</string> <string>CC8DC4F3-DD53-4B42-8573-B118D88EBAB2</string>
</dict> </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>
</dict> </dict>
</plist> </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"> <plist version="1.0">
<dict> <dict>
<key>LastAccessedDate</key> <key>LastAccessedDate</key>
<date>2026-10-21T13:38:54Z</date> <date>2026-10-21T13:49:49Z</date>
<key>WorkspacePath</key> <key>WorkspacePath</key>
<string>/Volumes/Developer/Applications/PegasusMonitor/PegasusMonitor.xcodeproj</string> <string>/Volumes/Developer/Applications/PegasusMonitor/PegasusMonitor.xcodeproj</string>
</dict> </dict>
+4 -4
View File
@@ -166,7 +166,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 2; CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_TEAM = 39R6U3437D; DEVELOPMENT_TEAM = 39R6U3437D;
ENABLE_APP_SANDBOX = NO; ENABLE_APP_SANDBOX = NO;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -174,7 +174,7 @@
INFOPLIST_KEY_LSUIElement = YES; INFOPLIST_KEY_LSUIElement = YES;
INFOPLIST_KEY_NSPrincipalClass = NSApplication; INFOPLIST_KEY_NSPrincipalClass = NSApplication;
MACOSX_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.1.0; MARKETING_VERSION = 1.2.0;
PRODUCT_BUNDLE_IDENTIFIER = net.absdev.PegasusMonitor; PRODUCT_BUNDLE_IDENTIFIER = net.absdev.PegasusMonitor;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx; SDKROOT = macosx;
@@ -189,7 +189,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 2; CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_TEAM = 39R6U3437D; DEVELOPMENT_TEAM = 39R6U3437D;
ENABLE_APP_SANDBOX = NO; ENABLE_APP_SANDBOX = NO;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -197,7 +197,7 @@
INFOPLIST_KEY_LSUIElement = YES; INFOPLIST_KEY_LSUIElement = YES;
INFOPLIST_KEY_NSPrincipalClass = NSApplication; INFOPLIST_KEY_NSPrincipalClass = NSApplication;
MACOSX_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.1.0; MARKETING_VERSION = 1.2.0;
PRODUCT_BUNDLE_IDENTIFIER = net.absdev.PegasusMonitor; PRODUCT_BUNDLE_IDENTIFIER = net.absdev.PegasusMonitor;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx; SDKROOT = macosx;
+21
View File
@@ -55,6 +55,27 @@ struct MonitorMenuView: View {
ScrollView { ScrollView {
LazyVStack(alignment: .leading, spacing: 0) { LazyVStack(alignment: .leading, spacing: 0) {
if let report = monitor.report { 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 ForEach(report.groups) { group in
HStack { HStack {
Text(group.name).font(.system(size: 12, weight: .medium)) 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. // Real independent windows: editing cannot be dismissed by the menu-bar popover.
Window("Pegasus Monitor Settings", id: "monitor-settings") { Window("Pegasus Monitor Settings", id: "monitor-settings") {
SettingsView(monitor: monitor) 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") { Window("Server Diagnostics", id: "monitor-diagnostics") {
DiagnosticsView(monitor: monitor) DiagnosticsView(monitor: monitor)
+112 -1
View File
@@ -20,6 +20,47 @@ struct HealthReport: Codable {
let groups: [HealthGroup] let groups: [HealthGroup]
let checks: [HealthCheck] let checks: [HealthCheck]
let diagnostics: String? 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 { struct ServerProfile: Identifiable, Codable, Equatable {
@@ -31,6 +72,33 @@ struct ServerProfile: Identifiable, Codable, Equatable {
var keyFile: String = "" var keyFile: String = ""
var remoteCommand: String = "/usr/local/bin/pegasus-monitor-health-json" var remoteCommand: String = "/usr/local/bin/pegasus-monitor-health-json"
var enabled: Bool = true 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 { static func legacy() -> ServerProfile {
let defaults = UserDefaults.standard let defaults = UserDefaults.standard
@@ -84,6 +152,47 @@ final class ServerMonitor: ObservableObject {
var selected: ServerProfile? { servers.first(where: { $0.id == selectedID }) } var selected: ServerProfile? { servers.first(where: { $0.id == selectedID }) }
var selectedResult: ServerResult? { selectedID.flatMap { results[$0] } } var selectedResult: ServerResult? { selectedID.flatMap { results[$0] } }
var report: HealthReport? { selectedResult?.report } 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 { var status: HealthStatus {
let statuses = servers.filter(\.enabled).map { results[$0.id]?.status ?? .warn } let statuses = servers.filter(\.enabled).map { results[$0.id]?.status ?? .warn }
if statuses.contains(.fail) { return .fail } if statuses.contains(.fail) { return .fail }
@@ -106,6 +215,7 @@ final class ServerMonitor: ObservableObject {
func save(_ profile: ServerProfile) { func save(_ profile: ServerProfile) {
if let index = servers.firstIndex(where: { $0.id == profile.id }) { servers[index] = profile } if let index = servers.firstIndex(where: { $0.id == profile.id }) { servers[index] = profile }
else { servers.append(profile) } else { servers.append(profile) }
results.removeValue(forKey: profile.id)
persist() persist()
select(profile.id) select(profile.id)
Task { await refresh(id: profile.id) } Task { await refresh(id: profile.id) }
@@ -151,7 +261,8 @@ final class ServerMonitor: ObservableObject {
do { do {
let data = try await SSHRunner.run(host: server.host, user: server.user, port: server.port, let data = try await SSHRunner.run(host: server.host, user: server.user, port: server.port,
keyFile: server.keyFile, remoteCommand: server.remoteCommand) 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 } guard report.schema == 1 else { throw SSHError.invalidResponse }
state.report = report state.report = report
state.error = nil state.error = nil
+70
View File
@@ -7,6 +7,8 @@ struct SettingsView: View {
@State private var validation = "" @State private var validation = ""
@State private var interval = 5 @State private var interval = 5
@State private var notifications = true @State private var notifications = true
@State private var newRequiredService = ""
@State private var showUninstalled = false
var body: some View { var body: some View {
HStack(spacing: 0) { HStack(spacing: 0) {
@@ -44,7 +46,9 @@ struct SettingsView: View {
TextField("SSH key path (optional)", text: $draft.keyFile) TextField("SSH key path (optional)", text: $draft.keyFile)
TextField("Remote health command", text: $draft.remoteCommand) TextField("Remote health command", text: $draft.remoteCommand)
Toggle("Monitoring enabled", isOn: $draft.enabled) 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.") 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) .font(.caption).foregroundStyle(.secondary)
if !validation.isEmpty { Text(validation).foregroundStyle(.red).font(.caption) } if !validation.isEmpty { Text(validation).foregroundStyle(.red).font(.caption) }
@@ -65,6 +69,7 @@ struct SettingsView: View {
} }
} }
.padding(20) .padding(20)
.frame(minWidth: 460)
} }
.onAppear { .onAppear {
interval = monitor.intervalMinutes interval = monitor.intervalMinutes
@@ -75,15 +80,80 @@ struct SettingsView: View {
.onChange(of: editingID) { _, newValue in .onChange(of: editingID) { _, newValue in
if let id = newValue, let server = monitor.servers.first(where: { $0.id == id }) { draft = server } if let id = newValue, let server = monitor.servers.first(where: { $0.id == id }) { draft = server }
validation = "" validation = ""
newRequiredService = ""
} }
.onChange(of: interval) { _, value in monitor.intervalMinutes = value } .onChange(of: interval) { _, value in monitor.intervalMinutes = value }
.onChange(of: notifications) { _, value in monitor.notifications = 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() { private func newProfile() {
editingID = nil editingID = nil
draft = ServerProfile() draft = ServerProfile()
validation = "" validation = ""
newRequiredService = ""
} }
private func save() { private func save() {
guard !draft.name.trimmingCharacters(in: .whitespaces).isEmpty, 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 #!/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. Install as /usr/local/bin/pegasus-monitor-health-json and run as an ordinary user.
No third-party Python packages required. No third-party Python packages required.
""" """
@@ -64,13 +64,48 @@ if shutil.which('docker'):
else: else:
check('Docker', 'Docker access', 'warn', 'Docker is installed but unavailable to this SSH user') check('Docker', 'Docker access', 'warn', 'Docker is installed but unavailable to this SSH user')
for service in ('nginx', 'mysql', 'postgresql', 'redis-server', 'gitea'): # Discover common services without assuming they are installed or enabled.
result = run(['systemctl', 'is-enabled', service]) # Each state is explicit so optional missing software never counts as a failure.
if not result or result.returncode != 0: SERVICES = (
continue ('nginx', 'nginx'), ('mysql', 'mysql'), ('mariadb', 'mariadb'),
active = run(['systemctl', 'is-active', service]) ('postgresql', 'postgresql'), ('redis-server', 'redis-server'),
working = active is not None and active.stdout.strip() == 'active' ('gitea', 'gitea'), ('docker', 'docker'), ('containerd', 'containerd'),
check('Services', service, 'ok' if working else 'fail', 'active' if working else 'inactive') ('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 = [] groups = []
for section in dict.fromkeys(c['section'] for c in checks): 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')} 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' 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(), 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)))