Investigate why network device scanning in NetworkMonitor-iOS is only finding 2 devices when there should be more.

Look at:
1. /Users/neog/Apps/iOS/NetworkMonitor-iOS/NetworkMonitor-iOS/Services/NetworkService.swift - specifically the scanNetwork, probeHost, pingPort, and Bonjour discovery functions
2. Check if there are any issues with:
   - The task group logic that could cause early termination
   - The AsyncSemaphore implementation
   - The Bonjour browser setup
   - Connection state handling

Focus on finding bugs or limitations that could prevent devices from being detected. Check if the TCP connection approach is the issue - some devices might reject TCP connections even on common ports.

Also look for:
- Whether the scan is completing all 254 IPs
- Whether Bonjour discovery is actually working
- Timeout issues
- Connection refused vs no response handling


I'll help you investigate the network device scanning issue in NetworkMonitor-iOS. Let me start by examining the relevant files.


Error: EISDIR: illegal operation on a directory, read


Now let me read the NetworkService.swift file:


     1→//
     2→//  NetworkService.swift
     3→//  NetworkMonitor-iOS
     4→//
     5→//  Created by Claude Code on 2025-12-30.
     6→//
     7→
     8→import Foundation
     9→import Network
    10→import SystemConfiguration.CaptiveNetwork
    11→
    12→@MainActor
    13→class NetworkService: ObservableObject {
    14→    // MARK: - Published Properties
    15→    @Published var networkStatus = NetworkStatus()
    16→    @Published var discoveredDevices: [NetworkDevice] = []
    17→    @Published var currentScan: ScanResult?
    18→    @Published var isScanning = false
    19→    @Published var pingResults: [PingResult] = []
    20→    @Published var trafficStats = TrafficStats()
    21→    @Published var monitorTargets: [MonitorTarget] = []
    22→    @Published var bonjourDevices: [NetworkDevice] = [] // Devices found via mDNS
    23→
    24→    // MARK: - Private Properties
    25→    private let monitor = NWPathMonitor()
    26→    private let queue = DispatchQueue(label: "NetworkMonitor")
    27→    private var pingTimers: [UUID: Timer] = [:]
    28→    private var scanTask: Task<Void, Never>?
    29→    private var lastTargetStatus: [String: Bool] = [:] // Track online/offline state
    30→    private var consecutiveFailures: [String: Int] = [:] // Track failures before alerting
    31→    private let failureThreshold = 3 // Number of failures before notification
    32→
    33→    // Bonjour/mDNS discovery
    34→    private var bonjourBrowsers: [NWBrowser] = []
    35→    private var discoveredEndpoints: Set<String> = []
    36→
    37→    // Bandwidth tracking
    38→    private var previousBytesReceived: UInt64 = 0
    39→    private var previousBytesSent: UInt64 = 0
    40→    private var previousTimestamp: Date?
    41→    private var trafficTimer: Timer?
    42→    private let maxHistorySamples = 60 // Keep 60 samples (1 minute at 1s interval)
    43→
    44→    // Signal quality tracking
    45→    private var signalTimer: Timer?
    46→    private var recentGatewayPings: [SignalSample] = []
    47→    private let maxSignalSamples = 20
    48→
    49→    // MARK: - Initialization
    50→    init() {
    51→        setupNetworkMonitor()
    52→        loadMonitorTargets()
    53→        updateTrafficStats()
    54→        startBandwidthMonitoring()
    55→        startSignalQualityMonitoring()
    56→    }
    57→
    58→    nonisolated func cleanup() {
    59→        monitor.cancel()
    60→    }
    61→
    62→    func stopBandwidthMonitoring() {
    63→        trafficTimer?.invalidate()
    64→        trafficTimer = nil
    65→    }
    66→
    67→    func stopSignalQualityMonitoring() {
    68→        signalTimer?.invalidate()
    69→        signalTimer = nil
    70→    }
    71→
    72→    // MARK: - Bonjour/mDNS Discovery
    73→    func startBonjourDiscovery() {
    74→        stopBonjourDiscovery()
    75→        discoveredEndpoints.removeAll()
    76→        bonjourDevices.removeAll()
    77→
    78→        // Common Bonjour service types to discover devices
    79→        let serviceTypes = [
    80→            "_http._tcp",           // Web servers
    81→            "_https._tcp",          // HTTPS servers
    82→            "_ssh._tcp",            // SSH (Macs, Linux)
    83→            "_sftp-ssh._tcp",       // SFTP
    84→            "_smb._tcp",            // Windows file sharing
    85→            "_afpovertcp._tcp",     // Apple File Protocol (Macs)
    86→            "_nfs._tcp",            // Network File System
    87→            "_ftp._tcp",            // FTP servers
    88→            "_printer._tcp",        // Printers
    89→            "_ipp._tcp",            // Internet Printing Protocol
    90→            "_pdl-datastream._tcp", // Printer
    91→            "_scanner._tcp",        // Scanners
    92→            "_airplay._tcp",        // Apple TV, AirPlay speakers
    93→            "_raop._tcp",           // AirPlay audio
    94→            "_homekit._tcp",        // HomeKit devices
    95→            "_hap._tcp",            // HomeKit Accessory Protocol
    96→            "_spotify-connect._tcp",// Spotify devices
    97→            "_googlecast._tcp",     // Chromecast
    98→            "_companion-link._tcp", // Apple devices
    99→            "_sleep-proxy._udp",    // Apple sleep proxy
   100→            "_device-info._tcp",    // Device info
   101→            "_workstation._tcp",    // Mac workstations
   102→        ]
   103→
   104→        for serviceType in serviceTypes {
   105→            let browser = NWBrowser(for: .bonjour(type: serviceType, domain: "local"), using: .tcp)
   106→
   107→            browser.stateUpdateHandler = { state in
   108→                // Browser state updates (connecting, ready, failed)
   109→            }
   110→
   111→            browser.browseResultsChangedHandler = { [weak self] results, _ in
   112→                Task { @MainActor in
   113→                    self?.handleBonjourResults(results, serviceType: serviceType)
   114→                }
   115→            }
   116→
   117→            browser.start(queue: queue)
   118→            bonjourBrowsers.append(browser)
   119→        }
   120→    }
   121→
   122→    func stopBonjourDiscovery() {
   123→        bonjourBrowsers.forEach { $0.cancel() }
   124→        bonjourBrowsers.removeAll()
   125→    }
   126→
   127→    private func handleBonjourResults(_ results: Set<NWBrowser.Result>, serviceType: String) {
   128→        for result in results {
   129→            if case let .service(name, type, domain, _) = result.endpoint {
   130→                let key = "\(name).\(type).\(domain)"
   131→
   132→                if !discoveredEndpoints.contains(key) {
   133→                    discoveredEndpoints.insert(key)
   134→
   135→                    // Resolve the service to get IP
   136→                    resolveService(name: name, type: type, domain: domain, serviceType: serviceType)
   137→                }
   138→            }
   139→        }
   140→    }
   141→
   142→    private func resolveService(name: String, type: String, domain: String, serviceType: String) {
   143→        let endpoint = NWEndpoint.service(name: name, type: type, domain: domain, interface: nil)
   144→        let connection = NWConnection(to: endpoint, using: .tcp)
   145→
   146→        connection.stateUpdateHandler = { [weak self] state in
   147→            switch state {
   148→            case .ready:
   149→                if let innerEndpoint = connection.currentPath?.remoteEndpoint,
   150→                   case let .hostPort(host, _) = innerEndpoint {
   151→                    let ipString = "\(host)"
   152→                    // Extract just the IP from the host description
   153→                    let ip = ipString.replacingOccurrences(of: "%.*", with: "", options: .regularExpression)
   154→
   155→                    Task { @MainActor in
   156→                        self?.addBonjourDevice(
   157→                            ip: ip,
   158→                            name: name,
   159→                            serviceType: serviceType
   160→                        )
   161→                    }
   162→                }
   163→                connection.cancel()
   164→            case .failed, .cancelled:
   165→                break
   166→            default:
   167→                break
   168→            }
   169→        }
   170→
   171→        connection.start(queue: queue)
   172→
   173→        // Cancel after timeout
   174→        DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
   175→            connection.cancel()
   176→        }
   177→    }
   178→
   179→    private func addBonjourDevice(ip: String, name: String, serviceType: String) {
   180→        // Check if device already exists
   181→        if bonjourDevices.contains(where: { $0.ipAddress == ip }) {
   182→            return
   183→        }
   184→
   185→        var device = NetworkDevice(ipAddress: ip)
   186→        device.hostname = name
   187→        device.isReachable = true
   188→        device.lastSeen = Date()
   189→        device.deviceType = guessDeviceTypeFromService(serviceType, name: name)
   190→
   191→        bonjourDevices.append(device)
   192→
   193→        // Also add to discovered devices if not already there
   194→        if !discoveredDevices.contains(where: { $0.ipAddress == ip }) {
   195→            discoveredDevices.append(device)
   196→        }
   197→    }
   198→
   199→    private func guessDeviceTypeFromService(_ serviceType: String, name: String) -> NetworkDevice.DeviceType {
   200→        let nameLower = name.lowercased()
   201→
   202→        // Check name first
   203→        if nameLower.contains("macbook") || nameLower.contains("imac") || nameLower.contains("mac mini") || nameLower.contains("mac pro") {
   204→            return .computer
   205→        }
   206→        if nameLower.contains("iphone") {
   207→            return .phone
   208→        }
   209→        if nameLower.contains("ipad") {
   210→            return .tablet
   211→        }
   212→        if nameLower.contains("apple tv") || nameLower.contains("appletv") {
   213→            return .iot
   214→        }
   215→        if nameLower.contains("printer") || nameLower.contains("hp ") || nameLower.contains("epson") || nameLower.contains("canon") || nameLower.contains("brother") {
   216→            return .printer
   217→        }
   218→        if nameLower.contains("synology") || nameLower.contains("qnap") || nameLower.contains("nas") {
   219→            return .nas
   220→        }
   221→        if nameLower.contains("router") || nameLower.contains("gateway") {
   222→            return .router
   223→        }
   224→
   225→        // Check service type
   226→        switch serviceType {
   227→        case "_printer._tcp", "_ipp._tcp", "_pdl-datastream._tcp":
   228→            return .printer
   229→        case "_scanner._tcp":
   230→            return .printer
   231→        case "_airplay._tcp", "_raop._tcp", "_homekit._tcp", "_hap._tcp", "_googlecast._tcp":
   232→            return .iot
   233→        case "_ssh._tcp", "_sftp-ssh._tcp", "_workstation._tcp":
   234→            return .computer
   235→        case "_smb._tcp", "_afpovertcp._tcp", "_nfs._tcp":
   236→            return .nas
   237→        case "_companion-link._tcp", "_device-info._tcp":
   238→            return .phone
   239→        default:
   240→            return .unknown
   241→        }
   242→    }
   243→
   244→    private func startBandwidthMonitoring() {
   245→        // Update every second for smooth bandwidth readings
   246→        trafficTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
   247→            Task { @MainActor in
   248→                self?.updateTrafficStats()
   249→            }
   250→        }
   251→    }
   252→
   253→    private func startSignalQualityMonitoring() {
   254→        // Measure signal quality every 3 seconds
   255→        signalTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { [weak self] _ in
   256→            Task { @MainActor in
   257→                await self?.measureSignalQuality()
   258→            }
   259→        }
   260→        // Initial measurement
   261→        Task {
   262→            await measureSignalQuality()
   263→        }
   264→    }
   265→
   266→    private func measureSignalQuality() async {
   267→        guard networkStatus.isConnected,
   268→              let gateway = networkStatus.gatewayIP else {
   269→            networkStatus.signalQuality.level = .none
   270→            return
   271→        }
   272→
   273→        // Ping gateway
   274→        let result = await ping(host: gateway, timeout: 2.0)
   275→
   276→        // Add sample
   277→        let sample = SignalSample(
   278→            timestamp: Date(),
   279→            latency: result.latency ?? 0,
   280→            success: result.success
   281→        )
   282→        recentGatewayPings.append(sample)
   283→
   284→        // Keep only recent samples
   285→        if recentGatewayPings.count > maxSignalSamples {
   286→            recentGatewayPings.removeFirst()
   287→        }
   288→
   289→        // Calculate metrics
   290→        let successfulPings = recentGatewayPings.filter { $0.success }
   291→        let failedPings = recentGatewayPings.filter { !$0.success }
   292→
   293→        // Packet loss percentage
   294→        let packetLoss = recentGatewayPings.isEmpty ? 100.0 :
   295→            Double(failedPings.count) / Double(recentGatewayPings.count) * 100
   296→
   297→        // Average latency (only successful pings)
   298→        let latencies = successfulPings.map { $0.latency }
   299→        let avgLatency = latencies.isEmpty ? 0 : latencies.reduce(0, +) / Double(latencies.count)
   300→
   301→        // Jitter (variation in latency)
   302→        var jitter: Double = 0
   303→        if latencies.count > 1 {
   304→            var diffs: [Double] = []
   305→            for i in 1..<latencies.count {
   306→                diffs.append(abs(latencies[i] - latencies[i-1]))
   307→            }
   308→            jitter = diffs.reduce(0, +) / Double(diffs.count)
   309→        }
   310→
   311→        // Update signal quality
   312→        networkStatus.signalQuality.latency = avgLatency
   313→        networkStatus.signalQuality.jitter = jitter
   314→        networkStatus.signalQuality.packetLoss = packetLoss
   315→        networkStatus.signalQuality.lastUpdated = Date()
   316→        networkStatus.signalQuality.history = recentGatewayPings
   317→        networkStatus.signalQuality.level = SignalQuality.calculateLevel(
   318→            latency: avgLatency,
   319→            jitter: jitter,
   320→            packetLoss: packetLoss
   321→        )
   322→    }
   323→
   324→    // MARK: - Network Status Monitoring
   325→    private func setupNetworkMonitor() {
   326→        monitor.pathUpdateHandler = { [weak self] path in
   327→            Task { @MainActor in
   328→                self?.updateNetworkStatus(path)
   329→            }
   330→        }
   331→        monitor.start(queue: queue)
   332→    }
   333→
   334→    private func updateNetworkStatus(_ path: NWPath) {
   335→        networkStatus.isConnected = path.status == .satisfied
   336→
   337→        if path.usesInterfaceType(.wifi) {
   338→            networkStatus.connectionType = .wifi
   339→            fetchWiFiInfo()
   340→        } else if path.usesInterfaceType(.cellular) {
   341→            networkStatus.connectionType = .cellular
   342→        } else if path.usesInterfaceType(.wiredEthernet) {
   343→            networkStatus.connectionType = .ethernet
   344→        } else {
   345→            networkStatus.connectionType = .unknown
   346→        }
   347→
   348→        fetchLocalIPAddress()
   349→        fetchExternalIP()
   350→    }
   351→
   352→    private func fetchWiFiInfo() {
   353→        #if !targetEnvironment(simulator)
   354→        if let interfaces = CNCopySupportedInterfaces() as? [String] {
   355→            for interface in interfaces {
   356→                if let info = CNCopyCurrentNetworkInfo(interface as CFString) as? [String: Any] {
   357→                    networkStatus.ssid = info[kCNNetworkInfoKeySSID as String] as? String
   358→                }
   359→            }
   360→        }
   361→        #else
   362→        networkStatus.ssid = "Simulator WiFi"
   363→        #endif
   364→    }
   365→
   366→    private func fetchLocalIPAddress() {
   367→        var address: String?
   368→        var ifaddr: UnsafeMutablePointer<ifaddrs>?
   369→
   370→        guard getifaddrs(&ifaddr) == 0 else { return }
   371→        defer { freeifaddrs(ifaddr) }
   372→
   373→        var ptr = ifaddr
   374→        while ptr != nil {
   375→            defer { ptr = ptr?.pointee.ifa_next }
   376→
   377→            guard let interface = ptr?.pointee else { continue }
   378→            let addrFamily = interface.ifa_addr.pointee.sa_family
   379→
   380→            if addrFamily == UInt8(AF_INET) {
   381→                let name = String(cString: interface.ifa_name)
   382→                if name == "en0" || name == "en1" { // WiFi or Ethernet
   383→                    var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
   384→                    getnameinfo(
   385→                        interface.ifa_addr,
   386→                        socklen_t(interface.ifa_addr.pointee.sa_len),
   387→                        &hostname,
   388→                        socklen_t(hostname.count),
   389→                        nil,
   390→                        0,
   391→                        NI_NUMERICHOST
   392→                    )
   393→                    address = String(cString: hostname)
   394→                }
   395→            }
   396→        }
   397→
   398→        networkStatus.localIP = address
   399→        if let ip = address {
   400→            let components = ip.split(separator: ".")
   401→            if components.count == 4 {
   402→                networkStatus.gatewayIP = "\(components[0]).\(components[1]).\(components[2]).1"
   403→                networkStatus.subnet = "\(components[0]).\(components[1]).\(components[2]).0/24"
   404→            }
   405→        }
   406→    }
   407→
   408→    private func fetchExternalIP() {
   409→        Task {
   410→            do {
   411→                let url = URL(string: "https://api.ipify.org")!
   412→                let (data, _) = try await URLSession.shared.data(from: url)
   413→                if let ip = String(data: data, encoding: .utf8) {
   414→                    await MainActor.run {
   415→                        self.networkStatus.externalIP = ip
   416→                    }
   417→                }
   418→            } catch {
   419→                print("Failed to fetch external IP: \(error)")
   420→            }
   421→        }
   422→    }
   423→
   424→    // MARK: - Network Scanning
   425→    func startScan() {
   426→        guard !isScanning, let localIP = networkStatus.localIP else { return }
   427→
   428→        isScanning = true
   429→        discoveredDevices = []
   430→        bonjourDevices = []
   431→
   432→        let components = localIP.split(separator: ".")
   433→        guard components.count == 4 else { return }
   434→
   435→        let baseIP = "\(components[0]).\(components[1]).\(components[2])"
   436→        let ipRange = "\(baseIP).1-254"
   437→
   438→        currentScan = ScanResult(
   439→            startTime: Date(),
   440→            ipRange: ipRange,
   441→            totalIPs: 254
   442→        )
   443→
   444→        // Start Bonjour discovery in parallel - finds Apple devices and services
   445→        startBonjourDiscovery()
   446→
   447→        scanTask = Task {
   448→            await scanNetwork(baseIP: baseIP)
   449→        }
   450→    }
   451→
   452→    func stopScan() {
   453→        scanTask?.cancel()
   454→        stopBonjourDiscovery()
   455→        isScanning = false
   456→        currentScan?.endTime = Date()
   457→        currentScan?.isComplete = true
   458→    }
   459→
   460→    private func scanNetwork(baseIP: String) async {
   461→        // Reduced concurrency for more reliable detection
   462→        // Testing 10 ports per IP means we need less parallel IPs
   463→        let concurrency = 25
   464→        let semaphore = AsyncSemaphore(limit: concurrency)
   465→
   466→        await withTaskGroup(of: NetworkDevice?.self) { group in
   467→            for i in 1...254 {
   468→                group.addTask {
   469→                    await semaphore.wait()
   470→                    defer { Task { await semaphore.signal() } }
   471→
   472→                    let ip = "\(baseIP).\(i)"
   473→                    let device = await self.probeHost(ip)
   474→
   475→                    await MainActor.run {
   476→                        self.currentScan?.scannedIPs = i
   477→                    }
   478→
   479→                    return device
   480→                }
   481→            }
   482→
   483→            for await device in group {
   484→                if let device = device {
   485→                    await MainActor.run {
   486→                        self.discoveredDevices.append(device)
   487→                        self.currentScan?.devicesFound.append(device)
   488→                    }
   489→                }
   490→            }
   491→        }
   492→
   493→        await MainActor.run {
   494→            // Stop Bonjour discovery
   495→            self.stopBonjourDiscovery()
   496→
   497→            // Merge Bonjour devices that weren't found by port scan
   498→            for bonjourDevice in self.bonjourDevices {
   499→                if !self.discoveredDevices.contains(where: { $0.ipAddress == bonjourDevice.ipAddress }) {
   500→                    self.discoveredDevices.append(bonjourDevice)
   501→                    self.currentScan?.devicesFound.append(bonjourDevice)
   502→                }
   503→            }
   504→
   505→            // Sort devices by IP
   506→            self.discoveredDevices.sort { ip1, ip2 in
   507→                let parts1 = ip1.ipAddress.split(separator: ".").compactMap { Int($0) }
   508→                let parts2 = ip2.ipAddress.split(separator: ".").compactMap { Int($0) }
   509→                if parts1.count == 4 && parts2.count == 4 {
   510→                    return parts1[3] < parts2[3]
   511→                }
   512→                return ip1.ipAddress < ip2.ipAddress
   513→            }
   514→
   515→            self.currentScan?.endTime = Date()
   516→            self.currentScan?.isComplete = true
   517→            self.isScanning = false
   518→        }
   519→    }
   520→
   521→    private func probeHost(_ ip: String) async -> NetworkDevice? {
   522→        // Try multiple ports to detect more devices
   523→        // Port 80: HTTP servers, routers
   524→        // Port 443: HTTPS servers
   525→        // Port 22: SSH (Linux, Mac)
   526→        // Port 445: SMB (Windows file sharing)
   527→        // Port 8080: Alternative HTTP
   528→        // Port 62078: Apple devices (lockdownd/usbmuxd)
   529→        // Port 548: AFP (Mac file sharing)
   530→        // Port 5353: mDNS (responds to TCP too sometimes)
   531→        // Port 139: NetBIOS (Windows)
   532→        // Port 7000: AirPlay (Apple TV)
   533→
   534→        let portsToTry: [UInt16] = [80, 443, 62078, 22, 445, 548, 8080, 7000, 139, 5353]
   535→        let timeout: TimeInterval = 1.5 // Increased timeout for slower devices
   536→
   537→        // Try all ports concurrently
   538→        let result = await withTaskGroup(of: (Bool, Double?, UInt16).self) { group in
   539→            for port in portsToTry {
   540→                group.addTask {
   541→                    let pingResult = await self.pingPort(host: ip, port: port, timeout: timeout)
   542→                    return (pingResult.success, pingResult.latency, port)
   543→                }
   544→            }
   545→
   546→            // Return first successful result
   547→            for await result in group {
   548→                if result.0 { // success
   549→                    return result
   550→                }
   551→            }
   552→            return (false, nil, UInt16(0))
   553→        }
   554→
   555→        if result.0 { // success
   556→            var device = NetworkDevice(ipAddress: ip)
   557→            device.isReachable = true
   558→            device.latency = result.1
   559→            device.lastSeen = Date()
   560→
   561→            // Try to resolve hostname
   562→            device.hostname = await resolveHostname(ip)
   563→
   564→            // Guess device type based on responding port and hostname
   565→            device.deviceType = guessDeviceType(ip: ip, port: result.2, hostname: device.hostname)
   566→
   567→            return device
   568→        }
   569→
   570→        return nil
   571→    }
   572→
   573→    private func pingPort(host: String, port: UInt16, timeout: TimeInterval) async -> PingResult {
   574→        let start = Date()
   575→
   576→        return await withCheckedContinuation { continuation in
   577→            let nwHost = NWEndpoint.Host(host)
   578→            guard let nwPort = NWEndpoint.Port(rawValue: port) else {
   579→                continuation.resume(returning: PingResult(
   580→                    timestamp: Date(),
   581→                    host: host,
   582→                    latency: nil,
   583→                    success: false
   584→                ))
   585→                return
   586→            }
   587→
   588→            let connection = NWConnection(host: nwHost, port: nwPort, using: .tcp)
   589→
   590→            class ResumeTracker { var hasResumed = false }
   591→            let tracker = ResumeTracker()
   592→
   593→            let safeResume: (PingResult) -> Void = { result in
   594→                guard !tracker.hasResumed else { return }
   595→                tracker.hasResumed = true
   596→                continuation.resume(returning: result)
   597→            }
   598→
   599→            let timer = DispatchSource.makeTimerSource(queue: self.queue)
   600→            timer.schedule(deadline: .now() + timeout)
   601→            timer.setEventHandler {
   602→                connection.cancel()
   603→                safeResume(PingResult(
   604→                    timestamp: Date(),
   605→                    host: host,
   606→                    latency: nil,
   607→                    success: false
   608→                ))
   609→            }
   610→            timer.resume()
   611→
   612→            connection.stateUpdateHandler = { state in
   613→                switch state {
   614→                case .ready:
   615→                    timer.cancel()
   616→                    let latency = Date().timeIntervalSince(start) * 1000
   617→                    connection.cancel()
   618→                    safeResume(PingResult(
   619→                        timestamp: Date(),
   620→                        host: host,
   621→                        latency: latency,
   622→                        success: true
   623→                    ))
   624→                case .failed:
   625→                    timer.cancel()
   626→                    safeResume(PingResult(
   627→                        timestamp: Date(),
   628→                        host: host,
   629→                        latency: nil,
   630→                        success: false
   631→                    ))
   632→                case .cancelled:
   633→                    timer.cancel()
   634→                default:
   635→                    break
   636→                }
   637→            }
   638→
   639→            connection.start(queue: self.queue)
   640→        }
   641→    }
   642→
   643→    private func guessDeviceType(ip: String, port: UInt16, hostname: String?) -> NetworkDevice.DeviceType {
   644→        // Router detection
   645→        if ip.hasSuffix(".1") || ip.hasSuffix(".254") {
   646→            return .router
   647→        }
   648→
   649→        // Apple device detection
   650→        if port == 62078 || port == 548 || port == 7000 {
   651→            if let name = hostname?.lowercased() {
   652→                if name.contains("macbook") || name.contains("imac") || name.contains("mac-mini") || name.contains("mac-pro") {
   653→                    return .computer
   654→                }
   655→                if name.contains("iphone") {
   656→                    return .phone
   657→                }
   658→                if name.contains("ipad") {
   659→                    return .tablet
   660→                }
   661→                if name.contains("appletv") || name.contains("apple-tv") {
   662→                    return .iot
   663→                }
   664→            }
   665→            return .phone // Default Apple device
   666→        }
   667→
   668→        // SSH suggests computer
   669→        if port == 22 {
   670→            return .computer
   671→        }
   672→
   673→        // Windows file sharing
   674→        if port == 445 || port == 139 {
   675→            return .computer
   676→        }
   677→
   678→        // Web servers could be anything
   679→        if port == 80 || port == 443 || port == 8080 {
   680→            if let name = hostname?.lowercased() {
   681→                if name.contains("printer") {
   682→                    return .printer
   683→                }
   684→                if name.contains("nas") || name.contains("synology") || name.contains("qnap") {
   685→                    return .nas
   686→                }
   687→            }
   688→        }
   689→
   690→        return .unknown
   691→    }
   692→
   693→    private func resolveHostname(_ ip: String) async -> String? {
   694→        return await withCheckedContinuation { continuation in
   695→            var hints = addrinfo()
   696→            hints.ai_family = AF_INET
   697→            hints.ai_socktype = SOCK_STREAM
   698→
   699→            var result: UnsafeMutablePointer<addrinfo>?
   700→            let status = getaddrinfo(ip, nil, &hints, &result)
   701→
   702→            guard status == 0, let res = result else {
   703→                continuation.resume(returning: nil)
   704→                return
   705→            }
   706→
   707→            defer { freeaddrinfo(res) }
   708→
   709→            var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
   710→            let success = getnameinfo(
   711→                res.pointee.ai_addr,
   712→                res.pointee.ai_addrlen,
   713→                &hostname,
   714→                socklen_t(hostname.count),
   715→                nil,
   716→                0,
   717→                0
   718→            )
   719→
   720→            if success == 0 {
   721→                let name = String(cString: hostname)
   722→                if name != ip {
   723→                    continuation.resume(returning: name)
   724→                    return
   725→                }
   726→            }
   727→
   728→            continuation.resume(returning: nil)
   729→        }
   730→    }
   731→
   732→    // MARK: - Ping
   733→    func ping(host: String, timeout: TimeInterval = 2.0) async -> PingResult {
   734→        let start = Date()
   735→        let hostString = host
   736→
   737→        return await withCheckedContinuation { continuation in
   738→            let nwHost = NWEndpoint.Host(host)
   739→            let port = NWEndpoint.Port(rawValue: 80)!
   740→            let connection = NWConnection(host: nwHost, port: port, using: .tcp)
   741→
   742→            // Use class to track if already resumed (reference type for closure capture)
   743→            class ResumeTracker { var hasResumed = false }
   744→            let tracker = ResumeTracker()
   745→
   746→            let safeResume: (PingResult) -> Void = { result in
   747→                guard !tracker.hasResumed else { return }
   748→                tracker.hasResumed = true
   749→                continuation.resume(returning: result)
   750→            }
   751→
   752→            let timer = DispatchSource.makeTimerSource(queue: self.queue)
   753→            timer.schedule(deadline: .now() + timeout)
   754→            timer.setEventHandler {
   755→                connection.cancel()
   756→                safeResume(PingResult(
   757→                    timestamp: Date(),
   758→                    host: hostString,
   759→                    latency: nil,
   760→                    success: false
   761→                ))
   762→            }
   763→            timer.resume()
   764→
   765→            connection.stateUpdateHandler = { state in
   766→                switch state {
   767→                case .ready:
   768→                    timer.cancel()
   769→                    let latency = Date().timeIntervalSince(start) * 1000
   770→                    connection.cancel()
   771→                    safeResume(PingResult(
   772→                        timestamp: Date(),
   773→                        host: hostString,
   774→                        latency: latency,
   775→                        success: true
   776→                    ))
   777→                case .failed:
   778→                    timer.cancel()
   779→                    safeResume(PingResult(
   780→                        timestamp: Date(),
   781→                        host: hostString,
   782→                        latency: nil,
   783→                        success: false
   784→                    ))
   785→                case .cancelled:
   786→                    // Don't resume here - timer already handled it
   787→                    timer.cancel()
   788→                default:
   789→                    break
   790→                }
   791→            }
   792→
   793→            connection.start(queue: self.queue)
   794→        }
   795→    }
   796→
   797→    // MARK: - Monitor Targets
   798→    func addMonitorTarget(host: String, name: String? = nil) {
   799→        let target = MonitorTarget(host: host, name: name)
   800→        monitorTargets.append(target)
   801→        saveMonitorTargets()
   802→        startPingMonitor(for: target)
   803→    }
   804→
   805→    func removeMonitorTarget(_ target: MonitorTarget) {
   806→        stopPingMonitor(for: target)
   807→        monitorTargets.removeAll { $0.id == target.id }
   808→        saveMonitorTargets()
   809→    }
   810→
   811→    func startPingMonitor(for target: MonitorTarget) {
   812→        guard target.isEnabled else { return }
   813→
   814→        // Initialize status tracking
   815→        if lastTargetStatus[target.host] == nil {
   816→            lastTargetStatus[target.host] = true // Assume online initially
   817→            consecutiveFailures[target.host] = 0
   818→        }
   819→
   820→        let timer = Timer.scheduledTimer(withTimeInterval: target.interval, repeats: true) { [weak self] _ in
   821→            Task { @MainActor in
   822→                guard let self = self else { return }
   823→                let result = await self.ping(host: target.host)
   824→                self.pingResults.append(result)
   825→
   826→                // Check for status change and send notifications
   827→                self.checkStatusChange(target: target, result: result)
   828→
   829→                // Keep only last 100 results per target
   830→                let targetResults = self.pingResults.filter { $0.host == target.host }
   831→                if targetResults.count > 100 {
   832→                    self.pingResults = self.pingResults.suffix(100 * self.monitorTargets.count)
   833→                }
   834→            }
   835→        }
   836→
   837→        pingTimers[target.id] = timer
   838→    }
   839→
   840→    private func checkStatusChange(target: MonitorTarget, result: PingResult) {
   841→        let wasOnline = lastTargetStatus[target.host] ?? true
   842→        let notificationService = NotificationService.shared
   843→
   844→        if result.success {
   845→            // Device is online
   846→            consecutiveFailures[target.host] = 0
   847→
   848→            if !wasOnline {
   849→                // Was offline, now online - send recovery notification
   850→                lastTargetStatus[target.host] = true
   851→                notificationService.sendDeviceOnlineNotification(
   852→                    device: target.name,
   853→                    ip: target.host
   854→                )
   855→            }
   856→
   857→            // Check for high latency warning (> 200ms)
   858→            if let latency = result.latency, latency > 200 {
   859→                notificationService.sendHighLatencyNotification(
   860→                    device: target.name,
   861→                    latency: latency
   862→                )
   863→            }
   864→        } else {
   865→            // Device failed to respond
   866→            consecutiveFailures[target.host] = (consecutiveFailures[target.host] ?? 0) + 1
   867→
   868→            if wasOnline && consecutiveFailures[target.host]! >= failureThreshold {
   869→                // Was online, now confirmed offline after threshold failures
   870→                lastTargetStatus[target.host] = false
   871→                notificationService.sendDeviceOfflineNotification(
   872→                    device: target.name,
   873→                    ip: target.host
   874→                )
   875→            }
   876→        }
   877→    }
   878→
   879→    func stopPingMonitor(for target: MonitorTarget) {
   880→        pingTimers[target.id]?.invalidate()
   881→        pingTimers.removeValue(forKey: target.id)
   882→    }
   883→
   884→    func stopAllPingMonitors() {
   885→        pingTimers.values.forEach { $0.invalidate() }
   886→        pingTimers.removeAll()
   887→    }
   888→
   889→    private func saveMonitorTargets() {
   890→        if let data = try? JSONEncoder().encode(monitorTargets) {
   891→            UserDefaults.standard.set(data, forKey: "monitorTargets")
   892→        }
   893→    }
   894→
   895→    private func loadMonitorTargets() {
   896→        if let data = UserDefaults.standard.data(forKey: "monitorTargets"),
   897→           let targets = try? JSONDecoder().decode([MonitorTarget].self, from: data) {
   898→            monitorTargets = targets
   899→            targets.forEach { startPingMonitor(for: $0) }
   900→        } else {
   901→            // Default targets
   902→            if let gateway = networkStatus.gatewayIP {
   903→                addMonitorTarget(host: gateway, name: "Router")
   904→            } else {
   905→                addMonitorTarget(host: "192.168.1.1", name: "Router")
   906→            }
   907→            addMonitorTarget(host: "8.8.8.8", name: "Google DNS")
   908→            addMonitorTarget(host: "1.1.1.1", name: "Cloudflare DNS")
   909→        }
   910→    }
   911→
   912→    func addRouterToMonitor() {
   913→        let gateway = networkStatus.gatewayIP ?? "192.168.1.1"
   914→        // Check if router already exists
   915→        if !monitorTargets.contains(where: { $0.host == gateway }) {
   916→            addMonitorTarget(host: gateway, name: "Router")
   917→        }
   918→    }
   919→
   920→    // MARK: - Traffic Stats
   921→    func updateTrafficStats() {
   922→        var ifaddr: UnsafeMutablePointer<ifaddrs>?
   923→        guard getifaddrs(&ifaddr) == 0 else { return }
   924→        defer { freeifaddrs(ifaddr) }
   925→
   926→        var bytesIn: UInt64 = 0
   927→        var bytesOut: UInt64 = 0
   928→
   929→        var ptr = ifaddr
   930→        while ptr != nil {
   931→            defer { ptr = ptr?.pointee.ifa_next }
   932→
   933→            guard let interface = ptr?.pointee else { continue }
   934→            let name = String(cString: interface.ifa_name)
   935→
   936→            if name.hasPrefix("en") || name.hasPrefix("pdp_ip") {
   937→                if let data = interface.ifa_data {
   938→                    let networkData = data.assumingMemoryBound(to: if_data.self)
   939→                    bytesIn += UInt64(networkData.pointee.ifi_ibytes)
   940→                    bytesOut += UInt64(networkData.pointee.ifi_obytes)
   941→                }
   942→            }
   943→        }
   944→
   945→        let now = Date()
   946→
   947→        // Calculate bandwidth rates
   948→        if let prevTime = previousTimestamp, previousBytesReceived > 0 {
   949→            let elapsed = now.timeIntervalSince(prevTime)
   950→            if elapsed > 0 {
   951→                let downloadDelta = bytesIn > previousBytesReceived ? bytesIn - previousBytesReceived : 0
   952→                let uploadDelta = bytesOut > previousBytesSent ? bytesOut - previousBytesSent : 0
   953→
   954→                trafficStats.downloadRate = Double(downloadDelta) / elapsed
   955→                trafficStats.uploadRate = Double(uploadDelta) / elapsed
   956→
   957→                // Add to history
   958→                let downloadSample = BandwidthSample(timestamp: now, bytesPerSecond: trafficStats.downloadRate)
   959→                let uploadSample = BandwidthSample(timestamp: now, bytesPerSecond: trafficStats.uploadRate)
   960→
   961→                trafficStats.downloadHistory.append(downloadSample)
   962→                trafficStats.uploadHistory.append(uploadSample)
   963→
   964→                // Keep only recent samples
   965→                if trafficStats.downloadHistory.count > maxHistorySamples {
   966→                    trafficStats.downloadHistory.removeFirst()
   967→                }
   968→                if trafficStats.uploadHistory.count > maxHistorySamples {
   969→                    trafficStats.uploadHistory.removeFirst()
   970→                }
   971→            }
   972→        }
   973→
   974→        // Store current values for next calculation
   975→        previousBytesReceived = bytesIn
   976→        previousBytesSent = bytesOut
   977→        previousTimestamp = now
   978→
   979→        trafficStats.bytesReceived = bytesIn
   980→        trafficStats.bytesSent = bytesOut
   981→        trafficStats.timestamp = now
   982→    }
   983→}
   984→
   985→// MARK: - Async Semaphore
   986→actor AsyncSemaphore {
   987→    private var count: Int
   988→    private var waiters: [CheckedContinuation<Void, Never>] = []
   989→
   990→    init(limit: Int) {
   991→        self.count = limit
   992→    }
   993→
   994→    func wait() async {
   995→        if count > 0 {
   996→            count -= 1
   997→            return
   998→        }
   999→
  1000→        await withCheckedContinuation { continuation in
  1001→            waiters.append(continuation)
  1002→        }
  1003→    }
  1004→
  1005→    func signal() {
  1006→        if let waiter = waiters.first {
  1007→            waiters.removeFirst()
  1008→            waiter.resume()
  1009→        } else {
  1010→            count += 1
  1011→        }
  1012→    }
  1013→}
  1014→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

