import SwiftUI /// Firmware picker for a single ONU. Lists images compatible with the ONU's /// model (with a "show all" escape hatch), and gates the actual push behind a /// service-affecting confirmation. The push stages the image in the ONU's /// **standby** bank, so the running image is untouched until the next reboot. struct FirmwareUpdateSheet: View { let vm: ONUDetailViewModel let state: ONUStateDoc @Environment(\.dismiss) private var dismiss @State private var showAll = false @State private var pending: FirmwareFile? private var compatible: [FirmwareFile] { vm.firmwareFiles.filter { $0.isCompatible(vendor: state.vendor, equipmentId: state.equipmentId) } } private var visible: [FirmwareFile] { showAll ? vm.firmwareFiles : compatible } var body: some View { NavigationStack { List { Section { LabeledContent("Current", value: state.fwVersion ?? "—") if let eq = state.equipmentId { LabeledContent("Model", value: eq) } } if let error = vm.firmwareLoadError { Section { Text(error).foregroundStyle(.red).font(.footnote) } } else if vm.firmwareFiles.isEmpty { Section { HStack(spacing: 10) { ProgressView(); Text("Loading firmware…").foregroundStyle(.secondary) } } } else { Section { ForEach(visible) { file in Button { pending = file } label: { fileRow(file) } .disabled(vm.isPerformingAction) } } header: { Text(showAll ? "All firmware" : "Compatible firmware") } footer: { if !showAll && compatible.isEmpty { Text("No image matches this ONU's model. Turn on “Show all” to choose one manually — make sure it's compatible.") } } if vm.firmwareFiles.count > compatible.count { Section { Toggle("Show all firmware", isOn: $showAll) } } } } .navigationTitle("Update firmware") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } } .task { if vm.firmwareFiles.isEmpty { await vm.loadFirmware() } } .confirmationDialog( "Stage firmware?", isPresented: Binding(get: { pending != nil }, set: { if !$0 { pending = nil } }), titleVisibility: .visible, presenting: pending ) { file in Button("Download \(file.version ?? "firmware")", role: .destructive) { let selected = file dismiss() Task { await vm.upgrade(to: selected) } } Button("Cancel", role: .cancel) { pending = nil } } message: { file in Text("Stage \(file.version ?? "this image") on \(state.id) in the standby bank. It becomes active after the ONU's next reboot — this is service-affecting.") } } .presentationDetents([.medium, .large]) } private func fileRow(_ file: FirmwareFile) -> some View { VStack(alignment: .leading, spacing: 2) { Text(file.version ?? file.filename ?? "Unknown").font(.body.weight(.medium)) Text(subtitle(file)).font(.caption).foregroundStyle(.secondary) } } private func subtitle(_ file: FirmwareFile) -> String { var parts: [String] = [] if let model = file.models.first { parts.append(model) } if let size = file.sizeBytes { parts.append(Format.bytes(size)) } return parts.joined(separator: " · ") } }