Explorar o código

Merge pull request #487 from nightscout/perform-data-migration

[Part 5 of 6] Perform data migration for 0.2.x JSON entries to CoreData
Sam King hai 1 ano
pai
achega
abfc3ee420

+ 111 - 4
Model/JSONImporter.swift

@@ -741,8 +741,115 @@ extension Determination: Codable {
 }
 
 extension JSONImporter {
-    func importGlucoseHistoryIfNeeded() async {}
-    func importPumpHistoryIfNeeded() async {}
-    func importCarbHistoryIfNeeded() async {}
-    func importDeterminationIfNeeded() async {}
+    private func openAPSFileURL(_ relativePath: String) -> URL {
+        FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
+            .appendingPathComponent(relativePath)
+    }
+
+    func importGlucoseHistoryIfNeeded() async throws {
+        debug(.coreData, "Checking for glucose history JSON file...")
+
+        let url = openAPSFileURL(OpenAPS.Monitor.glucose)
+        let suffix = "migrated.json"
+
+        guard FileManager.default.fileExists(atPath: url.path) else {
+            debug(.coreData, "❌ No JSON file to import at \(url.path)")
+            return
+        }
+
+        debug(.coreData, "Glucose history JSON file found, proceeding with import of glucose history...")
+
+        try await importGlucoseHistory(url: url, now: Date())
+
+        debug(.coreData, "Glucose history JSON file imported successfully, moving to \(suffix)")
+
+        try FileManager.default.moveItem(
+            at: url,
+            to: url.deletingPathExtension().appendingPathExtension(suffix)
+        )
+
+        debug(.coreData, "Import of glucose history completed successfully.")
+    }
+
+    func importPumpHistoryIfNeeded() async throws {
+        debug(.coreData, "Checking for pump history JSON file...")
+
+        let url = openAPSFileURL(OpenAPS.Monitor.pumpHistory)
+        let suffix = "migrated.json"
+
+        guard FileManager.default.fileExists(atPath: url.path) else {
+            debug(.coreData, "❌ No JSON file to import at \(url.path)")
+            return
+        }
+
+        debug(.coreData, "Pump history JSON file found, proceeding with import of glucose history...")
+
+        try await importPumpHistory(url: url, now: Date())
+
+        debug(.coreData, "Pump history JSON file imported successfully, moving to \(suffix)")
+
+        try FileManager.default.moveItem(
+            at: url,
+            to: url.deletingPathExtension().appendingPathExtension(suffix)
+        )
+
+        debug(.coreData, "Import of pump history completed successfully.")
+    }
+
+    func importCarbHistoryIfNeeded() async throws {
+        debug(.coreData, "Checking for carb history JSON file...")
+
+        let url = openAPSFileURL(OpenAPS.Monitor.carbHistory)
+        let suffix = "migrated.json"
+
+        guard FileManager.default.fileExists(atPath: url.path) else {
+            debug(.coreData, "❌ No JSON file to import at \(url.path)")
+            return
+        }
+
+        debug(.coreData, "Carb history JSON file found, proceeding with import of glucose history...")
+
+        try await importCarbHistory(url: url, now: Date())
+
+        debug(.coreData, "Carb history JSON file imported successfully, moving to \(suffix)")
+
+        try FileManager.default.moveItem(
+            at: url,
+            to: url.deletingPathExtension().appendingPathExtension(suffix)
+        )
+
+        debug(.coreData, "Import of carb history completed successfully.")
+    }
+
+    func importDeterminationIfNeeded() async throws {
+        debug(.coreData, "Checking for determination JSON files...")
+
+        let enactedPath = OpenAPS.Enact.enacted // "enact/enacted.json"
+        let suggestedPath = OpenAPS.Enact.suggested // "enact/suggested.json"
+        let suffix = "migrated.json"
+
+        let enactedURL = openAPSFileURL(enactedPath)
+        let suggestedURL = openAPSFileURL(suggestedPath)
+
+        guard FileManager.default.fileExists(atPath: enactedURL.path),
+              FileManager.default.fileExists(atPath: suggestedURL.path)
+        else {
+            debug(.coreData, "❌ No JSON file to import at \(enactedURL.path) and/or \(suggestedURL.path)")
+            return
+        }
+
+        debug(.coreData, "Determination JSON files found, proceeding with import...")
+
+        try await importOrefDetermination(enactedUrl: enactedURL, suggestedUrl: suggestedURL, now: Date())
+
+        debug(.coreData, "Determination JSON file(s) imported successfully, moving to \(suffix)")
+
+        try FileManager.default.moveItem(at: enactedURL, to: enactedURL.deletingPathExtension().appendingPathExtension(suffix))
+        try FileManager.default.moveItem(
+            at: suggestedURL,
+            to: suggestedURL.deletingPathExtension().appendingPathExtension(suffix)
+        )
+
+        debug(.coreData, "Import of determination data completed successfully.")
+    }
 }