//
//  NetworkService.swift
//  NetworkMonitor-iOS
//
//  Created by Claude Code on 2025-12-30.
//

import Foundation
import Network
import SystemConfiguration.CaptiveNetwork

@MainActor
class NetworkService: ObservableObject {
    // MARK: - Published Properties
    @Published var networkStatus = NetworkStatus()
    @Published var discoveredDevices: [NetworkDevice] = []
    @Published var currentScan: ScanResult?
    @Published var isScanning = false
    @Published var pingResults: [PingResult] = []
    @Published var trafficStats = TrafficStats()
    @Published var monitorTargets: [MonitorTarget] = []
    @Published var bonjourDevices: [NetworkDevice] = [] // Devices found via mDNS

    // MARK: - Private Properties
    private let monitor = NWPathMonitor()
    private let queue = DispatchQueue(label: "NetworkMonitor")
    private var pingTimers: [UUID: Timer] = [:]
    private var scanTask: Task<Void, Never>?
    private var lastTargetStatus: [String: Bool] = [:] // Track online/offline state
    private var consecutiveFailures: [String: Int] = [:] // Track failures before alerting
    private let failureThreshold = 3 // Number of failures before notification

    // Bonjour/mDNS discovery
    private var bonjourBrowsers: [NWBrowser] = []
    private var discoveredEndpoints: Set<String> = []

