Procházet zdrojové kódy

NS import handling, in-memory persistence, UI adaptions

Deniz Cengiz před 1 rokem
rodič
revize
35333ac657

+ 4 - 0
Trio.xcodeproj/project.pbxproj

@@ -639,6 +639,7 @@
 		DDE179702C910127003CDDB7 /* OverrideStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179502C910127003CDDB7 /* OverrideStored+CoreDataClass.swift */; };
 		DDE179712C910127003CDDB7 /* OverrideStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179512C910127003CDDB7 /* OverrideStored+CoreDataProperties.swift */; };
 		DDEBB05C2D89E9050032305D /* TimeInRangeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDEBB05B2D89E9050032305D /* TimeInRangeType.swift */; };
+		DDF68FFC2D9ECF7F008BF16C /* OnboardingStateModel+Nightscout.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF68FFB2D9ECF77008BF16C /* OnboardingStateModel+Nightscout.swift */; };
 		DDF847DD2C5C28720049BB3B /* LiveActivitySettingsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF847DC2C5C28720049BB3B /* LiveActivitySettingsDataFlow.swift */; };
 		DDF847DF2C5C28780049BB3B /* LiveActivitySettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF847DE2C5C28780049BB3B /* LiveActivitySettingsProvider.swift */; };
 		DDF847E12C5C287F0049BB3B /* LiveActivitySettingsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF847E02C5C287F0049BB3B /* LiveActivitySettingsStateModel.swift */; };
@@ -1423,6 +1424,7 @@
 		DDE179502C910127003CDDB7 /* OverrideStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OverrideStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		DDE179512C910127003CDDB7 /* OverrideStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OverrideStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDEBB05B2D89E9050032305D /* TimeInRangeType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeInRangeType.swift; sourceTree = "<group>"; };
+		DDF68FFB2D9ECF77008BF16C /* OnboardingStateModel+Nightscout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingStateModel+Nightscout.swift"; sourceTree = "<group>"; };
 		DDF847DC2C5C28720049BB3B /* LiveActivitySettingsDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivitySettingsDataFlow.swift; sourceTree = "<group>"; };
 		DDF847DE2C5C28780049BB3B /* LiveActivitySettingsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivitySettingsProvider.swift; sourceTree = "<group>"; };
 		DDF847E02C5C287F0049BB3B /* LiveActivitySettingsStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivitySettingsStateModel.swift; sourceTree = "<group>"; };
@@ -2729,6 +2731,7 @@
 				BD8E6B222D9036F700ABF8FA /* OnboardingDataFlow.swift */,
 				BD8E6B202D9036CA00ABF8FA /* OnboardingProvider.swift */,
 				BD47FD1A2D88AB4A0043966B /* OnboardingStateModel.swift */,
+				DDF68FFB2D9ECF77008BF16C /* OnboardingStateModel+Nightscout.swift */,
 				BD47FD152D88AAD80043966B /* View */,
 			);
 			path = Onboarding;
@@ -3953,6 +3956,7 @@
 				382C133725F13A1E00715CE1 /* InsulinSensitivities.swift in Sources */,
 				19D466A529AA2BD4004D5F33 /* MealSettingsProvider.swift in Sources */,
 				DD5DC9F72CF3DA9300AB8703 /* TargetPicker.swift in Sources */,
+				DDF68FFC2D9ECF7F008BF16C /* OnboardingStateModel+Nightscout.swift in Sources */,
 				383948D625CD4D8900E91849 /* FileStorage.swift in Sources */,
 				CEE9A6572BBB418300EB5194 /* CalibrationsChart.swift in Sources */,
 				3811DE4125C9D4A100A708ED /* SettingsRootView.swift in Sources */,

+ 265 - 0
Trio/Sources/Modules/Onboarding/OnboardingStateModel+Nightscout.swift