+ 4 - 0
Trio.xcodeproj/project.pbxproj

@@ -557,6 +557,7 @@
 		DD3A3CE72D29C93F00AE478E /* Helper+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3A3CE62D29C93F00AE478E /* Helper+Extensions.swift */; };
 		DD3A3CE92D29C97800AE478E /* Helper+ButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3A3CE82D29C97800AE478E /* Helper+ButtonStyles.swift */; };
 		DD3C47B32DC5608A003DD20D /* newerSuggested.json in Resources */ = {isa = PBXBuildFile; fileRef = DD3C47B22DC5608A003DD20D /* newerSuggested.json */; };
+		DD3C47B52DC57E06003DD20D /* MainMigrationErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3C47B42DC57E06003DD20D /* MainMigrationErrorView.swift */; };
 		DD3F1F832D9DC78800DCE7B3 /* UnitSelectionStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F822D9DC78300DCE7B3 /* UnitSelectionStepView.swift */; };
 		DD3F1F852D9DD84000DCE7B3 /* DeliveryLimitsStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F842D9DD83B00DCE7B3 /* DeliveryLimitsStepView.swift */; };
 		DD3F1F892D9E078D00DCE7B3 /* TherapySettingEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F882D9E078300DCE7B3 /* TherapySettingEditorView.swift */; };
