diff --git a/Screenshots/edit_server.png b/Screenshots/edit_server.png new file mode 100644 index 0000000..6d0bf2f Binary files /dev/null and b/Screenshots/edit_server.png differ diff --git a/Screenshots/general_view.png b/Screenshots/general_view.png new file mode 100644 index 0000000..42bc302 Binary files /dev/null and b/Screenshots/general_view.png differ diff --git a/Screenshots/resources_view.png b/Screenshots/resources_view.png new file mode 100644 index 0000000..5519ea6 Binary files /dev/null and b/Screenshots/resources_view.png differ diff --git a/Screenshots/services_view.png b/Screenshots/services_view.png new file mode 100644 index 0000000..172db7e Binary files /dev/null and b/Screenshots/services_view.png differ diff --git a/iKeyMon/Model/Server.swift b/iKeyMon/Model/Server.swift index a7a4caa..b37b883 100644 --- a/iKeyMon/Model/Server.swift +++ b/iKeyMon/Model/Server.swift @@ -13,17 +13,19 @@ struct Server: Identifiable, Codable, Hashable, Equatable { // runtime-only, skip for Codable / Hashable / Equatable var info: ServerInfo? = nil + var pingable: Bool = false - init(id: UUID = UUID(), hostname: String, info: ServerInfo? = nil) { + init(id: UUID = UUID(), hostname: String, info: ServerInfo? = nil, pingable: Bool = false) { self.id = id self.hostname = hostname self.info = info + self.pingable = pingable } // MARK: - Manual conformance static func == (lhs: Server, rhs: Server) -> Bool { - lhs.id == rhs.id && lhs.hostname == rhs.hostname + lhs.id == rhs.id && lhs.hostname == rhs.hostname && lhs.info == rhs.info && lhs.pingable == rhs.pingable } func hash(into hasher: inout Hasher) { diff --git a/iKeyMon/Model/ServerInfo.swift b/iKeyMon/Model/ServerInfo.swift index fca7c49..fd60e36 100644 --- a/iKeyMon/Model/ServerInfo.swift +++ b/iKeyMon/Model/ServerInfo.swift @@ -5,7 +5,13 @@ // Created by tracer on 30.03.25. // -struct ServerInfo: Decodable { +import Foundation + +struct ServerInfo: Decodable, Equatable { + static func == (lhs: ServerInfo, rhs: ServerInfo) -> Bool { + lhs.hostname == rhs.hostname && lhs.serverTime == rhs.serverTime + } + var hostname: String var ipAddresses: [String] // var processor: String @@ -24,6 +30,12 @@ struct ServerInfo: Decodable { var memory: Memory var swap: Memory var diskSpace: DiskSpace + var panelVersion: String + var panelBuild: String + var apiVersion: String + var additionalPHPInterpreters: [PHPInterpreter]? + + } extension ServerInfo { @@ -50,12 +62,33 @@ extension ServerInfo { ), memory: Memory(free: 1234, used: 4567, total: 123456, percent: 23.45), swap: Memory(free: 1234, used: 4567, total: 123456, percent: 23.45), - diskSpace: DiskSpace(free: 1234, used: 4567, total: 123456, percent: 23.45) + diskSpace: DiskSpace(free: 1234, used: 4567, total: 123456, percent: 23.45), + panelVersion: "25.0", + panelBuild: "3394", + apiVersion: "2.0" ) var cpuLoadDetail: String { "1min: \(load.minute1), 5min: \(load.minute5), 15min: \(load.minute15) (\(load.level))" } + + var formattedServerTime: String { + let isoFormatter = ISO8601DateFormatter() + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "de_DE") + formatter.dateStyle = .medium + formatter.timeStyle = .short + + if let date = isoFormatter.date(from: serverTime) { + return formatter.string(from: date) + } else { + return serverTime // fallback to raw string if parsing fails + } + } + + var formattedVersion: String { + "\(panelVersion) (Build \(panelBuild))" + } } extension ServerInfo { @@ -82,5 +115,10 @@ extension ServerInfo { self.memory = response.utilization.memory self.swap = response.utilization.swap self.diskSpace = response.utilization.diskSpace + + self.panelVersion = response.meta.panelVersion + self.panelBuild = response.meta.panelBuild + self.apiVersion = response.meta.apiVersion + self.additionalPHPInterpreters = response.additionalPHPInterpreters } } diff --git a/iKeyMon/ServerAPI.swift b/iKeyMon/ServerAPI.swift new file mode 100644 index 0000000..c062cd3 --- /dev/null +++ b/iKeyMon/ServerAPI.swift @@ -0,0 +1,67 @@ +// +// ServerAPI.swift +// iKeyMon +// +// Created by tracer on 06.04.25. +// +import Foundation + +final class ServerAPI { + private let hostname: String + private let apiKey: String + + init(hostname: String, apiKey: String) { + self.hostname = hostname + self.apiKey = apiKey + } + + init?(server: Server) { + guard let apiKey = KeychainHelper.loadApiKey(for: server.hostname)?.trimmingCharacters(in: .whitespacesAndNewlines) else { + return nil + } + self.hostname = server.hostname + self.apiKey = apiKey + } + + @discardableResult + func ping() async -> Bool { + guard let url = URL(string: "https://\(hostname)/api/v2/ping") else { + return false + } + + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue(apiKey, forHTTPHeaderField: "X-API-KEY") + + do { + let (data, _ /*response */) = try await URLSession.shared.data(for: request) +// if let httpResponse = response as? HTTPURLResponse { +// print("data: \(String(data: data, encoding: .utf8))") +// } + + if let result = try? JSONDecoder().decode([String: String].self, from: data), result["response"] == "pong" { + return true + } + } catch { + print("❌ Ping error: \(error)") + } + + return false + } + + func fetchServerInfo() async throws -> ServerResponse { + guard let url = URL(string: "https://\(hostname)/api/v2/server") else { + throw URLError(.badURL) + } + + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue(apiKey, forHTTPHeaderField: "X-API-KEY") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + let (data, _) = try await URLSession.shared.data(for: request) + let decoded = try JSONDecoder().decode(ServerResponse.self, from: data) + + return decoded + } +} diff --git a/iKeyMon/Views/AddServerView.swift b/iKeyMon/Views/AddServerView.swift deleted file mode 100644 index 40ebb1f..0000000 --- a/iKeyMon/Views/AddServerView.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// addServerView.swift -// iKeyMon -// -// Created by tracer on 30.03.25. -// - -import SwiftUI - -struct AddServerView: View { - @Environment(\.dismiss) private var dismiss - - @Binding var servers: [Server] - @State private var hostname: String = "" - @State private var apiKey: String = "" - - var saveAction: () -> Void - - var body: some View { - VStack { - Text("Add New Server") - .font(.headline) - - TextField("Hostname", text: $hostname) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .padding(.top) - - SecureField("API Key", text: $apiKey) - .textFieldStyle(RoundedBorderTextFieldStyle()) - - HStack { - Button("Cancel") { - dismiss() - } - Spacer() - Button("Add") { - let newServer = Server(hostname: hostname) - servers.append(newServer) - saveAction() - KeychainHelper.save(apiKey: apiKey, for: hostname) - dismiss() - } - .disabled(hostname.isEmpty || apiKey.isEmpty) - } - .padding(.top) - } - .padding() - .frame(width: 300) - } -} - -#Preview { - AddServerView(servers: .constant([]), saveAction: {}) -} diff --git a/iKeyMon/Views/Cells/InfoCell.swift b/iKeyMon/Views/Cells/InfoCell.swift new file mode 100644 index 0000000..4e23236 --- /dev/null +++ b/iKeyMon/Views/Cells/InfoCell.swift @@ -0,0 +1,47 @@ +// +// InfoBarCell.swift +// iKeyMon +// +// Created by tracer on 03.04.25. +// + +// +// ResourcesBarRow.swift +// iKeyMon +// +// Created by tracer on 31.03.25. +// + +import SwiftUI + +struct InfoCell: View { + var value: [String] + var monospaced: Bool = false + var color: Color = .primary + + init(value: [String], monospaced: Bool = false) { + self.value = value + self.monospaced = monospaced + } + + var body: some View { + VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 2) { + ForEach(value, id: \.self) { item in + Text(item) + .font(monospaced ? .system(.body, design: .monospaced) : .body) + } + } +// if let subtext { +// Text(subtext) +// .font(.caption) +// .foregroundColor(.secondary) +// } + } + } +} + +#Preview { + InfoCell(value: ["Some Text", "Another Text"]) +} + diff --git a/iKeyMon/Views/Rows/LoadBarCell.swift b/iKeyMon/Views/Cells/LoadBarCell.swift similarity index 100% rename from iKeyMon/Views/Rows/LoadBarCell.swift rename to iKeyMon/Views/Cells/LoadBarCell.swift diff --git a/iKeyMon/Views/Rows/UsageBarCell.swift b/iKeyMon/Views/Cells/UsageBarCell.swift similarity index 100% rename from iKeyMon/Views/Rows/UsageBarCell.swift rename to iKeyMon/Views/Cells/UsageBarCell.swift diff --git a/iKeyMon/Views/ContentView.swift b/iKeyMon/Views/ContentView.swift deleted file mode 100644 index d05c2dc..0000000 --- a/iKeyMon/Views/ContentView.swift +++ /dev/null @@ -1,96 +0,0 @@ -// -// ContentView.swift -// iKeyMon -// -// Created by tracer on 30.03.25. -// - -import SwiftUI - - -//struct ContentView: View { -// @State private var showingAddSheet = false -// @State private var showingEditSheet = false -// @State private var editingServer: Server? -// -// let serversKey = "storedServers" -// -// @State private var servers: [Server] = { -// if let data = UserDefaults.standard.data(forKey: "storedServers"), -// let saved = try? JSONDecoder().decode([Server].self, from: data) { -// return saved -// } -// return [] -// }() -// -// var body: some View { -// VStack(alignment: .leading) { -// Text("Monitored Servers") -// .font(.largeTitle) -// .padding(.top) -// -// List { -// ForEach(servers) { server in -// HStack { -// VStack(alignment: .leading) { -// Text(server.hostname) -// .font(.headline) -// if let apiKey = KeychainHelper.loadApiKey(for: server.hostname) { -// Text("API Key: \(apiKey.prefix(8))") -// .font(.caption) -// .foregroundColor(.secondary) -// } else { -// Text("No API key found") -// .font(.caption) -// .foregroundColor(.red) -// } -// } -// Spacer() -// Button("Edit") { -// editingServer = server -// showingEditSheet = true -// } -// .buttonStyle(BorderlessButtonStyle()) -// } -// } -// .onDelete(perform: deleteServer) -// } -// -// HStack { -// Spacer() -// Button("Add Server") { -// showingAddSheet = true -// } -// .padding(.bottom) -// } -// } -// .padding() -// .frame(minWidth: 400, minHeight: 300) -// .sheet(isPresented: $showingAddSheet) { -// AddServerView(servers: $servers, saveAction: saveServers) -// } -// .sheet(item: $editingServer) { server in -// let _ = print(server.hostname) -// EditServerView( -// server: server, -// servers: $servers, -// saveAction: saveServers, -// dismiss: { self.showingEditSheet = false } -// ) -// } -// } -// -// -// private func deleteServer(at offsets: IndexSet) { -// for index in offsets { -// let hostname = servers[index].hostname -// KeychainHelper.deleteApiKey(for: hostname) -// } -// servers.remove(atOffsets: offsets) -// saveServers() -// } -//} -// -//#Preview { -// ContentView() -//} diff --git a/iKeyMon/Views/MainView.swift b/iKeyMon/Views/MainView.swift index 766acf4..51a5f95 100644 --- a/iKeyMon/Views/MainView.swift +++ b/iKeyMon/Views/MainView.swift @@ -8,14 +8,33 @@ import SwiftUI struct MainView: View { + @State var showAddServerSheet: Bool = false @State private var serverBeingEdited: Server? @State private var serverToDelete: Server? @State private var showDeleteConfirmation = false - + @State private var isFetchingInfo: Bool = false + @State private var refreshTimer = Timer.publish(every: 60, on: .main, in: .common).autoconnect() + @State private var progress: Double = 0 + @State private var lastRefresh = Date() + @State private var pingTimer: Timer? + private let serverOrderKey = "serverOrder" + @State private var servers: [Server] = { if let data = UserDefaults.standard.data(forKey: "storedServers"), let saved = try? JSONDecoder().decode([Server].self, from: data) { + + if let idStrings = UserDefaults.standard.stringArray(forKey: "serverOrder") { + let idMap = idStrings.compactMap(UUID.init) + return saved.sorted { a, b in + guard + let i1 = idMap.firstIndex(of: a.id), + let i2 = idMap.firstIndex(of: b.id) + else { return false } + return i1 < i2 + } + } + return saved } return [] @@ -25,25 +44,29 @@ struct MainView: View { @State private var selectedServerID: UUID? var body: some View { + NavigationSplitView { - List(servers, selection: $selectedServerID) { server in - HStack { - Image(systemName: "dot.circle.fill") - .foregroundColor(.green) // ← later update based on ping - Text(server.hostname) - } - .tag(server) - .contextMenu { - Button("Edit") { - print("Editing:", server.hostname) - serverBeingEdited = server + List(selection: $selectedServerID) { + ForEach(servers) { server in + HStack { + Image(systemName: "dot.circle.fill") + .foregroundColor(server.pingable ? .green : .red) + Text(server.hostname) } - Divider() - Button("Delete", role: .destructive) { - serverToDelete = server - showDeleteConfirmation = true + .tag(server) + .contextMenu { + Button("Edit") { + print("Editing:", server.hostname) + serverBeingEdited = server + } + Divider() + Button("Delete", role: .destructive) { + serverToDelete = server + showDeleteConfirmation = true + } } } + .onMove(perform: moveServer) } .toolbar { ToolbarItem(placement: .primaryAction) { @@ -56,16 +79,14 @@ struct MainView: View { .navigationTitle("Servers") .onChange(of: selectedServerID) { if let selectedServerID { + UserDefaults.standard.set(selectedServerID.uuidString, forKey: "selectedServerID") fetchServerInfo(for: selectedServerID) } } } detail: { - let _ = print("selectedServerID: \(selectedServerID ?? UUID())") -// if let id = selectedServerID, if let selectedServerID, let index = servers.firstIndex(where: { selectedServerID == $0.id }) { - let _ = print( "index: %d\n", index) - ServerDetailView(server: $servers[index]) + ServerDetailView(server: $servers[index], isFetching: isFetchingInfo) } else { ContentUnavailableView("No Server Selected", systemImage: "server.rack") } @@ -78,7 +99,6 @@ struct MainView: View { ) } .sheet(item: $serverBeingEdited) { server in - let _ = print("serverBeingEdited: \(server)") ServerFormView( mode: .edit(server), servers: $servers, @@ -91,39 +111,44 @@ struct MainView: View { } Button("Cancel", role: .cancel) {} } + .onReceive(refreshTimer) { _ in + for server in servers { + print("fetching server: \(server.hostname)") + fetchServerInfo(for: server.id) + } + } + .onAppear { + if let storedID = UserDefaults.standard.string(forKey: "selectedServerID"), + let uuid = UUID(uuidString: storedID), + servers.contains(where: { $0.id == uuid }) { + selectedServerID = uuid + } else if selectedServerID == nil, let first = servers.first { + selectedServerID = first.id + } + pingAllServers() + pingTimer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: true) { _ in + pingAllServers() + } + } + .frame(minWidth: 800, minHeight: 450) + } private func fetchServerInfo(for id: UUID) { - guard let server = servers.first(where: { $0.id == id }) else { - print ("server not found: \(id)") + guard let server = servers.first(where: { $0.id == id }), + let api = ServerAPI(server: server) else { return } - print("after guard") - print("fetching server: \(server.hostname)") - let apiKey = (KeychainHelper.loadApiKey(for: server.hostname) ?? "") - .trimmingCharacters(in: .whitespacesAndNewlines) - guard let url = URL(string: "https://\(server.hostname)/api/v2/server") else { return } - print("after url") + isFetchingInfo = true - var request = URLRequest(url: url) - request.httpMethod = "GET" - request.setValue(apiKey, forHTTPHeaderField: "X-API-KEY") - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - - print("after request") Task { + defer { isFetchingInfo = false } do { - print("inside task") - let (data, _) = try await URLSession.shared.data(for: request) - let decoded = try JSONDecoder().decode(ServerResponse.self, from: data) - print("after decode") - + let info = try await api.fetchServerInfo() if let index = servers.firstIndex(where: { $0.id == id }) { - print("index found: \(index)") var updated = servers[index] - updated.info = ServerInfo(from: decoded) - dump(updated.info) + updated.info = ServerInfo(from: info) servers[index] = updated } } catch { @@ -132,7 +157,72 @@ struct MainView: View { } } -} + private func moveServer(from source: IndexSet, to destination: Int) { + servers.move(fromOffsets: source, toOffset: destination) + saveServerOrder() + } + + private func saveServerOrder() { + let ids = servers.map { $0.id.uuidString } + UserDefaults.standard.set(ids, forKey: serverOrderKey) + } + + private struct PingResponse: Codable { + let response: String + } + +// func pingServer(_ server: Server) async -> Bool { +// let hostname = server.hostname +// guard let url = URL(string: "https://\(hostname)/api/v2/ping") else { +// return false +// } +// +// var request = URLRequest(url: url) +// request.httpMethod = "GET" +// request.timeoutInterval = 5 +// request.setValue("application/json", forHTTPHeaderField: "Content-Type") +// +// let apiKey = KeychainHelper.loadApiKey(for: hostname)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" +// request.setValue(apiKey, forHTTPHeaderField: "X-API-KEY") +// +// do { +// let (data, response) = try await URLSession.shared.data(for: request) +// if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 { +// do { +// let decoded = try JSONDecoder().decode(PingResponse.self, from: data) +// if decoded.response == "pong" { +// return true +// } else { +// print("❌ Unexpected response: \(decoded.response)") +// return false +// } +// } catch { +// print("❌ Failed to decode JSON: \(error)") +// return false +// } +// } else { +// return false +// } +// } catch { +// print("[Ping] \(server.hostname): \(error.localizedDescription)") +// return false +// } +// } + + func pingAllServers() { + for (index, server) in servers.enumerated() { + Task { + let apiKey = KeychainHelper.loadApiKey(for: server.hostname)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + + let api = ServerAPI(hostname: server.hostname, apiKey: apiKey) + let pingable = await api.ping() + + servers[index].pingable = pingable + } + } + } + } + #Preview { MainView() diff --git a/iKeyMon/Views/Rows/ResouceRow.swift b/iKeyMon/Views/Rows/ResouceRow.swift deleted file mode 100644 index 5c8038d..0000000 --- a/iKeyMon/Views/Rows/ResouceRow.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// ResouceRow.swift -// iKeyMon -// -// Created by tracer on 31.03.25. -// - -//import SwiftUI -// -//struct ResourceRow: View { -// let label: String -// let value: String -// var subtext: String? = nil -// -// var body: some View { -// HStack(spacing: 6) { -// Text(label) -// .fontWeight(.semibold) -// -// Text(value) -// .foregroundColor(.green) -// .fontWeight(.semibold) -// -// if let subtext { -// Text("(\(subtext))") -// .foregroundColor(.secondary) -// .font(.callout) -// } -// -// Spacer() -// } -// .padding(.vertical, 2) -// } -//} -// -//#Preview { -// ResourceRow(label: "Test", value: "value", subtext: "some additional info") -//} diff --git a/iKeyMon/Views/Rows/TableRowView.swift b/iKeyMon/Views/Rows/TableRowView.swift index 9dce0a7..aafa070 100644 --- a/iKeyMon/Views/Rows/TableRowView.swift +++ b/iKeyMon/Views/Rows/TableRowView.swift @@ -8,18 +8,25 @@ import SwiftUI struct TableRowView<Label: View, Value: View>: View { + var showDivider: Bool = true @ViewBuilder let label: () -> Label @ViewBuilder let value: () -> Value var body: some View { - HStack(alignment: .top) { - label() - .frame(width: 100, alignment: .leading) - - value() - .frame(maxWidth: .infinity, alignment: .leading) + VStack(spacing: 0) { + HStack(alignment: .top) { + label() + .frame(width: 180, alignment: .leading) + + value() + .frame(maxWidth: .infinity, alignment: .leading) + } + .padding(.vertical, 2) + } + if showDivider { + Divider() + .opacity(0.6) } - .padding(.vertical, 2) } } diff --git a/iKeyMon/Views/ServerDetailView.swift b/iKeyMon/Views/ServerDetailView.swift index 9824384..d0b4f7a 100644 --- a/iKeyMon/Views/ServerDetailView.swift +++ b/iKeyMon/Views/ServerDetailView.swift @@ -9,27 +9,81 @@ import SwiftUI struct ServerDetailView: View { @Binding var server: Server - + var isFetching: Bool + + @State private var progress: Double = 0 + let timer = Timer.publish(every: 1.0 / 60.0, on: .main, in: .common).autoconnect() + var body: some View { - if server.info == nil { - ProgressView("Fetching server info...") - .frame(maxWidth: .infinity, maxHeight: .infinity) - } else { - TabView { - GeneralView(server: $server) - .tabItem { - Text("General") + VStack(spacing: 0) { + ProgressView(value: progress) + .progressViewStyle(LinearProgressViewStyle()) + .padding(.horizontal) + .frame(height: 2) + + if server.info == nil { + ProgressView("Fetching server info...") + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else { + ZStack(alignment: .topTrailing) { + VStack(spacing: 0) { + Spacer().frame(height: 6) + TabView { + GeneralView(server: $server) + .tabItem { + Text("General") + } + ResourcesView(server: $server) + .tabItem { + Text("Resources") + } + ServicesView(server: $server) + .tabItem { + Text("Services") + } + } } - ResourcesView(server: $server) - .tabItem { - Text("Resources") - } - ServicesView(server: $server) - .tabItem { - Text("Services") + + if isFetching { + ProgressView() + .scaleEffect(0.5) + .padding() } + } + .padding(0) + } + } + .onReceive(timer) { _ in + withAnimation(.linear(duration: 1.0 / 60.0)) { + progress += 1.0 / (60.0 * 60.0) + if progress >= 1 { progress = 0 } } - .padding(0) } } } + +#Preview { + ServerDetailView( + server: .constant(Server(id: UUID(), hostname: "preview.example.com", info: ServerInfo( + hostname: "preview.example.com", + ipAddresses: ["192.168.1.1", "fe80::1"], + cpuCores: 4, + serverTime: "2025-04-04T18:00:00+0200", + uptime: "3 Days / 12 Hours / 30 Minutes", + processCount: 123, + apacheVersion: "2.4.58", + phpVersion: "8.2.12", + mysqlVersion: "8.0.33", + mariadbVersion: nil, + ports: nil, + load: Load(minute1: 0.5, minute5: 0.3, minute15: 0.2, percent: 10.0, cpuCount: 4, level: "low"), + memory: Memory(free: 8_000_000_000, used: 4_000_000_000, total: 12_000_000_000, percent: 33.3), + swap: Memory(free: 4_000_000_000, used: 1_000_000_000, total: 5_000_000_000, percent: 20.0), + diskSpace: DiskSpace(free: 100_000_000_000, used: 50_000_000_000, total: 150_000_000_000, percent: 33.3), + panelVersion: "25.0", + panelBuild: "3394", + apiVersion: "2" + ))), + isFetching: false + ) +} diff --git a/iKeyMon/Views/ServerFormView.swift b/iKeyMon/Views/ServerFormView.swift index 11ceb9b..4b5a01f 100644 --- a/iKeyMon/Views/ServerFormView.swift +++ b/iKeyMon/Views/ServerFormView.swift @@ -61,7 +61,9 @@ struct ServerFormView: View { } Spacer() Button("Test connection") { - testConnection() + Task { + await testConnection() + } } Button("Save") { saveServer() @@ -69,7 +71,7 @@ struct ServerFormView: View { saveServers() dismiss() } -// .disabled(hostname.isEmpty || apiKey.isEmpty || !con nectionOK) + .disabled(hostname.isEmpty || apiKey.isEmpty || !connectionOK) } .padding(.top) } @@ -96,43 +98,45 @@ struct ServerFormView: View { } } - private func testConnection() { - // @State is no reliable source + private func testConnection() async { let host = hostname.trimmingCharacters(in: .whitespacesAndNewlines) let key = apiKey.trimmingCharacters(in: .whitespacesAndNewlines) - guard let url = URL(string: "https://\(host)/api/v2/ping") else { - print("❌ Invalid URL") - return - } - - var request = URLRequest(url: url) - request.httpMethod = "GET" - request.setValue(key, forHTTPHeaderField: "X-API-KEY") - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - - print("🔍 Headers: \(request.allHTTPHeaderFields ?? [:])") - - Task { - do { - let (data, response) = try await URLSession.shared.data(for: request) - if let httpResponse = response as? HTTPURLResponse { - print("🔄 Status: \(httpResponse.statusCode)") - } - - if let result = try? JSONDecoder().decode([String: String].self, from: data), - result["response"] == "pong" { - print("✅ Pong received") - connectionOK = true - } else { - print("⚠️ Unexpected response") - connectionOK = false - } - } catch { - print("❌ Error: \(error)") - connectionOK = false - } - } + let pinger = ServerAPI(hostname: host, apiKey: key) + connectionOK = await pinger.ping() +// +// guard let url = URL(string: "https://\(host)/api/v2/ping") else { +// print("❌ Invalid URL") +// return +// } +// +// var request = URLRequest(url: url) +// request.httpMethod = "GET" +// request.setValue(key, forHTTPHeaderField: "X-API-KEY") +// request.setValue("application/json", forHTTPHeaderField: "Content-Type") +// +// print("🔍 Headers: \(request.allHTTPHeaderFields ?? [:])") +// +// Task { +// do { +// let (data, response) = try await URLSession.shared.data(for: request) +// if let httpResponse = response as? HTTPURLResponse { +// print("🔄 Status: \(httpResponse.statusCode)") +// } +// +// if let result = try? JSONDecoder().decode([String: String].self, from: data), +// result["response"] == "pong" { +// print("✅ Pong received") +// connectionOK = true +// } else { +// print("⚠️ Unexpected response") +// connectionOK = false +// } +// } catch { +// print("❌ Error: \(error)") +// connectionOK = false +// } +// } } // func testKeyHelpConnection() { diff --git a/iKeyMon/Views/Tabs/GeneralView.swift b/iKeyMon/Views/Tabs/GeneralView.swift index 41ac0b1..faca5ac 100644 --- a/iKeyMon/Views/Tabs/GeneralView.swift +++ b/iKeyMon/Views/Tabs/GeneralView.swift @@ -11,26 +11,60 @@ struct GeneralView: View { @Binding var server: Server var body: some View { - VStack(alignment: .leading, spacing: 12) { - Form { - Section { - InfoRow(label: "Hostname", value: server.info?.hostname ?? "—") - InfoRow(label: "IP addresses", value: server.info?.ipAddresses ?? []) - // InfoRow(label: "Processor", value: info.processor) - // InfoRow(label: "CPU cores", value: "\(info.cpuCores)") - // InfoRow(label: "System virtualization", value: info.virtualization) - // InfoRow(label: "Server time", value: info.serverTime) - // InfoRow(label: "Uptime", value: info.uptime) - // InfoRow(label: "SSH fingerprint", value: info.sshFingerprint) - // InfoRow(label: "DKIM DNS record", value: "Show", color: .yellow) - // } header: { - Text("General").font(.headline) + GeometryReader { geometry in + ScrollView { + VStack(alignment: .leading, spacing: 6) { + TableRowView { + Text("Hostname") + } value: { + InfoCell(value: [server.hostname]) + } + + TableRowView { + Text("IP addresses") + } value: { + InfoCell(value: server.info?.ipAddresses ?? [], monospaced: true) + } + + TableRowView { + Text("Server time") + } value: { + InfoCell(value: [server.info?.formattedServerTime ?? ""], monospaced: true) + } + + TableRowView { + Text("Uptime") + } value: { + InfoCell(value: [server.info?.uptime ?? ""]) + } + + TableRowView { + Text("KeyHelp version") + } value: { + InfoCell(value: [server.info?.formattedVersion ?? ""], monospaced: true) + } + + TableRowView { + Text("Sytem PHP version") + } value: { + InfoCell(value: [server.info?.phpVersion ?? ""], monospaced: true) + } + + TableRowView(showDivider: false) { + Text("Additional PHP interpreters") + } value: { + InfoCell( + value: server.info?.additionalPHPInterpreters?.map { $0.versionFull } ?? [], + monospaced: true + ) + } } + .padding() + .frame(minHeight: geometry.size.height, alignment: .top) } - .formStyle(.grouped) - .padding(0) + .padding() + .scrollDisabled(true) } - .frame(maxWidth: .infinity, alignment: .leading) } } diff --git a/iKeyMon/Views/Tabs/ResourcesView.swift b/iKeyMon/Views/Tabs/ResourcesView.swift index caed2c5..983e518 100644 --- a/iKeyMon/Views/Tabs/ResourcesView.swift +++ b/iKeyMon/Views/Tabs/ResourcesView.swift @@ -62,7 +62,6 @@ struct ResourcesView: View { ) } } - .frame(maxWidth: .infinity, alignment: .topLeading) .padding() .frame(minHeight: geometry.size.height, alignment: .top) } diff --git a/iKeyMon/iKeyMonApp.swift b/iKeyMon/iKeyMonApp.swift index 63f2c49..31b741f 100644 --- a/iKeyMon/iKeyMonApp.swift +++ b/iKeyMon/iKeyMonApp.swift @@ -16,5 +16,6 @@ struct iKeyMonApp: App { NSApp.terminate(nil) } } + .windowResizability(.contentMinSize) } }