    // Bandwidth tracking
    private var previousBytesReceived: UInt64 = 0
    private var previousBytesSent: UInt64 = 0
    private var previousTimestamp: Date?
    private var trafficTimer: Timer?
    private let maxHistorySamples = 60 // Keep 60 samples (1 minute at 1s interval)

    // Signal quality tracking
    private var signalTimer: Timer?
    private var recentGatewayPings: [SignalSample] = []
    private let maxSignalSamples = 20

    // MARK: - Initialization
    init() {
        setupNetworkMonitor()
        loadMonitorTargets()
        updateTrafficStats()
        startBandwidthMonitoring()
        startSignalQualityMonitoring()
    }

    nonisolated func cleanup() {
        monitor.cancel()
    }

    func stopBandwidthMonitoring() {
        trafficTimer?.invalidate()
        trafficTimer = nil
    }

    func stopSignalQualityMonitoring() {
        signalTimer?.invalidate()
        signalTimer = nil
    }

    // MARK: - Bonjour/mDNS Discovery
    func startBonjourDiscovery() {
        stopBonjourDiscovery()
        discoveredEndpoints.removeAll()
        bonjourDevices.removeAll()

        // Common Bonjour service types to discover devices
        let serviceTypes = [
            "_http._tcp",           // Web servers
            "_https._tcp",          // HTTPS servers
            "_ssh._tcp",            // SSH (Macs, Linux)
            "_sftp-ssh._tcp",       // SFTP
            "_smb._tcp",            // Windows file sharing
            "_afpovertcp._tcp",     // Apple File Protocol (Macs)
            "_nfs._tcp",            // Network File System
            "_ftp._tcp",            // FTP servers
            "_printer._tcp",        // Printers
            "_ipp._tcp",            // Internet Printing Protocol
            "_pdl-datastream._tcp", // Printer
            "_scanner._tcp",        // Scanners
            "_airplay._tcp",        // Apple TV, AirPlay speakers
            "_raop._tcp",           // AirPlay audio
            "_homekit._tcp",        // HomeKit devices
            "_hap._tcp",            // HomeKit Accessory Protocol
            "_spotify-connect._tcp",// Spotify devices
            "_googlecast._tcp",     // Chromecast
            "_companion-link._tcp", // Apple devices
            "_sleep-proxy._udp",    // Apple sleep proxy
            "_device-info._tcp",    // Device info
            "_workstation._tcp",    // Mac workstations
        ]

        for serviceType in serviceTypes {
            let browser = NWBrowser(for: .bonjour(type: serviceType, domain: "local"), using: .tcp)

            browser.stateUpdateHandler = { state in
                // Browser state updates (connecting, ready, failed)
            }

            browser.browseResultsChangedHandler = { [weak self] results, _ in
                Task { @MainActor in
                    self?.handleBonjourResults(results, serviceType: serviceType)
                }
            }

            browser.start(queue: queue)
            bonjourBrowsers.append(browser)
        }
    }