@@ -1369,6 +1370,7 @@
 		DD3A3CE62D29C93F00AE478E /* Helper+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Helper+Extensions.swift"; sourceTree = "<group>"; };
 		DD3A3CE82D29C97800AE478E /* Helper+ButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Helper+ButtonStyles.swift"; sourceTree = "<group>"; };
 		DD3C47B22DC5608A003DD20D /* newerSuggested.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = newerSuggested.json; sourceTree = "<group>"; };
+		DD3C47B42DC57E06003DD20D /* MainMigrationErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMigrationErrorView.swift; sourceTree = "<group>"; };
 		DD3F1F822D9DC78300DCE7B3 /* UnitSelectionStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitSelectionStepView.swift; sourceTree = "<group>"; };
 		DD3F1F842D9DD83B00DCE7B3 /* DeliveryLimitsStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeliveryLimitsStepView.swift; sourceTree = "<group>"; };
 		DD3F1F882D9E078300DCE7B3 /* TherapySettingEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TherapySettingEditorView.swift; sourceTree = "<group>"; };
@@ -1938,6 +1940,7 @@
 		3811DE1F25C9D48300A708ED /* View */ = {
 			isa = PBXGroup;
 			children = (
+				DD3C47B42DC57E06003DD20D /* MainMigrationErrorView.swift */,
 				3BAD36B12D7CDC1400CC298D /* MainLoadingView.swift */,
 				3811DE2025C9D48300A708ED /* MainRootView.swift */,
 			);
@@ -4594,6 +4597,7 @@
 				CE7CA3502A064973004BE681 /* CancelTempPresetIntent.swift in Sources */,
 				6B1F539F9FF75646D1606066 /* SnoozeDataFlow.swift in Sources */,
 				6FFAE524D1D9C262F2407CAE /* SnoozeProvider.swift in Sources */,
+				DD3C47B52DC57E06003DD20D /* MainMigrationErrorView.swift in Sources */,
 				BD4ED4FD2CF9D5E8000EDC9C /* AppState.swift in Sources */,
 				DDF847DD2C5C28720049BB3B /* LiveActivitySettingsDataFlow.swift in Sources */,
 				8194B80890CDD6A3C13B0FEE /* SnoozeStateModel.swift in Sources */,

+ 57 - 0
Trio/Sources/Application/TrioApp.swift

@@ -24,6 +24,7 @@ extension Notification.Name {
     class InitState {
         var complete = false
         var error = false
+        var migrationErrors: [String] = []
     }
 
     // We use both InitState and @State variables to track coreDataStack
@@ -36,6 +37,7 @@ extension Notification.Name {
     @State private var showLoadingView = true
     @State private var showLoadingError = false
     @State private var showOnboardingCompletedSplash = false
+    @State private var showMigrationError: Bool = false
 
     // Dependencies Assembler
     // contain all dependencies Assemblies
@@ -129,6 +131,9 @@ extension Notification.Name {
             do {
                 try await coreDataStack.initializeStack()
 
+                // TODO: possibly wrap this in a UserDefault / TinyStorage flag check, so we do not even attempt to fetch files unnecessary, but early exit the import
+                await performJsonToCoreDataMigrationIfNeeded()
+
                 await Task { @MainActor in
                     // Only load services after successful Core Data initialization
                     loadServices()
@@ -165,6 +170,45 @@ extension Notification.Name {
         }
     }
 
+    @MainActor private func performJsonToCoreDataMigrationIfNeeded() async {
+        let importer = JSONImporter(context: coreDataStack.newTaskContext(), coreDataStack: coreDataStack)
+        var importErrors: [String] = []
+
+        do {
+            try await importer.importGlucoseHistoryIfNeeded()
+        } catch {
+            importErrors
+                .append(String(localized: "Failed to import glucose history."))
+            debug(.coreData, "❌ Failed to import JSON-based Glucose History: \(error)")
+        }
+
+        do {
+            try await importer.importPumpHistoryIfNeeded()
+        } catch {
+            importErrors.append(String(localized: "Failed to import pump history."))
+            debug(.coreData, "❌ Failed to import JSON-based Pump History: \(error)")
+        }
+
+        do {
+            try await importer.importCarbHistoryIfNeeded()
+        } catch {
+            importErrors.append(String(localized: "Failed to import algorithm data."))
+            debug(.coreData, "❌ Failed to import JSON-based Carb History: \(error)")
+        }
+
+        do {
+            try await importer.importDeterminationIfNeeded()
+        } catch {
+            importErrors
+                .append(
+                    String(localized: "Migration of JSON-based OpenAPS Determination Data failed: \(error.localizedDescription)")
+                )
+            debug(.coreData, "❌ Failed to import JSON-based OpenAPS Determination Data: \(error)")
+        }
+
+        initState.migrationErrors = importErrors
+    }
+
     /// Clears any legacy (Trio 0.2.x) delivered and pending notifications related to non-looping alerts.
     /// It targets the following notifications:
     /// - `noLoopFirstNotification`: The first notification for non-looping alerts.
@@ -213,6 +257,9 @@ extension Notification.Name {
                                 Task { @MainActor in
                                     try? await Task.sleep(for: .seconds(1.8))
                                     self.showLoadingView = false
+                                    if self.initState.migrationErrors.isNotEmpty {
+                                        self.showMigrationError = true
+                                    }
                                 }
                             }
                             if self.initState.error {
@@ -223,11 +270,21 @@ extension Notification.Name {
                             Task { @MainActor in
                                 try? await Task.sleep(for: .seconds(1.8))
                                 self.showLoadingView = false
+                                if self.initState.migrationErrors.isNotEmpty {
+                                    self.showMigrationError = true
+                                }
                             }
                         }
                         .onReceive(Foundation.NotificationCenter.default.publisher(for: .initializationError)) { _ in
                             self.showLoadingError = true
                         }
+                } else if showMigrationError { // FIXME: display of this is not yet working, despite migration errors
+                    Main.MainMigrationErrorView(migrationErrors: self.initState.migrationErrors, onConfirm: {
+                        Task { @MainActor in
+                            showMigrationError = false
+                            initState.migrationErrors = []
+                        }
+                    })
                 } else if showOnboardingCompletedSplash {
                     LogoBurstSplash(isActive: $showOnboardingCompletedSplash) {
                         Main.RootView(resolver: resolver)

+ 39 - 3
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -98681,6 +98681,15 @@
         }
       }
     },
+    "Failed to import algorithm data." : {
+
+    },
+    "Failed to import glucose history." : {
+
+    },
+    "Failed to import pump history." : {
+
+    },
     "Failed to Suspend Insulin Delivery" : {
       "comment" : "Alert title for suspend error",
       "extractionState" : "manual",
@@ -115704,6 +115713,9 @@
         }
       }
     },
+    "I understand! Proceed" : {
+
+    },
     "Identify and fix bugs and crashes" : {
       "localizations" : {
         "bg" : {
@@ -136289,6 +136301,9 @@
         }
       }
     },
+    "Manually backdate some recent carbs or insulin you’ve entered in the last 6 to 8 hours." : {
+
+    },
     "Manually entered blood glucose, such as a fingerstick test." : {
       "localizations" : {
         "bg" : {
@@ -142817,6 +142832,9 @@
         }
       }
     },
+    "Migration of JSON-based OpenAPS Determination Data failed: %@" : {
+
+    },
     "min" : {
       "comment" : "Minutes abbreviation\nShort form for minutes",
       "localizations" : {
@@ -157303,6 +157321,9 @@
         }
       }
     },
+    "Oops! Some data didn’t make it over." : {
+
+    },
     "Open %@" : {
       "localizations" : {
         "bg" : {
@@ -192843,6 +192864,9 @@
         }
       }
     },
+    "Stay in open loop (no automated dosing) for a bit to help Trio catch up to keep you safe" : {
+
+    },
     "Steps" : {
       "extractionState" : "manual",
       "localizations" : {
@@ -209701,6 +209725,9 @@
         }
       }
     },
