import Foundation import Observation @MainActor @Observable final class DashboardViewModel { var phase: LoadPhase = .idle var onuCounts: [(state: ONULifecycleState, count: Int)] = [] var onuTotal = 0 var oltCount = 0 var controllerCount = 0 var severityCounts: [(severity: AlarmSeverity, count: Int)] = [] // Aggregates derived from the state docs already fetched for the counts above // — no extra API calls. var totalDownstreamBps = 0.0 var totalUpstreamBps = 0.0 var abnormalRxCount = 0 // ONUs with Rx optical < −28 or > −10 dBm var lasersOffCount = 0 // OLTs with the laser shut down var opticalAvailable = false // whether the bulk ONU-STATE response carried Rx levels // The actual offenders, kept for drill-down (no re-fetch). var abnormalRxONUs: [ONUStateDoc] = [] var lasersOffOLTs: [OLTStateDoc] = [] private let connection: MCMSConnection init(connection: MCMSConnection) { self.connection = connection } /// Light by default: the ONU count, state breakdown, OLT traffic and /// laser-off count all come from the (small) OLT-STATE docs. The heavy /// per-ONU optical scan runs only when `extraStats` is on. func load(extraStats: Bool = false) async { phase = .loading do { // OLT-STATE drives the OLT count, ONU registration (dev guide // Procedure 2), total traffic, and laser state — all from a handful // of small docs. let olts = try await connection.olt.allStates() oltCount = olts.count let registration = OLTRepository.registrationMap(from: olts) onuTotal = registration.count onuCounts = Dictionary(grouping: registration.values, by: { $0 }) .map { (state: $0.key, count: $0.value.count) } .sorted { $0.count > $1.count } totalDownstreamBps = olts.compactMap(\.txRateBps).reduce(0, +) totalUpstreamBps = olts.compactMap(\.rxRateBps).reduce(0, +) lasersOffOLTs = olts.filter(\.isLaserOff) lasersOffCount = lasersOffOLTs.count controllerCount = (try? await connection.controller.allConfigs().count) ?? 0 if let alarms = try? await connection.onu.alarmHistories() { severityCounts = Dictionary(grouping: alarms, by: { $0.severity }) .map { (severity: $0.key, count: $0.value.count) } .sorted { $0.severity.rank < $1.severity.rank } } // Opt-in heavy scan: pull every ONU-STATE for the Rx optical check. if extraStats { let onus = try await connection.onu.allStates() opticalAvailable = onus.contains { $0.rxOpticalDBm != nil } abnormalRxONUs = onus.filter { onu in guard let rx = onu.rxOpticalDBm else { return false } return rx < -28 || rx > -10 } abnormalRxCount = abnormalRxONUs.count } else { opticalAvailable = false abnormalRxONUs = [] abnormalRxCount = 0 } phase = .loaded } catch { phase = .failed((error as? APIError)?.localizedDescription ?? error.localizedDescription) } } }