@@ -0,0 +1,265 @@
+import Combine
+import Foundation
+import SwiftUI
+
+// MARK: - Setup Nightscout Connection
+
+extension Onboarding.StateModel {
+    func connectToNightscout() {
+        if let CheckURL = url.last, CheckURL == "/" {
+            let fixedURL = url.dropLast()
+            url = String(fixedURL)
+        }
+
+        guard let url = URL(string: url), self.url.hasPrefix("https://") else {
+            message = "Invalid URL"
+            isValidURL = false
+            return
+        }
+
+        connecting = true
+        isValidURL = true
+        message = ""
+
+        NightscoutAPI(url: url, secret: secret).checkConnection()
+            .receive(on: DispatchQueue.main)
+            .sink { completion in
+                switch completion {
+                case .finished: break
+                case let .failure(error):
+                    self.message = "Error: \(error.localizedDescription)"
+                }
+                DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
+                    self.connecting = false
+                }
+            } receiveValue: {
+                self.keychain.setValue(self.url, forKey: NightscoutConfig.Config.urlKey)
+                self.keychain.setValue(self.secret, forKey: NightscoutConfig.Config.secretKey)
+                self.isConnectedToNS = true
+            }
+            .store(in: &lifetime)
+    }
+
+    private var nightscoutAPI: NightscoutAPI? {
+        guard let urlString = keychain.getValue(String.self, forKey: NightscoutConfig.Config.urlKey),
+              let url = URL(string: urlString),
+              let secret = keychain.getValue(String.self, forKey: NightscoutConfig.Config.secretKey)
+        else {
+            return nil
+        }
+        return NightscoutAPI(url: url, secret: secret)
+    }
+
+    func importSettingsFromNightscout(currentStep: Binding<OnboardingStep>) async {
+        guard nightscoutAPI != nil, isConnectedToNS else {
+            return
+        }
+
+        nightscoutImportStatus = .running
+
+        do {
+            guard let fetchedProfile = await nightscoutManager.importSettings() else {
+                await MainActor.run {
+                    nightscoutImportStatus = .failed
+                }
+                throw NSError(
+                    domain: "ImportError",
+                    code: 1,
+                    userInfo: [NSLocalizedDescriptionKey: "Cannot find the default Nightscout Profile."]
+                )
+            }
+
+            // determine, i.e. guesstimate, whether fetched values are mmol/L or mg/dL values
+            let shouldConvertToMgdL = fetchedProfile.units.contains("mmol") || fetchedProfile.target_low
+                .contains(where: { $0.value <= 39 }) || fetchedProfile.target_high.contains(where: { $0.value <= 39 })
+
+            // Carb Ratios
+            let carbratios = fetchedProfile.carbratio.map { carbratio in
+                CarbRatioEntry(
+                    start: carbratio.time,
+                    offset: offset(carbratio.time) / 60,
+                    ratio: carbratio.value
+                )
+            }
+
+            if carbratios.contains(where: { $0.ratio <= 0 }) {
+                await MainActor.run {
+                    nightscoutImportStatus = .failed
+                }
+                throw NSError(
+                    domain: "ImportError",
+                    code: 2,
+                    userInfo: [NSLocalizedDescriptionKey: "Invalid Carb Ratio settings in Nightscout. Import aborted."]
+                )
+            }
+
+            let carbratiosProfile = CarbRatios(units: .grams, schedule: carbratios)
+
+            // Basal Profile
+            let basals = fetchedProfile.basal.map { basal in
+                BasalProfileEntry(
+                    start: basal.time,
+                    minutes: offset(basal.time) / 60,
+                    rate: basal.value
+                )
+            }
+
+            if basals.contains(where: { $0.rate <= 0 }) {
+                await MainActor.run {
+                    nightscoutImportStatus = .failed
+                }
+                throw NSError(
+                    domain: "ImportError",
+                    code: 3,
+                    userInfo: [NSLocalizedDescriptionKey: "Invalid Nightscout basal rates found. Import aborted."]
+                )
+            }
+
+            if basals.reduce(0, { $0 + $1.rate }) <= 0 {
+                await MainActor.run {
+                    nightscoutImportStatus = .failed
+                }
+                throw NSError(
+                    domain: "ImportError",
+                    code: 4,
+                    userInfo: [
+                        NSLocalizedDescriptionKey: "Invalid Nightscout basal rates found. Basal rate total cannot be 0 U/hr. Import aborted."
+                    ]
+                )
+            }
+
+            // Sensitivities
+            let sensitivities = fetchedProfile.sens.map { sensitivity in
+                InsulinSensitivityEntry(
+                    sensitivity: shouldConvertToMgdL ? correctUnitParsingOffsets(sensitivity.value.asMgdL) : sensitivity
+                        .value,
+                    offset: offset(sensitivity.time) / 60,
+                    start: sensitivity.time
+                )
+            }
+
+            if sensitivities.contains(where: { $0.sensitivity <= 0 }) {
+                await MainActor.run {
+                    nightscoutImportStatus = .failed
+                }
+                throw NSError(
+                    domain: "ImportError",
+                    code: 5,
+                    userInfo: [NSLocalizedDescriptionKey: "Invalid Nightscout insulin sensitivity profile. Import aborted."]
+                )
+            }
+
+            let sensitivitiesProfile = InsulinSensitivities(
+                units: .mgdL,
+                userPreferredUnits: .mgdL,
+                sensitivities: sensitivities
+            )
+
+            // Targets
+            let targets = fetchedProfile.target_low.map { target in
+                BGTargetEntry(
+                    low: shouldConvertToMgdL ? correctUnitParsingOffsets(target.value.asMgdL) : target.value,
+                    high: shouldConvertToMgdL ? correctUnitParsingOffsets(target.value.asMgdL) : target.value,
+                    start: target.time,
+                    offset: offset(target.time) / 60
+                )
+            }
+
+            let targetsProfile = BGTargets(units: .mgdL, userPreferredUnits: .mgdL, targets: targets)
+
+            // Store therapy settings in-memory in state model for further review
+            finalizeImport(
+                targets: targetsProfile,
+                basals: basals,
+                carbRatios: carbratiosProfile,
+                sensitivities: sensitivitiesProfile,
+                userPreferredUnitsFromImport: fetchedProfile.units,
+                currentStep: currentStep
+            )
+        } catch {
+            await MainActor.run {
+                self.nightscoutImportErrors.append(error.localizedDescription)
+                debug(.service, "Settings import failed with error: \(error.localizedDescription)")
+            }
+        }
+    }
+
+    fileprivate func finalizeImport(
+        targets targetsProfile: BGTargets,
+        basals: [BasalProfileEntry],
+        carbRatios carbratiosProfile: CarbRatios,
+        sensitivities sensitivitiesProfile: InsulinSensitivities,
+        userPreferredUnitsFromImport: String,
+        currentStep: Binding<OnboardingStep>
+    ) {
+        // Parse: targetsProfile → targetItems
+        targetItems = targetsProfile.targets.map { entry in
+            let timeIndex = targetTimeValues.firstIndex(where: { Int($0) == entry.offset * 60 }) ?? 0
+            let lowIndex = targetRateValues.enumerated().min(by: {
+                abs(Double($0.element) - Double(entry.low)) < abs(Double($1.element) - Double(entry.low))
+            })?.offset ?? 0
+
+            return TargetsEditor.Item(lowIndex: lowIndex, highIndex: lowIndex, timeIndex: timeIndex)
+        }
+        initialTargetItems = targetItems
+
+        // Parse: basals → basalProfileItems
+        basalProfileItems = basals.map { entry in
+            let timeIndex = basalProfileTimeValues.firstIndex(where: { Int($0) == entry.minutes * 60 }) ?? 0
+            let rateIndex = basalProfileRateValues.enumerated().min(by: {
+                abs(Double($0.element) - Double(entry.rate)) < abs(Double($1.element) - Double(entry.rate))
+            })?.offset ?? 0
+
+            return BasalProfileEditor.Item(rateIndex: rateIndex, timeIndex: timeIndex)
+        }
+        initialBasalProfileItems = basalProfileItems
+
+        // Parse: carbratiosProfile → carbRatioItems
+        carbRatioItems = carbratiosProfile.schedule.map { entry in
+            let timeIndex = carbRatioTimeValues.firstIndex(where: { Int($0) == entry.offset * 60 }) ?? 0
+            let rateIndex = carbRatioRateValues.enumerated().min(by: {
+                abs(Double($0.element) - Double(entry.ratio)) < abs(Double($1.element) - Double(entry.ratio))
+            })?.offset ?? 0
+
+            return CarbRatioEditor.Item(rateIndex: rateIndex, timeIndex: timeIndex)
+        }
+        initialCarbRatioItems = carbRatioItems
+
+        // Parse: sensitivitiesProfile → isfItems
+        isfItems = sensitivitiesProfile.sensitivities.map { entry in
+            let timeIndex = isfTimeValues.firstIndex(where: { Int($0) == entry.offset * 60 }) ?? 0
+            let rateIndex = isfRateValues.enumerated().min(by: {
+                abs(Double($0.element) - Double(entry.sensitivity)) < abs(Double($1.element) - Double(entry.sensitivity))
+            })?.offset ?? 0
+
+            return ISFEditor.Item(rateIndex: rateIndex, timeIndex: timeIndex)
+        }
+        initialISFItems = isfItems
+
+        units = userPreferredUnitsFromImport.contains("mmol") ? .mmolL : .mgdL
+
+        DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
+            self.nightscoutImportStatus = .finished
+            // navigate to the next onboarding step
+            if let next = currentStep.wrappedValue.next {
+                currentStep.wrappedValue = next
+            }
+        }
+    }
+
+    fileprivate func correctUnitParsingOffsets(_ parsedValue: Decimal) -> Decimal {
+        Int(parsedValue) % 2 == 0 ? parsedValue : parsedValue + 1
+    }
+
+    fileprivate func offset(_ string: String) -> Int {
+        let hours = Int(string.prefix(2)) ?? 0
+        let minutes = Int(string.suffix(2)) ?? 0
+        return ((hours * 60) + minutes) * 60
+    }
+
+    enum ImportStatus {
+        case running
+        case finished
+        case failed
+    }
+}