+    "This means Trio may not have complete information about how much active insulin or carbs were still on board when you switched over." : {
+
+    },
     "This might be an intermittent problem, but please check that your transmitter is tightly secured over your sensor" : {
       "comment" : "This might be an intermittent problem, but please check that your transmitter is tightly secured over your sensor",
       "extractionState" : "manual",
@@ -216060,6 +216087,9 @@
         }
       }
     },
+    "To stay safe, we recommend:" : {
+
+    },
     "Today" : {
       "extractionState" : "manual",
       "localizations" : {
@@ -221127,6 +221157,9 @@
         }
       }
     },
+    "Trio is still fully functional and will adapt quickly — but your awareness right now helps it keep you safer." : {
+
+    },
     "Trio lets you create automations using iOS Shortcuts. Go to the Shortcuts app to create new automations." : {
       "localizations" : {
         "bg" : {
@@ -236363,6 +236396,9 @@
         }
       }
     },
+    "While upgrading Trio to the new version, we ran into an issue transferring some of your historical data." : {
+
+    },
     "Why does Trio collect this data?" : {
       "localizations" : {
         "bg" : {
@@ -239715,6 +239751,9 @@
         }
       }
     },
+    "Your last 24 hr of treatment data (pump events, carb entries, glucose trace, etc.) are migrated." : {
+
+    },
     "Your phone or app is not enabled for NFC communications, which is needed to pair to libre2 sensors" : {
       "extractionState" : "manual",
       "localizations" : {
@@ -240338,9 +240377,6 @@
         }
       }
     },
