feat: add remote reboot support
This commit is contained in:
@@ -26,6 +26,7 @@ struct MainView: View {
|
||||
@State private var refreshTimer: Timer.TimerPublisher?
|
||||
@State private var refreshSubscription: AnyCancellable?
|
||||
@State private var pingTimer: Timer?
|
||||
@State private var restartingServerID: UUID?
|
||||
@State private var lastRefreshInterval: Int?
|
||||
@State private var previousServiceStates: [String: String] = [:]
|
||||
private let serverOrderKey = MainView.serverOrderKeyStatic
|
||||
@@ -79,7 +80,15 @@ struct MainView: View {
|
||||
} detail: {
|
||||
if let selectedServerID,
|
||||
let index = servers.firstIndex(where: { selectedServerID == $0.id }) {
|
||||
ServerDetailView(server: $servers[index], isFetching: isFetchingInfo)
|
||||
let serverID = servers[index].id
|
||||
ServerDetailView(
|
||||
server: $servers[index],
|
||||
isFetching: isFetchingInfo,
|
||||
canRestart: servers[index].info?.supportsRestartCommand == true,
|
||||
isRestarting: restartingServerID == serverID
|
||||
) {
|
||||
await restartServer(for: serverID)
|
||||
}
|
||||
} else {
|
||||
ContentUnavailableView("No Server Selected", systemImage: "server.rack")
|
||||
}
|
||||
@@ -235,9 +244,6 @@ struct MainView: View {
|
||||
await MainActor.run {
|
||||
servers[index].pingable = pingable
|
||||
}
|
||||
if !pingable {
|
||||
print("📶 [MainView] Ping \(server.hostname): offline")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -332,6 +338,87 @@ struct MainView: View {
|
||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
}
|
||||
|
||||
private func restartServer(for id: UUID) async -> ServerActionFeedback {
|
||||
guard let server = servers.first(where: { $0.id == id }) else {
|
||||
return ServerActionFeedback(
|
||||
title: "Reboot Failed",
|
||||
message: "The selected server could not be found."
|
||||
)
|
||||
}
|
||||
|
||||
guard server.info?.supportsRestartCommand == true else {
|
||||
return ServerActionFeedback(
|
||||
title: "Reboot Unavailable",
|
||||
message: "\(server.hostname) does not support remote reboot via the API."
|
||||
)
|
||||
}
|
||||
|
||||
guard let apiKey = KeychainHelper.loadApiKey(for: server.hostname)?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!apiKey.isEmpty else {
|
||||
return ServerActionFeedback(
|
||||
title: "Reboot Failed",
|
||||
message: "No API key is configured for \(server.hostname)."
|
||||
)
|
||||
}
|
||||
|
||||
guard let baseURL = URL(string: "https://\(server.hostname)") else {
|
||||
return ServerActionFeedback(
|
||||
title: "Reboot Failed",
|
||||
message: "The server URL for \(server.hostname) is invalid."
|
||||
)
|
||||
}
|
||||
|
||||
restartingServerID = id
|
||||
defer { restartingServerID = nil }
|
||||
|
||||
do {
|
||||
let api: AnyServerAPI
|
||||
if let versionString = server.info?.apiVersion,
|
||||
let versionedAPI = APIFactory.createAPI(baseURL: baseURL, versionString: versionString) {
|
||||
api = versionedAPI
|
||||
} else {
|
||||
api = try await APIFactory.detectAndCreateAPI(baseURL: baseURL, apiKey: apiKey)
|
||||
}
|
||||
try await api.restartServer(apiKey: apiKey)
|
||||
PingService.suppressChecks(for: server.hostname, duration: 90)
|
||||
|
||||
return ServerActionFeedback(
|
||||
title: "Reboot Requested",
|
||||
message: "The reboot command was sent to \(server.hostname). The host may become unavailable briefly while it restarts."
|
||||
)
|
||||
} catch let error as URLError where Self.isExpectedRestartDisconnect(error) {
|
||||
PingService.suppressChecks(for: server.hostname, duration: 90)
|
||||
return ServerActionFeedback(
|
||||
title: "Reboot Requested",
|
||||
message: "The reboot command appears to have been accepted by \(server.hostname). The connection dropped while the host was going away, which is expected during a reboot."
|
||||
)
|
||||
} catch APIError.httpError(404, let message) {
|
||||
return ServerActionFeedback(
|
||||
title: "Reboot Unavailable",
|
||||
message: message ?? "\(server.hostname) returned 404 for /api/v2/server/reboot."
|
||||
)
|
||||
} catch {
|
||||
return ServerActionFeedback(
|
||||
title: "Reboot Failed",
|
||||
message: error.localizedDescription
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private static func isExpectedRestartDisconnect(_ error: URLError) -> Bool {
|
||||
switch error.code {
|
||||
case .timedOut,
|
||||
.cannotConnectToHost,
|
||||
.networkConnectionLost,
|
||||
.notConnectedToInternet,
|
||||
.cannotFindHost,
|
||||
.dnsLookupFailed:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
|
||||
Reference in New Issue
Block a user