initial commit

This commit is contained in:
Jon Vanvik 2026-05-31 14:11:29 +02:00
commit 8de962eeb8
44 changed files with 4843 additions and 0 deletions

125
ONUDetailViewModel.swift Normal file
View 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 }
}
}