+ 5 - 52
Trio/Sources/Modules/Onboarding/OnboardingStateModel.swift

@@ -9,9 +9,9 @@ extension Onboarding {
     @Observable final class StateModel: BaseStateModel<Provider> {
         @ObservationIgnored @Injected() var fileStorage: FileStorage!
         @ObservationIgnored @Injected() var deviceManager: DeviceDataManager!
-        @ObservationIgnored @Injected() private var broadcaster: Broadcaster!
-        @ObservationIgnored @Injected() private var keychain: Keychain!
-        @ObservationIgnored @Injected() private var nightscoutManager: NightscoutManager!
+        @ObservationIgnored @Injected() var broadcaster: Broadcaster!
+        @ObservationIgnored @Injected() var keychain: Keychain!
+        @ObservationIgnored @Injected() var nightscoutManager: NightscoutManager!
 
         private let settingsProvider = PickerSettingsProvider.shared
 
@@ -24,6 +24,8 @@ extension Onboarding {
         var isValidURL: Bool = false
         var connecting: Bool = false
         var isConnectedToNS: Bool = false
+        var nightscoutImportErrors: [String] = []
+        var nightscoutImportStatus: ImportStatus = .finished
 
         // Carb Ratio related
         var carbRatioItems: [CarbRatioEditor.Item] = []
@@ -241,55 +243,6 @@ extension Onboarding {
     }
 }
 
-// MARK: - Setup Nightscout Connection
-
-extension Onboarding.StateModel {
-    func connectToNightscout() {
-        if let CheckURL = url.last, CheckURL == "/" {
-            let fixedURL = url.dropLast()
-            url = String(fixedURL)
-        }
-
-        guard let url = URL(string: url), self.url.hasPrefix("https://") else {
-            message = "Invalid URL"
-            isValidURL = false
-            return
-        }
-
-        connecting = true
-        isValidURL = true
-        message = ""
-
-        NightscoutAPI(url: url, secret: secret).checkConnection()
-            .receive(on: DispatchQueue.main)
-            .sink { completion in
-                switch completion {
-                case .finished: break
-                case let .failure(error):
-                    self.message = "Error: \(error.localizedDescription)"
-                }
-                DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
-                    self.connecting = false
-                }
-            } receiveValue: {
-                self.keychain.setValue(self.url, forKey: NightscoutConfig.Config.urlKey)
-                self.keychain.setValue(self.secret, forKey: NightscoutConfig.Config.secretKey)
-                self.isConnectedToNS = true
-            }
-            .store(in: &lifetime)
-    }
-
-    private var nightscoutAPI: NightscoutAPI? {
-        guard let urlString = keychain.getValue(String.self, forKey: NightscoutConfig.Config.urlKey),
-              let url = URL(string: urlString),
-              let secret = keychain.getValue(String.self, forKey: NightscoutConfig.Config.secretKey)
-        else {
-            return nil
-        }
-        return NightscoutAPI(url: url, secret: secret)
-    }
-}
-
 // MARK: - Setup Carb Ratios
 
 extension Onboarding.StateModel {

+ 2 - 2
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/BasalProfileStepView.swift

@@ -43,7 +43,7 @@ struct BasalProfileStepView: View {
                             .padding(.horizontal)
                     }
                     .padding(.vertical)
-                    .background(Color.chart.opacity(0.45))
+                    .background(Color.chart.opacity(0.65))
                     .cornerRadius(10)
                 }
 
@@ -73,7 +73,7 @@ struct BasalProfileStepView: View {
                         }
                     }
                     .padding()
