// ConnectivitySettings.swift // Jarvis — persisted connection profiles: a Direct/LAN address and a Remote/VPN // address for the same server, the chosen provider, and the auto-switch policy. import Foundation @MainActor final class ConnectivitySettings: ObservableObject { static let shared = ConnectivitySettings() private let key = "jarvis_connectivity_v2" @Published var provider: VPNProvider { didSet { persist() } } @Published var directHost: String { didSet { persist() } } // LAN, e.g. 192.168.30.50:8080 @Published var remoteHost: String { didSet { persist() } } // Tailscale/VPN address /// "Use when remote" — when off, only the LAN address is ever used. @Published var remoteEnabled: Bool { didSet { persist() } } /// "Auto-connect" — re-resolve automatically when the network path changes. @Published var autoConnect: Bool { didSet { persist() } } /// Debounce before switching endpoints after a network change (seconds). @Published var switchDelaySeconds: Int { didSet { persist() } } private struct Stored: Codable { var provider: VPNProvider var directHost: String var remoteHost: String var remoteEnabled: Bool? var autoConnect: Bool? var switchDelaySeconds: Int? } private init() { if let data = UserDefaults.standard.data(forKey: key), let s = try? JSONDecoder().decode(Stored.self, from: data) { provider = s.provider directHost = s.directHost remoteHost = s.remoteHost remoteEnabled = s.remoteEnabled ?? true autoConnect = s.autoConnect ?? true switchDelaySeconds = s.switchDelaySeconds ?? 5 } else { provider = .tailscale directHost = UserDefaults.standard.string(forKey: "jarvis_server_host") ?? "" remoteHost = "" remoteEnabled = true autoConnect = true switchDelaySeconds = 5 } } private func persist() { let s = Stored(provider: provider, directHost: directHost, remoteHost: remoteHost, remoteEnabled: remoteEnabled, autoConnect: autoConnect, switchDelaySeconds: switchDelaySeconds) if let data = try? JSONEncoder().encode(s) { UserDefaults.standard.set(data, forKey: key) } } /// Whether the remote endpoint participates in resolution right now. var remoteActiveInPolicy: Bool { provider.needsRemote && remoteEnabled } /// Hosts to probe, in priority order (LAN first), de-duplicated and trimmed. var candidates: [String] { let d = directHost.trimmingCharacters(in: .whitespaces) let r = remoteActiveInPolicy ? remoteHost.trimmingCharacters(in: .whitespaces) : "" var seen = Set(); var out: [String] = [] for h in [d, r] where !h.isEmpty && !seen.contains(h) { seen.insert(h); out.append(h) } return out } var isConfigured: Bool { !candidates.isEmpty } }