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>
153 lines
5.6 KiB
Swift
153 lines
5.6 KiB
Swift
import Foundation
|
||
import Observation
|
||
|
||
enum ONUDetailTab: String, CaseIterable, Identifiable {
|
||
case stats = "Overview"
|
||
case cpe = "CPE"
|
||
case config = "Config"
|
||
case alarms = "Alarms"
|
||
case logs = "Logs"
|
||
var id: String { rawValue }
|
||
}
|
||
|
||
@MainActor
|
||
@Observable
|
||
final class ONUDetailViewModel {
|
||
let onuId: String
|
||
private let connection: MCMSConnection
|
||
/// Bumped per (re)load so a late/cancelled request — or one for a tab the user
|
||
/// has since left — can't commit its results over the current tab's data.
|
||
private var loadGeneration = 0
|
||
|
||
var tab: ONUDetailTab = .stats
|
||
var phase: LoadPhase = .idle
|
||
|
||
var config: ONUConfigDoc?
|
||
var liveState: ONUStateDoc?
|
||
var stats: [StatSample] = []
|
||
var alarms: [AlarmHistoryDoc] = []
|
||
var logs: [LogEntry] = []
|
||
var cpes: [CPEState] = []
|
||
|
||
var firmwareFiles: [FirmwareFile] = []
|
||
var firmwareLoadError: String?
|
||
|
||
var isPerformingAction = false
|
||
var actionMessage: String?
|
||
|
||
init(connection: MCMSConnection, onuId: String) {
|
||
self.connection = connection
|
||
self.onuId = onuId
|
||
}
|
||
|
||
/// Tab switch / first load: blanks to a spinner while the new tab loads.
|
||
func loadCurrentTab() async {
|
||
phase = .loading
|
||
await reloadCurrentTab()
|
||
}
|
||
|
||
/// Pull-to-refresh: reloads in place WITHOUT flipping to `.loading`, so the
|
||
/// scrollable content (and the refresh task driving it) isn't torn down
|
||
/// mid-request — which otherwise surfaces as a spurious "cancelled" error.
|
||
func refresh() async {
|
||
await reloadCurrentTab()
|
||
}
|
||
|
||
private func reloadCurrentTab() async {
|
||
loadGeneration += 1
|
||
let gen = loadGeneration
|
||
let requested = tab
|
||
// Commit only if this is still the latest load AND the user is still on the
|
||
// tab we fetched for — otherwise drop the result.
|
||
func current() -> Bool { gen == loadGeneration && requested == tab && !Task.isCancelled }
|
||
do {
|
||
switch requested {
|
||
case .stats:
|
||
// Live ONU-STATE carries the current optical/FEC levels (§3.12);
|
||
// the /onus/stats/ time-series is a best-effort supplement.
|
||
let state = try await connection.onu.state(id: onuId)
|
||
let samples = (try? await connection.onu.stats(id: onuId, lastHours: 24)) ?? []
|
||
guard current() else { return }
|
||
liveState = state
|
||
stats = samples
|
||
case .config:
|
||
let doc = try await connection.onu.config(id: onuId)
|
||
guard current() else { return }
|
||
config = doc
|
||
case .cpe:
|
||
let list = try await connection.onu.cpes(id: onuId)
|
||
guard current() else { return }
|
||
cpes = list
|
||
case .alarms:
|
||
let list = try await connection.onu.alarmHistories(query: queryForONU())
|
||
.sorted { $0.severity.rank < $1.severity.rank }
|
||
guard current() else { return }
|
||
alarms = list
|
||
case .logs:
|
||
let list = try await connection.onu.logs(id: onuId, lastHours: 24)
|
||
guard current() else { return }
|
||
logs = list
|
||
}
|
||
phase = .loaded
|
||
} catch {
|
||
guard current() else { return }
|
||
phase = .failed((error as? APIError)?.localizedDescription ?? error.localizedDescription)
|
||
}
|
||
}
|
||
|
||
/// Filter the fleet-wide alarm-history list to this ONU. ⚠️ The `_id` key is
|
||
/// UNVERIFIED — if it's wrong the server ignores the filter and returns EVERY
|
||
/// ONU's alarms into this detail screen (no error). Confirm the real key (or
|
||
/// switch to the per-id `.onuAlarmHistory(id:)` endpoint) against openapi.json /
|
||
/// a live response before trusting this tab.
|
||
private func queryForONU() -> APIQuery {
|
||
var q = APIQuery()
|
||
q.query = ["_id": onuId]
|
||
return q
|
||
}
|
||
|
||
func reset() async {
|
||
isPerformingAction = true
|
||
actionMessage = nil
|
||
do {
|
||
try await connection.onu.reset(id: onuId)
|
||
actionMessage = "Reset requested."
|
||
} catch {
|
||
actionMessage = (error as? APIError)?.localizedDescription ?? error.localizedDescription
|
||
}
|
||
isPerformingAction = false
|
||
}
|
||
|
||
func loadFirmware() async {
|
||
firmwareLoadError = nil
|
||
do { firmwareFiles = try await connection.onu.firmwareFiles() }
|
||
catch { firmwareLoadError = (error as? APIError)?.localizedDescription ?? error.localizedDescription }
|
||
}
|
||
|
||
/// Stage a firmware image to the ONU's inactive bank (Procedure 7). Service-affecting.
|
||
func upgrade(to file: FirmwareFile) async {
|
||
isPerformingAction = true
|
||
actionMessage = nil
|
||
do {
|
||
let slot = try await connection.onu.upgradeFirmware(id: onuId, file: file)
|
||
actionMessage = "Firmware \(file.version ?? "image") staged to bank \(slot). The ONU starts it after its next reboot."
|
||
} catch {
|
||
actionMessage = (error as? APIError)?.localizedDescription ?? error.localizedDescription
|
||
}
|
||
isPerformingAction = false
|
||
}
|
||
}
|
||
|
||
extension StatSample {
|
||
/// All numeric counters in the sample, sorted by key — for display/charting.
|
||
var numericMetrics: [(key: String, value: Double)] {
|
||
(raw.objectValue ?? [:]).compactMap { key, value in
|
||
switch value {
|
||
case .double(let d): return (key, d)
|
||
case .int(let i): return (key, Double(i))
|
||
default: return nil
|
||
}
|
||
}
|
||
.sorted { $0.key < $1.key }
|
||
}
|
||
}
|