-                    .background(Color.chart.opacity(0.45))
+                    .background(Color.chart.opacity(0.65))
                     .cornerRadius(10)
                 }
             }

+ 2 - 2
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/CarbRatioStepView.swift

@@ -43,7 +43,7 @@ struct CarbRatioStepView: View {
                             .padding(.horizontal)
                     }
                     .padding(.vertical)
-                    .background(Color.chart.opacity(0.45))
+                    .background(Color.chart.opacity(0.65))
                     .cornerRadius(10)
                 }
 
@@ -80,7 +80,7 @@ struct CarbRatioStepView: View {
                             .foregroundColor(.orange)
                             .padding()
                             .frame(maxWidth: .infinity, alignment: .center)
-                            .background(Color.chart.opacity(0.45))
+                            .background(Color.chart.opacity(0.65))
                             .cornerRadius(10)
                         }
                     }

+ 1 - 1
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/DeliveryLimitsStepView.swift

@@ -81,7 +81,7 @@ struct DeliveryLimitsStepView: View {
             }
         }
         .padding()
-        .background(Color.chart.opacity(0.45))
+        .background(Color.chart.opacity(0.65))
         .cornerRadius(10)
     }
 

+ 1 - 1
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/GlucoseTargetStepView.swift

@@ -44,7 +44,7 @@ struct GlucoseTargetStepView: View {
                             .padding(.horizontal)
                     }
                     .padding(.vertical)
