// // MainView.swift // iKeyMon // // Created by tracer on 30.03.25. // import SwiftUI struct MainView: View { private static let serverOrderKeyStatic = "serverOrder" private static let storedServersKeyStatic = "storedServers" @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 = MainView.serverOrderKeyStatic private let storedServersKey = MainView.storedServersKeyStatic @State private var servers: [Server] = MainView.loadStoredServers() // @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 { fetchServerInfo(for: server.id) } } .onAppear { let initialID: UUID? if let storedID = UserDefaults.standard.string(forKey: "selectedServerID"), let uuid = UUID(uuidString: storedID), servers.contains(where: { $0.id == uuid }) { print("✅ [MainView] Restored selected server \(uuid)") initialID = uuid } else if let first = servers.first { print("✅ [MainView] Selecting first server \(first.hostname)") initialID = first.id } else { print("ℹ️ [MainView] No stored selection") initialID = nil } selectedServerID = initialID if let initialID { Task { await prefetchOtherServers(activeID: initialID) } } pingAllServers() pingTimer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: true) { _ in pingAllServers() } } .frame(minWidth: 800, minHeight: 450) } private func fetchServerInfo(for id: UUID) { guard let server = servers.first(where: { $0.id == id }) else { print("❌ [MainView] fetchServerInfo: server not found for id \(id)") return } guard let apiKey = KeychainHelper.loadApiKey(for: server.hostname)?.trimmingCharacters(in: .whitespacesAndNewlines), !apiKey.isEmpty else { print("❌ [MainView] fetchServerInfo: missing API key for \(server.hostname)") return } guard let baseURL = URL(string: "https://\(server.hostname)") else { print("❌ [MainView] Invalid base URL for \(server.hostname)") return } isFetchingInfo = true Task { defer { isFetchingInfo = false } do { let api = try await APIFactory.detectAndCreateAPI(baseURL: baseURL, apiKey: apiKey) let info = try await api.fetchServerSummary(apiKey: apiKey) await MainActor.run { if let index = servers.firstIndex(where: { $0.id == id }) { var updated = servers[index] updated.info = info servers[index] = updated } } } catch { print("❌ Failed to fetch server data: \(error)") } } } private func prefetchOtherServers(activeID: UUID) async { let others = servers.filter { $0.id != activeID } await withTaskGroup(of: Void.self) { group in for server in others { group.addTask { await fetchServerInfoAsync(for: server.id) } } } } private func fetchServerInfoAsync(for id: UUID) async { guard let server = servers.first(where: { $0.id == id }) else { return } guard let apiKey = KeychainHelper.loadApiKey(for: server.hostname)?.trimmingCharacters(in: .whitespacesAndNewlines), !apiKey.isEmpty, let baseURL = URL(string: "https://\(server.hostname)") else { return } do { let api = try await APIFactory.detectAndCreateAPI(baseURL: baseURL, apiKey: apiKey) let info = try await api.fetchServerSummary(apiKey: apiKey) await MainActor.run { if let index = servers.firstIndex(where: { $0.id == id }) { var updated = servers[index] updated.info = info servers[index] = updated } } } catch { print("❌ Prefetch failed for \(server.hostname): \(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) print("💾 [MainView] Saved server order with \(ids.count) entries") } private struct PingResponse: Codable { let response: String } func pingAllServers() { for (index, server) in servers.enumerated() { Task { let apiKey = KeychainHelper.loadApiKey(for: server.hostname)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" let pingable = await PingService.ping(hostname: server.hostname, apiKey: apiKey) await MainActor.run { servers[index].pingable = pingable } if !pingable { print("📶 [MainView] Ping \(server.hostname): offline") } } } } private static func loadStoredServers() -> [Server] { let defaults = UserDefaults.standard guard let data = defaults.data(forKey: storedServersKeyStatic) else { print("ℹ️ [MainView] No storedServers data found") return [] } do { let saved = try JSONDecoder().decode([Server].self, from: data) print("📦 [MainView] Loaded \(saved.count) servers from UserDefaults") if let order = defaults.stringArray(forKey: serverOrderKeyStatic) { let idMap = order.compactMap(UUID.init) let sorted = 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 sorted } return saved } catch { print("❌ [MainView] Failed to decode stored servers: \(error)") return [] } } } #Preview { MainView() }