56 lines
1.8 KiB
Swift
56 lines
1.8 KiB
Swift
import Foundation
|
|
import Combine
|
|
|
|
/// Polls /api/health on the C++ backend. Drives the menu-bar indicator.
|
|
@MainActor
|
|
final class HealthMonitor: ObservableObject {
|
|
enum Status { case unknown, up, down }
|
|
|
|
@Published private(set) var status: Status = .unknown
|
|
@Published private(set) var lastChecked: Date?
|
|
@Published private(set) var lastError: String?
|
|
|
|
private var timer: Timer?
|
|
private let session = URLSession(configuration: .ephemeral)
|
|
|
|
func start(url: String, every interval: TimeInterval = 10) {
|
|
timer?.invalidate()
|
|
check(url: url)
|
|
timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
|
|
Task { @MainActor in self?.check(url: url) }
|
|
}
|
|
}
|
|
|
|
func stop() { timer?.invalidate(); timer = nil }
|
|
|
|
func check(url: String) {
|
|
guard let base = URL(string: url),
|
|
let healthURL = URL(string: "/api/health", relativeTo: base) else {
|
|
status = .down
|
|
lastError = "invalid url: \(url)"
|
|
return
|
|
}
|
|
var req = URLRequest(url: healthURL)
|
|
req.timeoutInterval = 5
|
|
session.dataTask(with: req) { [weak self] _, resp, err in
|
|
Task { @MainActor in
|
|
guard let self else { return }
|
|
self.lastChecked = Date()
|
|
if let err = err {
|
|
self.status = .down
|
|
self.lastError = err.localizedDescription
|
|
return
|
|
}
|
|
let code = (resp as? HTTPURLResponse)?.statusCode ?? 0
|
|
if (200..<300).contains(code) {
|
|
self.status = .up
|
|
self.lastError = nil
|
|
} else {
|
|
self.status = .down
|
|
self.lastError = "HTTP \(code)"
|
|
}
|
|
}
|
|
}.resume()
|
|
}
|
|
}
|