import SwiftUI struct DashboardView: View { private let connection: MCMSConnection @State private var vm: DashboardViewModel @AppStorage("dashboard.extraStats") private var extraStats = false @AppStorage("dashboard.autoRefresh") private var autoRefresh = false init(connection: MCMSConnection) { self.connection = connection _vm = State(initialValue: DashboardViewModel(connection: connection)) } private let columns = [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())] var body: some View { Group { switch vm.phase { case .idle: LoadingView(label: "Loading network…") case .loading where vm.onuTotal == 0: LoadingView(label: "Loading network…") case .failed(let msg): ErrorStateView(message: msg) { Task { await vm.load(extraStats: extraStats) } } default: content } } .navigationTitle("Dashboard") .toolbar { ToolbarItem(placement: .topBarTrailing) { Menu { Toggle("Detailed stats (Rx scan)", isOn: $extraStats) Toggle("Auto-refresh · 30s", isOn: $autoRefresh) } label: { Image(systemName: autoRefresh ? "arrow.clockwise.circle.fill" : "ellipsis.circle") } } } // Reloads on appear, and whenever a toggle flips; keeps refreshing every // 30s while auto-refresh is on. Cancelled when the view goes away. .task(id: "\(extraStats)-\(autoRefresh)") { while !Task.isCancelled { await vm.load(extraStats: extraStats) guard autoRefresh else { break } try? await Task.sleep(for: .seconds(30)) } } .refreshable { await vm.load(extraStats: extraStats) } } private var content: some View { ScrollView { VStack(alignment: .leading, spacing: 20) { LazyVGrid(columns: columns, spacing: 12) { CountTile(title: "ONUs", value: vm.onuTotal, systemImage: "point.3.connected.trianglepath.dotted", tint: .blue) CountTile(title: "OLTs", value: vm.oltCount, systemImage: "antenna.radiowaves.left.and.right", tint: .teal) CountTile(title: "Controllers", value: vm.controllerCount, systemImage: "cpu", tint: .indigo) } if vm.totalDownstreamBps > 0 || vm.totalUpstreamBps > 0 { section(title: "Traffic · all OLTs") { HStack { trafficStat("Downstream", systemImage: "arrow.down", bps: vm.totalDownstreamBps, tint: .blue) Spacer() trafficStat("Upstream", systemImage: "arrow.up", bps: vm.totalUpstreamBps, tint: .teal) } } } if vm.opticalAvailable || vm.lasersOffCount > 0 { section(title: "Health") { if vm.opticalAvailable { healthLink(title: "ONUs with abnormal Rx", detail: "< −28 or > −10 dBm", count: vm.abnormalRxCount) { AbnormalRxListView(connection: connection, onus: vm.abnormalRxONUs) } } healthLink(title: "OLTs with laser off", detail: nil, count: vm.lasersOffCount) { LaserOffListView(connection: connection, olts: vm.lasersOffOLTs) } } } if !vm.onuCounts.isEmpty { section(title: "ONU states") { ForEach(vm.onuCounts, id: \.state) { row in NavigationLink { ONUListView(connection: connection, stateFilter: row.state, title: row.state.label) } label: { HStack { StateBadge(text: row.state.label, color: row.state.color) Spacer() Text("\(row.count)").font(.body.monospacedDigit()) Image(systemName: "chevron.right") .font(.caption2.weight(.semibold)) .foregroundStyle(.tertiary) } .padding(.vertical, 4) .contentShape(Rectangle()) } .buttonStyle(.plain) } } } if !vm.severityCounts.isEmpty { section(title: "ONU alarms by severity") { ForEach(vm.severityCounts, id: \.severity) { row in HStack { SeverityBadge(severity: row.severity) Spacer() Text("\(row.count)").font(.body.monospacedDigit()) } .padding(.vertical, 4) } } } } .padding() } } @ViewBuilder private func section(title: String, @ViewBuilder content: () -> Content) -> some View { VStack(alignment: .leading, spacing: 8) { Text(title).font(.headline) VStack(spacing: 0) { content() } .padding() .background(Color(.secondarySystemBackground)) .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) } } private func trafficStat(_ title: String, systemImage: String, bps: Double, tint: Color) -> some View { VStack(alignment: .leading, spacing: 4) { Label(title, systemImage: systemImage).font(.caption).foregroundStyle(.secondary) Text(Format.bps(bps)) .font(.system(.title3, design: .rounded).weight(.semibold)) .foregroundStyle(tint) .monospacedDigit() } } /// A health row that becomes a navigation link into the affected devices /// when the count is non-zero. @ViewBuilder private func healthLink( title: String, detail: String?, count: Int, @ViewBuilder destination: () -> Destination ) -> some View { if count > 0 { NavigationLink(destination: destination()) { healthRow(title, detail: detail, count: count, tappable: true) } .buttonStyle(.plain) } else { healthRow(title, detail: detail, count: count, tappable: false) } } private func healthRow(_ title: String, detail: String?, count: Int, tappable: Bool) -> some View { HStack { VStack(alignment: .leading, spacing: 2) { Text(title) if let detail { Text(detail).font(.caption).foregroundStyle(.secondary) } } Spacer() Text("\(count)") .font(.body.monospacedDigit().weight(.semibold)) .foregroundStyle(count > 0 ? .orange : .green) if tappable { Image(systemName: "chevron.right") .font(.caption2.weight(.semibold)) .foregroundStyle(.tertiary) } } .padding(.vertical, 4) .contentShape(Rectangle()) } }