    func stopBonjourDiscovery() {
        bonjourBrowsers.forEach { $0.cancel() }
        bonjourBrowsers.removeAll()
    }

    private func handleBonjourResults(_ results: Set<NWBrowser.Result>, serviceType: String) {
        for result in results {
            if case let .service(name, type, domain, _) = result.endpoint {
                let key = "\(name).\(type).\(domain)"

                if !discoveredEndpoints.contains(key) {
                    discoveredEndpoints.insert(key)

                    // Resolve the service to get IP
                    resolveService(name: name, type: type, domain: domain, serviceType: serviceType)
                }
            }
        }
    }

    private func resolveService(name: String, type: String, domain: String, serviceType: String) {
        let endpoint = NWEndpoint.service(name: name, type: type, domain: domain, interface: nil)
        let connection = NWConnection(to: endpoint, using: .tcp)

        connection.stateUpdateHandler = { [weak self] state in
            switch state {
            case .ready:
                if let innerEndpoint = connection.currentPath?.remoteEndpoint,
                   case let .hostPort(host, _) = innerEndpoint {
                    let ipString = "\(host)"
                    // Extract just the IP from the host description
                    let ip = ipString.replacingOccurrences(of: "%.*", with: "", options: .regularExpression)

                    Task { @MainActor in
                        self?.addBonjourDevice(
                            ip: ip,
                            name: name,
                            serviceType: serviceType
                        )
                    }
                }
                connection.cancel()
            case .failed, .cancelled:
                break
            default:
                break
            }
        }

        connection.start(queue: queue)

        // Cancel after timeout
        DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
            connection.cancel()
        }
    }

    private func addBonjourDevice(ip: String, name: String, serviceType: String) {
        // Check if device already exists
        if bonjourDevices.contains(where: { $0.ipAddress == ip }) {
            return
        }

        var device = NetworkDevice(ipAddress: ip)
        device.hostname = name
        device.isReachable = true
        device.lastSeen = Date()
        device.deviceType = guessDeviceTypeFromService(serviceType, name: name)

        bonjourDevices.append(device)

        // Also add to discovered devices if not already there
        if !discoveredDevices.contains(where: { $0.ipAddress == ip }) {
            discoveredDevices.append(device)
        }
    }

    private func guessDeviceTypeFromService(_ serviceType: String, name: String) -> NetworkDevice.DeviceType {
        let nameLower = name.lowercased()

        // Check name first
        if nameLower.contains("macbook") || nameLower.contains("imac") || nameLower.contains("mac mini") || nameLower.contains("mac pro") {
            return .computer
        }
        if nameLower.contains("iphone") {
            return .phone
        }
        if nameLower.contains("ipad") {
            return .tablet
        }
        if nameLower.contains("apple tv") || nameLower.contains("appletv") {
            return .iot
        }
        if nameLower.contains("printer") || nameLower.contains("hp ") || nameLower.contains("epson") || nameLower.contains("canon") || nameLower.contains("brother") {
            return .printer
        }
        if nameLower.contains("synology") || nameLower.contains("qnap") || nameLower.contains("nas") {
            return .nas
        }
        if nameLower.contains("router") || nameLower.contains("gateway") {
            return .router
        }

        // Check service type
        switch serviceType {
        case "_printer._tcp", "_ipp._tcp", "_pdl-datastream._tcp":
            return .printer
        case "_scanner._tcp":
            return .printer
        case "_airplay._tcp", "_raop._tcp", "_homekit._tcp", "_hap._tcp", "_googlecast._tcp":
            return .iot
        case "_ssh._tcp", "_sftp-ssh._tcp", "_workstation._tcp":
            return .computer
        case "_smb._tcp", "_afpovertcp._tcp", "_nfs._tcp":
            return .nas
        case "_companion-link._tcp", "_device-info._tcp":
            return .phone
        default:
            return .unknown
        }
    }

    private func startBandwidthMonitoring() {
        // Update every second for smooth bandwidth readings
        trafficTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
            Task { @MainActor in
                self?.updateTrafficStats()
            }
        }
    }

    private func startSignalQualityMonitoring() {
        // Measure signal quality every 3 seconds
        signalTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { [weak self] _ in
            Task { @MainActor in
                await self?.measureSignalQuality()
            }
        }
        // Initial measurement
        Task {
            await measureSignalQuality()
        }
    }

    private func measureSignalQuality() async {
        guard networkStatus.isConnected,
              let gateway = networkStatus.gatewayIP else {
            networkStatus.signalQuality.level = .none
            return
        }

        // Ping gateway
        let result = await ping(host: gateway, timeout: 2.0)

        // Add sample
        let sample = SignalSample(
            timestamp: Date(),
            latency: result.latency ?? 0,
            success: result.success
        )
        recentGatewayPings.append(sample)

        // Keep only recent samples
        if recentGatewayPings.count > maxSignalSamples {
            recentGatewayPings.removeFirst()
        }

        // Calculate metrics
        let successfulPings = recentGatewayPings.filter { $0.success }
        let failedPings = recentGatewayPings.filter { !$0.success }

        // Packet loss percentage
        let packetLoss = recentGatewayPings.isEmpty ? 100.0 :
            Double(failedPings.count) / Double(recentGatewayPings.count) * 100

        // Average latency (only successful pings)
        let latencies = successfulPings.map { $0.latency }
        let avgLatency = latencies.isEmpty ? 0 : latencies.reduce(0, +) / Double(latencies.count)

        // Jitter (variation in latency)
        var jitter: Double = 0
        if latencies.count > 1 {
            var diffs: [Double] = []
            for i in 1..<latencies.count {
                diffs.append(abs(latencies[i] - latencies[i-1]))
            }
            jitter = diffs.reduce(0, +) / Double(diffs.count)
        }

        // Update signal quality
        networkStatus.signalQuality.latency = avgLatency
        networkStatus.signalQuality.jitter = jitter
        networkStatus.signalQuality.packetLoss = packetLoss
        networkStatus.signalQuality.lastUpdated = Date()
        networkStatus.signalQuality.history = recentGatewayPings
        networkStatus.signalQuality.level = SignalQuality.calculateLevel(
            latency: avgLatency,
            jitter: jitter,
            packetLoss: packetLoss
        )
    }

    // MARK: - Network Status Monitoring
    private func setupNetworkMonitor() {
        monitor.pathUpdateHandler = { [weak self] path in
            Task { @MainActor in
                self?.updateNetworkStatus(path)
            }
        }
        monitor.start(queue: queue)
    }

    private func updateNetworkStatus(_ path: NWPath) {
        networkStatus.isConnected = path.status == .satisfied

        if path.usesInterfaceType(.wifi) {
            networkStatus.connectionType = .wifi
            fetchWiFiInfo()
        } else if path.usesInterfaceType(.cellular) {
            networkStatus.connectionType = .cellular
        } else if path.usesInterfaceType(.wiredEthernet) {
            networkStatus.connectionType = .ethernet
        } else {
            networkStatus.connectionType = .unknown
        }

        fetchLocalIPAddress()
        fetchExternalIP()
    }

    private func fetchWiFiInfo() {
        #if !targetEnvironment(simulator)
        if let interfaces = CNCopySupportedInterfaces() as? [String] {
            for interface in interfaces {
                if let info = CNCopyCurrentNetworkInfo(interface as CFString) as? [String: Any] {
                    networkStatus.ssid = info[kCNNetworkInfoKeySSID as String] as? String
                }
            }
        }
        #else
        networkStatus.ssid = "Simulator WiFi"
        #endif
    }

    private func fetchLocalIPAddress() {
        var address: String?
        var ifaddr: UnsafeMutablePointer<ifaddrs>?

        guard getifaddrs(&ifaddr) == 0 else { return }
        defer { freeifaddrs(ifaddr) }

        var ptr = ifaddr
        while ptr != nil {
            defer { ptr = ptr?.pointee.ifa_next }

            guard let interface = ptr?.pointee else { continue }
            let addrFamily = interface.ifa_addr.pointee.sa_family

            if addrFamily == UInt8(AF_INET) {
                let name = String(cString: interface.ifa_name)
                if name == "en0" || name == "en1" { // WiFi or Ethernet
                    var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
                    getnameinfo(
                        interface.ifa_addr,
                        socklen_t(interface.ifa_addr.pointee.sa_len),
                        &hostname,
                        socklen_t(hostname.count),
                        nil,
                        0,
                        NI_NUMERICHOST
                    )
                    address = String(cString: hostname)
                }
            }
        }

        networkStatus.localIP = address
        if let ip = address {
            let components = ip.split(separator: ".")
            if components.count == 4 {
                networkStatus.gatewayIP = "\(components[0]).\(components[1]).\(components[2]).1"
                networkStatus.subnet = "\(components[0]).\(components[1]).\(components[2]).0/24"
            }
        }
    }

    private func fetchExternalIP() {
        Task {
            do {
                let url = URL(string: "https://api.ipify.org")!
                let (data, _) = try await URLSession.shared.data(from: url)
                if let ip = String(data: data, encoding: .utf8) {
                    await MainActor.run {
                        self.networkStatus.externalIP = ip
                    }
                }
            } catch {
                print("Failed to fetch external IP: \(error)")
            }
        }
    }

    // MARK: - Network Scanning
    func startScan() {
        guard !isScanning, let localIP = networkStatus.localIP else { return }

        isScanning = true
        discoveredDevices = []
        bonjourDevices = []

        let components = localIP.split(separator: ".")
        guard components.count == 4 else { return }

        let baseIP = "\(components[0]).\(components[1]).\(components[2])"
        let ipRange = "\(baseIP).1-254"

        currentScan = ScanResult(
            startTime: Date(),
            ipRange: ipRange,
            totalIPs: 254
        )

        // Start Bonjour discovery in parallel - finds Apple devices and services
        startBonjourDiscovery()

        scanTask = Task {
            await scanNetwork(baseIP: baseIP)
        }
    }

    func stopScan() {
        scanTask?.cancel()
        stopBonjourDiscovery()
        isScanning = false
        currentScan?.endTime = Date()
        currentScan?.isComplete = true
    }

    private func scanNetwork(baseIP: String) async {
        // Reduced concurrency for more reliable detection
        // Testing 10 ports per IP means we need less parallel IPs
        let concurrency = 25
        let semaphore = AsyncSemaphore(limit: concurrency)

        await withTaskGroup(of: NetworkDevice?.self) { group in
            for i in 1...254 {
                group.addTask {
                    await semaphore.wait()
                    defer { Task { await semaphore.signal() } }

                    let ip = "\(baseIP).\(i)"
                    let device = await self.probeHost(ip)

                    await MainActor.run {
                        self.currentScan?.scannedIPs = i
                    }

                    return device
                }
            }

            for await device in group {
                if let device = device {
                    await MainActor.run {
                        self.discoveredDevices.append(device)
                        self.currentScan?.devicesFound.append(device)
                    }
                }
            }
        }

        await MainActor.run {
            // Stop Bonjour discovery
            self.stopBonjourDiscovery()

            // Merge Bonjour devices that weren't found by port scan
            for bonjourDevice in self.bonjourDevices {
                if !self.discoveredDevices.contains(where: { $0.ipAddress == bonjourDevice.ipAddress }) {
                    self.discoveredDevices.append(bonjourDevice)
                    self.currentScan?.devicesFound.append(bonjourDevice)
                }
            }

            // Sort devices by IP
            self.discoveredDevices.sort { ip1, ip2 in
                let parts1 = ip1.ipAddress.split(separator: ".").compactMap { Int($0) }
                let parts2 = ip2.ipAddress.split(separator: ".").compactMap { Int($0) }
                if parts1.count == 4 && parts2.count == 4 {
                    return parts1[3] < parts2[3]
                }
                return ip1.ipAddress < ip2.ipAddress
            }

            self.currentScan?.endTime = Date()
            self.currentScan?.isComplete = true
            self.isScanning = false
        }
    }

    private func probeHost(_ ip: String) async -> NetworkDevice? {
        // Try multiple ports to detect more devices
        // Port 80: HTTP servers, routers
        // Port 443: HTTPS servers
        // Port 22: SSH (Linux, Mac)
        // Port 445: SMB (Windows file sharing)
        // Port 8080: Alternative HTTP
        // Port 62078: Apple devices (lockdownd/usbmuxd)
        // Port 548: AFP (Mac file sharing)
        // Port 5353: mDNS (responds to TCP too sometimes)
        // Port 139: NetBIOS (Windows)
        // Port 7000: AirPlay (Apple TV)

        let portsToTry: [UInt16] = [80, 443, 62078, 22, 445, 548, 8080, 7000, 139, 5353]
        let timeout: TimeInterval = 1.5 // Increased timeout for slower devices

        // Try all ports concurrently
        let result = await withTaskGroup(of: (Bool, Double?, UInt16).self) { group in
            for port in portsToTry {
                group.addTask {
                    let pingResult = await self.pingPort(host: ip, port: port, timeout: timeout)
                    return (pingResult.success, pingResult.latency, port)
                }
            }

            // Return first successful result
            for await result in group {
                if result.0 { // success
                    return result
                }
            }
            return (false, nil, UInt16(0))
        }

        if result.0 { // success
            var device = NetworkDevice(ipAddress: ip)
            device.isReachable = true
            device.latency = result.1
            device.lastSeen = Date()

            // Try to resolve hostname
            device.hostname = await resolveHostname(ip)

            // Guess device type based on responding port and hostname
            device.deviceType = guessDeviceType(ip: ip, port: result.2, hostname: device.hostname)

            return device
        }

        return nil
    }

    private func pingPort(host: String, port: UInt16, timeout: TimeInterval) async -> PingResult {
        let start = Date()

        return await withCheckedContinuation { continuation in
            let nwHost = NWEndpoint.Host(host)
            guard let nwPort = NWEndpoint.Port(rawValue: port) else {
                continuation.resume(returning: PingResult(
                    timestamp: Date(),
                    host: host,
                    latency: nil,
                    success: false
                ))
                return
            }

            let connection = NWConnection(host: nwHost, port: nwPort, using: .tcp)

            class ResumeTracker { var hasResumed = false }
            let tracker = ResumeTracker()

            let safeResume: (PingResult) -> Void = { result in
                guard !tracker.hasResumed else { return }
                tracker.hasResumed = true
                continuation.resume(returning: result)
            }

            let timer = DispatchSource.makeTimerSource(queue: self.queue)
            timer.schedule(deadline: .now() + timeout)
            timer.setEventHandler {
                connection.cancel()
                safeResume(PingResult(
                    timestamp: Date(),
                    host: host,
                    latency: nil,
                    success: false
                ))
            }
            timer.resume()

            connection.stateUpdateHandler = { state in
                switch state {
                case .ready:
                    timer.cancel()
                    let latency = Date().timeIntervalSince(start) * 1000
                    connection.cancel()
                    safeResume(PingResult(
                        timestamp: Date(),
                        host: host,
                        latency: latency,
                        success: true
                    ))
                case .failed:
                    timer.cancel()
                    safeResume(PingResult(
                        timestamp: Date(),
                        host: host,
                        latency: nil,
                        success: false
                    ))
                case .cancelled:
                    timer.cancel()
                default:
                    break
                }
            }

            connection.start(queue: self.queue)
        }
    }

    private func guessDeviceType(ip: String, port: UInt16, hostname: String?) -> NetworkDevice.DeviceType {
        // Router detection
        if ip.hasSuffix(".1") || ip.hasSuffix(".254") {
            return .router
        }

        // Apple device detection
        if port == 62078 || port == 548 || port == 7000 {
            if let name = hostname?.lowercased() {
                if name.contains("macbook") || name.contains("imac") || name.contains("mac-mini") || name.contains("mac-pro") {
                    return .computer
                }
                if name.contains("iphone") {
                    return .phone
                }
                if name.contains("ipad") {
                    return .tablet
                }
                if name.contains("appletv") || name.contains("apple-tv") {
                    return .iot
                }
            }
            return .phone // Default Apple device
        }

        // SSH suggests computer
        if port == 22 {
            return .computer
        }

        // Windows file sharing
        if port == 445 || port == 139 {
            return .computer
        }

        // Web servers could be anything
        if port == 80 || port == 443 || port == 8080 {
            if let name = hostname?.lowercased() {
                if name.contains("printer") {
                    return .printer
                }
                if name.contains("nas") || name.contains("synology") || name.contains("qnap") {
                    return .nas
                }
            }
        }

        return .unknown
    }

    private func resolveHostname(_ ip: String) async -> String? {
        return await withCheckedContinuation { continuation in
            var hints = addrinfo()
            hints.ai_family = AF_INET
            hints.ai_socktype = SOCK_STREAM

            var result: UnsafeMutablePointer<addrinfo>?
            let status = getaddrinfo(ip, nil, &hints, &result)

            guard status == 0, let res = result else {
                continuation.resume(returning: nil)
                return
            }

            defer { freeaddrinfo(res) }

            var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
            let success = getnameinfo(
                res.pointee.ai_addr,
                res.pointee.ai_addrlen,
                &hostname,
                socklen_t(hostname.count),
                nil,
                0,
                0
            )

            if success == 0 {
                let name = String(cString: hostname)
                if name != ip {
                    continuation.resume(returning: name)
                    return
                }
            }

            continuation.resume(returning: nil)
        }
    }

    // MARK: - Ping
    func ping(host: String, timeout: TimeInterval = 2.0) async -> PingResult {
        let start = Date()
        let hostString = host

        return await withCheckedContinuation { continuation in
            let nwHost = NWEndpoint.Host(host)
            let port = NWEndpoint.Port(rawValue: 80)!
            let connection = NWConnection(host: nwHost, port: port, using: .tcp)

            // Use class to track if already resumed (reference type for closure capture)
            class ResumeTracker { var hasResumed = false }
            let tracker = ResumeTracker()

            let safeResume: (PingResult) -> Void = { result in
                guard !tracker.hasResumed else { return }
                tracker.hasResumed = true
                continuation.resume(returning: result)
            }

            let timer = DispatchSource.makeTimerSource(queue: self.queue)
            timer.schedule(deadline: .now() + timeout)
            timer.setEventHandler {
                connection.cancel()
                safeResume(PingResult(
                    timestamp: Date(),
                    host: hostString,
                    latency: nil,
                    success: false
                ))
            }
            timer.resume()

            connection.stateUpdateHandler = { state in
                switch state {
                case .ready:
                    timer.cancel()
                    let latency = Date().timeIntervalSince(start) * 1000
                    connection.cancel()
                    safeResume(PingResult(
                        timestamp: Date(),
                        host: hostString,
                        latency: latency,
                        success: true
                    ))
                case .failed:
                    timer.cancel()
                    safeResume(PingResult(
                        timestamp: Date(),
                        host: hostString,
                        latency: nil,
                        success: false
                    ))
                case .cancelled:
                    // Don't resume here - timer already handled it
                    timer.cancel()
                default:
                    break
                }
            }

            connection.start(queue: self.queue)
        }
    }

    // MARK: - Monitor Targets
    func addMonitorTarget(host: String, name: String? = nil) {
        let target = MonitorTarget(host: host, name: name)
        monitorTargets.append(target)
        saveMonitorTargets()
        startPingMonitor(for: target)
    }

    func removeMonitorTarget(_ target: MonitorTarget) {
        stopPingMonitor(for: target)
        monitorTargets.removeAll { $0.id == target.id }
        saveMonitorTargets()
    }

    func startPingMonitor(for target: MonitorTarget) {
        guard target.isEnabled else { return }

        // Initialize status tracking
        if lastTargetStatus[target.host] == nil {
            lastTargetStatus[target.host] = true // Assume online initially
            consecutiveFailures[target.host] = 0
        }

        let timer = Timer.scheduledTimer(withTimeInterval: target.interval, repeats: true) { [weak self] _ in
            Task { @MainActor in
                guard let self = self else { return }
                let result = await self.ping(host: target.host)
                self.pingResults.append(result)

                // Check for status change and send notifications
                self.checkStatusChange(target: target, result: result)

                // Keep only last 100 results per target
                let targetResults = self.pingResults.filter { $0.host == target.host }
                if targetResults.count > 100 {
                    self.pingResults = self.pingResults.suffix(100 * self.monitorTargets.count)
                }
            }
        }

        pingTimers[target.id] = timer
    }

    private func checkStatusChange(target: MonitorTarget, result: PingResult) {
        let wasOnline = lastTargetStatus[target.host] ?? true
        let notificationService = NotificationService.shared

        if result.success {
            // Device is online
            consecutiveFailures[target.host] = 0

            if !wasOnline {
                // Was offline, now online - send recovery notification
                lastTargetStatus[target.host] = true
                notificationService.sendDeviceOnlineNotification(
                    device: target.name,
                    ip: target.host
                )
            }

            // Check for high latency warning (> 200ms)
            if let latency = result.latency, latency > 200 {
                notificationService.sendHighLatencyNotification(
                    device: target.name,
                    latency: latency
                )
            }
        } else {
            // Device failed to respond
            consecutiveFailures[target.host] = (consecutiveFailures[target.host] ?? 0) + 1

            if wasOnline && consecutiveFailures[target.host]! >= failureThreshold {
                // Was online, now confirmed offline after threshold failures
                lastTargetStatus[target.host] = false
                notificationService.sendDeviceOfflineNotification(
                    device: target.name,
                    ip: target.host
                )
            }
        }
    }

    func stopPingMonitor(for target: MonitorTarget) {
        pingTimers[target.id]?.invalidate()
        pingTimers.removeValue(forKey: target.id)
    }

    func stopAllPingMonitors() {
        pingTimers.values.forEach { $0.invalidate() }
        pingTimers.removeAll()
    }

    private func saveMonitorTargets() {
        if let data = try? JSONEncoder().encode(monitorTargets) {
            UserDefaults.standard.set(data, forKey: "monitorTargets")
        }
    }

    private func loadMonitorTargets() {
        if let data = UserDefaults.standard.data(forKey: "monitorTargets"),
           let targets = try? JSONDecoder().decode([MonitorTarget].self, from: data) {
            monitorTargets = targets
            targets.forEach { startPingMonitor(for: $0) }
        } else {
            // Default targets
            if let gateway = networkStatus.gatewayIP {
                addMonitorTarget(host: gateway, name: "Router")
            } else {
                addMonitorTarget(host: "192.168.1.1", name: "Router")
            }
            addMonitorTarget(host: "8.8.8.8", name: "Google DNS")
            addMonitorTarget(host: "1.1.1.1", name: "Cloudflare DNS")
        }
    }

    func addRouterToMonitor() {
        let gateway = networkStatus.gatewayIP ?? "192.168.1.1"
        // Check if router already exists
        if !monitorTargets.contains(where: { $0.host == gateway }) {
            addMonitorTarget(host: gateway, name: "Router")
        }
    }

    // MARK: - Traffic Stats
    func updateTrafficStats() {
        var ifaddr: UnsafeMutablePointer<ifaddrs>?
        guard getifaddrs(&ifaddr) == 0 else { return }
        defer { freeifaddrs(ifaddr) }

        var bytesIn: UInt64 = 0
        var bytesOut: UInt64 = 0

        var ptr = ifaddr
        while ptr != nil {
            defer { ptr = ptr?.pointee.ifa_next }

            guard let interface = ptr?.pointee else { continue }
            let name = String(cString: interface.ifa_name)

            if name.hasPrefix("en") || name.hasPrefix("pdp_ip") {
                if let data = interface.ifa_data {
                    let networkData = data.assumingMemoryBound(to: if_data.self)
                    bytesIn += UInt64(networkData.pointee.ifi_ibytes)
                    bytesOut += UInt64(networkData.pointee.ifi_obytes)
                }
            }
        }

        let now = Date()

        // Calculate bandwidth rates
        if let prevTime = previousTimestamp, previousBytesReceived > 0 {
            let elapsed = now.timeIntervalSince(prevTime)
            if elapsed > 0 {
                let downloadDelta = bytesIn > previousBytesReceived ? bytesIn - previousBytesReceived : 0
                let uploadDelta = bytesOut > previousBytesSent ? bytesOut - previousBytesSent : 0

                trafficStats.downloadRate = Double(downloadDelta) / elapsed
                trafficStats.uploadRate = Double(uploadDelta) / elapsed

                // Add to history
                let downloadSample = BandwidthSample(timestamp: now, bytesPerSecond: trafficStats.downloadRate)
                let uploadSample = BandwidthSample(timestamp: now, bytesPerSecond: trafficStats.uploadRate)

                trafficStats.downloadHistory.append(downloadSample)
                trafficStats.uploadHistory.append(uploadSample)

                // Keep only recent samples
                if trafficStats.downloadHistory.count > maxHistorySamples {
                    trafficStats.downloadHistory.removeFirst()
                }
                if trafficStats.uploadHistory.count > maxHistorySamples {
                    trafficStats.uploadHistory.removeFirst()
                }
            }
        }

        // Store current values for next calculation
        previousBytesReceived = bytesIn
        previousBytesSent = bytesOut
        previousTimestamp = now

        trafficStats.bytesReceived = bytesIn
        trafficStats.bytesSent = bytesOut
        trafficStats.timestamp = now
    }
}

