Addresses a multi-agent code review (26 confirmed findings + a URLSession leak) across correctness, concurrency, security, and cleanup: - MongoDocument.id: deterministic fallback (was UUID() on every access) to stop SwiftUI List/ForEach identity churn; getAll pagination drives its cursor off the real _id via documentId, so a missing _id stops paging instead of looping on a fabricated cursor. - View models: per-load generation guard (+ captured-tab guard on ONU detail) so a cancelled or overlapping load can't commit stale results or land data on the wrong tab. - JSONValue.intValue: guard Int(d) against NaN/inf/out-of-range (crash). - ServerTrustEvaluator: hash the real SubjectPublicKeyInfo (prepend the algorithm-specific ASN.1 SPKI header) so a standard openssl-captured public-key pin actually matches; unknown key types fail closed. - AppEnvironment.configure: clear stale cookies + Keychain creds on host change. - FirmwareFile.isCompatible: empty metadata no longer passes as compatible-for-all. - APIClient: invalidate URLSession on deinit (per-configure/probe leak). - Dashboard best-effort sections keep last-good on transient failure; unified optical thresholds; LoginView prefill-once; populatedBuckets unique ForEach id; Format rounding rollover; dead-code/docstring/dedup cleanups. Verification pending on the Mac (no Swift toolchain on the Linux dev box). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
89 lines
4.1 KiB
Swift
89 lines
4.1 KiB
Swift
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
|
||
/// Bumped per load; only the latest load commits, so the 30s auto-refresh loop
|
||
/// and a manual pull-to-refresh can't interleave partial writes.
|
||
private var loadGeneration = 0
|
||
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 {
|
||
loadGeneration += 1
|
||
let gen = loadGeneration
|
||
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. Fetch everything into locals first, then commit once,
|
||
// so an overlapping load can't publish a half-updated dashboard.
|
||
let olts = try await connection.olt.allStates()
|
||
let registration = OLTRepository.registrationMap(from: olts)
|
||
|
||
// Best-effort sections: nil means "fetch failed this cycle" → keep the
|
||
// last good value rather than flicker to empty/zero on a transient blip.
|
||
let controllers = try? await connection.controller.allConfigs().count
|
||
let alarms = try? await connection.onu.alarmHistories()
|
||
|
||
// Opt-in heavy scan: pull every ONU-STATE for the Rx optical check.
|
||
var optAvailable = false
|
||
var abnormal: [ONUStateDoc] = []
|
||
if extraStats {
|
||
let onus = try await connection.onu.allStates()
|
||
optAvailable = onus.contains { $0.rxOpticalDBm != nil }
|
||
abnormal = onus.filter { $0.rxOpticalDBm.map(OpticalThreshold.rxAbnormal) ?? false }
|
||
}
|
||
|
||
guard gen == loadGeneration, !Task.isCancelled else { return }
|
||
|
||
oltCount = olts.count
|
||
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
|
||
|
||
if let controllers { controllerCount = controllers } // else keep last good
|
||
if let alarms {
|
||
severityCounts = Dictionary(grouping: alarms, by: { $0.severity })
|
||
.map { (severity: $0.key, count: $0.value.count) }
|
||
.sorted { $0.severity.rank < $1.severity.rank }
|
||
} // else keep last good
|
||
|
||
opticalAvailable = optAvailable
|
||
abnormalRxONUs = abnormal
|
||
abnormalRxCount = abnormal.count
|
||
phase = .loaded
|
||
} catch {
|
||
guard gen == loadGeneration, !Task.isCancelled else { return }
|
||
phase = .failed((error as? APIError)?.localizedDescription ?? error.localizedDescription)
|
||
}
|
||
}
|
||
}
|