125 lines
4.3 KiB
Swift
125 lines
4.3 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
|
|
|
|
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 {
|
|
do {
|
|
switch tab {
|
|
case .stats:
|
|
// Live ONU-STATE carries the current optical/FEC levels (§3.12);
|
|
// the /onus/stats/ time-series is a best-effort supplement.
|
|
liveState = try await connection.onu.state(id: onuId)
|
|
stats = (try? await connection.onu.stats(id: onuId, lastHours: 24)) ?? []
|
|
case .config: config = try await connection.onu.config(id: onuId)
|
|
case .cpe: cpes = try await connection.onu.cpes(id: onuId)
|
|
case .alarms: alarms = try await connection.onu.alarmHistories(
|
|
query: queryForONU()).sorted { $0.severity.rank < $1.severity.rank }
|
|
case .logs: logs = try await connection.onu.logs(id: onuId, lastHours: 24)
|
|
}
|
|
phase = .loaded
|
|
} catch {
|
|
phase = .failed((error as? APIError)?.localizedDescription ?? error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
/// Filter alarm-history list to this ONU. Reconcile the key with the real
|
|
/// document shape; `_id` is a safe default for the per-ONU collection.
|
|
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 }
|
|
}
|
|
}
|