// MARK: - Async Semaphore
actor AsyncSemaphore {
    private var count: Int
    private var waiters: [CheckedContinuation<Void, Never>] = []

    init(limit: Int) {
        self.count = limit
    }

    func wait() async {
        if count > 0 {
            count -= 1
            return
        }

        await withCheckedContinuation { continuation in
            waiters.append(continuation)
        }
    }

    func signal() {
        if let waiter = waiters.first {
            waiters.removeFirst()
            waiter.resume()
        } else {
            count += 1
        }
    }
}


Now let me check the related models and other service files to understand the full picture:


     1→//
     2→//  NetworkModels.swift
     3→//  NetworkMonitor-iOS
     4→//
     5→//  Created by Claude Code on 2025-12-30.
     6→//
     7→
     8→import Foundation
     9→import Network
    10→
    11→// MARK: - Network Device
    12→struct NetworkDevice: Identifiable, Hashable {
    13→    let id = UUID()
    14→    let ipAddress: String
    15→    var hostname: String?
    16→    var macAddress: String?
    17→    var isReachable: Bool = false
    18→    var latency: Double? // in ms
    19→    var lastSeen: Date = Date()
    20→    var deviceType: DeviceType = .unknown
    21→
    22→    var displayName: String {
    23→        hostname ?? ipAddress
    24→    }
    25→
    26→    enum DeviceType: String, CaseIterable {
    27→        case router = "Router"
    28→        case computer = "Computer"
    29→        case phone = "Phone"
    30→        case tablet = "Tablet"
    31→        case printer = "Printer"
    32→        case nas = "NAS"
    33→        case iot = "IoT Device"
    34→        case unknown = "Unknown"
    35→
    36→        var icon: String {
    37→            switch self {
    38→            case .router: return "wifi.router"
    39→            case .computer: return "desktopcomputer"
    40→            case .phone: return "iphone"
    41→            case .tablet: return "ipad"
    42→            case .printer: return "printer.fill"
    43→            case .nas: return "externaldrive.fill.badge.wifi"
    44→            case .iot: return "sensor"
    45→            case .unknown: return "questionmark.circle"
    46→            }
    47→        }
    48→    }
    49→}
    50→
    51→// MARK: - Network Status
    52→struct NetworkStatus {
    53→    var connectionType: ConnectionType = .unknown
    54→    var isConnected: Bool = false
    55→    var ssid: String?
    56→    var localIP: String?
    57→    var gatewayIP: String?
    58→    var subnet: String?
    59→    var externalIP: String?
    60→    var signalQuality: SignalQuality = SignalQuality()
    61→
    62→    enum ConnectionType: String {
    63→        case wifi = "WiFi"
    64→        case cellular = "Cellular"
    65→        case ethernet = "Ethernet"
    66→        case vpn = "VPN"
    67→        case unknown = "Unknown"
    68→
    69→        var icon: String {
    70→            switch self {
    71→            case .wifi: return "wifi"
    72→            case .cellular: return "antenna.radiowaves.left.and.right"
    73→            case .ethernet: return "cable.connector"
    74→            case .vpn: return "lock.shield"
    75→            case .unknown: return "network.slash"
    76→            }
    77→        }
    78→    }
    79→}
    80→
    81→// MARK: - Signal Quality
    82→struct SignalQuality {
    83→    var level: SignalLevel = .unknown
    84→    var latency: Double = 0 // ms to gateway
    85→    var jitter: Double = 0 // latency variation
    86→    var packetLoss: Double = 0 // percentage
    87→    var lastUpdated: Date = Date()
    88→    var history: [SignalSample] = []
    89→
    90→    enum SignalLevel: Int, CaseIterable {
    91→        case excellent = 4
    92→        case good = 3
    93→        case fair = 2
    94→        case poor = 1
    95→        case none = 0
    96→        case unknown = -1
    97→
    98→        var description: String {
    99→            switch self {
   100→            case .excellent: return "Excellent"
   101→            case .good: return "Good"
   102→            case .fair: return "Fair"
   103→            case .poor: return "Poor"
   104→            case .none: return "No Signal"
   105→            case .unknown: return "Unknown"
   106→            }
   107→        }
   108→
   109→        var color: String {
   110→            switch self {
   111→            case .excellent: return "green"
   112→            case .good: return "green"
   113→            case .fair: return "yellow"
   114→            case .poor: return "orange"
   115→            case .none: return "red"
   116→            case .unknown: return "gray"
   117→            }
   118→        }
   119→
   120→        var bars: Int {
   121→            max(0, rawValue)
   122→        }
   123→    }
   124→
   125→    static func calculateLevel(latency: Double, jitter: Double, packetLoss: Double) -> SignalLevel {
   126→        // No connection
   127→        if packetLoss >= 100 { return .none }
   128→
   129→        // Score based on metrics (lower is better)
   130→        var score = 0
   131→
   132→        // Latency scoring (to gateway)
   133→        switch latency {
   134→        case 0..<10: score += 40
   135→        case 10..<30: score += 30
   136→        case 30..<50: score += 20
   137→        case 50..<100: score += 10
   138→        default: score += 0
   139→        }
   140→
   141→        // Jitter scoring
   142→        switch jitter {
   143→        case 0..<5: score += 30
   144→        case 5..<15: score += 20
   145→        case 15..<30: score += 10
   146→        default: score += 0
   147→        }
   148→
   149→        // Packet loss scoring
   150→        switch packetLoss {
   151→        case 0: score += 30
   152→        case 0..<2: score += 20
   153→        case 2..<5: score += 10
   154→        default: score += 0
   155→        }
   156→
   157→        // Convert score to level
   158→        switch score {
   159→        case 80...100: return .excellent
   160→        case 60..<80: return .good
   161→        case 40..<60: return .fair
   162→        case 20..<40: return .poor
   163→        default: return .none
   164→        }
   165→    }
   166→}
   167→
   168→// MARK: - Signal Sample
   169→struct SignalSample: Identifiable {
   170→    let id = UUID()
   171→    let timestamp: Date
   172→    let latency: Double
   173→    let success: Bool
   174→}
   175→
   176→// MARK: - Scan Result
   177→struct ScanResult: Identifiable {
   178→    let id = UUID()
   179→    let startTime: Date
   180→    var endTime: Date?
   181→    var devicesFound: [NetworkDevice] = []
   182→    var ipRange: String
   183→    var totalIPs: Int
   184→    var scannedIPs: Int = 0
   185→    var isComplete: Bool = false
   186→
   187→    var progress: Double {
   188→        guard totalIPs > 0 else { return 0 }
   189→        return Double(scannedIPs) / Double(totalIPs)
   190→    }
   191→
   192→    var duration: TimeInterval {
   193→        (endTime ?? Date()).timeIntervalSince(startTime)
   194→    }
   195→}
   196→
   197→// MARK: - Ping Result
   198→struct PingResult: Identifiable {
   199→    let id = UUID()
   200→    let timestamp: Date
   201→    let host: String
   202→    let latency: Double? // nil means timeout
   203→    let success: Bool
   204→
   205→    var displayLatency: String {
   206→        if let ms = latency {
   207→            return String(format: "%.1f ms", ms)
   208→        }
   209→        return "Timeout"
   210→    }
   211→}
   212→
   213→// MARK: - Traffic Stats
   214→struct TrafficStats {
   215→    var bytesReceived: UInt64 = 0
   216→    var bytesSent: UInt64 = 0
   217→    var packetsReceived: UInt64 = 0
   218→    var packetsSent: UInt64 = 0
   219→    var timestamp: Date = Date()
   220→
   221→    // Bandwidth rates (bytes per second)
   222→    var downloadRate: Double = 0
   223→    var uploadRate: Double = 0
   224→
   225→    // History for graphs
   226→    var downloadHistory: [BandwidthSample] = []
   227→    var uploadHistory: [BandwidthSample] = []
   228→
   229→    var totalBytes: UInt64 {
   230→        bytesReceived + bytesSent
   231→    }
   232→
   233→    var totalRate: Double {
   234→        downloadRate + uploadRate
   235→    }
   236→
   237→    func formattedBytes(_ bytes: UInt64) -> String {
   238→        let formatter = ByteCountFormatter()
   239→        formatter.countStyle = .binary
   240→        return formatter.string(fromByteCount: Int64(bytes))
   241→    }
   242→
   243→    func formattedRate(_ bytesPerSecond: Double) -> String {
   244→        let formatter = ByteCountFormatter()
   245→        formatter.countStyle = .binary
   246→        if bytesPerSecond < 1024 {
   247→            return String(format: "%.0f B/s", bytesPerSecond)
   248→        } else if bytesPerSecond < 1024 * 1024 {
   249→            return String(format: "%.1f KB/s", bytesPerSecond / 1024)
   250→        } else {
   251→            return String(format: "%.2f MB/s", bytesPerSecond / 1024 / 1024)
   252→        }
   253→    }
   254→}
   255→
   256→// MARK: - Bandwidth Sample
   257→struct BandwidthSample: Identifiable {
   258→    let id = UUID()
   259→    let timestamp: Date
   260→    let bytesPerSecond: Double
   261→}
   262→
   263→// MARK: - Monitor Target
   264→struct MonitorTarget: Identifiable, Codable, Hashable {
   265→    let id: UUID
   266→    var host: String
   267→    var name: String
   268→    var isEnabled: Bool = true
   269→    var interval: TimeInterval = 5.0 // seconds
   270→
   271→    init(host: String, name: String? = nil) {
   272→        self.id = UUID()
   273→        self.host = host
   274→        self.name = name ?? host
   275→    }
   276→}
   277→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

