PONGo_ios/FirmwareUpdateSheet.swift
2026-05-31 14:11:29 +02:00

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: " · ")
}
}