add/edit/delete servers done
This commit is contained in:
parent
57ad42480f
commit
5cf094da16
@ -1,24 +0,0 @@
|
|||||||
//
|
|
||||||
// ContentView.swift
|
|
||||||
// iKeyMon
|
|
||||||
//
|
|
||||||
// Created by tracer on 30.03.25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct ContentView: View {
|
|
||||||
var body: some View {
|
|
||||||
VStack {
|
|
||||||
Image(systemName: "globe")
|
|
||||||
.imageScale(.large)
|
|
||||||
.foregroundStyle(.tint)
|
|
||||||
Text("Hello, world!")
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
ContentView()
|
|
||||||
}
|
|
23
iKeyMon/Extensions/ByteFormatting.swift
Normal file
23
iKeyMon/Extensions/ByteFormatting.swift
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
//
|
||||||
|
// ByteFormatting.swift
|
||||||
|
// iKeyMon
|
||||||
|
//
|
||||||
|
// Created by tracer on 01.04.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Int {
|
||||||
|
func toNiceBinaryUnit() -> String {
|
||||||
|
let units = ["B", "KB", "MB", "GB", "TB", "PB"]
|
||||||
|
var value = Double(self)
|
||||||
|
var index = 0
|
||||||
|
|
||||||
|
while value >= 1024 && index < units.count - 1 {
|
||||||
|
value /= 1024
|
||||||
|
index += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(format: "%.2f %@", value, units[index])
|
||||||
|
}
|
||||||
|
}
|
51
iKeyMon/KeychainHelper.swift
Normal file
51
iKeyMon/KeychainHelper.swift
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
//
|
||||||
|
// KeychainHelper.swift
|
||||||
|
// iKeyMon
|
||||||
|
//
|
||||||
|
// Created by tracer on 30.03.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Security
|
||||||
|
|
||||||
|
enum KeychainHelper {
|
||||||
|
static func save(apiKey: String, for hostname: String) {
|
||||||
|
let data = Data(apiKey.utf8)
|
||||||
|
let query: [String: Any] = [
|
||||||
|
kSecClass as String: kSecClassGenericPassword,
|
||||||
|
kSecAttrAccount as String: hostname,
|
||||||
|
kSecValueData as String: data
|
||||||
|
]
|
||||||
|
|
||||||
|
SecItemDelete(query as CFDictionary) // Overwrite existing
|
||||||
|
SecItemAdd(query as CFDictionary, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func loadApiKey(for hostname: String) -> String? {
|
||||||
|
let query: [String: Any] = [
|
||||||
|
kSecClass as String: kSecClassGenericPassword,
|
||||||
|
kSecAttrAccount as String: hostname,
|
||||||
|
kSecReturnData as String: true,
|
||||||
|
kSecMatchLimit as String: kSecMatchLimitOne
|
||||||
|
]
|
||||||
|
|
||||||
|
var result: AnyObject?
|
||||||
|
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||||
|
|
||||||
|
if status == errSecSuccess,
|
||||||
|
let data = result as? Data,
|
||||||
|
let string = String(data: data, encoding: .utf8) {
|
||||||
|
return string
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
static func deleteApiKey(for hostname: String) {
|
||||||
|
let query: [String: Any] = [
|
||||||
|
kSecClass as String: kSecClassGenericPassword,
|
||||||
|
kSecAttrAccount as String: hostname
|
||||||
|
]
|
||||||
|
SecItemDelete(query as CFDictionary)
|
||||||
|
}
|
||||||
|
}
|
179
iKeyMon/Model/API/ServerResponse.swift
Normal file
179
iKeyMon/Model/API/ServerResponse.swift
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
//
|
||||||
|
// ServerResponse.swift
|
||||||
|
// iKeyMon
|
||||||
|
//
|
||||||
|
// Created by tracer on 30.03.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
// MARK: - Root Response
|
||||||
|
struct ServerResponse: Decodable {
|
||||||
|
let meta: Meta
|
||||||
|
let operatingSystem: OperatingSystem
|
||||||
|
let utilization: Utilization
|
||||||
|
let resources: Resources
|
||||||
|
let components: Components
|
||||||
|
let additionalPHPInterpreters: [PHPInterpreter]?
|
||||||
|
let ports: [ServicePort]?
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case meta
|
||||||
|
case operatingSystem = "operating_system"
|
||||||
|
case utilization, resources, components
|
||||||
|
case additionalPHPInterpreters = "additional_php_interpreters"
|
||||||
|
case ports
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Meta
|
||||||
|
struct Meta: Decodable {
|
||||||
|
let hostname: String
|
||||||
|
let ipAddresses: [String]
|
||||||
|
let serverTime: String
|
||||||
|
let uptime: Uptime
|
||||||
|
let panelVersion: String
|
||||||
|
let panelBuild: String
|
||||||
|
let apiVersion: String
|
||||||
|
let apiDocs: String
|
||||||
|
let keyhelpPro: Bool
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case hostname
|
||||||
|
case ipAddresses = "ip_addresses"
|
||||||
|
case serverTime = "server_time"
|
||||||
|
case uptime
|
||||||
|
case panelVersion = "panel_version"
|
||||||
|
case panelBuild = "panel_build"
|
||||||
|
case apiVersion = "api_version"
|
||||||
|
case apiDocs = "api_docs"
|
||||||
|
case keyhelpPro = "keyhelp_pro"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Uptime: Decodable {
|
||||||
|
let days, hours, minutes, seconds: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - OperatingSystem
|
||||||
|
struct OperatingSystem: Decodable {
|
||||||
|
let label, distribution, version, architecture: String
|
||||||
|
let endOfLife: Bool
|
||||||
|
let updates: Updates
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case label, distribution, version, architecture
|
||||||
|
case endOfLife = "end_of_life"
|
||||||
|
case updates
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Updates: Decodable {
|
||||||
|
let updateCount, securityUpdateCount: Int
|
||||||
|
let rebootRequired: Bool
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case updateCount = "update_count"
|
||||||
|
case securityUpdateCount = "security_update_count"
|
||||||
|
case rebootRequired = "reboot_required"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Utilization
|
||||||
|
struct Utilization: Decodable {
|
||||||
|
let processCount, emailsInQueue: Int
|
||||||
|
let load: Load
|
||||||
|
let diskSpace: DiskSpace
|
||||||
|
let inodes: Inodes
|
||||||
|
let memory, swap: Memory
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case processCount = "process_count"
|
||||||
|
case emailsInQueue = "emails_in_queue"
|
||||||
|
case load
|
||||||
|
case diskSpace = "disk_space"
|
||||||
|
case inodes, memory, swap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Load: Decodable {
|
||||||
|
let minute1, minute5, minute15, percent: Double
|
||||||
|
let cpuCount: Int
|
||||||
|
let level: String
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case minute1 = "minute_1"
|
||||||
|
case minute5 = "minute_5"
|
||||||
|
case minute15 = "minute_15"
|
||||||
|
case cpuCount = "cpu_count"
|
||||||
|
case percent, level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DiskSpace: Decodable {
|
||||||
|
let free, used, total: Int
|
||||||
|
let percent: Double
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Inodes: Decodable {
|
||||||
|
let free, used, total: Int
|
||||||
|
let percent: Double
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Memory: Decodable {
|
||||||
|
let free, used, total: Int
|
||||||
|
let percent: Double
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Resources
|
||||||
|
struct Resources: Decodable {
|
||||||
|
let adminAccounts, clientAccounts, domains, subdomains: Int
|
||||||
|
let emailAccounts, emailAddresses, emailForwardings, databases: Int
|
||||||
|
let ftpUsers, scheduledTasks: Int
|
||||||
|
let consumedDiskSpace, traffic: Int
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case adminAccounts = "admin_accounts"
|
||||||
|
case clientAccounts = "client_accounts"
|
||||||
|
case domains, subdomains
|
||||||
|
case emailAccounts = "email_accounts"
|
||||||
|
case emailAddresses = "email_addresses"
|
||||||
|
case emailForwardings = "email_forwardings"
|
||||||
|
case databases
|
||||||
|
case ftpUsers = "ftp_users"
|
||||||
|
case scheduledTasks = "scheduled_tasks"
|
||||||
|
case consumedDiskSpace = "consumed_disk_space"
|
||||||
|
case traffic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Components
|
||||||
|
struct Components: Decodable {
|
||||||
|
let kernel, apache, php, proftpd, dovecot, postfix: String
|
||||||
|
let mariadb, mysql: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PHPInterpreter: Decodable {
|
||||||
|
let version, versionFull: String
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case version
|
||||||
|
case versionFull = "version_full"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ServicePort: Decodable, Identifiable {
|
||||||
|
let service: String
|
||||||
|
let port: Int
|
||||||
|
let proto: String
|
||||||
|
let status: String
|
||||||
|
var id: String { service }
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case service
|
||||||
|
case port
|
||||||
|
case proto = "protocol"
|
||||||
|
case status
|
||||||
|
}
|
||||||
|
}
|
37
iKeyMon/Model/Server.swift
Normal file
37
iKeyMon/Model/Server.swift
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
//
|
||||||
|
// Server.swift
|
||||||
|
// iKeyMon
|
||||||
|
//
|
||||||
|
// Created by tracer on 30.03.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct Server: Identifiable, Codable, Hashable, Equatable {
|
||||||
|
let id: UUID
|
||||||
|
var hostname: String
|
||||||
|
|
||||||
|
// runtime-only, skip for Codable / Hashable / Equatable
|
||||||
|
var info: ServerInfo? = nil
|
||||||
|
|
||||||
|
init(id: UUID = UUID(), hostname: String, info: ServerInfo? = nil) {
|
||||||
|
self.id = id
|
||||||
|
self.hostname = hostname
|
||||||
|
self.info = info
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Manual conformance
|
||||||
|
|
||||||
|
static func == (lhs: Server, rhs: Server) -> Bool {
|
||||||
|
lhs.id == rhs.id && lhs.hostname == rhs.hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(id)
|
||||||
|
hasher.combine(hostname)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case id, hostname
|
||||||
|
}
|
||||||
|
}
|
86
iKeyMon/Model/ServerInfo.swift
Normal file
86
iKeyMon/Model/ServerInfo.swift
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
//
|
||||||
|
// ServerInfo.swift
|
||||||
|
// iKeyMon
|
||||||
|
//
|
||||||
|
// Created by tracer on 30.03.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
struct ServerInfo: Decodable {
|
||||||
|
var hostname: String
|
||||||
|
var ipAddresses: [String]
|
||||||
|
// var processor: String
|
||||||
|
var cpuCores: Int
|
||||||
|
// var virtualization: String
|
||||||
|
var serverTime: String
|
||||||
|
var uptime: String
|
||||||
|
// var sshFingerprint: String
|
||||||
|
var processCount: Int
|
||||||
|
var apacheVersion: String
|
||||||
|
var phpVersion: String
|
||||||
|
var mysqlVersion: String?
|
||||||
|
var mariadbVersion: String?
|
||||||
|
var ports: [ServicePort]?
|
||||||
|
var load: Load
|
||||||
|
var memory: Memory
|
||||||
|
var swap: Memory
|
||||||
|
var diskSpace: DiskSpace
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ServerInfo {
|
||||||
|
static let placeholder = ServerInfo(
|
||||||
|
hostname: "keyhelp.lab.24unix.net",
|
||||||
|
ipAddresses: ["192.168.99.44", "2a03:..."],
|
||||||
|
// processor: "Common processor (arm64)",
|
||||||
|
cpuCores: 8,
|
||||||
|
// virtualization: "QEMU",
|
||||||
|
serverTime: "Sunday, March 30, 2025 at 08:01 PM (Europe/Berlin)",
|
||||||
|
uptime: "6 Days / 7 Hours / 16 Minutes",
|
||||||
|
// sshFingerprint: "Ed25519 / ECDSA / RSA",
|
||||||
|
processCount: 123,
|
||||||
|
apacheVersion: "2.4",
|
||||||
|
phpVersion: "8.4",
|
||||||
|
ports: [],
|
||||||
|
load: Load(
|
||||||
|
minute1: 0.42,
|
||||||
|
minute5: 0.31,
|
||||||
|
minute15: 0.29,
|
||||||
|
percent: 10.5,
|
||||||
|
cpuCount: 4,
|
||||||
|
level: "low"
|
||||||
|
),
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
|
||||||
|
var cpuLoadDetail: String {
|
||||||
|
"1min: \(load.minute1), 5min: \(load.minute5), 15min: \(load.minute15) (\(load.level))"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ServerInfo {
|
||||||
|
init(from response: ServerResponse) {
|
||||||
|
self.hostname = response.meta.hostname
|
||||||
|
self.ipAddresses = response.meta.ipAddresses
|
||||||
|
self.serverTime = response.meta.serverTime
|
||||||
|
|
||||||
|
let u = response.meta.uptime
|
||||||
|
self.uptime = "\(u.days) Days / \(u.hours) Hours / \(u.minutes) Minutes"
|
||||||
|
|
||||||
|
self.cpuCores = response.utilization.load.cpuCount
|
||||||
|
self.processCount = response.utilization.processCount
|
||||||
|
|
||||||
|
self.apacheVersion = response.components.apache
|
||||||
|
self.phpVersion = response.components.php
|
||||||
|
|
||||||
|
self.mysqlVersion = response.components.mysql ?? ""
|
||||||
|
self.mariadbVersion = response.components.mariadb ?? ""
|
||||||
|
|
||||||
|
self.ports = response.ports
|
||||||
|
|
||||||
|
self.load = response.utilization.load
|
||||||
|
self.memory = response.utilization.memory
|
||||||
|
self.swap = response.utilization.swap
|
||||||
|
self.diskSpace = response.utilization.diskSpace
|
||||||
|
}
|
||||||
|
}
|
54
iKeyMon/Views/AddServerView.swift
Normal file
54
iKeyMon/Views/AddServerView.swift
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
//
|
||||||
|
// 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: {})
|
||||||
|
}
|
96
iKeyMon/Views/ContentView.swift
Normal file
96
iKeyMon/Views/ContentView.swift
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
//
|
||||||
|
// 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()
|
||||||
|
//}
|
139
iKeyMon/Views/MainView.swift
Normal file
139
iKeyMon/Views/MainView.swift
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
//
|
||||||
|
// MainView.swift
|
||||||
|
// iKeyMon
|
||||||
|
//
|
||||||
|
// Created by tracer on 30.03.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
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 servers: [Server] = {
|
||||||
|
if let data = UserDefaults.standard.data(forKey: "storedServers"),
|
||||||
|
let saved = try? JSONDecoder().decode([Server].self, from: data) {
|
||||||
|
return saved
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}()
|
||||||
|
|
||||||
|
// @State private var selectedServer: Server?
|
||||||
|
@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
|
||||||
|
}
|
||||||
|
Divider()
|
||||||
|
Button("Delete", role: .destructive) {
|
||||||
|
serverToDelete = server
|
||||||
|
showDeleteConfirmation = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .primaryAction) {
|
||||||
|
Button(action: { showAddServerSheet = true }) {
|
||||||
|
Image(systemName: "plus")
|
||||||
|
}
|
||||||
|
.help("Add Host")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Servers")
|
||||||
|
.onChange(of: selectedServerID) {
|
||||||
|
if let 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])
|
||||||
|
} else {
|
||||||
|
ContentUnavailableView("No Server Selected", systemImage: "server.rack")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showAddServerSheet) {
|
||||||
|
ServerFormView(
|
||||||
|
mode: .add,
|
||||||
|
servers: $servers,
|
||||||
|
dismiss: { showAddServerSheet = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.sheet(item: $serverBeingEdited) { server in
|
||||||
|
let _ = print("serverBeingEdited: \(server)")
|
||||||
|
ServerFormView(
|
||||||
|
mode: .edit(server),
|
||||||
|
servers: $servers,
|
||||||
|
dismiss: { serverBeingEdited = nil }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.alert("Are you sure you want to delete this server?", isPresented: $showDeleteConfirmation, presenting: serverToDelete) { server in
|
||||||
|
Button("Delete", role: .destructive) {
|
||||||
|
ServerFormView.delete(server: server, from: &servers)
|
||||||
|
}
|
||||||
|
Button("Cancel", role: .cancel) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func fetchServerInfo(for id: UUID) {
|
||||||
|
guard let server = servers.first(where: { $0.id == id }) else {
|
||||||
|
print ("server not found: \(id)")
|
||||||
|
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")
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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")
|
||||||
|
|
||||||
|
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)
|
||||||
|
servers[index] = updated
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("❌ Failed to fetch server data: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
MainView()
|
||||||
|
}
|
42
iKeyMon/Views/Rows/InfoRow.swift
Normal file
42
iKeyMon/Views/Rows/InfoRow.swift
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
//
|
||||||
|
// InfoView.swift
|
||||||
|
// iKeyMon
|
||||||
|
//
|
||||||
|
// Created by tracer on 30.03.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct InfoRow: View {
|
||||||
|
var label: String
|
||||||
|
var value: [String]
|
||||||
|
var color: Color = .primary
|
||||||
|
|
||||||
|
init(label: String, value: String) {
|
||||||
|
self.label = label
|
||||||
|
self.value = [value]
|
||||||
|
}
|
||||||
|
|
||||||
|
init(label: String, value: [String]) {
|
||||||
|
self.label = label
|
||||||
|
self.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(alignment: .top) {
|
||||||
|
Text(label)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.frame(width: 160, alignment: .leading)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
ForEach(value, id: \.self) { item in
|
||||||
|
Text(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#Preview {
|
||||||
|
InfoRow(label: "Hostname", value: "keyhelp.lab.24unix.net")
|
||||||
|
}
|
37
iKeyMon/Views/Rows/LoadBarCell.swift
Normal file
37
iKeyMon/Views/Rows/LoadBarCell.swift
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
//
|
||||||
|
// ResourcesBarRow.swift
|
||||||
|
// iKeyMon
|
||||||
|
//
|
||||||
|
// Created by tracer on 31.03.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct LoadBarCell: View {
|
||||||
|
let percent: Double
|
||||||
|
let load1: Double
|
||||||
|
let load5: Double
|
||||||
|
let load15: Double
|
||||||
|
var subtext: String? = nil
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
HStack {
|
||||||
|
Text(String(format: "%.2f%%", percent))
|
||||||
|
.foregroundColor(.green)
|
||||||
|
Text(String(format: "(%.2f / %.2f / %.2f)", load1, load5, load15))
|
||||||
|
}
|
||||||
|
if let subtext {
|
||||||
|
Text(subtext)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
ProgressView(value: percent / 100)
|
||||||
|
.progressViewStyle(.linear)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
LoadBarCell(percent: 2.34, load1: 1.23, load5: 0.08, load15: 0.06)
|
||||||
|
}
|
38
iKeyMon/Views/Rows/ResouceRow.swift
Normal file
38
iKeyMon/Views/Rows/ResouceRow.swift
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
//
|
||||||
|
// 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")
|
||||||
|
//}
|
41
iKeyMon/Views/Rows/TableRowView.swift
Normal file
41
iKeyMon/Views/Rows/TableRowView.swift
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
//
|
||||||
|
// TableRowView.swift
|
||||||
|
// iKeyMon
|
||||||
|
//
|
||||||
|
// Created by tracer on 01.04.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct TableRowView<Label: View, Value: View>: View {
|
||||||
|
@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)
|
||||||
|
}
|
||||||
|
.padding(.vertical, 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
TableRowView {
|
||||||
|
Text("Label")
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
} value: {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Text("42%")
|
||||||
|
.foregroundColor(.green)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
Text("(extra info)")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.font(.callout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
50
iKeyMon/Views/Rows/UsageBarCell.swift
Normal file
50
iKeyMon/Views/Rows/UsageBarCell.swift
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
//
|
||||||
|
// UsageBarCell.swift
|
||||||
|
// iKeyMon
|
||||||
|
//
|
||||||
|
// Created by tracer on 01.04.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct UsageBarCell: View {
|
||||||
|
let free: Int
|
||||||
|
let used: Int
|
||||||
|
let total: Int
|
||||||
|
let percent: Double
|
||||||
|
var subtext: String? = nil
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
HStack {
|
||||||
|
Text("Free:")
|
||||||
|
.fontWeight(.bold)
|
||||||
|
Text(free.toNiceBinaryUnit())
|
||||||
|
|
||||||
|
Text("Used:")
|
||||||
|
.fontWeight(.bold)
|
||||||
|
Text(used.toNiceBinaryUnit())
|
||||||
|
|
||||||
|
Text("Total:")
|
||||||
|
.fontWeight(.bold)
|
||||||
|
Text(total.toNiceBinaryUnit())
|
||||||
|
|
||||||
|
}
|
||||||
|
if let subtext {
|
||||||
|
Text(subtext)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Text(String(format: "%.2f %%", percent))
|
||||||
|
}
|
||||||
|
ProgressView(value: percent / 100)
|
||||||
|
.progressViewStyle(.linear)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
UsageBarCell(free: 1024, used: 16238, total: 1232312323123, percent: 33.33)
|
||||||
|
}
|
35
iKeyMon/Views/ServerDetailView.swift
Normal file
35
iKeyMon/Views/ServerDetailView.swift
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
//
|
||||||
|
// ServerDetailView.swift
|
||||||
|
// iKeyMon
|
||||||
|
//
|
||||||
|
// Created by tracer on 30.03.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ServerDetailView: View {
|
||||||
|
@Binding var server: Server
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
ResourcesView(server: $server)
|
||||||
|
.tabItem {
|
||||||
|
Text("Resources")
|
||||||
|
}
|
||||||
|
ServicesView(server: $server)
|
||||||
|
.tabItem {
|
||||||
|
Text("Services")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
242
iKeyMon/Views/ServerFormView.swift
Normal file
242
iKeyMon/Views/ServerFormView.swift
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
//
|
||||||
|
// EditServerView.swift
|
||||||
|
// iKeyMon
|
||||||
|
//
|
||||||
|
// Created by tracer on 30.03.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ServerFormView: View {
|
||||||
|
|
||||||
|
enum Mode {
|
||||||
|
case add
|
||||||
|
case edit(Server)
|
||||||
|
}
|
||||||
|
var mode: Mode
|
||||||
|
|
||||||
|
@Binding var servers: [Server]
|
||||||
|
|
||||||
|
@State private var hostname: String
|
||||||
|
@State private var apiKey: String
|
||||||
|
@State private var connectionOK: Bool = false
|
||||||
|
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
|
||||||
|
init(
|
||||||
|
mode: Mode,
|
||||||
|
servers: Binding<[Server]>,
|
||||||
|
dismiss: @escaping () -> Void
|
||||||
|
) {
|
||||||
|
self.mode = mode
|
||||||
|
self._servers = servers
|
||||||
|
|
||||||
|
switch mode {
|
||||||
|
case .add:
|
||||||
|
self._hostname = State(initialValue: "")
|
||||||
|
self._apiKey = State(initialValue: "")
|
||||||
|
case .edit(let server):
|
||||||
|
self._hostname = State(initialValue: server.hostname)
|
||||||
|
self._apiKey = State(initialValue: KeychainHelper.loadApiKey(for: server.hostname) ?? "")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
Text("Edit Server")
|
||||||
|
.font(.headline)
|
||||||
|
|
||||||
|
TextField("Hostname", text: $hostname)
|
||||||
|
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||||
|
.padding(.top)
|
||||||
|
|
||||||
|
SecureField("API Key", text: $apiKey)
|
||||||
|
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Button("Cancel") {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
Button("Test connection") {
|
||||||
|
testConnection()
|
||||||
|
}
|
||||||
|
Button("Save") {
|
||||||
|
saveServer()
|
||||||
|
updateServer()
|
||||||
|
saveServers()
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
// .disabled(hostname.isEmpty || apiKey.isEmpty || !con nectionOK)
|
||||||
|
}
|
||||||
|
.padding(.top)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.frame(width: 300)
|
||||||
|
.onAppear {
|
||||||
|
print("on appear")
|
||||||
|
if case let .edit(server) = mode {
|
||||||
|
print("serve \(server)")
|
||||||
|
hostname = server.hostname
|
||||||
|
apiKey = KeychainHelper.loadApiKey(for: server.hostname) ?? ""
|
||||||
|
print("💡 Loaded server: \(hostname)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private var modeTitle: String {
|
||||||
|
switch mode {
|
||||||
|
case .add:
|
||||||
|
return "Add Server"
|
||||||
|
case .edit(let server):
|
||||||
|
return "Edit \(server.hostname)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func testConnection() {
|
||||||
|
// @State is no reliable source
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// func testKeyHelpConnection() {
|
||||||
|
// let url = URL(string: "https://keyhelp.lab.24unix.net/api/v2/ping")!
|
||||||
|
// var request = URLRequest(url: url)
|
||||||
|
// request.httpMethod = "GET"
|
||||||
|
//
|
||||||
|
// // @State is no reliable source
|
||||||
|
// let key = apiKey.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
// request.setValue(key, forHTTPHeaderField: "X-API-KEY")
|
||||||
|
//
|
||||||
|
// request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
//
|
||||||
|
// print("🔍 Headers: \(request.allHTTPHeaderFields ?? [:])")
|
||||||
|
//
|
||||||
|
// let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||||
|
// if let error = error {
|
||||||
|
// print("❌ Error: \(error.localizedDescription)")
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if let httpResponse = response as? HTTPURLResponse {
|
||||||
|
// print("🔄 Status Code: \(httpResponse.statusCode)")
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if let data = data,
|
||||||
|
// let json = try? JSONSerialization.jsonObject(with: data) {
|
||||||
|
// print("✅ Response: \(json)")
|
||||||
|
// } else {
|
||||||
|
// print("⚠️ No JSON response")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// task.resume()
|
||||||
|
// }
|
||||||
|
|
||||||
|
private func saveServer() {
|
||||||
|
print("in save server")
|
||||||
|
let trimmedHost = hostname.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
let trimmedKey = apiKey.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|
||||||
|
switch mode {
|
||||||
|
case .add:
|
||||||
|
print("adding server")
|
||||||
|
let newServer = Server(hostname: trimmedHost)
|
||||||
|
servers.append(newServer)
|
||||||
|
KeychainHelper.save(apiKey: trimmedKey, for: trimmedHost)
|
||||||
|
saveServers()
|
||||||
|
case .edit(let oldServer):
|
||||||
|
if let index = servers.firstIndex(where: { $0.id == oldServer.id }) {
|
||||||
|
let oldHostname = servers[index].hostname
|
||||||
|
servers[index].hostname = trimmedHost
|
||||||
|
if oldHostname != trimmedHost {
|
||||||
|
KeychainHelper.deleteApiKey(for: oldHostname)
|
||||||
|
}
|
||||||
|
KeychainHelper.save(apiKey: trimmedKey, for: trimmedHost)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateServer() {
|
||||||
|
print ("in edit server")
|
||||||
|
guard case let .edit(server) = mode else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let index = servers.firstIndex(where: { $0.id == server.id }) {
|
||||||
|
// Only replace hostname if changed
|
||||||
|
let oldHostname = servers[index].hostname
|
||||||
|
servers[index].hostname = hostname
|
||||||
|
|
||||||
|
// Update Keychain
|
||||||
|
if oldHostname != hostname {
|
||||||
|
KeychainHelper.deleteApiKey(for: oldHostname)
|
||||||
|
}
|
||||||
|
KeychainHelper.save(apiKey: apiKey, for: hostname)
|
||||||
|
saveServers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveServers() {
|
||||||
|
if let data = try? JSONEncoder().encode(servers) {
|
||||||
|
UserDefaults.standard.set(data, forKey: "storedServers")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func delete(server: Server, from servers: inout [Server]) {
|
||||||
|
if let index = servers.firstIndex(where: { $0.id == server.id }) {
|
||||||
|
servers.remove(at: index)
|
||||||
|
|
||||||
|
if let data = try? JSONEncoder().encode(servers) {
|
||||||
|
UserDefaults.standard.set(data, forKey: "storedServers")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
ServerFormView(
|
||||||
|
mode: .edit(Server(hostname: "example.com")),
|
||||||
|
servers: .constant([
|
||||||
|
Server(hostname: "example.com")
|
||||||
|
]),
|
||||||
|
dismiss: {}
|
||||||
|
)
|
||||||
|
}
|
47
iKeyMon/Views/Tabs/GeneralView.swift
Normal file
47
iKeyMon/Views/Tabs/GeneralView.swift
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
//
|
||||||
|
// GeneralTab.swift
|
||||||
|
// iKeyMon
|
||||||
|
//
|
||||||
|
// Created by tracer on 30.03.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.formStyle(.grouped)
|
||||||
|
.padding(0)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
struct PreviewWrapper: View {
|
||||||
|
@State var previewServer = Server(hostname: "example.com", info: .placeholder)
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
GeneralView(server: $previewServer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return PreviewWrapper()
|
||||||
|
}
|
117
iKeyMon/Views/Tabs/ResourcesView.swift
Normal file
117
iKeyMon/Views/Tabs/ResourcesView.swift
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
//
|
||||||
|
// RecourcesView.swift
|
||||||
|
// iKeyMon
|
||||||
|
//
|
||||||
|
// Created by tracer on 31.03.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
//
|
||||||
|
// GeneralTab.swift
|
||||||
|
// iKeyMon
|
||||||
|
//
|
||||||
|
// Created by tracer on 30.03.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ResourcesView: View {
|
||||||
|
@Binding var server: Server
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
GeometryReader { geometry in
|
||||||
|
ScrollView {
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
TableRowView {
|
||||||
|
Text("CPU Load")
|
||||||
|
} value: {
|
||||||
|
LoadBarCell(
|
||||||
|
percent: (server.info?.load.percent)!,
|
||||||
|
load1: (server.info?.load.minute1)!,
|
||||||
|
load5: (server.info?.load.minute5)!,
|
||||||
|
load15: (server.info?.load.minute15)!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TableRowView {
|
||||||
|
Text("Memory")
|
||||||
|
} value: {
|
||||||
|
UsageBarCell(
|
||||||
|
free: (server.info?.memory.free)!,
|
||||||
|
used: (server.info?.memory.used)!,
|
||||||
|
total: (server.info?.memory.total)!,
|
||||||
|
percent: (server.info?.memory.percent)!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TableRowView {
|
||||||
|
Text("Swap")
|
||||||
|
} value: {
|
||||||
|
UsageBarCell(
|
||||||
|
free: (server.info?.swap.free)!,
|
||||||
|
used: (server.info?.swap.used)!,
|
||||||
|
total: (server.info?.swap.total)!,
|
||||||
|
percent: (server.info?.swap.percent)!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TableRowView {
|
||||||
|
Text("SSD")
|
||||||
|
} value: {
|
||||||
|
UsageBarCell(
|
||||||
|
free: (server.info?.diskSpace.free)!,
|
||||||
|
used: (server.info?.diskSpace.used)!,
|
||||||
|
total: (server.info?.diskSpace.total)!,
|
||||||
|
percent: (server.info?.diskSpace.percent)!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .topLeading)
|
||||||
|
.padding()
|
||||||
|
.frame(minHeight: geometry.size.height, alignment: .top)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// VStack(alignment: .leading, spacing: 16) {
|
||||||
|
// if let info = server.info {
|
||||||
|
// Text("Server Utilization")
|
||||||
|
// .font(.headline)
|
||||||
|
// .padding(.bottom, 4)
|
||||||
|
//
|
||||||
|
// ResourceRow(label: "CPU Load", value: "\(info.load.percent)", subtext: info.cpuLoadDetail)
|
||||||
|
// ResourceRow(label: "Process Count", value: "\(info.processCount)")
|
||||||
|
//// ResourceRow(label: "Emails in Queue", value: "\(info.emailsInQueue)")
|
||||||
|
//
|
||||||
|
// ResourceBarRow(
|
||||||
|
// label: "Memory",
|
||||||
|
// free: info.memory.free,
|
||||||
|
// used: info.memory.used,
|
||||||
|
// total: info.memory.total,
|
||||||
|
// percent: info.memory.percent
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// ResourceBarRow(
|
||||||
|
// label: "Swap",
|
||||||
|
// free: info.memory.free,
|
||||||
|
// used: info.memory.used,
|
||||||
|
// total: info.memory.total,
|
||||||
|
// percent: info.memory.percent
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// Spacer()
|
||||||
|
// } else {
|
||||||
|
// Text("No data")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
.padding()
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
struct PreviewWrapper: View {
|
||||||
|
@State var previewServer = Server(hostname: "example.com", info: .placeholder)
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ResourcesView(server: $previewServer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return PreviewWrapper()
|
||||||
|
}
|
62
iKeyMon/Views/Tabs/ServicesView.swift
Normal file
62
iKeyMon/Views/Tabs/ServicesView.swift
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
//
|
||||||
|
// ServicesView.swift
|
||||||
|
// iKeyMon
|
||||||
|
//
|
||||||
|
// Created by tracer on 31.03.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
|
||||||
|
struct ServicesView: View {
|
||||||
|
@Binding var server: Server
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
if let ports = server.info?.ports {
|
||||||
|
Table(ports) {
|
||||||
|
|
||||||
|
TableColumn("Service") { port in
|
||||||
|
Text(port.service)
|
||||||
|
}
|
||||||
|
|
||||||
|
TableColumn("Status") { port in
|
||||||
|
Text(port.status)
|
||||||
|
.foregroundColor(
|
||||||
|
port.status.lowercased() == "online" ? .green : .red
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
TableColumn("Port") { port in
|
||||||
|
Text("\(port.port) \(port.proto.uppercased())")
|
||||||
|
}
|
||||||
|
|
||||||
|
TableColumn("Protocol") { port in
|
||||||
|
Text("\(port.proto.uppercased())")
|
||||||
|
.monospacedDigit()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
.padding(0)
|
||||||
|
} else {
|
||||||
|
Text("No service information available")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Service/port monitoring")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
struct PreviewWrapper: View {
|
||||||
|
@State var previewServer = Server(hostname: "example.com", info: .placeholder)
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ServicesView(server: $previewServer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return PreviewWrapper()
|
||||||
|
}
|
@ -6,5 +6,7 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.files.user-selected.read-only</key>
|
<key>com.apple.security.files.user-selected.read-only</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@ -11,7 +11,10 @@ import SwiftUI
|
|||||||
struct iKeyMonApp: App {
|
struct iKeyMonApp: App {
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentView()
|
MainView()
|
||||||
|
.onDisappear {
|
||||||
|
NSApp.terminate(nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user