Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b7747db5c | |||
| d3af580f07 |
@@ -5,6 +5,9 @@
|
|||||||
- Added persisted metric history with SwiftData for CPU, memory, disk, and swap charts.
|
- Added persisted metric history with SwiftData for CPU, memory, disk, and swap charts.
|
||||||
- Added `Hour`, `Day`, `Week`, and `Month` ranges to summary charts.
|
- Added `Hour`, `Day`, `Week`, and `Month` ranges to summary charts.
|
||||||
- Added CPU, memory, disk, and swap history widgets that expand across the available summary width.
|
- Added CPU, memory, disk, and swap history widgets that expand across the available summary width.
|
||||||
|
- Added a configurable alert grace period in Preferences → Notifications.
|
||||||
|
- Delayed offline host and service alerts until the configured grace period has elapsed, while keeping recovery notifications immediate.
|
||||||
|
- Added reboot-required state to host status indicators by turning online markers yellow and showing an explanatory tooltip.
|
||||||
- Reworked `General` to remain the more traditional detailed information tab while `Summary` focuses on quick status and trends.
|
- Reworked `General` to remain the more traditional detailed information tab while `Summary` focuses on quick status and trends.
|
||||||
- Isolated metric history into an app-specific SwiftData store and recover cleanly from incompatible history stores.
|
- Isolated metric history into an app-specific SwiftData store and recover cleanly from incompatible history stores.
|
||||||
- Fixed the summary CPU chart to use the summary payload's reported CPU percentage and allow values above `100%` with a dynamic Y axis.
|
- Fixed the summary CPU chart to use the summary payload's reported CPU percentage and allow values above `100%` with a dynamic Y axis.
|
||||||
|
|||||||
@@ -50,7 +50,12 @@ enum PingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static func handlePingFailure(for hostname: String, notificationsEnabled: Bool) async {
|
private static func handlePingFailure(for hostname: String, notificationsEnabled: Bool) async {
|
||||||
if let notification = await stateStore.recordFailure(for: hostname, notificationsEnabled: notificationsEnabled) {
|
let alertGracePeriod = UserDefaults.standard.integer(forKey: "alertGracePeriod")
|
||||||
|
if let notification = await stateStore.recordFailure(
|
||||||
|
for: hostname,
|
||||||
|
notificationsEnabled: notificationsEnabled,
|
||||||
|
gracePeriod: TimeInterval(alertGracePeriod)
|
||||||
|
) {
|
||||||
sendNotification(title: notification.title, body: notification.body)
|
sendNotification(title: notification.title, body: notification.body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,10 +74,12 @@ enum PingService {
|
|||||||
private actor PingStateStore {
|
private actor PingStateStore {
|
||||||
private var previousPingStates: [String: Bool] = [:]
|
private var previousPingStates: [String: Bool] = [:]
|
||||||
private var suppressedUntil: [String: Date] = [:]
|
private var suppressedUntil: [String: Date] = [:]
|
||||||
|
private var failureStartedAt: [String: Date] = [:]
|
||||||
|
|
||||||
func suppressChecks(for hostname: String, duration: TimeInterval) {
|
func suppressChecks(for hostname: String, duration: TimeInterval) {
|
||||||
suppressedUntil[hostname] = Date().addingTimeInterval(duration)
|
suppressedUntil[hostname] = Date().addingTimeInterval(duration)
|
||||||
previousPingStates[hostname] = false
|
previousPingStates[hostname] = false
|
||||||
|
failureStartedAt.removeValue(forKey: hostname)
|
||||||
}
|
}
|
||||||
|
|
||||||
func shouldSkipPing(for hostname: String) -> Bool {
|
func shouldSkipPing(for hostname: String) -> Bool {
|
||||||
@@ -87,6 +94,7 @@ private actor PingStateStore {
|
|||||||
func recordSuccess(for hostname: String, notificationsEnabled: Bool) -> PingNotification? {
|
func recordSuccess(for hostname: String, notificationsEnabled: Bool) -> PingNotification? {
|
||||||
let wasPreviouslyDown = previousPingStates[hostname] == false
|
let wasPreviouslyDown = previousPingStates[hostname] == false
|
||||||
previousPingStates[hostname] = true
|
previousPingStates[hostname] = true
|
||||||
|
failureStartedAt.removeValue(forKey: hostname)
|
||||||
|
|
||||||
guard wasPreviouslyDown, notificationsEnabled else {
|
guard wasPreviouslyDown, notificationsEnabled else {
|
||||||
return nil
|
return nil
|
||||||
@@ -98,9 +106,20 @@ private actor PingStateStore {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func recordFailure(for hostname: String, notificationsEnabled: Bool) -> PingNotification? {
|
func recordFailure(for hostname: String, notificationsEnabled: Bool, gracePeriod: TimeInterval) -> PingNotification? {
|
||||||
|
let now = Date()
|
||||||
|
let startedAt = failureStartedAt[hostname] ?? now
|
||||||
|
if failureStartedAt[hostname] == nil {
|
||||||
|
failureStartedAt[hostname] = startedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
guard now.timeIntervalSince(startedAt) >= gracePeriod else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
let wasPreviouslyUp = previousPingStates[hostname] != false
|
let wasPreviouslyUp = previousPingStates[hostname] != false
|
||||||
previousPingStates[hostname] = false
|
previousPingStates[hostname] = false
|
||||||
|
failureStartedAt.removeValue(forKey: hostname)
|
||||||
|
|
||||||
guard wasPreviouslyUp, notificationsEnabled else {
|
guard wasPreviouslyUp, notificationsEnabled else {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -201,6 +201,17 @@ struct ServerInfo: Codable, Hashable, Equatable {
|
|||||||
ServerInfo.version(apiVersion, isAtLeast: "2.14")
|
ServerInfo.version(apiVersion, isAtLeast: "2.14")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var rebootRequired: Bool {
|
||||||
|
operatingSystem?.updates?.rebootRequired ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
var statusTooltip: String {
|
||||||
|
if rebootRequired {
|
||||||
|
return "This host is online, but a reboot is required to complete pending updates."
|
||||||
|
}
|
||||||
|
return "This host is online."
|
||||||
|
}
|
||||||
|
|
||||||
var summaryCPUPercent: Double {
|
var summaryCPUPercent: Double {
|
||||||
if let cpuUtilizationPercent {
|
if let cpuUtilizationPercent {
|
||||||
return min(max(cpuUtilizationPercent, 0), 100)
|
return min(max(cpuUtilizationPercent, 0), 100)
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ struct MainView: View {
|
|||||||
@AppStorage("refreshInterval") private var refreshInterval: Int = 60
|
@AppStorage("refreshInterval") private var refreshInterval: Int = 60
|
||||||
@AppStorage("enableStatusNotifications") private var enableStatusNotifications: Bool = true
|
@AppStorage("enableStatusNotifications") private var enableStatusNotifications: Bool = true
|
||||||
@AppStorage("enableAlertNotifications") private var enableAlertNotifications: Bool = true
|
@AppStorage("enableAlertNotifications") private var enableAlertNotifications: Bool = true
|
||||||
|
@AppStorage("alertGracePeriod") private var alertGracePeriod: Int = 30
|
||||||
@State private var refreshTimer: Timer.TimerPublisher?
|
@State private var refreshTimer: Timer.TimerPublisher?
|
||||||
@State private var refreshSubscription: AnyCancellable?
|
@State private var refreshSubscription: AnyCancellable?
|
||||||
@State private var pingTimer: Timer?
|
@State private var pingTimer: Timer?
|
||||||
@@ -40,6 +41,8 @@ struct MainView: View {
|
|||||||
@State private var lastRefreshInterval: Int?
|
@State private var lastRefreshInterval: Int?
|
||||||
@State private var lastMetricPrune: Date?
|
@State private var lastMetricPrune: Date?
|
||||||
@State private var previousServiceStates: [String: String] = [:]
|
@State private var previousServiceStates: [String: String] = [:]
|
||||||
|
@State private var serviceFailureStartedAt: [String: Date] = [:]
|
||||||
|
@State private var serviceOfflineAlerted: [String: Bool] = [:]
|
||||||
private let serverOrderKey = MainView.serverOrderKeyStatic
|
private let serverOrderKey = MainView.serverOrderKeyStatic
|
||||||
private let storedGroupsKey = MainView.storedGroupsKeyStatic
|
private let storedGroupsKey = MainView.storedGroupsKeyStatic
|
||||||
@Environment(\.modelContext) private var modelContext
|
@Environment(\.modelContext) private var modelContext
|
||||||
@@ -242,9 +245,10 @@ struct MainView: View {
|
|||||||
private func sidebarRow(for server: Server) -> some View {
|
private func sidebarRow(for server: Server) -> some View {
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemName: "dot.circle.fill")
|
Image(systemName: "dot.circle.fill")
|
||||||
.foregroundColor(server.pingable ? .green : .red)
|
.foregroundColor(sidebarStatusColor(for: server))
|
||||||
Text(server.hostname)
|
Text(server.hostname)
|
||||||
}
|
}
|
||||||
|
.help(sidebarStatusTooltip(for: server))
|
||||||
.tag(server.id)
|
.tag(server.id)
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
Button("Edit") {
|
Button("Edit") {
|
||||||
@@ -259,6 +263,23 @@ struct MainView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func sidebarStatusColor(for server: Server) -> Color {
|
||||||
|
if !server.pingable {
|
||||||
|
return .red
|
||||||
|
}
|
||||||
|
if server.info?.rebootRequired == true {
|
||||||
|
return .yellow
|
||||||
|
}
|
||||||
|
return .green
|
||||||
|
}
|
||||||
|
|
||||||
|
private func sidebarStatusTooltip(for server: Server) -> String {
|
||||||
|
if !server.pingable {
|
||||||
|
return "This host is offline."
|
||||||
|
}
|
||||||
|
return server.info?.statusTooltip ?? "This host is online."
|
||||||
|
}
|
||||||
|
|
||||||
private func groupHeader(for group: ServerGroup) -> some View {
|
private func groupHeader(for group: ServerGroup) -> some View {
|
||||||
let activePlacement = groupDropIndicator?.groupID == group.id ? groupDropIndicator?.placement : nil
|
let activePlacement = groupDropIndicator?.groupID == group.id ? groupDropIndicator?.placement : nil
|
||||||
|
|
||||||
@@ -616,17 +637,45 @@ struct MainView: View {
|
|||||||
for port in ports {
|
for port in ports {
|
||||||
let key = "\(hostname)-\(port.id)"
|
let key = "\(hostname)-\(port.id)"
|
||||||
let previousStatus = previousServiceStates[key]
|
let previousStatus = previousServiceStates[key]
|
||||||
let currentStatus = port.status
|
let currentStatus = port.status.lowercased()
|
||||||
|
let now = Date()
|
||||||
previousServiceStates[key] = currentStatus
|
|
||||||
|
|
||||||
if let previousStatus, previousStatus != currentStatus {
|
if let previousStatus, previousStatus != currentStatus {
|
||||||
if currentStatus == "offline" && enableStatusNotifications {
|
if currentStatus == "offline" {
|
||||||
sendServiceNotification(service: port.service, hostname: hostname, status: "offline")
|
serviceFailureStartedAt[key] = now
|
||||||
} else if currentStatus == "online" && previousStatus == "offline" && enableStatusNotifications {
|
serviceOfflineAlerted[key] = false
|
||||||
sendServiceNotification(service: port.service, hostname: hostname, status: "online")
|
} else if currentStatus == "online" {
|
||||||
|
let hadOfflineAlert = serviceOfflineAlerted[key] == true
|
||||||
|
serviceFailureStartedAt.removeValue(forKey: key)
|
||||||
|
serviceOfflineAlerted.removeValue(forKey: key)
|
||||||
|
if hadOfflineAlert && enableStatusNotifications {
|
||||||
|
sendServiceNotification(service: port.service, hostname: hostname, status: "online")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if previousStatus == nil {
|
||||||
|
if currentStatus == "offline" {
|
||||||
|
serviceFailureStartedAt[key] = now
|
||||||
|
serviceOfflineAlerted[key] = false
|
||||||
|
} else if currentStatus == "online" {
|
||||||
|
serviceFailureStartedAt.removeValue(forKey: key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if currentStatus == "offline" {
|
||||||
|
let startedAt = serviceFailureStartedAt[key] ?? now
|
||||||
|
if serviceFailureStartedAt[key] == nil {
|
||||||
|
serviceFailureStartedAt[key] = startedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
if serviceOfflineAlerted[key] != true,
|
||||||
|
enableAlertNotifications,
|
||||||
|
now.timeIntervalSince(startedAt) >= TimeInterval(alertGracePeriod) {
|
||||||
|
sendServiceNotification(service: port.service, hostname: hostname, status: "offline")
|
||||||
|
serviceOfflineAlerted[key] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
previousServiceStates[key] = currentStatus
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,15 +28,19 @@ struct PreferencesView: View {
|
|||||||
@AppStorage("pingInterval") private var storedPingInterval: Int = 10
|
@AppStorage("pingInterval") private var storedPingInterval: Int = 10
|
||||||
@AppStorage("refreshInterval") private var storedRefreshInterval: Int = 60
|
@AppStorage("refreshInterval") private var storedRefreshInterval: Int = 60
|
||||||
@AppStorage("showIntervalIndicator") private var showIntervalIndicator: Bool = true
|
@AppStorage("showIntervalIndicator") private var showIntervalIndicator: Bool = true
|
||||||
|
@AppStorage("alertGracePeriod") private var storedAlertGracePeriod: Int = 30
|
||||||
|
|
||||||
@State private var pingIntervalSlider: Double = 10
|
@State private var pingIntervalSlider: Double = 10
|
||||||
@State private var refreshIntervalSlider: Double = 60
|
@State private var refreshIntervalSlider: Double = 60
|
||||||
|
@State private var alertGracePeriodSlider: Double = 30
|
||||||
@State private var selection: Tab = .monitor
|
@State private var selection: Tab = .monitor
|
||||||
@State private var hoveredTab: Tab?
|
@State private var hoveredTab: Tab?
|
||||||
|
|
||||||
private let minimumInterval: Double = 10
|
private let minimumInterval: Double = 10
|
||||||
private let maximumPingInterval: Double = 60
|
private let maximumPingInterval: Double = 60
|
||||||
private let maximumRefreshInterval: Double = 600
|
private let maximumRefreshInterval: Double = 600
|
||||||
|
private let minimumAlertGracePeriod: Double = 0
|
||||||
|
private let maximumAlertGracePeriod: Double = 300
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
@@ -53,6 +57,7 @@ struct PreferencesView: View {
|
|||||||
.onAppear {
|
.onAppear {
|
||||||
pingIntervalSlider = Double(storedPingInterval)
|
pingIntervalSlider = Double(storedPingInterval)
|
||||||
refreshIntervalSlider = Double(storedRefreshInterval)
|
refreshIntervalSlider = Double(storedRefreshInterval)
|
||||||
|
alertGracePeriodSlider = Double(storedAlertGracePeriod)
|
||||||
}
|
}
|
||||||
.onChange(of: pingIntervalSlider) { _, newValue in
|
.onChange(of: pingIntervalSlider) { _, newValue in
|
||||||
storedPingInterval = Int(newValue)
|
storedPingInterval = Int(newValue)
|
||||||
@@ -60,6 +65,9 @@ struct PreferencesView: View {
|
|||||||
.onChange(of: refreshIntervalSlider) { _, newValue in
|
.onChange(of: refreshIntervalSlider) { _, newValue in
|
||||||
storedRefreshInterval = Int(newValue)
|
storedRefreshInterval = Int(newValue)
|
||||||
}
|
}
|
||||||
|
.onChange(of: alertGracePeriodSlider) { _, newValue in
|
||||||
|
storedAlertGracePeriod = Int(newValue)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var sidebar: some View {
|
private var sidebar: some View {
|
||||||
@@ -121,7 +129,12 @@ struct PreferencesView: View {
|
|||||||
refreshChanged: handleRefreshSliderEditing(_:)
|
refreshChanged: handleRefreshSliderEditing(_:)
|
||||||
)
|
)
|
||||||
case .notifications:
|
case .notifications:
|
||||||
NotificationsPreferencesView()
|
NotificationsPreferencesView(
|
||||||
|
alertGracePeriodSlider: $alertGracePeriodSlider,
|
||||||
|
minimumAlertGracePeriod: minimumAlertGracePeriod,
|
||||||
|
maximumAlertGracePeriod: maximumAlertGracePeriod,
|
||||||
|
alertGraceChanged: handleAlertGraceSliderEditing(_:)
|
||||||
|
)
|
||||||
case .alerts:
|
case .alerts:
|
||||||
AlertsPreferencesView()
|
AlertsPreferencesView()
|
||||||
case .updates:
|
case .updates:
|
||||||
@@ -142,6 +155,13 @@ struct PreferencesView: View {
|
|||||||
storedRefreshInterval = Int(refreshIntervalSlider)
|
storedRefreshInterval = Int(refreshIntervalSlider)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func handleAlertGraceSliderEditing(_ editing: Bool) {
|
||||||
|
if !editing {
|
||||||
|
storedAlertGracePeriod = Int(alertGracePeriodSlider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func backgroundColor(for tab: Tab) -> Color {
|
private func backgroundColor(for tab: Tab) -> Color {
|
||||||
if selection == tab {
|
if selection == tab {
|
||||||
return sidebarSelectionColor
|
return sidebarSelectionColor
|
||||||
@@ -238,7 +258,7 @@ private struct MonitorPreferencesView: View {
|
|||||||
in: range,
|
in: range,
|
||||||
step: 5
|
step: 5
|
||||||
) {
|
) {
|
||||||
Text(title)
|
EmptyView()
|
||||||
} minimumValueLabel: {
|
} minimumValueLabel: {
|
||||||
Text("\(Int(range.lowerBound))s")
|
Text("\(Int(range.lowerBound))s")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
@@ -290,12 +310,62 @@ private struct UpdatesPreferencesView: View {
|
|||||||
private struct NotificationsPreferencesView: View {
|
private struct NotificationsPreferencesView: View {
|
||||||
@AppStorage("enableStatusNotifications") private var enableStatusNotifications: Bool = true
|
@AppStorage("enableStatusNotifications") private var enableStatusNotifications: Bool = true
|
||||||
@AppStorage("enableAlertNotifications") private var enableAlertNotifications: Bool = true
|
@AppStorage("enableAlertNotifications") private var enableAlertNotifications: Bool = true
|
||||||
|
@Binding var alertGracePeriodSlider: Double
|
||||||
|
@State private var showAlertGraceHelp = false
|
||||||
|
|
||||||
|
let minimumAlertGracePeriod: Double
|
||||||
|
let maximumAlertGracePeriod: Double
|
||||||
|
let alertGraceChanged: (Bool) -> Void
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 18) {
|
VStack(alignment: .leading, spacing: 18) {
|
||||||
Toggle("Status Notifications", isOn: $enableStatusNotifications)
|
Toggle("Status Notifications", isOn: $enableStatusNotifications)
|
||||||
Toggle("Alert Notifications", isOn: $enableAlertNotifications)
|
Toggle("Alert Notifications", isOn: $enableAlertNotifications)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
HStack {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Text("Alert grace period")
|
||||||
|
.font(.headline)
|
||||||
|
Button {
|
||||||
|
showAlertGraceHelp.toggle()
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "questionmark.circle")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.popover(isPresented: $showAlertGraceHelp, arrowEdge: .bottom) {
|
||||||
|
Text("Wait this long before raising an alert after a host or service first appears unavailable.")
|
||||||
|
.font(.callout)
|
||||||
|
.padding(12)
|
||||||
|
.frame(width: 280, alignment: .leading)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
Text("\(Int(alertGracePeriodSlider)) seconds")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Slider(
|
||||||
|
value: $alertGracePeriodSlider,
|
||||||
|
in: minimumAlertGracePeriod...maximumAlertGracePeriod,
|
||||||
|
step: 5
|
||||||
|
) {
|
||||||
|
EmptyView()
|
||||||
|
} minimumValueLabel: {
|
||||||
|
Text("\(Int(minimumAlertGracePeriod))s")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
} maximumValueLabel: {
|
||||||
|
Text("\(Int(maximumAlertGracePeriod))s")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
} onEditingChanged: { editing in
|
||||||
|
alertGraceChanged(editing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.toggleStyle(.switch)
|
.toggleStyle(.switch)
|
||||||
|
|||||||
@@ -172,13 +172,15 @@ struct SummaryView: View {
|
|||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
HStack(alignment: .center, spacing: 10) {
|
HStack(alignment: .center, spacing: 10) {
|
||||||
Circle()
|
Circle()
|
||||||
.fill(server.pingable ? Color.green : Color.red)
|
.fill(statusTint)
|
||||||
.frame(width: 10, height: 10)
|
.frame(width: 10, height: 10)
|
||||||
|
.help(statusTooltip)
|
||||||
|
|
||||||
Text(server.hostname)
|
Text(server.hostname)
|
||||||
.font(.system(size: 22, weight: .semibold))
|
.font(.system(size: 22, weight: .semibold))
|
||||||
|
|
||||||
statusBadge(server.pingable ? "Online" : "Offline", tint: server.pingable ? .green : .red)
|
statusBadge(statusText, tint: statusTint)
|
||||||
|
.help(statusTooltip)
|
||||||
}
|
}
|
||||||
|
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
@@ -417,6 +419,27 @@ struct SummaryView: View {
|
|||||||
return .green
|
return .green
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var statusTint: Color {
|
||||||
|
if !server.pingable {
|
||||||
|
return .red
|
||||||
|
}
|
||||||
|
if server.info?.rebootRequired == true {
|
||||||
|
return .yellow
|
||||||
|
}
|
||||||
|
return .green
|
||||||
|
}
|
||||||
|
|
||||||
|
private var statusText: String {
|
||||||
|
server.pingable ? "Online" : "Offline"
|
||||||
|
}
|
||||||
|
|
||||||
|
private var statusTooltip: String {
|
||||||
|
if !server.pingable {
|
||||||
|
return "This host is offline."
|
||||||
|
}
|
||||||
|
return server.info?.statusTooltip ?? "This host is online."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
|
|||||||
Vendored
+8
-8
@@ -2,6 +2,14 @@
|
|||||||
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
|
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
|
||||||
<channel>
|
<channel>
|
||||||
<title>iKeyMon</title>
|
<title>iKeyMon</title>
|
||||||
|
<item>
|
||||||
|
<title>26.1.12</title>
|
||||||
|
<pubDate>Fri, 24 Apr 2026 19:17:55 +0200</pubDate>
|
||||||
|
<sparkle:version>189</sparkle:version>
|
||||||
|
<sparkle:shortVersionString>26.1.12</sparkle:shortVersionString>
|
||||||
|
<sparkle:minimumSystemVersion>15.2</sparkle:minimumSystemVersion>
|
||||||
|
<enclosure url="https://git.24unix.net/tracer/iKeyMon/releases/download/v26.1.12/iKeyMon-26.1.12.zip" length="3308758" type="application/octet-stream" sparkle:edSignature="auIwX5iy4DyrDSYHPFr696KD3UlFbFB1+1W55N2j868KkQJu36j/Wzj5UVursaakrBK49YrZRH4SqfClteQCAA=="/>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<title>26.1.11</title>
|
<title>26.1.11</title>
|
||||||
<pubDate>Tue, 21 Apr 2026 18:06:37 +0200</pubDate>
|
<pubDate>Tue, 21 Apr 2026 18:06:37 +0200</pubDate>
|
||||||
@@ -18,13 +26,5 @@
|
|||||||
<sparkle:minimumSystemVersion>15.2</sparkle:minimumSystemVersion>
|
<sparkle:minimumSystemVersion>15.2</sparkle:minimumSystemVersion>
|
||||||
<enclosure url="https://git.24unix.net/tracer/iKeyMon/releases/download/v26.1.10/iKeyMon-26.1.10.zip" length="3193339" type="application/octet-stream" sparkle:edSignature="SwXVc4kmCUAec7VcDcoxDySDhGTXwdLz30q9SiyvaGK/P7cel3iljlvDd6Rc4/M1wtHoOZXXo+93lFhnLMB+Aw=="/>
|
<enclosure url="https://git.24unix.net/tracer/iKeyMon/releases/download/v26.1.10/iKeyMon-26.1.10.zip" length="3193339" type="application/octet-stream" sparkle:edSignature="SwXVc4kmCUAec7VcDcoxDySDhGTXwdLz30q9SiyvaGK/P7cel3iljlvDd6Rc4/M1wtHoOZXXo+93lFhnLMB+Aw=="/>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
|
||||||
<title>26.1.9</title>
|
|
||||||
<pubDate>Sun, 19 Apr 2026 23:04:07 +0200</pubDate>
|
|
||||||
<sparkle:version>181</sparkle:version>
|
|
||||||
<sparkle:shortVersionString>26.1.9</sparkle:shortVersionString>
|
|
||||||
<sparkle:minimumSystemVersion>15.2</sparkle:minimumSystemVersion>
|
|
||||||
<enclosure url="https://git.24unix.net/tracer/iKeyMon/releases/download/v26.1.9/iKeyMon-26.1.9.zip" length="3109488" type="application/octet-stream" sparkle:edSignature="ZV96uUMdYC/X90H3G10FMzmZHKUEWpe1geSe/5IBJ7EOCUmx7Mz352i6VMWumFnCtDD4jHo173W9eySUX9KvDA=="/>
|
|
||||||
</item>
|
|
||||||
</channel>
|
</channel>
|
||||||
</rss>
|
</rss>
|
||||||
@@ -322,7 +322,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = iKeyMon.entitlements;
|
CODE_SIGN_ENTITLEMENTS = iKeyMon.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 187;
|
CURRENT_PROJECT_VERSION = 189;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = Q5486ZVAFT;
|
DEVELOPMENT_TEAM = Q5486ZVAFT;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
@@ -337,7 +337,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 26.1.11;
|
MARKETING_VERSION = 26.1.12;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = net.24unix.iKeyMon;
|
PRODUCT_BUNDLE_IDENTIFIER = net.24unix.iKeyMon;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
@@ -353,7 +353,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = iKeyMon.entitlements;
|
CODE_SIGN_ENTITLEMENTS = iKeyMon.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 187;
|
CURRENT_PROJECT_VERSION = 189;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = Q5486ZVAFT;
|
DEVELOPMENT_TEAM = Q5486ZVAFT;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
@@ -368,7 +368,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 26.1.11;
|
MARKETING_VERSION = 26.1.12;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = net.24unix.iKeyMon;
|
PRODUCT_BUNDLE_IDENTIFIER = net.24unix.iKeyMon;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"marketing_version": "26.1.11"
|
"marketing_version": "26.1.12"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user