import SwiftUI import ProxyCore import GRDB struct HomeView: View { @Environment(AppState.self) private var appState @State private var domains: [DomainGroup] = [] @State private var searchText = "" @State private var showClearConfirmation = false @State private var observation: AnyDatabaseCancellable? @State private var didInitializeObservers = false @State private var refreshSequence = 0 private let trafficRepo = TrafficRepository() var filteredDomains: [DomainGroup] { if searchText.isEmpty { return domains } return domains.filter { $0.domain.localizedCaseInsensitiveContains(searchText) } } var body: some View { Group { if !appState.isVPNConnected && domains.isEmpty { ContentUnavailableView { Label("VPN Not Connected", systemImage: "bolt.slash") } description: { Text("Enable the VPN to start capturing network traffic.") } actions: { Button("Enable VPN") { Task { await appState.toggleVPN() } } .buttonStyle(.borderedProminent) } } else if domains.isEmpty { ContentUnavailableView { Label("No Traffic", systemImage: "network.slash") } description: { Text("Waiting for network requests. Open Safari or another app to generate traffic.") } } else { List { ForEach(filteredDomains) { group in NavigationLink(value: group) { HStack { Image(systemName: "globe") .foregroundStyle(.secondary) Text(group.domain) .lineLimit(1) Spacer() Text("\(group.requestCount)") .foregroundStyle(.secondary) Image(systemName: "chevron.right") .font(.caption) .foregroundStyle(.tertiary) } } .contextMenu { Button { addToSSLProxyingList(domain: group.domain) } label: { Label("Add to SSL Proxying", systemImage: "lock.shield") } Button { addToBlockList(domain: group.domain) } label: { Label("Add to Block List", systemImage: "xmark.shield") } Button(role: .destructive) { try? trafficRepo.deleteForDomain(group.domain) } label: { Label("Delete Domain", systemImage: "trash") } } } } } } .searchable(text: $searchText, prompt: "Filter Domains") .navigationTitle("Home") .navigationDestination(for: DomainGroup.self) { group in DomainDetailView(domain: group.domain) } .toolbar { ToolbarItem(placement: .topBarLeading) { Button { showClearConfirmation = true } label: { Image(systemName: "trash") } .disabled(domains.isEmpty) } ToolbarItem(placement: .topBarTrailing) { Button { Task { await appState.toggleVPN() } } label: { Image(systemName: appState.isVPNConnected ? "bolt.fill" : "bolt.slash") .foregroundStyle(appState.isVPNConnected ? .yellow : .secondary) } } } .confirmationDialog("Clear All Domains", isPresented: $showClearConfirmation) { Button("Clear All", role: .destructive) { try? trafficRepo.deleteAll() } } message: { Text("This will permanently delete all captured traffic.") } .onAppear { ProxyLogger.ui.info("HomeView: onAppear vpnConnected=\(appState.isVPNConnected) domains=\(domains.count)") } .onDisappear { ProxyLogger.ui.info("HomeView: onDisappear domains=\(domains.count)") } .task { guard !didInitializeObservers else { ProxyLogger.ui.info("HomeView: task rerun ignored; observers already initialized") return } didInitializeObservers = true ProxyLogger.ui.info("HomeView: initial task setup") startObservation(source: "initial") observeNewTraffic() } } private let rulesRepo = RulesRepository() private func addToSSLProxyingList(domain: String) { var entry = SSLProxyingEntry(domainPattern: domain, isInclude: true) try? rulesRepo.insertSSLEntry(&entry) // Auto-enable SSL proxying if not already if !IPCManager.shared.isSSLProxyingEnabled { IPCManager.shared.isSSLProxyingEnabled = true } IPCManager.shared.post(.configurationChanged) } private func addToBlockList(domain: String) { var entry = BlockListEntry(urlPattern: "*\(domain)*") try? rulesRepo.insertBlockEntry(&entry) if !IPCManager.shared.isBlockListEnabled { IPCManager.shared.isBlockListEnabled = true } IPCManager.shared.post(.configurationChanged) } private func startObservation(source: String) { refreshSequence += 1 let sequence = refreshSequence ProxyLogger.ui.info("HomeView: starting GRDB observation source=\(source) seq=\(sequence)") observation?.cancel() observation = trafficRepo.observeDomainGroups() .start(in: DatabaseManager.shared.dbPool) { error in ProxyLogger.ui.error("HomeView: observation error source=\(source) seq=\(sequence) error=\(error.localizedDescription)") } onChange: { newDomains in let preview = newDomains.prefix(3).map(\.domain).joined(separator: ", ") ProxyLogger.ui.info("HomeView: domains updated source=\(source) seq=\(sequence) count=\(newDomains.count) preview=\(preview)") withAnimation { domains = newDomains } } } private func observeNewTraffic() { ProxyLogger.ui.info("HomeView: registering Darwin notification observer") IPCManager.shared.observe(.newTrafficCaptured) { ProxyLogger.ui.info("HomeView: Darwin notification received") DispatchQueue.main.async { startObservation(source: "darwin") } } } }