// // APIv2_12.swift // iKeyMon // // Created by tracer on 13.11.25. // import Foundation extension APIv2_12 { struct Load: Codable { let current: LoadMetrics let historical: [LoadMetrics] struct LoadMetrics: Codable { let oneMinute: Double let fiveMinute: Double let fifteenMinute: Double let timestamp: Date enum CodingKeys: String, CodingKey { case oneMinute = "load_1" case fiveMinute = "load_5" case fifteenMinute = "load_15" case timestamp } } } struct Memory: Codable { let system: SystemMemory let swap: SwapMemory? struct SystemMemory: Codable { let total: Int64 let used: Int64 let free: Int64 let available: Int64 let buffers: Int64? let cached: Int64? } struct SwapMemory: Codable { let total: Int64 let used: Int64 let free: Int64 } } struct Utilization: Codable { let cpu: CPUUtilization let memory: MemoryUtilization let disk: [DiskUtilization] let network: [NetworkUtilization]? struct CPUUtilization: Codable { let overall: Double let cores: [Double] let processes: [ProcessInfo]? struct ProcessInfo: Codable { let pid: Int let name: String let cpuPercent: Double enum CodingKeys: String, CodingKey { case pid case name case cpuPercent = "cpu_percent" } } } struct MemoryUtilization: Codable { let percent: Double let topProcesses: [ProcessMemoryInfo]? struct ProcessMemoryInfo: Codable { let pid: Int let name: String let memoryMB: Double enum CodingKeys: String, CodingKey { case pid case name case memoryMB = "memory_mb" } } } struct DiskUtilization: Codable { let device: String let mountpoint: String let usedPercent: Double let totalBytes: Int64 let usedBytes: Int64 let freeBytes: Int64 enum CodingKeys: String, CodingKey { case device case mountpoint case usedPercent = "used_percent" case totalBytes = "total_bytes" case usedBytes = "used_bytes" case freeBytes = "free_bytes" } } struct NetworkUtilization: Codable { let interface: String let bytesIn: Int64 let bytesOut: Int64 let packetsIn: Int64 let packetsOut: Int64 enum CodingKeys: String, CodingKey { case interface case bytesIn = "bytes_in" case bytesOut = "bytes_out" case packetsIn = "packets_in" case packetsOut = "packets_out" } } } } class APIv2_12: BaseAPIClient, ServerAPIProtocol { typealias LoadType = APIv2_12.Load typealias MemoryType = APIv2_12.Memory typealias UtilizationType = APIv2_12.Utilization private enum Endpoint: String { case systemInfo = "/api/v2/system/info" case load = "/api/v2/metrics/load" case memory = "/api/v2/metrics/memory" case utilization = "/api/v2/metrics/utilization" func url(baseURL: URL) -> URL { return baseURL.appendingPathComponent(self.rawValue) } } func fetchSystemInfo() async throws -> SystemInfo { let url = Endpoint.systemInfo.url(baseURL: baseURL) let request = URLRequest(url: url) return try await performRequest(request, responseType: SystemInfo.self) } func fetchLoad() async throws -> LoadType { let url = Endpoint.load.url(baseURL: baseURL) var request = URLRequest(url: url) request.setValue("application/json", forHTTPHeaderField: "Accept") return try await performRequest(request, responseType: LoadType.self) } func fetchMemory() async throws -> MemoryType { let url = Endpoint.memory.url(baseURL: baseURL) var request = URLRequest(url: url) request.setValue("application/json", forHTTPHeaderField: "Accept") return try await performRequest(request, responseType: MemoryType.self) } func fetchUtilization() async throws -> UtilizationType { let url = Endpoint.utilization.url(baseURL: baseURL) var request = URLRequest(url: url) request.setValue("application/json", forHTTPHeaderField: "Accept") return try await performRequest(request, responseType: UtilizationType.self) } func fetchServerSummary(apiKey: String) async throws -> ServerInfo { let summaryURL = baseURL.appendingPathComponent("api/v2/server") var request = URLRequest(url: summaryURL) request.httpMethod = "GET" request.setValue(apiKey, forHTTPHeaderField: "X-API-KEY") request.timeoutInterval = 30 let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw APIError.invalidResponse } guard httpResponse.statusCode == 200 else { throw APIError.httpError(httpResponse.statusCode) } let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase let envelope = try decoder.decode(ServerSummaryEnvelope.self, from: data) return envelope.toDomain() } } // MARK: - Server Summary Mapping private extension APIv2_12 { struct ServerSummaryEnvelope: Decodable { let meta: Meta let operatingSystem: OperatingSystem? let utilization: Utilization let components: Components let ports: [Port]? let additionalPhpInterpreters: [AdditionalInterpreter]? struct Meta: Decodable { struct Uptime: Decodable { let days: Int let hours: Int let minutes: Int let seconds: Int var formatted: String { "\(days) Days / \(hours) Hours / \(minutes) Minutes / \(seconds) Seconds" } } let hostname: String let ipAddresses: [String] let serverTime: String let uptime: Uptime let panelVersion: String let panelBuild: Int let apiVersion: String } struct Utilization: Decodable { struct Load: Decodable { let minute1: Double let minute5: Double let minute15: Double let cpuCount: Int let percent: Double let level: String } struct Memory: Decodable { let free: Int let used: Int let total: Int let percent: Double } struct Disk: Decodable { let free: Int let used: Int let total: Int let percent: Double } let processCount: Int let emailsInQueue: Int? let load: Load let diskSpace: Disk let memory: Memory let swap: Memory } struct Components: Decodable { let apache: String let php: String let mysql: String? let mariadb: String? } struct Port: Decodable { let service: String let status: String let port: Int let proto: String enum CodingKeys: String, CodingKey { case service case status case port case proto = "protocol" } } struct AdditionalInterpreter: Decodable { let version: String let path: String? let configFile: String? } struct OperatingSystem: Decodable { struct Updates: Decodable { let updateCount: Int let securityUpdateCount: Int let rebootRequired: Bool enum CodingKeys: String, CodingKey { case updateCount = "update_count" case securityUpdateCount = "security_update_count" case rebootRequired = "reboot_required" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) updateCount = try container.decodeIfPresent(Int.self, forKey: .updateCount) ?? 0 securityUpdateCount = try container.decodeIfPresent(Int.self, forKey: .securityUpdateCount) ?? 0 rebootRequired = try container.decodeIfPresent(Bool.self, forKey: .rebootRequired) ?? false } } let label: String let distribution: String let version: String let architecture: String let endOfLife: Bool let updates: Updates? enum CodingKeys: String, CodingKey { case label case distribution case version case architecture case endOfLife = "end_of_life" case updates } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) label = try container.decodeIfPresent(String.self, forKey: .label) ?? "" distribution = try container.decodeIfPresent(String.self, forKey: .distribution) ?? "" version = try container.decodeIfPresent(String.self, forKey: .version) ?? "" architecture = try container.decodeIfPresent(String.self, forKey: .architecture) ?? "" endOfLife = try container.decodeIfPresent(Bool.self, forKey: .endOfLife) ?? false updates = try container.decodeIfPresent(Updates.self, forKey: .updates) } } func toDomain() -> ServerInfo { let load = utilization.load let disk = utilization.diskSpace let memory = utilization.memory let swapMemory = utilization.swap return ServerInfo( hostname: meta.hostname, ipAddresses: meta.ipAddresses, cpuCores: load.cpuCount, serverTime: meta.serverTime, uptime: meta.uptime.formatted, processCount: utilization.processCount, apacheVersion: components.apache, phpVersion: components.php, mysqlVersion: components.mysql, mariadbVersion: components.mariadb, emailsInQueue: utilization.emailsInQueue, operatingSystem: operatingSystem.map { ServerInfo.OperatingSystem( label: $0.label, distribution: $0.distribution, version: $0.version, architecture: $0.architecture, endOfLife: $0.endOfLife, updates: $0.updates.map { ServerInfo.OperatingSystem.UpdateStatus( updateCount: $0.updateCount, securityUpdateCount: $0.securityUpdateCount, rebootRequired: $0.rebootRequired ) } ) }, ports: ports?.map { ServerInfo.ServicePort(service: $0.service, status: $0.status, port: $0.port, proto: $0.proto) }, load: ServerInfo.Load( minute1: load.minute1, minute5: load.minute5, minute15: load.minute15, percent: load.percent, cpuCount: load.cpuCount, level: load.level ), memory: ServerInfo.Memory( free: memory.free, used: memory.used, total: memory.total, percent: memory.percent ), swap: ServerInfo.Memory( free: swapMemory.free, used: swapMemory.used, total: swapMemory.total, percent: swapMemory.percent ), diskSpace: ServerInfo.DiskSpace( free: disk.free, used: disk.used, total: disk.total, percent: disk.percent ), panelVersion: meta.panelVersion, panelBuild: String(meta.panelBuild), apiVersion: meta.apiVersion, additionalPHPInterpreters: additionalPhpInterpreters?.map { ServerInfo.PHPInterpreter( version: $0.version, path: $0.path, configFile: $0.configFile, extensions: [], memoryLimit: nil, maxExecutionTime: nil ) } ) } } }