first public commit
This commit is contained in:
parent
5cf094da16
commit
6bb7db1fa4
Screenshots
iKeyMon
BIN
Screenshots/edit_server.png
Normal file
BIN
Screenshots/edit_server.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 163 KiB |
BIN
Screenshots/general_view.png
Normal file
BIN
Screenshots/general_view.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 184 KiB |
BIN
Screenshots/resources_view.png
Normal file
BIN
Screenshots/resources_view.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 174 KiB |
BIN
Screenshots/services_view.png
Normal file
BIN
Screenshots/services_view.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 185 KiB |
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
67
iKeyMon/ServerAPI.swift
Normal file
67
iKeyMon/ServerAPI.swift
Normal file
@ -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
|
||||
}
|
||||
}
|
@ -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: {})
|
||||
}
|
47
iKeyMon/Views/Cells/InfoCell.swift
Normal file
47
iKeyMon/Views/Cells/InfoCell.swift
Normal file
@ -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"])
|
||||
}
|
||||
|
@ -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()
|
||||
//}
|
@ -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()
|
||||
|
@ -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")
|
||||
//}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -62,7 +62,6 @@ struct ResourcesView: View {
|
||||
)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .topLeading)
|
||||
.padding()
|
||||
.frame(minHeight: geometry.size.height, alignment: .top)
|
||||
}
|
||||
|
@ -16,5 +16,6 @@ struct iKeyMonApp: App {
|
||||
NSApp.terminate(nil)
|
||||
}
|
||||
}
|
||||
.windowResizability(.contentMinSize)
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user