PONGo_ios/ONUDetailView.swift
Jon Vanvik 203465913c Apply code-review fixes: identity/pagination, load races, TLS pin, leaks
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>
2026-05-31 16:36:49 +02:00

402 lines
16 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SwiftUI
struct ONUDetailView: View {
@State private var vm: ONUDetailViewModel
@State private var showResetConfirm = false
@State private var showFirmwareSheet = false
init(connection: MCMSConnection, onuId: String) {
_vm = State(initialValue: ONUDetailViewModel(connection: connection, onuId: onuId))
}
var body: some View {
@Bindable var vm = vm
VStack(spacing: 0) {
Picker("Tab", selection: $vm.tab) {
ForEach(ONUDetailTab.allCases) { Text($0.rawValue).tag($0) }
}
.pickerStyle(.segmented)
.padding()
tabContent
}
.navigationTitle(vm.onuId)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Menu {
Button(role: .destructive) { showResetConfirm = true } label: {
Label("Reset ONU", systemImage: "arrow.clockwise")
}
} label: {
Image(systemName: "ellipsis.circle")
}
.disabled(vm.isPerformingAction)
}
}
.sheet(isPresented: $showResetConfirm) {
ResetConfirmSheet(deviceKind: "ONU", deviceName: vm.onuId) {
Task { await vm.reset() }
}
}
.sheet(isPresented: $showFirmwareSheet) {
if let state = vm.liveState {
FirmwareUpdateSheet(vm: vm, state: state)
}
}
.alert("Operation", isPresented: Binding(
get: { vm.actionMessage != nil },
set: { if !$0 { vm.actionMessage = nil } }
)) {
Button("OK") { vm.actionMessage = nil }
} message: {
Text(vm.actionMessage ?? "")
}
.task(id: vm.tab) { await vm.loadCurrentTab() }
.refreshable { await vm.refresh() }
}
@ViewBuilder
private var tabContent: some View {
switch vm.phase {
case .idle, .loading:
LoadingView()
case .failed(let msg):
ErrorStateView(message: msg) { Task { await vm.loadCurrentTab() } }
case .loaded:
switch vm.tab {
case .stats: overviewView
case .cpe: cpeView
case .config: configView
case .alarms: alarmsView
case .logs: logsView
}
}
}
// MARK: Stats
private var overviewView: some View {
List {
if let s = vm.liveState {
statusSection(s)
firmwareSection(s)
opticalSection(s)
uniSection(s)
trafficSection(s)
if let updated = MongoTimestamp.displayString(s.lastUpdate) {
Section { LabeledContent("Last update", value: updated) }
}
}
historySection
}
}
@ViewBuilder private func statusSection(_ s: ONUStateDoc) -> some View {
Section("Status") {
if let reg = s.registrationState { LabeledContent("Registration", value: reg) }
if let eq = s.equipmentId { LabeledContent("Equipment", value: eq) }
if let mode = s.ponMode { LabeledContent("PON mode", value: mode) }
if let temp = s.temperatureC {
LabeledContent("Temperature", value: "\(temp.formatted(.number.precision(.fractionLength(1)))) °C")
}
if let online = s.onlineTime {
LabeledContent("Online since", value: MongoTimestamp.displayString(online) ?? online)
if let up = MongoTimestamp.duration(from: online, to: s.lastUpdate) {
LabeledContent("Uptime", value: up)
}
}
}
}
@ViewBuilder private func firmwareSection(_ s: ONUStateDoc) -> some View {
Section("Firmware") {
if let v = s.fwVersion { LabeledContent("Active version", value: v) }
ForEach(Array(s.fwBankVersions.enumerated()), id: \.offset) { idx, ver in
LabeledContent("Bank \(idx)" + (s.fwBankPtr == idx ? " · active" : ""),
value: ver.isEmpty ? "" : ver)
}
if let st = s.fwUpgradeStatus {
if let status = st["Status"]?.stringValue { LabeledContent("Upgrade", value: status) }
if let prog = st["Progress"]?.intValue { LabeledContent("Progress", value: "\(prog)%") }
}
Button { showFirmwareSheet = true } label: {
Label("Update firmware…", systemImage: "arrow.up.circle")
}
.disabled(vm.isPerformingAction)
}
}
@ViewBuilder private func opticalSection(_ s: ONUStateDoc) -> some View {
Section {
opticalRow("RX optical", s.rxOpticalDBm,
green: OpticalThreshold.rxGreenFloor, warn: OpticalThreshold.rxRedFloor)
opticalRow("TX optical", s.txOpticalDBm,
green: OpticalThreshold.txGreenFloor, warn: OpticalThreshold.txGreenFloor)
} header: {
Text("Live optical")
} footer: {
Text("Green RX ≥ 28 dBm, TX ≥ 3 dBm (user guide §7).")
}
Section("FEC error counters") {
fecRow("ONU pre-FEC", s.onuPreFEC, isPost: false)
fecRow("ONU post-FEC", s.onuPostFEC, isPost: true)
fecRow("OLT pre-FEC", s.oltPreFEC, isPost: false)
fecRow("OLT post-FEC", s.oltPostFEC, isPost: true)
}
}
@ViewBuilder private func uniSection(_ s: ONUStateDoc) -> some View {
if !s.uniPorts.isEmpty {
Section("UNI ports") {
ForEach(s.uniPorts) { port in
HStack {
VStack(alignment: .leading, spacing: 2) {
Text(port.name).font(.subheadline)
if let detail = uniDetail(port) {
Text(detail).font(.caption).foregroundStyle(.secondary)
}
}
Spacer()
if let enabled = port.enabled {
StateBadge(text: enabled ? "enabled" : "disabled",
color: enabled ? .green : .gray)
}
}
}
}
}
}
private func uniDetail(_ port: UNIPort) -> String? {
if let state = port.state { return state }
let parts = [port.speed, port.duplex].compactMap { $0 }
return parts.isEmpty ? nil : parts.joined(separator: " · ")
}
@ViewBuilder private func trafficSection(_ s: ONUStateDoc) -> some View {
if !s.ponServices.isEmpty {
Section("Traffic") {
ForEach(s.ponServices) { svc in
VStack(alignment: .leading, spacing: 6) {
Text(svc.name).font(.subheadline)
HStack(alignment: .top) {
trafficStat("↓ Rx", rate: svc.rxRateBps, total: svc.rxTotalOctets)
trafficStat("↑ Tx", rate: svc.txRateBps, total: svc.txTotalOctets)
}
}
.padding(.vertical, 2)
}
}
}
}
private func trafficStat(_ label: String, rate: Double?, total: Double?) -> some View {
VStack(alignment: .leading, spacing: 2) {
Text(label).font(.caption2).foregroundStyle(.secondary)
Text(Format.bps(rate)).font(.callout.monospacedDigit())
Text("\(Format.bytes(total)) total").font(.caption2).foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
@ViewBuilder private var historySection: some View {
Section("History (24h)") {
LabeledContent("Samples", value: "\(vm.stats.count)")
if let latest = vm.stats.first, let ts = MongoTimestamp.displayString(latest.timestamp) {
LabeledContent("Latest sample", value: ts)
}
}
if let latest = vm.stats.first, !latest.numericMetrics.isEmpty {
Section("Latest counters") {
ForEach(latest.numericMetrics, id: \.key) { metric in
LabeledContent(metric.key) {
Text(metric.value.formatted()).monospacedDigit()
}
}
}
}
}
/// Optical level row, tinted by the user-guide thresholds (§7.3). `green` is
/// the full-pass floor, `warn` the margin floor; below `warn` is red.
private func opticalRow(_ title: String, _ value: Double?, green: Double, warn: Double) -> some View {
LabeledContent(title) {
if let value {
Text("\(value.formatted(.number.precision(.fractionLength(2)))) dBm")
.monospacedDigit()
.foregroundStyle(value >= green ? .green : (value >= warn ? .yellow : .red))
} else {
Text("").foregroundStyle(.secondary)
}
}
}
/// FEC counter row. Post-FEC > 0 means uncorrected errors reached the user
/// (red); pre-FEC > 0 is informational; all-zero is healthy (§7.17.2).
private func fecRow(_ title: String, _ value: Double?, isPost: Bool) -> some View {
LabeledContent(title) {
if let value {
Text(Int(value).formatted())
.monospacedDigit()
.foregroundStyle(isPost && value > 0 ? .red : (value > 0 ? .secondary : .green))
} else {
Text("").foregroundStyle(.secondary)
}
}
}
// MARK: CPE (DHCP leases)
private var cpeView: some View {
Group {
if vm.cpes.isEmpty {
ContentUnavailableView("No CPEs", systemImage: "laptopcomputer.slash")
} else {
List(vm.cpes) { cpe in
Section {
if let ip = cpe.ipv4 {
LabeledContent("IPv4") {
HStack(spacing: 6) {
Text(ip).monospacedDigit()
if let state = cpe.dhcpState { dhcpBadge(state) }
}
}
if let server = cpe.dhcpServer { LabeledContent("DHCP server", value: server) }
if let lease = cpe.dhcpLeaseSeconds { LabeledContent("Lease", value: leaseText(lease)) }
if let renew = MongoTimestamp.displayString(cpe.dhcpLastSuccess) {
LabeledContent("Last renew", value: renew)
}
}
if let ip6 = cpe.ipv6 {
LabeledContent("IPv6") {
HStack(spacing: 6) {
Text(ip6).font(.callout.monospaced())
if let state = cpe.dhcpv6State { dhcpBadge(state) }
}
}
if let prefix = cpe.ipv6Prefix { LabeledContent("Delegated prefix", value: prefix) }
if let valid = MongoTimestamp.displayString(cpe.ipv6ValidLifetime) {
LabeledContent("Valid until", value: valid)
}
}
if let circuit = cpe.circuitId { LabeledContent("Circuit ID", value: circuit) }
if !cpe.hasLease { Text("No active lease.").foregroundStyle(.secondary) }
} header: {
Text(cpe.mac ?? cpe.id)
}
}
}
}
}
private func dhcpBadge(_ state: String) -> some View {
let ok = state.lowercased().contains("bound") || state.lowercased() == "ack"
return StateBadge(text: state, color: ok ? .green : .orange)
}
private func leaseText(_ seconds: Int) -> String {
if seconds % 86400 == 0 { return "\(seconds / 86400)d" }
if seconds % 3600 == 0 { return "\(seconds / 3600)h" }
return "\(seconds / 60)m"
}
// MARK: Config
private var configView: some View {
List {
if let config = vm.config {
if let name = config.netconfName {
Section("Identity") { LabeledContent("NETCONF name", value: name) }
}
Section("Raw document") {
JSONTreeView(value: config.raw)
}
} else {
Text("No configuration loaded.").foregroundStyle(.secondary)
}
}
}
// MARK: Alarms
private var alarmsView: some View {
Group {
if vm.alarms.isEmpty {
ContentUnavailableView("No alarms", systemImage: "bell.slash")
} else {
List(vm.alarms) { alarm in
VStack(alignment: .leading, spacing: 4) {
HStack {
SeverityBadge(severity: alarm.severity)
Spacer()
if let at = MongoTimestamp.displayString(alarm.raisedAt) {
Text(at).font(.caption).foregroundStyle(.secondary)
}
}
Text(alarm.message ?? alarm.id).font(.subheadline)
}
.padding(.vertical, 2)
}
.listStyle(.plain)
}
}
}
// MARK: Logs
private var logsView: some View {
Group {
if vm.logs.isEmpty {
ContentUnavailableView("No logs", systemImage: "doc.text")
} else {
List(vm.logs) { log in
VStack(alignment: .leading, spacing: 4) {
HStack {
SeverityBadge(severity: log.severity)
Spacer()
if let t = MongoTimestamp.displayString(log.time) {
Text(t).font(.caption).foregroundStyle(.secondary)
}
}
Text(log.message ?? log.id).font(.caption).foregroundStyle(.primary)
}
.padding(.vertical, 2)
}
.listStyle(.plain)
}
}
}
}
/// Minimal recursive viewer for a `JSONValue` (read-only). Renders nested
/// objects/arrays as disclosure groups so the full Mongo document is browsable
/// before specific fields are typed.
struct JSONTreeView: View {
let value: JSONValue
var key: String? = nil
var body: some View {
switch value {
case .object(let dict):
DisclosureGroup(key ?? "{ }") {
ForEach(dict.keys.sorted(), id: \.self) { k in
JSONTreeView(value: dict[k]!, key: k)
}
}
case .array(let arr):
DisclosureGroup(key.map { "\($0) [\(arr.count)]" } ?? "[ \(arr.count) ]") {
ForEach(Array(arr.enumerated()), id: \.offset) { idx, item in
JSONTreeView(value: item, key: "\(idx)")
}
}
default:
LabeledContent(key ?? "value") {
Text(value.compactDescription)
.font(.callout.monospaced())
.foregroundStyle(.secondary)
.multilineTextAlignment(.trailing)
}
}
}
}