-    "Your treatment data (pump events, carb entries, glucose trace, etc.) are not migrated." : {
-
-    },
     "ZT" : {
       "localizations" : {
         "bg" : {

+ 2 - 2
Trio/Sources/Modules/Main/View/MainLoadingView.swift

@@ -70,9 +70,9 @@ extension Main {
                 .font(.title3).bold()
                 .background(
                     Capsule()
-                        .fill(Color.tabBar)
+                        .fill(Color.blue)
                 )
-                .foregroundColor(.white)
+                .foregroundColor(Color.white)
                 .shadow(color: Color.black.opacity(0.1), radius: 5, x: 0, y: 2)
             }
         }

+ 131 - 0
Trio/Sources/Modules/Main/View/MainMigrationErrorView.swift

@@ -0,0 +1,131 @@
+//
+//  MainMigrationErrorView.swift
+//  Trio
+//
+//  Created by Cengiz Deniz on 21.04.25.
+//
+import SwiftUI
+
+extension Main {
+    struct MainMigrationErrorView: View {
+        let migrationErrors: [String]
+        let onConfirm: () -> Void
+
+        private let versionNumber = Bundle.main.releaseVersionNumber ?? String(localized: "Unknown")
+
+        var body: some View {
+            ZStack(alignment: .bottom) {
+                LinearGradient(
+                    gradient: Gradient(colors: [Color.bgDarkBlue, Color.bgDarkerDarkBlue]),
+                    startPoint: .top,
+                    endPoint: .bottom
+                )
+                .ignoresSafeArea()
+
+                ScrollView {
+                    VStack {
+                        Spacer().frame(maxHeight: 20)
+
+                        Image(.trioCircledNoBackground)
+                            .resizable()
+                            .scaledToFit()
+                            .frame(width: 80, height: 80)
+                            .shadow(color: Color.white.opacity(0.1), radius: 5, x: 0, y: 0)
+
+                        Text("Trio v\(versionNumber)")
+                            .fontWeight(.heavy)
+                            .foregroundStyle(Color(red: 148 / 255, green: 102 / 255, blue: 234 / 255))
+                            .padding(.vertical)
+
+                        Spacer().frame(maxHeight: 20)
+
+                        VStack(alignment: .leading, spacing: 20) {
+                            Text("Oops! Some data didn’t make it over.").font(.title3).bold()
+
+                            Text(
+                                "While upgrading Trio to the new version, we ran into an issue transferring some of your historical data."
+                            )
+                            .multilineTextAlignment(.leading)
+
+                            VStack(alignment: .leading, spacing: 10) {
+                                ForEach(migrationErrors, id: \.self) { message in
+                                    BulletPoint(message)
+                                }
+                            }
+
+                            Text(
+                                "This means Trio may not have complete information about how much active insulin or carbs were still on board when you switched over."
+                            )
+                            .bold()
+
+                            VStack(alignment: .leading, spacing: 10) {
+                                HStack(alignment: .top, spacing: 10) {
+                                    Image(systemName: "exclamationmark.triangle.fill")
+                                        .foregroundStyle(Color.bgDarkBlue, Color.orange)
+                                        .symbolRenderingMode(.palette)
+                                    Text("To stay safe, we recommend:").foregroundStyle(Color.orange)
+                                }.bold()
+
+                                VStack(alignment: .leading, spacing: 10) {
+                                    BulletPoint(
+                                        String(
+                                            localized: "Manually backdate some recent carbs or insulin you’ve entered in the last 6 to 8 hours."
+                                        )
+                                    )
+                                    BulletPoint(
+                                        String(
+                                            localized: "Stay in open loop (no automated dosing) for a bit to help Trio catch up to keep you safe"
+                                        )
+                                    )
+                                }
+                            }
+                            .frame(maxWidth: .infinity)
+                            .padding()
+                            .background(Color.clear)
+                            .overlay(
+                                RoundedRectangle(cornerRadius: 10)
+                                    .stroke(Color.orange, lineWidth: 2)
+                            )
+                            .cornerRadius(10)
+
+                            Text(
+                                "Trio is still fully functional and will adapt quickly — but your awareness right now helps it keep you safer."
+                            )
+                            .multilineTextAlignment(.leading)
+                            .padding(.bottom)
+                        }
+                        .padding(.horizontal, 24)
+                        .foregroundStyle(.white)
+                    }
+                }
+                .padding(.bottom, 80)
+
+                Button(action: onConfirm) {
+                    Text("I understand! Proceed")
+                        .frame(width: UIScreen.main.bounds.width - 60, height: 50)
+                        .background(
+                            Capsule()
+                                .fill(Color.blue)
+                        )
+                        .foregroundColor(Color.white)
+                }.padding(.bottom)
+            }
+        }
+    }
+}
+
+struct MainMigrationErrorView_Previews: PreviewProvider {
+    static var previews: some View {
+        Group {
+            Main.MainMigrationErrorView(
+                migrationErrors: [
+                    "Failed to import glucose history.",
+                    "Failed to import pump history.",
+                    "Failed to import carb history.",
+                    "Failed to import algorithm data."
+                ],
+                onConfirm: { print("Proceed") }
+            )
+        }
+    }
+}

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

@@ -29,7 +29,7 @@ struct CompletedStepView: View {
                     completedItemsView(
                         stepIndex: index + 1,
                         title: chapter.title,
-                        description: chapter.completedDescription,
+                        description: isChapterCompleted(chapter) ? chapter.completedDescription : chapter.overviewDescription,
                         isCompleted: isChapterCompleted(chapter)
                     )
 

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

@@ -25,7 +25,7 @@ struct StartupReturningUserStepView: View {
                     Text("Important").foregroundStyle(Color.orange)
                 }.bold()
 
-                Text("Your treatment data (pump events, carb entries, glucose trace, etc.) are not migrated.")
+                Text("Your last 24 hr of treatment data (pump events, carb entries, glucose trace, etc.) are migrated.")
 
                 Divider().overlay(Color.orange)