//
//  NetworkModels.swift
//  NetworkMonitor-iOS
//
//  Created by Claude Code on 2025-12-30.
//

import Foundation
import Network

// MARK: - Network Device
struct NetworkDevice: Identifiable, Hashable {
    let id = UUID()
    let ipAddress: String
    var hostname: String?
    var macAddress: String?
    var isReachable: Bool = false
    var latency: Double? // in ms
    var lastSeen: Date = Date()
    var deviceType: DeviceType = .unknown

    var displayName: String {
        hostname ?? ipAddress
    }

    enum DeviceType: String, CaseIterable {
        case router = "Router"
        case computer = "Computer"
        case phone = "Phone"
        case tablet = "Tablet"
        case printer = "Printer"
        case nas = "NAS"
        case iot = "IoT Device"
        case unknown = "Unknown"

        var icon: String {
            switch self {
            case .router: return "wifi.router"
            case .computer: return "desktopcomputer"
            case .phone: return "iphone"
            case .tablet: return "ipad"
            case .printer: return "printer.fill"
            case .nas: return "externaldrive.fill.badge.wifi"
            case .iot: return "sensor"
            case .unknown: return "questionmark.circle"
            }
        }
    }
}

// MARK: - Network Status
struct NetworkStatus {
    var connectionType: ConnectionType = .unknown
    var isConnected: Bool = false
    var ssid: String?
    var localIP: String?
    var gatewayIP: String?
    var subnet: String?
    var externalIP: String?
    var signalQuality: SignalQuality = SignalQuality()