-                    .background(Color.chart.opacity(0.45))
+                    .background(Color.chart.opacity(0.65))
                     .clipShape(
                         .rect(
                             topLeadingRadius: 10,

+ 2 - 2
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/InsulinSensitivityStepView.swift

@@ -43,7 +43,7 @@ struct InsulinSensitivityStepView: View {
                             .padding(.horizontal)
                     }
                     .padding(.vertical, 5)
-                    .background(Color.chart.opacity(0.45))
+                    .background(Color.chart.opacity(0.65))
                     .cornerRadius(10)
                 }
 
@@ -89,7 +89,7 @@ struct InsulinSensitivityStepView: View {
                             .foregroundColor(.red)
                             .padding()
                             .frame(maxWidth: .infinity, alignment: .center)
-                            .background(Color.chart.opacity(0.45))
+                            .background(Color.chart.opacity(0.65))
                             .cornerRadius(10)
                         }
                     }

+ 67 - 33
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/Nightscout/NightscoutImportStepView.swift

@@ -3,48 +3,82 @@ import SwiftUI
 struct NightscoutImportStepView: View {
     @Bindable var state: Onboarding.StateModel
 
+    @State var importAlert: Alert?
+    @State var isImportAlertPresented: Bool = false
+
     var body: some View {
-        VStack(alignment: .leading, spacing: 20) {
-            Text(
-                "Please choose if you want to import existing therapy settings from Nightscout or start from scratch."
-            ).font(.headline)
-                .padding(.horizontal)
+        ZStack {
+            if state.nightscoutImportStatus == .running {
+                VStack(alignment: .center) {
+                    Spacer(minLength: 150)
+                    CustomProgressView(
+                        text: String(
+                            localized: "Importing Profile...",
+                            comment: "Progress text when importing profile via Nightscout"
+                        )
+                    )
+                }
+                .frame(maxWidth: .infinity, maxHeight: .infinity)
+                .background(Color.clear)
+            } else {
+                VStack(alignment: .leading, spacing: 20) {
+                    Text(
+                        "Please choose if you want to import existing therapy settings from Nightscout or start from scratch."
+                    )
+                    .font(.headline)
+                    .padding(.horizontal)
+
+                    ForEach([NightscoutImportOption.useImport, NightscoutImportOption.skipImport], id: \.self) { option in
+                        Button(action: {
+                            state.nightscoutImportOption = option
+                        }) {
+                            HStack {
+                                Image(systemName: state.nightscoutImportOption == option ? "largecircle.fill.circle" : "circle")
+                                    .foregroundColor(state.nightscoutImportOption == option ? .accentColor : .secondary)
+                                    .imageScale(.large)
 
-            ForEach([NightscoutImportOption.useImport, NightscoutImportOption.skipImport], id: \.self) { option in
-                Button(action: {
-                    state.nightscoutImportOption = option
-                }) {
-                    HStack {
-                        Image(systemName: state.nightscoutImportOption == option ? "largecircle.fill.circle" : "circle")
-                            .foregroundColor(state.nightscoutImportOption == option ? .accentColor : .secondary)
-                            .imageScale(.large)
+                                Text(option.displayName)
+                                    .foregroundColor(.primary)
 
-                        Text(option.displayName)
-                            .foregroundColor(.primary)
+                                Spacer()
+                            }
+                            .padding()
+                            .background(Color.chart.opacity(0.65))
+                            .cornerRadius(10)
+                        }
+                        .buttonStyle(.plain)
+                    }
 
-                        Spacer()
+                    VStack(alignment: .leading, spacing: 10) {
+                        Text("Trio will import the following therapy settings from your Nightscout instance:")
+                        VStack(alignment: .leading) {
+                            Text("• Glucose Targets")
+                            Text("• Basal Rates")
+                            Text("• Carb Ratios")
+                            Text("• Insulin Sensitivities")
+                        }
                     }
-                    .padding()
-                    .background(Color.chart.opacity(0.45))
-                    .cornerRadius(10)
+                    .padding(.horizontal)
+                    .font(.footnote)
+                    .foregroundStyle(Color.secondary)
                 }
-                .buttonStyle(.plain)
             }
-
-            VStack(alignment: .leading, spacing: 10) {
-                Text(
-                    "Trio will import the following therapy settings from your Nightscout instance:"
-                )
-                VStack(alignment: .leading) {
-                    Text("• Glucose Targets")
-                    Text("• Basal Rates")
-                    Text("• Carb Ratios")
-                    Text("• Insulin Sensitivities")
+        }
+        .frame(maxWidth: .infinity, maxHeight: .infinity)
+        .alert(isPresented: $isImportAlertPresented) {
+            if state.nightscoutImportStatus == .failed, state.nightscoutImportErrors.isNotEmpty,
+               let errorMessage = state.nightscoutImportErrors.first
+            {
+                DispatchQueue.main.async {
+                    importAlert = Alert(
+                        title: Text("Import Failed"),
+                        message: Text(errorMessage.description),
+                        dismissButton: .default(Text("OK"))
+                    )
+                    isImportAlertPresented = true
                 }
             }
-            .padding(.horizontal)
-            .font(.footnote)
-            .foregroundStyle(Color.secondary)
+            return importAlert ?? Alert(title: Text("Unknown Error"))
         }
     }
 }

+ 2 - 2
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/Nightscout/NightscoutLoginStepView.swift

@@ -20,7 +20,7 @@ struct NightscoutLoginStepView: View {
                         .foregroundStyle(.orange)
                 }
             }.padding()
-                .background(Color.chart.opacity(0.45))
+                .background(Color.chart.opacity(0.65))
                 .cornerRadius(10)
 
             HStack {
@@ -30,7 +30,7 @@ struct NightscoutLoginStepView: View {
                     .textContentType(.password)
                     .keyboardType(.asciiCapable)
             }.padding()
-                .background(Color.chart.opacity(0.45))
+                .background(Color.chart.opacity(0.65))
                 .cornerRadius(10)
 
             Spacer(minLength: 10)

+ 1 - 1
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/Nightscout/NightscoutStepView.swift

@@ -24,7 +24,7 @@ struct NightscoutStepView: View {
                         Spacer()
                     }
                     .padding()
-                    .background(Color.chart.opacity(0.45))
+                    .background(Color.chart.opacity(0.65))
                     .cornerRadius(10)
                 }
                 .buttonStyle(.plain)

+ 2 - 2
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/UnitSelectionStepView.swift

@@ -20,7 +20,7 @@ struct UnitSelectionStepView: View {
                 }
             }
             .padding()
-            .background(Color.chart.opacity(0.45))
+            .background(Color.chart.opacity(0.65))
             .cornerRadius(10)
 
             HStack {
@@ -33,7 +33,7 @@ struct UnitSelectionStepView: View {
                 }
             }
             .padding()
-            .background(Color.chart.opacity(0.45))
+            .background(Color.chart.opacity(0.65))
             .cornerRadius(10)
 
             Text(

+ 3 - 0
Trio/Sources/Modules/Onboarding/View/OnboardingView.swift

@@ -225,6 +225,9 @@ extension Onboarding {
                                                   state.nightscoutImportOption == .useImport
                                         {
                                             // TODO: trigger import, show animation, then proceed to next step
+                                            Task {
+                                                await state.importSettingsFromNightscout(currentStep: $currentStep)
+                                            }
                                         } else if let next = currentStep.next {
                                             currentStep = next
                                         }

+ 3 - 3
Trio/Sources/Modules/Onboarding/View/TimeValueEditorView.swift

@@ -25,7 +25,7 @@ struct TimeValueEditorView: View {
                 }
                 .disabled(items.count >= 48)
             }
-            .listRowBackground(Color.chart.opacity(0.45))
+            .listRowBackground(Color.chart.opacity(0.65))
             .padding(.vertical, 5)
 
             ForEach($items) { $item in
@@ -74,9 +74,9 @@ struct TimeValueEditorView: View {
                     }
                 }
             }
-            .listRowBackground(Color.chart.opacity(0.45))
+            .listRowBackground(Color.chart.opacity(0.65))
 
-            Rectangle().fill(Color.chart.opacity(0.45)).frame(height: 10)
+            Rectangle().fill(Color.chart.opacity(0.65)).frame(height: 10)
                 .clipShape(
                     .rect(
                         topLeadingRadius: 0,