- Show stable group health in a fixed 2x2 metric grid - Add link-chain state to fleet cards and parse hardware/control/SIM/cellular - Replace the wrapped icon with a full-bleed app icon - Bump the IPA build number
98 lines
3.1 KiB
Swift
98 lines
3.1 KiB
Swift
import Foundation
|
|
|
|
public protocol SimAdminClientProtocol: Sendable {
|
|
func send(_ route: UpstreamRoute, baseURL: URL, cookie: String?) async throws -> UpstreamResponse
|
|
}
|
|
|
|
extension SimAdminClient: SimAdminClientProtocol {}
|
|
|
|
public enum ConnectionStatus: String, Equatable, Sendable {
|
|
case online
|
|
case offline
|
|
case authRequired
|
|
case degraded
|
|
case unknown
|
|
|
|
public var label: String {
|
|
switch self {
|
|
case .online: return "在线"
|
|
case .offline: return "离线"
|
|
case .authRequired: return "需登录"
|
|
case .degraded: return "降级"
|
|
case .unknown: return "未知"
|
|
}
|
|
}
|
|
}
|
|
|
|
public struct ConnectionResult: Equatable, Sendable {
|
|
public let status: ConnectionStatus
|
|
public let requestLatencyMilliseconds: Int?
|
|
public let checkedAt: Date
|
|
|
|
public init(
|
|
status: ConnectionStatus,
|
|
requestLatencyMilliseconds: Int? = nil,
|
|
checkedAt: Date
|
|
) {
|
|
self.status = status
|
|
self.requestLatencyMilliseconds = requestLatencyMilliseconds
|
|
self.checkedAt = checkedAt
|
|
}
|
|
}
|
|
|
|
public struct ConnectionProbe: Sendable {
|
|
private let client: any SimAdminClientProtocol
|
|
|
|
public init(client: any SimAdminClientProtocol) {
|
|
self.client = client
|
|
}
|
|
|
|
public func test(_ instance: Instance) async throws -> ConnectionResult {
|
|
let startedAt = DispatchTime.now()
|
|
do {
|
|
let response = try await client.send(.health, baseURL: instance.origin, cookie: nil)
|
|
let latency = Self.elapsedMilliseconds(since: startedAt)
|
|
if response.status == 401 || response.status == 403 {
|
|
return ConnectionResult(
|
|
status: .authRequired,
|
|
requestLatencyMilliseconds: latency,
|
|
checkedAt: Date()
|
|
)
|
|
}
|
|
return ConnectionResult(
|
|
status: .online,
|
|
requestLatencyMilliseconds: latency,
|
|
checkedAt: Date()
|
|
)
|
|
} catch {
|
|
let latency = Self.elapsedMilliseconds(since: startedAt)
|
|
switch error {
|
|
case UpstreamError.unauthorized:
|
|
return ConnectionResult(
|
|
status: .authRequired,
|
|
requestLatencyMilliseconds: latency,
|
|
checkedAt: Date()
|
|
)
|
|
case UpstreamError.timeout, is URLError:
|
|
return ConnectionResult(
|
|
status: .offline,
|
|
requestLatencyMilliseconds: latency,
|
|
checkedAt: Date()
|
|
)
|
|
default:
|
|
return ConnectionResult(
|
|
status: .degraded,
|
|
requestLatencyMilliseconds: latency,
|
|
checkedAt: Date()
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func elapsedMilliseconds(since startedAt: DispatchTime) -> Int? {
|
|
let nanoseconds = DispatchTime.now().uptimeNanoseconds &- startedAt.uptimeNanoseconds
|
|
let milliseconds = Double(nanoseconds) / 1_000_000
|
|
return milliseconds.isFinite ? Int(milliseconds.rounded()) : nil
|
|
}
|
|
}
|