    enum ConnectionType: String {
        case wifi = "WiFi"
        case cellular = "Cellular"
        case ethernet = "Ethernet"
        case vpn = "VPN"
        case unknown = "Unknown"

        var icon: String {
            switch self {
            case .wifi: return "wifi"
            case .cellular: return "antenna.radiowaves.left.and.right"
            case .ethernet: return "cable.connector"
            case .vpn: return "lock.shield"
            case .unknown: return "network.slash"
            }
        }
    }
}

// MARK: - Signal Quality
struct SignalQuality {
    var level: SignalLevel = .unknown
    var latency: Double = 0 // ms to gateway
    var jitter: Double = 0 // latency variation
    var packetLoss: Double = 0 // percentage
    var lastUpdated: Date = Date()
    var history: [SignalSample] = []

    enum SignalLevel: Int, CaseIterable {
        case excellent = 4
        case good = 3
        case fair = 2
        case poor = 1
        case none = 0
        case unknown = -1

        var description: String {
            switch self {
            case .excellent: return "Excellent"
            case .good: return "Good"
            case .fair: return "Fair"
            case .poor: return "Poor"
            case .none: return "No Signal"
            case .unknown: return "Unknown"
            }
        }

        var color: String {
            switch self {
            case .excellent: return "green"
            case .good: return "green"
            case .fair: return "yellow"
            case .poor: return "orange"
            case .none: return "red"
            case .unknown: return "gray"
            }
        }

