90 lines
4 KiB
Swift
90 lines
4 KiB
Swift
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: " · ")
|
|
}
|
|
}
|