import Foundation import UserNotifications enum PingService { private static let stateStore = PingStateStore() static func suppressChecks(for hostname: String, duration: TimeInterval) async { await stateStore.suppressChecks(for: hostname, duration: duration) } static func ping(hostname: String, apiKey: String, notificationsEnabled: Bool = true) async -> Bool { if await stateStore.shouldSkipPing(for: hostname) { return false } guard let url = URL(string: "https://\(hostname)/api/v2/ping") else { print("❌ [PingService] Invalid URL for \(hostname)") return false } var request = URLRequest(url: url) request.httpMethod = "GET" request.setValue(apiKey, forHTTPHeaderField: "X-API-KEY") request.timeoutInterval = 10 do { let (data, response) = try await URLSession.shared.data(for: request) if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 { await handlePingFailure(for: hostname, notificationsEnabled: notificationsEnabled) return false } if let result = try? JSONDecoder().decode([String: String].self, from: data), result["response"] == "pong" { await handlePingSuccess(for: hostname, notificationsEnabled: notificationsEnabled) return true } else { await handlePingFailure(for: hostname, notificationsEnabled: notificationsEnabled) return false } } catch { await handlePingFailure(for: hostname, notificationsEnabled: notificationsEnabled) return false } } private static func handlePingSuccess(for hostname: String, notificationsEnabled: Bool) async { if let notification = await stateStore.recordSuccess(for: hostname, notificationsEnabled: notificationsEnabled) { sendNotification(title: notification.title, body: notification.body) } } private static func handlePingFailure(for hostname: String, notificationsEnabled: Bool) async { if let notification = await stateStore.recordFailure(for: hostname, notificationsEnabled: notificationsEnabled) { sendNotification(title: notification.title, body: notification.body) } } private static func sendNotification(title: String, body: String) { let content = UNMutableNotificationContent() content.title = title content.body = body content.sound = .default let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) UNUserNotificationCenter.current().add(request) } } private actor PingStateStore { private var previousPingStates: [String: Bool] = [:] private var suppressedUntil: [String: Date] = [:] func suppressChecks(for hostname: String, duration: TimeInterval) { suppressedUntil[hostname] = Date().addingTimeInterval(duration) previousPingStates[hostname] = false } func shouldSkipPing(for hostname: String) -> Bool { if let suppressedUntil = suppressedUntil[hostname], suppressedUntil > Date() { return true } suppressedUntil.removeValue(forKey: hostname) return false } func recordSuccess(for hostname: String, notificationsEnabled: Bool) -> PingNotification? { let wasPreviouslyDown = previousPingStates[hostname] == false previousPingStates[hostname] = true guard wasPreviouslyDown, notificationsEnabled else { return nil } return PingNotification( title: "Server Online", body: "\(hostname) is now online" ) } func recordFailure(for hostname: String, notificationsEnabled: Bool) -> PingNotification? { let wasPreviouslyUp = previousPingStates[hostname] != false previousPingStates[hostname] = false guard wasPreviouslyUp, notificationsEnabled else { return nil } return PingNotification( title: "Server Offline", body: "\(hostname) is offline" ) } } private struct PingNotification { let title: String let body: String }