// // 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 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 [] }() // @State private var selectedServer: Server? @State private var selectedServerID: UUID? var body: some View { NavigationSplitView { List(selection: $selectedServerID) { ForEach(servers) { server in HStack { Image(systemName: "dot.circle.fill") .foregroundColor(server.pingable ? .green : .red) Text(server.hostname) } .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) { Button(action: { showAddServerSheet = true }) { Image(systemName: "plus") } .help("Add Host") } } .navigationTitle("Servers") .onChange(of: selectedServerID) { if let selectedServerID { UserDefaults.standard.set(selectedServerID.uuidString, forKey: "selectedServerID") fetchServerInfo(for: selectedServerID) } } } detail: { if let selectedServerID, let index = servers.firstIndex(where: { selectedServerID == $0.id }) { ServerDetailView(server: $servers[index], isFetching: isFetchingInfo) } else { ContentUnavailableView("No Server Selected", systemImage: "server.rack") } } .sheet(isPresented: $showAddServerSheet) { ServerFormView( mode: .add, servers: $servers, dismiss: { showAddServerSheet = false } ) } .sheet(item: $serverBeingEdited) { server in 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) {} } .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 }), let api = ServerAPI(server: server) else { return } isFetchingInfo = true Task { defer { isFetchingInfo = false } do { let info = try await api.fetchServerInfo() if let index = servers.firstIndex(where: { $0.id == id }) { var updated = servers[index] updated.info = try ServerInfo(from: info as! Decoder) servers[index] = updated } } catch { print("❌ Failed to fetch server data: \(error)") } } } 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() }