initial commit
This commit is contained in:
commit
8de962eeb8
44 changed files with 4843 additions and 0 deletions
125
ONUDetailViewModel.swift
Normal file
125
ONUDetailViewModel.swift
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
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 }
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue