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/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -11,7 +11,10 @@ import SwiftUI
|
||||
struct iKeyMonApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
MainView()
|
||||
.onDisappear {
|
||||
NSApp.terminate(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user