        var bars: Int {
            max(0, rawValue)
        }
    }

    static func calculateLevel(latency: Double, jitter: Double, packetLoss: Double) -> SignalLevel {
        // No connection
        if packetLoss >= 100 { return .none }

        // Score based on metrics (lower is better)
        var score = 0

        // Latency scoring (to gateway)
        switch latency {
        case 0..<10: score += 40
        case 10..<30: score += 30
        case 30..<50: score += 20
        case 50..<100: score += 10
        default: score += 0
        }

        // Jitter scoring
        switch jitter {
        case 0..<5: score += 30
        case 5..<15: score += 20
        case 15..<30: score += 10
        default: score += 0
        }

        // Packet loss scoring
        switch packetLoss {
        case 0: score += 30
        case 0..<2: score += 20
        case 2..<5: score += 10
        default: score += 0
        }

        // Convert score to level
        switch score {
        case 80...100: return .excellent
        case 60..<80: return .good
        case 40..<60: return .fair
        case 20..<40: return .poor
        default: return .none
        }
    }
}

// MARK: - Signal Sample
struct SignalSample: Identifiable {
    let id = UUID()
    let timestamp: Date
    let latency: Double
    let success: Bool
}

// MARK: - Scan Result
struct ScanResult: Identifiable {
    let id = UUID()
    let startTime: Date
    var endTime: Date?
    var devicesFound: [NetworkDevice] = []
    var ipRange: String
    var totalIPs: Int
    var scannedIPs: Int = 0
    var isComplete: Bool = false

    var progress: Double {
        guard totalIPs > 0 else { return 0 }
        return Double(scannedIPs) / Double(totalIPs)
    }

    var duration: TimeInterval {
        (endTime ?? Date()).timeIntervalSince(startTime)
    }
}

// MARK: - Ping Result
struct PingResult: Identifiable {
    let id = UUID()
    let timestamp: Date
    let host: String
    let latency: Double? // nil means timeout
    let success: Bool

    var displayLatency: String {
        if let ms = latency {
            return String(format: "%.1f ms", ms)
        }
        return "Timeout"
    }
}

// MARK: - Traffic Stats
struct TrafficStats {
    var bytesReceived: UInt64 = 0
    var bytesSent: UInt64 = 0
    var packetsReceived: UInt64 = 0
    var packetsSent: UInt64 = 0
    var timestamp: Date = Date()

    // Bandwidth rates (bytes per second)
    var downloadRate: Double = 0
    var uploadRate: Double = 0

    // History for graphs
    var downloadHistory: [BandwidthSample] = []
    var uploadHistory: [BandwidthSample] = []

    var totalBytes: UInt64 {
        bytesReceived + bytesSent
    }

    var totalRate: Double {
        downloadRate + uploadRate
    }

    func formattedBytes(_ bytes: UInt64) -> String {
        let formatter = ByteCountFormatter()
        formatter.countStyle = .binary
        return formatter.string(fromByteCount: Int64(bytes))
    }

    func formattedRate(_ bytesPerSecond: Double) -> String {
        let formatter = ByteCountFormatter()
        formatter.countStyle = .binary
        if bytesPerSecond < 1024 {
            return String(format: "%.0f B/s", bytesPerSecond)
        } else if bytesPerSecond < 1024 * 1024 {
            return String(format: "%.1f KB/s", bytesPerSecond / 1024)
        } else {
            return String(format: "%.2f MB/s", bytesPerSecond / 1024 / 1024)
        }
    }
}

// MARK: - Bandwidth Sample
struct BandwidthSample: Identifiable {
    let id = UUID()
    let timestamp: Date
    let bytesPerSecond: Double
}

// MARK: - Monitor Target
struct MonitorTarget: Identifiable, Codable, Hashable {
    let id: UUID
    var host: String
    var name: String
    var isEnabled: Bool = true
    var interval: TimeInterval = 5.0 // seconds

    init(host: String, name: String? = nil) {
        self.id = UUID()
        self.host = host
        self.name = name ?? host
    }
}