updates
This commit is contained in:
@@ -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 = 3;
|
CURRENT_PROJECT_VERSION = 4;
|
||||||
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.2.0;
|
MARKETING_VERSION = 1.2.1;
|
||||||
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 = 3;
|
CURRENT_PROJECT_VERSION = 4;
|
||||||
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.2.0;
|
MARKETING_VERSION = 1.2.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = net.absdev.PegasusMonitor;
|
PRODUCT_BUNDLE_IDENTIFIER = net.absdev.PegasusMonitor;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
|
|||||||
@@ -35,7 +35,12 @@ struct MonitorMenuView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
if monitor.results[server.id]?.refreshing == true { ProgressView().controlSize(.mini) }
|
if monitor.results[server.id]?.refreshing == true { ProgressView().controlSize(.mini) }
|
||||||
else if let report = monitor.results[server.id]?.report {
|
else if let report = monitor.results[server.id]?.report {
|
||||||
Text("\(report.counts.ok) OK").font(.caption2).foregroundStyle(.secondary)
|
VStack(alignment: .trailing, spacing: 2) {
|
||||||
|
Text("\(report.counts.ok) OK").font(.caption2)
|
||||||
|
if let uptime = report.uptimeLabel {
|
||||||
|
Text("Up \(uptime)").font(.caption2)
|
||||||
|
}
|
||||||
|
}.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
if server.id == monitor.selectedID { Image(systemName: "checkmark") }
|
if server.id == monitor.selectedID { Image(systemName: "checkmark") }
|
||||||
}.padding(.horizontal, 14).padding(.vertical, 8)
|
}.padding(.horizontal, 14).padding(.vertical, 8)
|
||||||
@@ -55,6 +60,17 @@ 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 {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Image(systemName: "clock.arrow.circlepath")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Text("Uptime").font(.system(size: 12, weight: .medium))
|
||||||
|
Spacer()
|
||||||
|
Text(report.uptimeLabel ?? "Not available")
|
||||||
|
.font(.system(size: 12, weight: .medium, design: .monospaced))
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 14).padding(.vertical, 10)
|
||||||
|
Divider()
|
||||||
if !monitor.selectedServices.isEmpty {
|
if !monitor.selectedServices.isEmpty {
|
||||||
Text("SERVICES")
|
Text("SERVICES")
|
||||||
.font(.caption.bold()).foregroundStyle(.secondary)
|
.font(.caption.bold()).foregroundStyle(.secondary)
|
||||||
|
|||||||
@@ -15,6 +15,16 @@ struct HealthReport: Codable {
|
|||||||
let schema: Int
|
let schema: Int
|
||||||
let host: String?
|
let host: String?
|
||||||
let generatedAt: String?
|
let generatedAt: String?
|
||||||
|
let uptimeSeconds: Int?
|
||||||
|
var uptimeLabel: String? {
|
||||||
|
guard let uptimeSeconds, uptimeSeconds >= 0 else { return nil }
|
||||||
|
let days = uptimeSeconds / 86400
|
||||||
|
let hours = (uptimeSeconds % 86400) / 3600
|
||||||
|
let minutes = (uptimeSeconds % 3600) / 60
|
||||||
|
if days > 0 { return "\(days)d \(hours)h \(minutes)m" }
|
||||||
|
if hours > 0 { return "\(hours)h \(minutes)m" }
|
||||||
|
return "\(minutes)m"
|
||||||
|
}
|
||||||
let overall: HealthStatus
|
let overall: HealthStatus
|
||||||
let counts: HealthCounts
|
let counts: HealthCounts
|
||||||
let groups: [HealthGroup]
|
let groups: [HealthGroup]
|
||||||
@@ -190,7 +200,7 @@ final class ServerMonitor: ObservableObject {
|
|||||||
let checks = report.checks + extra
|
let checks = report.checks + extra
|
||||||
let counts = HealthCounts(ok: report.counts.ok, warn: report.counts.warn, fail: report.counts.fail + extra.count)
|
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))]
|
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,
|
return HealthReport(schema: report.schema, host: report.host, generatedAt: report.generatedAt, uptimeSeconds: report.uptimeSeconds, overall: .fail,
|
||||||
counts: counts, groups: groups, checks: checks, diagnostics: report.diagnostics, services: report.services)
|
counts: counts, groups: groups, checks: checks, diagnostics: report.diagnostics, services: report.services)
|
||||||
}
|
}
|
||||||
var status: HealthStatus {
|
var status: HealthStatus {
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Pegasus Monitor 1.2.1 (Build 4) — Server uptime
|
||||||
|
|
||||||
|
- Display Linux server uptime beneath each server's health count in the menu-bar server list.
|
||||||
|
- Show an **Uptime** row in the selected-server overview.
|
||||||
|
- Use compact days, hours and minutes; for example `12d 4h 8m`.
|
||||||
|
- Include `uptimeSeconds` in JSON from both `server/remote-health-json` and `server/pegasus-health-json`, obtained from `/proc/uptime` without elevated privileges.
|
||||||
|
- Remain compatible with older monitoring agents: uptime shows **Not available** until the agent is updated.
|
||||||
|
- Preserve 1.2.0 service discovery, required service rules, configuration, signing and notarization scripts.
|
||||||
|
|
||||||
|
## Deploy the updated agent
|
||||||
|
|
||||||
|
For ordinary Ubuntu/Debian servers:
|
||||||
|
|
||||||
|
```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'
|
||||||
|
```
|
||||||
|
|
||||||
|
For Pegasus's specialised agent:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
scp server/pegasus-health-json mag@192.168.1.2:/tmp/pegasus-health-json
|
||||||
|
ssh mag@192.168.1.2 'sudo install -m 755 /tmp/pegasus-health-json /usr/local/bin/pegasus-health-json'
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep the original SSH command configuration for each server. Refresh the Mac application after agent updates.
|
||||||
@@ -2,6 +2,15 @@
|
|||||||
"""Read-only JSON adapter for an existing pegasus-health --verbose command."""
|
"""Read-only JSON adapter for an existing pegasus-health --verbose command."""
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
def system_uptime_seconds():
|
||||||
|
"""Read Linux kernel uptime without privilege or external commands."""
|
||||||
|
try:
|
||||||
|
with open('/proc/uptime', encoding='ascii') as handle:
|
||||||
|
return max(0, int(float(handle.read().split()[0])))
|
||||||
|
except (OSError, ValueError, IndexError):
|
||||||
|
return None
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
@@ -56,7 +65,7 @@ def output():
|
|||||||
if not checks:
|
if not checks:
|
||||||
counts['fail'] = max(1, counts['fail'])
|
counts['fail'] = max(1, counts['fail'])
|
||||||
overall = 'fail' if counts['fail'] or any(g['status']=='fail' for g in groups) else 'warn' if counts['warn'] or any(g['status']=='warn' for g in groups) else 'ok'
|
overall = 'fail' if counts['fail'] or any(g['status']=='fail' for g in groups) else 'warn' if counts['warn'] or any(g['status']=='warn' for g in groups) else 'ok'
|
||||||
print(json.dumps({'schema':1, 'host':'pegasus', 'generatedAt':datetime.datetime.now(datetime.timezone.utc).isoformat(), 'overall':overall, 'counts':counts,'groups':groups,'checks':checks, 'diagnostics':plain[-24000:], 'commandExitCode':p.returncode}, ensure_ascii=False))
|
print(json.dumps({'schema':1, 'host':'pegasus', 'uptimeSeconds':system_uptime_seconds(), 'generatedAt':datetime.datetime.now(datetime.timezone.utc).isoformat(), 'overall':overall, 'counts':counts,'groups':groups,'checks':checks, 'diagnostics':plain[-24000:], 'commandExitCode':p.returncode}, ensure_ascii=False))
|
||||||
return {'ok':0,'warn':1,'fail':2}[overall]
|
return {'ok':0,'warn':1,'fail':2}[overall]
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -5,6 +5,15 @@ No third-party Python packages required.
|
|||||||
"""
|
"""
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
def system_uptime_seconds():
|
||||||
|
"""Read Linux kernel uptime without privilege or external commands."""
|
||||||
|
try:
|
||||||
|
with open('/proc/uptime', encoding='ascii') as handle:
|
||||||
|
return max(0, int(float(handle.read().split()[0])))
|
||||||
|
except (OSError, ValueError, IndexError):
|
||||||
|
return None
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import socket
|
import socket
|
||||||
@@ -115,4 +124,5 @@ 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, services=services)))
|
overall=overall, counts=counts, groups=groups, checks=checks, diagnostics=None, services=services,
|
||||||
|
uptimeSeconds=system_uptime_seconds())))
|
||||||
|
|||||||
Reference in New Issue
Block a user