소스 검색

Merge remote-tracking branch 'origin/dev' into submodule-sha

Jonas Björkert 1 년 전
부모
커밋
8f7bc3157e
96개의 변경된 파일4331개의 추가작업 그리고 2963개의 파일을 삭제
  1. 1 1
      LiveActivity/Views/WidgetItems/LiveActivityCOBLabelView.swift
  2. 1 1
      LiveActivity/Views/WidgetItems/LiveActivityIOBLabelView.swift
  3. 1 1
      LiveActivity/Views/WidgetItems/LiveActivityTotalDailyDoseView.swift
  4. 90 16
      Model/CoreDataObserver.swift
  5. 129 80
      Model/CoreDataStack.swift
  6. 24 21
      Model/Helper/CoreDataError.swift
  7. 21 0
      Model/Helper/GlucoseStored+helper.swift
  8. 31 1
      Model/Helper/PumpEvent+helper.swift
  9. 7 1
      Model/TrioCoreDataPersistentContainer.xcdatamodeld/TrioCoreDataPersistentContainer.xcdatamodel/contents
  10. 1 1
      Trio Watch App Extension/Views/BolusConfirmationView.swift
  11. 9 3
      Trio Watch App Extension/Views/BolusInputView.swift
  12. 4 1
      Trio Watch App Extension/Views/BolusProgressOverlay.swift
  13. 3 2
      Trio Watch App Extension/Views/CarbsInputView.swift
  14. 8 3
      Trio Watch App Extension/Views/GlucoseTrendView.swift
  15. 3 3
      Trio Watch App Extension/Views/TreatmentMenuView.swift
  16. 33 13
      Trio.xcodeproj/project.pbxproj
  17. 1 1
      Trio.xcodeproj/xcshareddata/xcschemes/Trio Watch App.xcscheme
  18. 1 1
      Trio.xcodeproj/xcshareddata/xcschemes/Trio Watch Complication Extension.xcscheme
  19. 2 2
      Trio.xcodeproj/xcshareddata/xcschemes/Trio.xcscheme
  20. 325 298
      Trio/Sources/APS/APSManager.swift
  21. 2 2
      Trio/Sources/APS/CGM/PluginSource.swift
  22. 15 9
      Trio/Sources/APS/DeviceDataManager.swift
  23. 58 85
      Trio/Sources/APS/FetchGlucoseManager.swift
  24. 39 35
      Trio/Sources/APS/FetchTreatmentsManager.swift
  25. 35 34
      Trio/Sources/APS/OpenAPS/OpenAPS.swift
  26. 31 29
      Trio/Sources/APS/Storage/CarbsStorage.swift
  27. 34 27
      Trio/Sources/APS/Storage/ContactImageStorage.swift
  28. 49 15
      Trio/Sources/APS/Storage/DeterminationStorage.swift
  29. 225 157
      Trio/Sources/APS/Storage/GlucoseStorage.swift
  30. 55 47
      Trio/Sources/APS/Storage/OverrideStorage.swift
  31. 187 206
      Trio/Sources/APS/Storage/PumpHistoryStorage.swift
  32. 49 38
      Trio/Sources/APS/Storage/TempTargetsStorage.swift
  33. 17 3
      Trio/Sources/Application/AppDelegate.swift
  34. 15 10
      Trio/Sources/Application/TrioApp.swift
  35. 1 1
      Trio/Sources/Helpers/MainChartHelper.swift
  36. 228 237
      Trio/Sources/Localizations/Main/Localizable.xcstrings
  37. 4 0
      Trio/Sources/Logger/Logger.swift
  38. 133 91
      Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+Overrides.swift
  39. 90 70
      Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+TempTargets.swift
  40. 11 7
      Trio/Sources/Modules/Adjustments/AdjustmentsStateModel.swift
  41. 23 21
      Trio/Sources/Modules/Adjustments/View/Overrides/EditOverrideForm.swift
  42. 16 8
      Trio/Sources/Modules/Adjustments/View/TempTargets/AddTempTargetForm.swift
  43. 2 2
      Trio/Sources/Modules/Adjustments/View/TempTargets/EditTempTargetForm.swift
  44. 9 2
      Trio/Sources/Modules/AlgorithmAdvancedSettings/AlgorithmAdvancedSettingsStateModel.swift
  45. 11 4
      Trio/Sources/Modules/AutosensSettings/AutosensSettingsStateModel.swift
  46. 6 2
      Trio/Sources/Modules/BasalProfileEditor/BasalProfileEditorStateModel.swift
  47. 26 22
      Trio/Sources/Modules/Calibrations/CalibrationsStateModel.swift
  48. 6 2
      Trio/Sources/Modules/CarbRatioEditor/CarbRatioEditorStateModel.swift
  49. 54 40
      Trio/Sources/Modules/DataTable/DataTableStateModel.swift
  50. 22 14
      Trio/Sources/Modules/DataTable/View/DataTableRootView.swift
  51. 17 8
      Trio/Sources/Modules/Home/HomeStateModel+Setup/BatterySetup.swift
  52. 28 14
      Trio/Sources/Modules/Home/HomeStateModel+Setup/CarbSetup.swift
  53. 22 16
      Trio/Sources/Modules/Home/HomeStateModel+Setup/DeterminationSetup.swift
  54. 39 61
      Trio/Sources/Modules/Home/HomeStateModel+Setup/ForecastSetup.swift
  55. 17 7
      Trio/Sources/Modules/Home/HomeStateModel+Setup/GlucoseSetup.swift
  56. 32 17
      Trio/Sources/Modules/Home/HomeStateModel+Setup/OverrideSetup.swift
  57. 30 13
      Trio/Sources/Modules/Home/HomeStateModel+Setup/PumpHistorySetup.swift
  58. 34 16
      Trio/Sources/Modules/Home/HomeStateModel+Setup/TempTargetSetup.swift
  59. 12 12
      Trio/Sources/Modules/Home/HomeStateModel.swift
  60. 22 0
      Trio/Sources/Modules/Home/View/Chart/ChartElements/GlucoseChartView.swift
  61. 4 3
      Trio/Sources/Modules/Home/View/Chart/ChartElements/SelectionPopoverView.swift
  62. 2 2
      Trio/Sources/Modules/Home/View/HomeRootView.swift
  63. 9 2
      Trio/Sources/Modules/ISFEditor/ISFEditorStateModel.swift
  64. 75 36
      Trio/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift
  65. 19 1
      Trio/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift
  66. 57 1
      Trio/Sources/Modules/Settings/View/SettingsRootView.swift
  67. 32 26
      Trio/Sources/Modules/Stat/StatStateModel.swift
  68. 9 2
      Trio/Sources/Modules/TargetsEditor/TargetsEditorStateModel.swift
  69. 173 145
      Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift
  70. 9 16
      Trio/Sources/Modules/Treatments/View/TreatmentsRootView.swift
  71. 342 0
      Trio/Sources/Services/AppVersionChecker/AppVersionChecker.swift
  72. 102 67
      Trio/Sources/Services/BolusCalculator/BolusCalculationManager.swift
  73. 22 18
      Trio/Sources/Services/Calendar/CalendarManager.swift
  74. 76 69
      Trio/Sources/Services/ContactImage/ContactImageManager.swift
  75. 108 77
      Trio/Sources/Services/HealthKit/HealthKitManager.swift
  76. 12 12
      Trio/Sources/Services/LiveActivity/Data/DataManager.swift
  77. 34 21
      Trio/Sources/Services/LiveActivity/LiveActivityBridge.swift
  78. 147 113
      Trio/Sources/Services/Network/Nightscout/NightscoutManager.swift
  79. 120 98
      Trio/Sources/Services/Network/TidepoolManager.swift
  80. 2 2
      Trio/Sources/Services/RemoteControl/TrioRemoteControl+APNS.swift
  81. 10 9
      Trio/Sources/Services/RemoteControl/TrioRemoteControl+Bolus.swift
  82. 2 2
      Trio/Sources/Services/RemoteControl/TrioRemoteControl+Meal.swift
  83. 37 23
      Trio/Sources/Services/RemoteControl/TrioRemoteControl+Override.swift
  84. 15 15
      Trio/Sources/Services/RemoteControl/TrioRemoteControl+TempTarget.swift
  85. 5 5
      Trio/Sources/Services/RemoteControl/TrioRemoteControl.swift
  86. 74 21
      Trio/Sources/Services/Storage/FileStorage.swift
  87. 8 6
      Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift
  88. 219 175
      Trio/Sources/Services/WatchManager/AppleWatchManager.swift
  89. 138 100
      Trio/Sources/Services/WatchManager/GarminManager.swift
  90. 1 1
      Trio/Sources/Shortcuts/Carbs/CarbPresetIntentRequest.swift
  91. 2 2
      Trio/Sources/Shortcuts/Override/OverridePresetEntity.swift
  92. 45 48
      Trio/Sources/Shortcuts/Override/OverridePresetsIntentRequest.swift
  93. 2 2
      Trio/Sources/Shortcuts/State/StateIntentRequest.swift
  94. 1 1
      Trio/Sources/Shortcuts/TempPresets/TempPresetIntent.swift
  95. 7 8
      Trio/Sources/Shortcuts/TempPresets/TempPresetsIntentRequest.swift
  96. 10 0
      blacklisted-versions.json

+ 1 - 1
LiveActivity/Views/WidgetItems/LiveActivityCOBLabelView.swift

@@ -22,7 +22,7 @@ struct LiveActivityCOBLabelView: View {
                     .foregroundStyle(context.isStale ? .secondary : .primary)
                     .foregroundStyle(context.isStale ? .secondary : .primary)
                     .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
                     .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
 
 
-                Text("g")
+                Text(String(localized: "g", comment: "gram of carbs"))
                     .font(.headline).fontWeight(.bold)
                     .font(.headline).fontWeight(.bold)
                     .foregroundStyle(context.isStale ? .secondary : .primary)
                     .foregroundStyle(context.isStale ? .secondary : .primary)
                     .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
                     .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))

+ 1 - 1
LiveActivity/Views/WidgetItems/LiveActivityIOBLabelView.swift

@@ -30,7 +30,7 @@ struct LiveActivityIOBLabelView: View {
                 .foregroundStyle(context.isStale ? .secondary : .primary)
                 .foregroundStyle(context.isStale ? .secondary : .primary)
                 .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
                 .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
 
 
-                Text("U")
+                Text(String(localized: "U", comment: "Insulin unit"))
                     .font(.headline).fontWeight(.bold)
                     .font(.headline).fontWeight(.bold)
                     .foregroundStyle(context.isStale ? .secondary : .primary)
                     .foregroundStyle(context.isStale ? .secondary : .primary)
                     .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
                     .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))

+ 1 - 1
LiveActivity/Views/WidgetItems/LiveActivityTotalDailyDoseView.swift

@@ -24,7 +24,7 @@ struct LiveActivityTotalDailyDoseView: View {
                 .foregroundStyle(context.isStale ? .secondary : .primary)
                 .foregroundStyle(context.isStale ? .secondary : .primary)
                 .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
                 .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
 
 
-                Text("U")
+                Text(String(localized: "U", comment: "Insulin unit"))
                     .font(.headline).fontWeight(.bold)
                     .font(.headline).fontWeight(.bold)
                     .foregroundStyle(context.isStale ? .secondary : .primary)
                     .foregroundStyle(context.isStale ? .secondary : .primary)
                     .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
                     .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))

+ 90 - 16
Model/CoreDataObserver.swift

@@ -2,34 +2,108 @@ import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
 
 
-func changedObjectsOnManagedObjectContextDidSavePublisher() -> some Publisher<Set<NSManagedObjectID>, Never> {
+/// Represents the types of Core Data changes that can be observed
+/// Use as an option set to specify which types of changes to monitor
+public struct CoreDataChangeTypes: OptionSet {
+    /// The raw integer value used to store the option set bits
+    public let rawValue: Int
+
+    /// Required initializer for OptionSet conformance
+    public init(rawValue: Int) {
+        self.rawValue = rawValue
+    }
+
+    /// Represents newly created/inserted objects in Core Data
+    /// Binary: 001 (1 << 0)
+    public static let inserted = CoreDataChangeTypes(rawValue: 1 << 0)
+
+    /// Represents modified/updated objects in Core Data
+    /// Binary: 010 (1 << 1)
+    public static let updated = CoreDataChangeTypes(rawValue: 1 << 1)
+
+    /// Represents removed/deleted objects in Core Data
+    /// Binary: 100 (1 << 2)
+    public static let deleted = CoreDataChangeTypes(rawValue: 1 << 2)
+
+    /// Convenience option that includes all possible change types
+    /// This combines inserted, updated, and deleted into a single option
+    public static let all: CoreDataChangeTypes = [.inserted, .updated, .deleted]
+}
+
+/// Creates a publisher that emits sets of NSManagedObjectIDs when Core Data changes occur
+/// - Parameter changeTypes: The types of changes to observe (defaults to .all)
+/// - Returns: A publisher that emits Sets of NSManagedObjectIDs for the specified change types
+func changedObjectsOnManagedObjectContextDidSavePublisher(
+    observing changeTypes: CoreDataChangeTypes = .all
+) -> some Publisher<Set<NSManagedObjectID>, Never> {
     Foundation.NotificationCenter.default
     Foundation.NotificationCenter.default
-        .publisher(for: NSNotification.Name.NSManagedObjectContextDidSave)
-        .map { notification in
-            guard let userInfo = notification.userInfo else { return Set<NSManagedObjectID>() }
+        .publisher(for: .NSManagedObjectContextDidSave)
+        .compactMap { notification -> Set<NSManagedObjectID>? in
 
 
             var objectIDs = Set<NSManagedObjectID>()
             var objectIDs = Set<NSManagedObjectID>()
 
 
-            if let inserted = userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject> {
-                objectIDs.formUnion(inserted.map(\.objectID))
+            // Process inserted objects if requested
+            if changeTypes.contains(.inserted) {
+                objectIDs.formUnion(notification.insertedObjectIDs)
             }
             }
-            if let updated = userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject> {
-                objectIDs.formUnion(updated.map(\.objectID))
+
+            // Process updated objects if requested
+            if changeTypes.contains(.updated) {
+                objectIDs.formUnion(notification.updatedObjectIDs)
             }
             }
-            if let deleted = userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject> {
-                objectIDs.formUnion(deleted.map(\.objectID))
+
+            // Process deleted objects if requested
+            if changeTypes.contains(.deleted) {
+                objectIDs.formUnion(notification.deletedObjectIDs)
             }
             }
 
 
-            return objectIDs
+            // Only emit non-empty sets
+            return objectIDs.isEmpty ? nil : objectIDs
         }
         }
 }
 }
 
 
 extension Publisher where Output == Set<NSManagedObjectID> {
 extension Publisher where Output == Set<NSManagedObjectID> {
-    func filterByEntityName(_ name: String) -> some Publisher<Self.Output, Self.Failure> {
-        filter { objectIDs in
-            objectIDs.contains { objectID in
-                objectID.entity.name == name
-            }
+    /// Filters Core Data changes by entity name
+    ///
+    /// This method allows filtering Core Data changes by entity name.
+    ///
+    /// Example usage:
+    /// ```swift
+    /// // Filter changes for "GlucoseStored" entity
+    /// publisher.filteredByEntityName("GlucoseStored")
+    /// ```
+    ///
+    /// - Parameters:
+    ///   - name: The name of the Core Data entity to filter for
+    /// - Returns: A publisher emitting filtered sets of NSManagedObjectIDs
+    func filteredByEntityName(
+        _ name: String
+    ) -> some Publisher<Set<NSManagedObjectID>, Self.Failure> {
+        compactMap { objectIDs -> Set<NSManagedObjectID>? in
+            // Early exit for empty sets
+            guard !objectIDs.isEmpty else { return nil }
+
+            // Use lazy evaluation for better performance
+            let filtered = objectIDs.lazy.filter { $0.entity.name == name }
+            let result = Set(filtered)
+            return result.isEmpty ? nil : result
         }
         }
     }
     }
 }
 }
+
+extension Notification {
+    var insertedObjectIDs: Set<NSManagedObjectID> {
+        guard let objects = userInfo?[NSInsertedObjectsKey] as? Set<NSManagedObject> else { return [] }
+        return Set(objects.lazy.map(\.objectID))
+    }
+
+    var updatedObjectIDs: Set<NSManagedObjectID> {
+        guard let objects = userInfo?[NSUpdatedObjectsKey] as? Set<NSManagedObject> else { return [] }
+        return Set(objects.lazy.map(\.objectID))
+    }
+
+    var deletedObjectIDs: Set<NSManagedObjectID> {
+        guard let objects = userInfo?[NSDeletedObjectsKey] as? Set<NSManagedObject> else { return [] }
+        return Set(objects.lazy.map(\.objectID))
+    }
+}

+ 129 - 80
Model/CoreDataStack.swift

@@ -9,9 +9,51 @@ class CoreDataStack: ObservableObject {
     private var notificationToken: NSObjectProtocol?
     private var notificationToken: NSObjectProtocol?
     private let inMemory: Bool
     private let inMemory: Bool
 
 
+    let persistentContainer: NSPersistentContainer
+
     private init(inMemory: Bool = false) {
     private init(inMemory: Bool = false) {
         self.inMemory = inMemory
         self.inMemory = inMemory
 
 
+        // Initialize persistent container immediately
+        persistentContainer = NSPersistentContainer(
+            name: "TrioCoreDataPersistentContainer",
+            managedObjectModel: Self.managedObjectModel
+        )
+
+        guard let description = persistentContainer.persistentStoreDescriptions.first else {
+            fatalError("Failed \(DebuggingIdentifiers.failed) to retrieve a persistent store description")
+        }
+
+        if inMemory {
+            description.url = URL(fileURLWithPath: "/dev/null")
+        }
+
+        // Enable persistent store remote change notifications
+        /// - Tag: persistentStoreRemoteChange
+        description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
+
+        // Enable persistent history tracking
+        /// - Tag: persistentHistoryTracking
+        description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
+
+        // Enable lightweight migration
+        /// - Tag: lightweightMigration
+        description.shouldMigrateStoreAutomatically = true
+        description.shouldInferMappingModelAutomatically = true
+
+        persistentContainer.loadPersistentStores { _, error in
+            if let error = error as NSError? {
+                fatalError("Unresolved Error \(DebuggingIdentifiers.failed) \(error), \(error.userInfo)")
+            }
+        }
+
+        persistentContainer.viewContext.automaticallyMergesChangesFromParent = false
+        persistentContainer.viewContext.name = "viewContext"
+        /// - Tag: viewContextmergePolicy
+        persistentContainer.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
+        persistentContainer.viewContext.undoManager = nil
+        persistentContainer.viewContext.shouldDeleteInaccessibleFaults = true
+
         // Observe Core Data remote change notifications on the queue where the changes were made
         // Observe Core Data remote change notifications on the queue where the changes were made
         notificationToken = Foundation.NotificationCenter.default.addObserver(
         notificationToken = Foundation.NotificationCenter.default.addObserver(
             forName: .NSPersistentStoreRemoteChange,
             forName: .NSPersistentStoreRemoteChange,
@@ -41,44 +83,33 @@ class CoreDataStack: ObservableObject {
         }
         }
     }
     }
 
 
-    /// A persistent container to set up the Core Data Stack
-    lazy var persistentContainer: NSPersistentContainer = {
-        let container = NSPersistentContainer(name: "TrioCoreDataPersistentContainer")
-
-        guard let description = container.persistentStoreDescriptions.first else {
-            fatalError("Failed \(DebuggingIdentifiers.failed) to retrieve a persistent store description")
-        }
+    // Factory method for tests
+    static func createForTests() -> CoreDataStack {
+        CoreDataStack(inMemory: true)
+    }
 
 
-        if inMemory {
-            description.url = URL(fileURLWithPath: "/dev/null")
-        }
+    // Used for Canvas Preview
+    static var preview: CoreDataStack = {
+        let stack = CoreDataStack(inMemory: true)
+        let context = stack.persistentContainer.viewContext
 
 
-        // Enable persistent store remote change notifications
-        /// - Tag: persistentStoreRemoteChange
-        description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
+        let pumpHistory = PumpEventStored.makePreviewEvents(count: 10, provider: stack)
 
 
-        // Enable persistent history tracking
-        /// - Tag: persistentHistoryTracking
-        description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
+        return stack
+    }()
 
 
-        // Enable lightweight migration
-        /// - Tag: lightweightMigration
-        description.shouldMigrateStoreAutomatically = true
-        description.shouldInferMappingModelAutomatically = true
+    // Shared managed object model
+    static var managedObjectModel: NSManagedObjectModel = {
+        let bundle = Bundle(for: CoreDataStack.self)
+        guard let url = bundle.url(forResource: "TrioCoreDataPersistentContainer", withExtension: "momd") else {
+            fatalError("Failed \(DebuggingIdentifiers.failed) to locate momd file")
+        }
 
 
-        container.loadPersistentStores { _, error in
-            if let error = error as NSError? {
-                fatalError("Unresolved Error \(DebuggingIdentifiers.failed) \(error), \(error.userInfo)")
-            }
+        guard let model = NSManagedObjectModel(contentsOf: url) else {
+            fatalError("Failed \(DebuggingIdentifiers.failed) to load momd file")
         }
         }
 
 
-        container.viewContext.automaticallyMergesChangesFromParent = false
-        container.viewContext.name = "viewContext"
-        /// - Tag: viewContextmergePolicy
-        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
-        container.viewContext.undoManager = nil
-        container.viewContext.shouldDeleteInaccessibleFaults = true
-        return container
+        return model
     }()
     }()
 
 
     /// Creates and configures a private queue context
     /// Creates and configures a private queue context
@@ -88,7 +119,7 @@ class CoreDataStack: ObservableObject {
         let taskContext = persistentContainer.newBackgroundContext()
         let taskContext = persistentContainer.newBackgroundContext()
 
 
         /// ensure that the background contexts stay in sync with the main context
         /// ensure that the background contexts stay in sync with the main context
-        taskContext.automaticallyMergesChangesFromParent = true
+        taskContext.automaticallyMergesChangesFromParent = false
         taskContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
         taskContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
         taskContext.undoManager = nil
         taskContext.undoManager = nil
         return taskContext
         return taskContext
@@ -98,14 +129,14 @@ class CoreDataStack: ObservableObject {
         do {
         do {
             try await fetchPersistentHistoryTransactionsAndChanges()
             try await fetchPersistentHistoryTransactionsAndChanges()
         } catch {
         } catch {
-            debugPrint("\(error.localizedDescription)")
+            debug(.coreData, "\(error.localizedDescription)")
         }
         }
     }
     }
 
 
     private func fetchPersistentHistoryTransactionsAndChanges() async throws {
     private func fetchPersistentHistoryTransactionsAndChanges() async throws {
         let taskContext = newTaskContext()
         let taskContext = newTaskContext()
         taskContext.name = "persistentHistoryContext"
         taskContext.name = "persistentHistoryContext"
-        //        debugPrint("Start fetching persistent history changes from the store ... \(DebuggingIdentifiers.inProgress)")
+//        debug(.coreData,"Start fetching persistent history changes from the store ... \(DebuggingIdentifiers.inProgress)")
 
 
         try await taskContext.perform {
         try await taskContext.perform {
             // Execute the persistent history change since the last transaction
             // Execute the persistent history change since the last transaction
@@ -120,7 +151,7 @@ class CoreDataStack: ObservableObject {
     }
     }
 
 
     private func mergePersistentHistoryChanges(from history: [NSPersistentHistoryTransaction]) {
     private func mergePersistentHistoryChanges(from history: [NSPersistentHistoryTransaction]) {
-        //        debugPrint("Received \(history.count) persistent history transactions")
+//        debug(.coreData,"Received \(history.count) persistent history transactions")
         // Update view context with objectIDs from history change request
         // Update view context with objectIDs from history change request
         /// - Tag: mergeChanges
         /// - Tag: mergeChanges
         let viewContext = persistentContainer.viewContext
         let viewContext = persistentContainer.viewContext
@@ -142,10 +173,11 @@ class CoreDataStack: ObservableObject {
             let deleteHistoryTokensRequest = NSPersistentHistoryChangeRequest.deleteHistory(before: date)
             let deleteHistoryTokensRequest = NSPersistentHistoryChangeRequest.deleteHistory(before: date)
             do {
             do {
                 try taskContext.execute(deleteHistoryTokensRequest)
                 try taskContext.execute(deleteHistoryTokensRequest)
-                debugPrint("\(DebuggingIdentifiers.succeeded) Successfully deleted persistent history before \(date)")
+                debug(.coreData, "\(DebuggingIdentifiers.succeeded) Successfully deleted persistent history from before \(date)")
             } catch {
             } catch {
-                debugPrint(
-                    "\(DebuggingIdentifiers.failed) Failed to delete persistent history before \(date): \(error.localizedDescription)"
+                debug(
+                    .coreData,
+                    "\(DebuggingIdentifiers.failed) Failed to delete persistent history from before \(date): \(error.localizedDescription)"
                 )
                 )
             }
             }
         }
         }
@@ -155,9 +187,9 @@ class CoreDataStack: ObservableObject {
         // Force initialization of persistent container
         // Force initialization of persistent container
         let container = persistentContainer
         let container = persistentContainer
 
 
-        // Verify the store is loaded and available
-        guard container.persistentStoreCoordinator.persistentStores.isNotEmpty else {
-            throw CoreDataError.storeNotInitializedError
+        // Verify the store is loaded
+        guard container.persistentStoreCoordinator.persistentStores.isEmpty == false else {
+            throw CoreDataError.storeNotInitializedError(function: #function, file: #file)
         }
         }
     }
     }
 }
 }
@@ -169,7 +201,7 @@ extension CoreDataStack {
     ///  - Tag: synchronousDelete
     ///  - Tag: synchronousDelete
     func deleteObject(identifiedBy objectID: NSManagedObjectID) async {
     func deleteObject(identifiedBy objectID: NSManagedObjectID) async {
         let viewContext = persistentContainer.viewContext
         let viewContext = persistentContainer.viewContext
-        debugPrint("Start deleting data from the store ...\(DebuggingIdentifiers.inProgress)")
+        debug(.coreData, "Start deleting data from the store ...\(DebuggingIdentifiers.inProgress)")
 
 
         await viewContext.perform {
         await viewContext.perform {
             do {
             do {
@@ -178,9 +210,9 @@ extension CoreDataStack {
 
 
                 guard viewContext.hasChanges else { return }
                 guard viewContext.hasChanges else { return }
                 try viewContext.save()
                 try viewContext.save()
-                debugPrint("Successfully deleted data. \(DebuggingIdentifiers.succeeded)")
+                debug(.coreData, "Successfully deleted data. \(DebuggingIdentifiers.succeeded)")
             } catch {
             } catch {
-                debugPrint("Failed to delete data: \(error.localizedDescription)")
+                debug(.coreData, "Failed to delete data: \(error.localizedDescription)")
             }
             }
         }
         }
     }
     }
@@ -191,7 +223,9 @@ extension CoreDataStack {
         _ objectType: T.Type,
         _ objectType: T.Type,
         dateKey: String,
         dateKey: String,
         days: Int,
         days: Int,
-        isPresetKey: String? = nil
+        isPresetKey: String? = nil,
+        callingFunction: String = #function,
+        callingClass: String = #fileID
     ) async throws {
     ) async throws {
         let taskContext = newTaskContext()
         let taskContext = newTaskContext()
         taskContext.name = "deleteContext"
         taskContext.name = "deleteContext"
@@ -219,7 +253,7 @@ extension CoreDataStack {
 
 
             // Guard check if there are NSManagedObjects older than the specified days
             // Guard check if there are NSManagedObjects older than the specified days
             guard !objectIDs.isEmpty else {
             guard !objectIDs.isEmpty else {
-//                debugPrint("No objects found older than \(days) days.")
+//                debug(.coreData,"No objects found older than \(days) days.")
                 return
                 return
             }
             }
 
 
@@ -230,15 +264,15 @@ extension CoreDataStack {
                       let batchDeleteResult = fetchResult as? NSBatchDeleteResult,
                       let batchDeleteResult = fetchResult as? NSBatchDeleteResult,
                       let success = batchDeleteResult.result as? Bool, success
                       let success = batchDeleteResult.result as? Bool, success
                 else {
                 else {
-                    debugPrint("Failed to execute batch delete request \(DebuggingIdentifiers.failed)")
-                    throw CoreDataError.batchDeleteError
+                    debug(.coreData, "Failed to execute batch delete request \(DebuggingIdentifiers.failed)")
+                    throw CoreDataError.batchDeleteError(function: callingFunction, file: callingClass)
                 }
                 }
             }
             }
 
 
-            debugPrint("Successfully deleted data older than \(days) days. \(DebuggingIdentifiers.succeeded)")
+            debug(.coreData, "Successfully deleted data older than \(days) days. \(DebuggingIdentifiers.succeeded)")
         } catch {
         } catch {
-            debugPrint("Failed to fetch or delete data: \(error.localizedDescription) \(DebuggingIdentifiers.failed)")
-            throw CoreDataError.batchDeleteError
+            debug(.coreData, "Failed to fetch or delete data: \(error.localizedDescription) \(DebuggingIdentifiers.failed)")
+            throw CoreDataError.unexpectedError(error: error, function: callingFunction, file: callingClass)
         }
         }
     }
     }
 
 
@@ -247,7 +281,9 @@ extension CoreDataStack {
         childType: Child.Type,
         childType: Child.Type,
         dateKey: String,
         dateKey: String,
         days: Int,
         days: Int,
-        relationshipKey: String // The key of the Child Entity that links to the parent Entity
+        relationshipKey: String, // The key of the Child Entity that links to the parent Entity
+        callingFunction: String = #function,
+        callingClass: String = #fileID
     ) async throws {
     ) async throws {
         let taskContext = newTaskContext()
         let taskContext = newTaskContext()
         taskContext.name = "deleteContext"
         taskContext.name = "deleteContext"
@@ -267,7 +303,7 @@ extension CoreDataStack {
             }
             }
 
 
             guard !parentObjectIDs.isEmpty else {
             guard !parentObjectIDs.isEmpty else {
-//                debugPrint("No \(parentType) objects found older than \(days) days.")
+//                debug(.coreData,"No \(parentType) objects found older than \(days) days.")
                 return
                 return
             }
             }
 
 
@@ -281,7 +317,7 @@ extension CoreDataStack {
             }
             }
 
 
             guard !childObjectIDs.isEmpty else {
             guard !childObjectIDs.isEmpty else {
-//                debugPrint("No \(childType) objects found related to \(parentType) objects older than \(days) days.")
+//                debug(.coreData,"No \(childType) objects found related to \(parentType) objects older than \(days) days.")
                 return
                 return
             }
             }
 
 
@@ -292,17 +328,18 @@ extension CoreDataStack {
                       let batchDeleteResult = fetchResult as? NSBatchDeleteResult,
                       let batchDeleteResult = fetchResult as? NSBatchDeleteResult,
                       let success = batchDeleteResult.result as? Bool, success
                       let success = batchDeleteResult.result as? Bool, success
                 else {
                 else {
-                    debugPrint("Failed to execute batch delete request \(DebuggingIdentifiers.failed)")
-                    throw CoreDataError.batchDeleteError
+                    debug(.coreData, "Failed to execute batch delete request \(DebuggingIdentifiers.failed)")
+                    throw CoreDataError.batchDeleteError(function: callingFunction, file: callingClass)
                 }
                 }
             }
             }
 
 
-            debugPrint(
+            debug(
+                .coreData,
                 "Successfully deleted \(childType) data related to \(parentType) objects older than \(days) days. \(DebuggingIdentifiers.succeeded)"
                 "Successfully deleted \(childType) data related to \(parentType) objects older than \(days) days. \(DebuggingIdentifiers.succeeded)"
             )
             )
         } catch {
         } catch {
-            debugPrint("Failed to fetch or delete data: \(error.localizedDescription) \(DebuggingIdentifiers.failed)")
-            throw CoreDataError.batchDeleteError
+            debug(.coreData, "Failed to fetch or delete data: \(error.localizedDescription) \(DebuggingIdentifiers.failed)")
+            throw CoreDataError.unexpectedError(error: error, function: callingFunction, file: callingClass)
         }
         }
     }
     }
 }
 }
@@ -323,7 +360,7 @@ extension CoreDataStack {
         propertiesToFetch: [String]? = nil,
         propertiesToFetch: [String]? = nil,
         callingFunction: String = #function,
         callingFunction: String = #function,
         callingClass: String = #fileID
         callingClass: String = #fileID
-    ) -> [Any] {
+    ) throws -> [Any] {
         let request = NSFetchRequest<NSFetchRequestResult>(entityName: String(describing: type))
         let request = NSFetchRequest<NSFetchRequestResult>(entityName: String(describing: type))
         request.sortDescriptors = [NSSortDescriptor(key: key, ascending: ascending)]
         request.sortDescriptors = [NSSortDescriptor(key: key, ascending: ascending)]
         request.predicate = predicate
         request.predicate = predicate
@@ -344,7 +381,7 @@ extension CoreDataStack {
         context.transactionAuthor = "fetchEntities"
         context.transactionAuthor = "fetchEntities"
 
 
         /// we need to ensure that the fetch immediately returns a value as long as the whole app does not use the async await pattern, otherwise we could perform this asynchronously with backgroundContext.perform and not block the thread
         /// we need to ensure that the fetch immediately returns a value as long as the whole app does not use the async await pattern, otherwise we could perform this asynchronously with backgroundContext.perform and not block the thread
-        return context.performAndWait {
+        return try context.performAndWait {
             do {
             do {
                 if propertiesToFetch != nil {
                 if propertiesToFetch != nil {
                     return try context.fetch(request) as? [[String: Any]] ?? []
                     return try context.fetch(request) as? [[String: Any]] ?? []
@@ -352,11 +389,10 @@ extension CoreDataStack {
                     return try context.fetch(request) as? [T] ?? []
                     return try context.fetch(request) as? [T] ?? []
                 }
                 }
             } catch let error as NSError {
             } catch let error as NSError {
-                debugPrint(
-                    "Fetching \(T.self) in \(callingFunction) from \(callingClass): \(DebuggingIdentifiers.failed) \(error) on Thread: \(Thread.current)"
+                throw CoreDataError.fetchError(
+                    function: callingFunction,
+                    file: callingClass
                 )
                 )
-
-                return []
             }
             }
         }
         }
     }
     }
@@ -371,12 +407,14 @@ extension CoreDataStack {
         fetchLimit: Int? = nil,
         fetchLimit: Int? = nil,
         batchSize: Int? = nil,
         batchSize: Int? = nil,
         propertiesToFetch: [String]? = nil,
         propertiesToFetch: [String]? = nil,
+        relationshipKeyPathsForPrefetching: [String]? = nil,
         callingFunction: String = #function,
         callingFunction: String = #function,
         callingClass: String = #fileID
         callingClass: String = #fileID
-    ) async -> Any {
+    ) async throws -> Any {
         let request: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: String(describing: type))
         let request: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: String(describing: type))
         request.sortDescriptors = [NSSortDescriptor(key: key, ascending: ascending)]
         request.sortDescriptors = [NSSortDescriptor(key: key, ascending: ascending)]
         request.predicate = predicate
         request.predicate = predicate
+
         if let limit = fetchLimit {
         if let limit = fetchLimit {
             request.fetchLimit = limit
             request.fetchLimit = limit
         }
         }
@@ -389,11 +427,14 @@ extension CoreDataStack {
         } else {
         } else {
             request.resultType = .managedObjectResultType
             request.resultType = .managedObjectResultType
         }
         }
+        if let prefetchKeyPaths = relationshipKeyPathsForPrefetching {
+            request.relationshipKeyPathsForPrefetching = prefetchKeyPaths
+        }
 
 
         context.name = "fetchContext"
         context.name = "fetchContext"
         context.transactionAuthor = "fetchEntities"
         context.transactionAuthor = "fetchEntities"
 
 
-        return await context.perform {
+        return try await context.perform {
             do {
             do {
                 if propertiesToFetch != nil {
                 if propertiesToFetch != nil {
                     return try context.fetch(request) as? [[String: Any]] ?? []
                     return try context.fetch(request) as? [[String: Any]] ?? []
@@ -401,10 +442,11 @@ extension CoreDataStack {
                     return try context.fetch(request) as? [T] ?? []
                     return try context.fetch(request) as? [T] ?? []
                 }
                 }
             } catch let error as NSError {
             } catch let error as NSError {
-                debugPrint(
-                    "Fetching \(T.self) in \(callingFunction) from \(callingClass): \(DebuggingIdentifiers.failed) \(error) on Thread: \(Thread.current)"
+                throw CoreDataError.unexpectedError(
+                    error: error,
+                    function: callingFunction,
+                    file: callingClass
                 )
                 )
-                return []
             }
             }
         }
         }
     }
     }
@@ -412,9 +454,11 @@ extension CoreDataStack {
     // Get NSManagedObject
     // Get NSManagedObject
     func getNSManagedObject<T: NSManagedObject>(
     func getNSManagedObject<T: NSManagedObject>(
         with ids: [NSManagedObjectID],
         with ids: [NSManagedObjectID],
-        context: NSManagedObjectContext
-    ) async -> [T] {
-        await context.perform {
+        context: NSManagedObjectContext,
+        callingFunction: String = #function,
+        callingClass: String = #fileID
+    ) async throws -> [T] {
+        try await context.perform {
             var objects = [T]()
             var objects = [T]()
             do {
             do {
                 for id in ids {
                 for id in ids {
@@ -422,10 +466,13 @@ extension CoreDataStack {
                         objects.append(object)
                         objects.append(object)
                     }
                     }
                 }
                 }
+                return objects
             } catch {
             } catch {
-                debugPrint("Failed to fetch objects: \(error.localizedDescription)")
+                throw CoreDataError.fetchError(
+                    function: callingFunction,
+                    file: callingClass
+                )
             }
             }
-            return objects
         }
         }
     }
     }
 }
 }
@@ -442,7 +489,7 @@ extension CoreDataStack {
         do {
         do {
             try context.save()
             try context.save()
         } catch {
         } catch {
-            debugPrint("Error saving context \(DebuggingIdentifiers.failed): \(error)")
+            debug(.coreData, "Error saving context \(DebuggingIdentifiers.failed): \(error)")
         }
         }
     }
     }
 }
 }
@@ -458,11 +505,13 @@ extension NSManagedObjectContext {
         do {
         do {
             guard onContext.hasChanges else { return }
             guard onContext.hasChanges else { return }
             try onContext.save()
             try onContext.save()
-//            debugPrint(
-//                "Saving to Core Data successful in \(callingFunction) in \(callingClass): \(DebuggingIdentifiers.succeeded)"
-//            )
+            debug(
+                .coreData,
+                "Saving to Core Data successful in \(callingFunction) in \(callingClass): \(DebuggingIdentifiers.succeeded)"
+            )
         } catch let error as NSError {
         } catch let error as NSError {
-            debugPrint(
+            debug(
+                .coreData,
                 "Saving to Core Data failed in \(callingFunction) in \(callingClass): \(DebuggingIdentifiers.failed) with error \(error), \(error.userInfo)"
                 "Saving to Core Data failed in \(callingFunction) in \(callingClass): \(DebuggingIdentifiers.failed) with error \(error), \(error.userInfo)"
             )
             )
             throw error
             throw error

+ 24 - 21
Model/Helper/CoreDataError.swift

@@ -1,32 +1,35 @@
 import Foundation
 import Foundation
 
 
 enum CoreDataError: Error {
 enum CoreDataError: Error {
-    case creationError
-    case batchInsertError
-    case batchDeleteError
-    case persistentHistoryChangeError
-    case unexpectedError(error: Error)
-    case fetchError
-    case storeNotInitializedError
+    case validationError(function: String, file: String)
+    case creationError(function: String, file: String)
+    case batchInsertError(function: String, file: String)
+    case batchDeleteError(function: String, file: String)
+    case persistentHistoryChangeError(function: String, file: String)
+    case unexpectedError(error: Error, function: String, file: String)
+    case fetchError(function: String, file: String)
+    case storeNotInitializedError(function: String, file: String)
 }
 }
 
 
 extension CoreDataError: LocalizedError {
 extension CoreDataError: LocalizedError {
     var errorDescription: String? {
     var errorDescription: String? {
         switch self {
         switch self {
-        case .creationError:
-            return String(localized: "Failed to create a new object.", comment: "")
-        case .batchInsertError:
-            return String(localized: "Failed to execute a batch insert request.", comment: "")
-        case .batchDeleteError:
-            return String(localized: "Failed to execute a batch delete request.", comment: "")
-        case .persistentHistoryChangeError:
-            return String(localized: "Failed to execute a persistent history change request.", comment: "")
-        case let .unexpectedError(error):
-            return String(localized: "Received unexpected error. \(error.localizedDescription)", comment: "")
-        case .fetchError:
-            return String(localized: "Failed to fetch object \(DebuggingIdentifiers.failed).", comment: "")
-        case .storeNotInitializedError:
-            return String(localized: "Failed to initialize Core Data's persistent store.", comment: "")
+        case let .creationError(function, file):
+            return String(localized: "Failed to create a new object in \(function) from \(file).")
+        case let .batchInsertError(function, file):
+            return String(localized: "Failed to execute a batch insert request in \(function) from \(file).")
+        case let .batchDeleteError(function, file):
+            return String(localized: "Failed to execute a batch delete request in \(function) from \(file).")
+        case let .persistentHistoryChangeError(function, file):
+            return String(localized: "Failed to execute a persistent history change request in \(function) from \(file).")
+        case let .unexpectedError(error, function, file):
+            return String(localized: "Received unexpected error in \(function) from \(file): \(error.localizedDescription)")
+        case let .fetchError(function, file):
+            return String(localized: "Failed to fetch object \(DebuggingIdentifiers.failed) in \(function) from \(file).")
+        case let .validationError(function, file):
+            return String(localized: "Failed to validate object in \(function) from \(file).")
+        case let .storeNotInitializedError(function, file):
+            return String(localized: "Store not initialized in \(function) from \(file).")
         }
         }
     }
     }
 }
 }

+ 21 - 0
Model/Helper/GlucoseStored+helper.swift

@@ -27,6 +27,27 @@ extension GlucoseStored {
 
 
         return glucose.allSatisfy { $0.glucose == firstValue }
         return glucose.allSatisfy { $0.glucose == firstValue }
     }
     }
+
+    // Preview
+    @discardableResult static func makePreviewGlucose(count: Int, provider: CoreDataStack) -> [GlucoseStored] {
+        let context = provider.persistentContainer.viewContext
+        let baseGlucose = 120
+        let glucoseValues = (0 ..< count).map { index -> GlucoseStored in
+            let glucose = GlucoseStored(context: context)
+            glucose.id = UUID()
+            glucose.date = Date.now.addingTimeInterval(Double(index) * -300) // Every 5 minutes
+            glucose.glucose = Int16(baseGlucose + (index % 3) * 10) // Varying between 120-140
+            glucose.direction = BloodGlucose.Direction.flat.rawValue
+            glucose.isManual = false
+            glucose.isUploadedToNS = false
+            glucose.isUploadedToHealth = false
+            glucose.isUploadedToTidepool = false
+            return glucose
+        }
+
+        try? context.save()
+        return glucoseValues
+    }
 }
 }
 
 
 extension NSPredicate {
 extension NSPredicate {

+ 31 - 1
Model/Helper/PumpEvent+helper.swift

@@ -12,6 +12,32 @@ extension PumpEventStored {
         }
         }
         return request
         return request
     }
     }
+
+    // Preview
+    @discardableResult static func makePreviewEvents(count: Int, provider: CoreDataStack) -> [PumpEventStored] {
+        let context = provider.persistentContainer.viewContext
+        let events = (0 ..< count).map { index -> PumpEventStored in
+            let event = PumpEventStored(context: context)
+            event.id = UUID().uuidString
+            event.timestamp = Date.now.addingTimeInterval(Double(index) * -300) // Every 5 minutes
+            event.type = EventType.bolus.rawValue
+            event.isUploadedToNS = false
+            event.isUploadedToHealth = false
+            event.isUploadedToTidepool = false
+
+            // Add a bolus
+            let bolus = BolusStored(context: context)
+            bolus.amount = 2.5 as NSDecimalNumber
+            bolus.isExternal = false
+            bolus.isSMB = false
+            bolus.pumpEvent = event
+
+            return event
+        }
+
+        try? context.save()
+        return events
+    }
 }
 }
 
 
 public extension PumpEventStored {
 public extension PumpEventStored {
@@ -65,7 +91,11 @@ extension NSPredicate {
 
 
     static var recentPumpHistory: NSPredicate {
     static var recentPumpHistory: NSPredicate {
         let date = Date.twentyMinutesAgo
         let date = Date.twentyMinutesAgo
-        return NSPredicate(format: "timestamp >= %@", date as NSDate)
+        return NSPredicate(
+            format: "type == %@ AND timestamp <= %@",
+            PumpEventStored.EventType.tempBasal.rawValue,
+            date as NSDate
+        )
     }
     }
 
 
     static var lastPumpBolus: NSPredicate {
     static var lastPumpBolus: NSPredicate {

+ 7 - 1
Model/TrioCoreDataPersistentContainer.xcdatamodeld/TrioCoreDataPersistentContainer.xcdatamodel/contents

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23231" systemVersion="24C101" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
+<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23605" systemVersion="24C101" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
     <entity name="BolusStored" representedClassName="BolusStored" syncable="YES">
     <entity name="BolusStored" representedClassName="BolusStored" syncable="YES">
         <attribute name="amount" optional="YES" attributeType="Decimal" defaultValueString="0"/>
         <attribute name="amount" optional="YES" attributeType="Decimal" defaultValueString="0"/>
         <attribute name="isExternal" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
         <attribute name="isExternal" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
@@ -51,6 +51,9 @@
         <fetchIndex name="byDate">
         <fetchIndex name="byDate">
             <fetchIndexElement property="date" type="Binary" order="descending"/>
             <fetchIndexElement property="date" type="Binary" order="descending"/>
         </fetchIndex>
         </fetchIndex>
+        <fetchIndex name="byValue">
+            <fetchIndexElement property="forecastValues" type="Binary" order="ascending"/>
+        </fetchIndex>
     </entity>
     </entity>
     <entity name="ForecastValue" representedClassName="ForecastValue" syncable="YES">
     <entity name="ForecastValue" representedClassName="ForecastValue" syncable="YES">
         <attribute name="index" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
         <attribute name="index" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
@@ -140,6 +143,9 @@
         <fetchIndex name="byDate">
         <fetchIndex name="byDate">
             <fetchIndexElement property="deliverAt" type="Binary" order="descending"/>
             <fetchIndexElement property="deliverAt" type="Binary" order="descending"/>
         </fetchIndex>
         </fetchIndex>
+        <fetchIndex name="byTimestamp">
+            <fetchIndexElement property="timestamp" type="Binary" order="ascending"/>
+        </fetchIndex>
     </entity>
     </entity>
     <entity name="OverrideRunStored" representedClassName="OverrideRunStored" syncable="YES">
     <entity name="OverrideRunStored" representedClassName="OverrideRunStored" syncable="YES">
         <attribute name="endDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
         <attribute name="endDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>

+ 1 - 1
Trio Watch App Extension/Views/BolusConfirmationView.swift

@@ -37,7 +37,7 @@ struct BolusConfirmationView: View {
                 HStack {
                 HStack {
                     Text("Bolus")
                     Text("Bolus")
                     Spacer()
                     Spacer()
-                    Text(String(format: "%.2f U", adjustedBolusAmount))
+                    Text(String(format: "%.2f \(String(localized: "U", comment: "Insulin unit"))", adjustedBolusAmount))
                         .bold()
                         .bold()
                         .foregroundStyle(Color.insulin)
                         .foregroundStyle(Color.insulin)
                 }.padding(.horizontal)
                 }.padding(.horizontal)

+ 9 - 3
Trio Watch App Extension/Views/BolusInputView.swift

@@ -25,7 +25,10 @@ struct BolusInputView: View {
     var body: some View {
     var body: some View {
         VStack {
         VStack {
             if state.showBolusCalculationProgress {
             if state.showBolusCalculationProgress {
-                ProgressView("Calculating Bolus...")
+                ProgressView(String(
+                    localized: "Calculating Bolus...",
+                    comment: "Progress view text on watch when calculating bolus"
+                ))
                 Spacer()
                 Spacer()
             } else {
             } else {
                 if effectiveBolusLimit <= 0 {
                 if effectiveBolusLimit <= 0 {
@@ -63,7 +66,7 @@ struct BolusInputView: View {
                         let bolusIncrement = Double(truncating: state.bolusIncrement as NSNumber)
                         let bolusIncrement = Double(truncating: state.bolusIncrement as NSNumber)
                         let adjustedBolusAmount = floor(bolusAmount / bolusIncrement) * bolusIncrement
                         let adjustedBolusAmount = floor(bolusAmount / bolusIncrement) * bolusIncrement
 
 
-                        Text(String(format: "%.2f U", adjustedBolusAmount))
+                        Text(String(format: "%.2f \(String(localized: "U", comment: "Insulin unit"))", adjustedBolusAmount))
                             .fontWeight(.bold)
                             .fontWeight(.bold)
                             .font(.system(.title2, design: .rounded))
                             .font(.system(.title2, design: .rounded))
                             .foregroundColor(bolusAmount > 0.0 && bolusAmount >= effectiveBolusLimit ? .loopRed : .primary)
                             .foregroundColor(bolusAmount > 0.0 && bolusAmount >= effectiveBolusLimit ? .loopRed : .primary)
@@ -117,7 +120,10 @@ struct BolusInputView: View {
                     .tint(Color.insulin)
                     .tint(Color.insulin)
                     .disabled(!(bolusAmount > 0.0) || bolusAmount > effectiveBolusLimit)
                     .disabled(!(bolusAmount > 0.0) || bolusAmount > effectiveBolusLimit)
 
 
-                    Text(String(format: "Recommended: %.1f U", NSDecimalNumber(decimal: state.recommendedBolus).doubleValue))
+                    Text(String(
+                        format: "\(String(localized: "Recommended:", comment: "Recommended bolus on Watch")) %.1f \(String(localized: "U", comment: "Insulin unit"))",
+                        NSDecimalNumber(decimal: state.recommendedBolus).doubleValue
+                    ))
                         .font(.footnote)
                         .font(.footnote)
                         .foregroundStyle(.secondary)
                         .foregroundStyle(.secondary)
                 }
                 }

+ 4 - 1
Trio Watch App Extension/Views/BolusProgressOverlay.swift

@@ -47,7 +47,10 @@ struct BolusProgressOverlay: View {
                     .tint(progressGradient)
                     .tint(progressGradient)
 
 
                 Text(String(
                 Text(String(
-                    format: "%.2f U of %.2f U",
+                    format: String(
+                        localized: "%.2f U of %.2f U",
+                        comment: "Format for showing delivered and active bolus amounts, 'x U of y U' on watch"
+                    ),
                     state.deliveredAmount,
                     state.deliveredAmount,
                     state.activeBolusAmount
                     state.activeBolusAmount
                 ))
                 ))

+ 3 - 2
Trio Watch App Extension/Views/CarbsInputView.swift

@@ -22,7 +22,8 @@ struct CarbsInputView: View {
     )
     )
 
 
     var body: some View {
     var body: some View {
-        let buttonLabel = continueToBolus ? "Proceed" : "Log Carbs"
+        let buttonLabel = continueToBolus ? String(localized: "Proceed", comment: "Button Label to Proceed to Bolus on Watch") :
+            String(localized: "Log Carbs", comment: "Button Label to Log Carbs on Watch")
 
 
         // TODO: introduce meal setting fpu enablement to conditional handle FPU
         // TODO: introduce meal setting fpu enablement to conditional handle FPU
         VStack {
         VStack {
@@ -45,7 +46,7 @@ struct CarbsInputView: View {
                 Spacer()
                 Spacer()
 
 
                 // Display the current carb amount
                 // Display the current carb amount
-                Text(String(format: "%.0f g", carbsAmount))
+                Text(String(format: "%.0f \(String(localized: "g", comment: "gram of carbs"))", carbsAmount))
                     .fontWeight(.bold)
                     .fontWeight(.bold)
                     .font(.system(.title2, design: .rounded))
                     .font(.system(.title2, design: .rounded))
                     .foregroundColor(carbsAmount > 0.0 && carbsAmount >= effectiveCarbsLimit ? .loopRed : .primary)
                     .foregroundColor(carbsAmount > 0.0 && carbsAmount >= effectiveCarbsLimit ? .loopRed : .primary)

+ 8 - 3
Trio Watch App Extension/Views/GlucoseTrendView.swift

@@ -148,9 +148,14 @@ struct GlucoseTrendView: View {
 
 
             Spacer()
             Spacer()
 
 
-            Text(isWatchStateDated ? "STALE DATA" : state.lastLoopTime ?? "--")
-                .font(.system(size: minutesAgoFontSize))
-                .fontWidth(isWatchStateDated ? .expanded : .standard)
+            Text(
+                isWatchStateDated ?
+                    String(localized: "STALE DATA", comment: "Information displayed when watch app data outdated or stale.") :
+                    state
+                    .lastLoopTime ?? "--"
+            )
+            .font(.system(size: minutesAgoFontSize))
+            .fontWidth(isWatchStateDated ? .expanded : .standard)
 
 
             Spacer()
             Spacer()
 
 

+ 3 - 3
Trio Watch App Extension/Views/TreatmentMenuView.swift

@@ -103,9 +103,9 @@ enum TreatmentOption: String, CaseIterable, Identifiable {
 
 
     var displayName: String {
     var displayName: String {
         switch self {
         switch self {
-        case .mealBolusCombo: return "Meal & Bolus"
-        case .meal: return "Meal"
-        case .bolus: return "Bolus"
+        case .mealBolusCombo: return String(localized: "Meal & Bolus", comment: "Watch App Treatment Option 'Meal & Bolus'")
+        case .meal: return String(localized: "Meal", comment: "Watch App Treatment Option 'Meal'")
+        case .bolus: return String(localized: "Bolus", comment: "Watch App Treatment Option 'Bolus'")
         }
         }
     }
     }
 }
 }

+ 33 - 13
Trio.xcodeproj/project.pbxproj

@@ -472,6 +472,9 @@
 		DD32CFA22CC824E2003686D6 /* TrioRemoteControl+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD32CFA12CC824E1003686D6 /* TrioRemoteControl+Helpers.swift */; };
 		DD32CFA22CC824E2003686D6 /* TrioRemoteControl+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD32CFA12CC824E1003686D6 /* TrioRemoteControl+Helpers.swift */; };
 		DD3A3CE72D29C93F00AE478E /* Helper+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3A3CE62D29C93F00AE478E /* Helper+Extensions.swift */; };
 		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 */; };
 		DD3A3CE92D29C97800AE478E /* Helper+ButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3A3CE82D29C97800AE478E /* Helper+ButtonStyles.swift */; };
+		DD498F2B2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
+		DD498F2C2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
+		DD498F2D2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
 		DD4FFF332D458EE600B6CFF9 /* GarminWatchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4FFF322D458EE600B6CFF9 /* GarminWatchState.swift */; };
 		DD4FFF332D458EE600B6CFF9 /* GarminWatchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4FFF322D458EE600B6CFF9 /* GarminWatchState.swift */; };
 		DD5DC9F12CF3D97C00AB8703 /* AdjustmentsStateModel+Overrides.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5DC9F02CF3D96E00AB8703 /* AdjustmentsStateModel+Overrides.swift */; };
 		DD5DC9F12CF3D97C00AB8703 /* AdjustmentsStateModel+Overrides.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5DC9F02CF3D96E00AB8703 /* AdjustmentsStateModel+Overrides.swift */; };
 		DD5DC9F32CF3D9DD00AB8703 /* AdjustmentsStateModel+TempTargets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5DC9F22CF3D9D600AB8703 /* AdjustmentsStateModel+TempTargets.swift */; };
 		DD5DC9F32CF3D9DD00AB8703 /* AdjustmentsStateModel+TempTargets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5DC9F22CF3D9D600AB8703 /* AdjustmentsStateModel+TempTargets.swift */; };
@@ -501,6 +504,7 @@
 		DDA6E3202D258E0500C2988C /* OverrideHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E31F2D258E0500C2988C /* OverrideHelpView.swift */; };
 		DDA6E3202D258E0500C2988C /* OverrideHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E31F2D258E0500C2988C /* OverrideHelpView.swift */; };
 		DDA6E3222D25901100C2988C /* TempTargetHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E3212D25901100C2988C /* TempTargetHelpView.swift */; };
 		DDA6E3222D25901100C2988C /* TempTargetHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E3212D25901100C2988C /* TempTargetHelpView.swift */; };
 		DDA6E3572D25988500C2988C /* ContactImageHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E3562D25988500C2988C /* ContactImageHelpView.swift */; };
 		DDA6E3572D25988500C2988C /* ContactImageHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E3562D25988500C2988C /* ContactImageHelpView.swift */; };
+		DDA9AC092D672CF100E6F1A9 /* AppVersionChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA9AC082D672CEB00E6F1A9 /* AppVersionChecker.swift */; };
 		DDAA29832D2D1D93006546A1 /* AdjustmentsRootView+Overrides.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAA29822D2D1D7B006546A1 /* AdjustmentsRootView+Overrides.swift */; };
 		DDAA29832D2D1D93006546A1 /* AdjustmentsRootView+Overrides.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAA29822D2D1D7B006546A1 /* AdjustmentsRootView+Overrides.swift */; };
 		DDAA29852D2D1D9E006546A1 /* AdjustmentsRootView+TempTargets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAA29842D2D1D98006546A1 /* AdjustmentsRootView+TempTargets.swift */; };
 		DDAA29852D2D1D9E006546A1 /* AdjustmentsRootView+TempTargets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAA29842D2D1D98006546A1 /* AdjustmentsRootView+TempTargets.swift */; };
 		DDB37CC52D05048F00D99BF4 /* ContactImageStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB37CC42D05048F00D99BF4 /* ContactImageStorage.swift */; };
 		DDB37CC52D05048F00D99BF4 /* ContactImageStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB37CC42D05048F00D99BF4 /* ContactImageStorage.swift */; };
@@ -1192,6 +1196,8 @@
 		DDA6E31F2D258E0500C2988C /* OverrideHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideHelpView.swift; sourceTree = "<group>"; };
 		DDA6E31F2D258E0500C2988C /* OverrideHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideHelpView.swift; sourceTree = "<group>"; };
 		DDA6E3212D25901100C2988C /* TempTargetHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetHelpView.swift; sourceTree = "<group>"; };
 		DDA6E3212D25901100C2988C /* TempTargetHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetHelpView.swift; sourceTree = "<group>"; };
 		DDA6E3562D25988500C2988C /* ContactImageHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactImageHelpView.swift; sourceTree = "<group>"; };
 		DDA6E3562D25988500C2988C /* ContactImageHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactImageHelpView.swift; sourceTree = "<group>"; };
+		DDA9AC082D672CEB00E6F1A9 /* AppVersionChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersionChecker.swift; sourceTree = "<group>"; };
+		DDA9AC0A2D678DAD00E6F1A9 /* blacklisted-versions.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "blacklisted-versions.json"; sourceTree = "<group>"; };
 		DDAA29822D2D1D7B006546A1 /* AdjustmentsRootView+Overrides.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentsRootView+Overrides.swift"; sourceTree = "<group>"; };
 		DDAA29822D2D1D7B006546A1 /* AdjustmentsRootView+Overrides.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentsRootView+Overrides.swift"; sourceTree = "<group>"; };
 		DDAA29842D2D1D98006546A1 /* AdjustmentsRootView+TempTargets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentsRootView+TempTargets.swift"; sourceTree = "<group>"; };
 		DDAA29842D2D1D98006546A1 /* AdjustmentsRootView+TempTargets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentsRootView+TempTargets.swift"; sourceTree = "<group>"; };
 		DDB37CC22D05044D00D99BF4 /* ContactTrickEntryStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContactTrickEntryStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		DDB37CC22D05044D00D99BF4 /* ContactTrickEntryStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContactTrickEntryStored+CoreDataClass.swift"; sourceTree = "<group>"; };
@@ -1721,6 +1727,7 @@
 		3811DE9125C9D88200A708ED /* Services */ = {
 		3811DE9125C9D88200A708ED /* Services */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				DDA9AC072D67291600E6F1A9 /* AppVersionChecker */,
 				BD7DB88C2D2C49FF003D3155 /* BolusCalculator */,
 				BD7DB88C2D2C49FF003D3155 /* BolusCalculator */,
 				3811DE9225C9D88200A708ED /* Appearance */,
 				3811DE9225C9D88200A708ED /* Appearance */,
 				CEB434E128B8F9BC00B70274 /* Bluetooth */,
 				CEB434E128B8F9BC00B70274 /* Bluetooth */,
@@ -1966,21 +1973,22 @@
 		388E594F25AD948C0019842D = {
 		388E594F25AD948C0019842D = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				DDA9AC0A2D678DAD00E6F1A9 /* blacklisted-versions.json */,
 				CE1F6DE62BAF1A180064EB8D /* BuildDetails.plist */,
 				CE1F6DE62BAF1A180064EB8D /* BuildDetails.plist */,
 				38F3783A2613555C009DB701 /* Config.xcconfig */,
 				38F3783A2613555C009DB701 /* Config.xcconfig */,
 				BD1CF8B72C1A4A8400CB930A /* ConfigOverride.xcconfig */,
 				BD1CF8B72C1A4A8400CB930A /* ConfigOverride.xcconfig */,
-				3818AA48274C267000843DB3 /* Frameworks */,
-				6B1A8D1C2B14D91600E76752 /* LiveActivity */,
-				587A54C82BCDCE0F009D38E2 /* Model */,
-				3818AA44274C229000843DB3 /* Packages */,
-				388E595925AD948C0019842D /* Products */,
-				192F0FF5276AC36D0085BE4D /* Recovered References */,
 				388E595A25AD948C0019842D /* Trio */,
 				388E595A25AD948C0019842D /* Trio */,
+				587A54C82BCDCE0F009D38E2 /* Model */,
 				38FCF3EE25E9028E0078B0D1 /* TrioTests */,
 				38FCF3EE25E9028E0078B0D1 /* TrioTests */,
+				6B1A8D1C2B14D91600E76752 /* LiveActivity */,
 				BDFF7AA12D25FAC70016C40C /* Trio Watch App */,
 				BDFF7AA12D25FAC70016C40C /* Trio Watch App */,
 				BDFF7A9C2D25FA730016C40C /* Trio Watch App Extension */,
 				BDFF7A9C2D25FA730016C40C /* Trio Watch App Extension */,
 				BDFF7AA02D25FAA80016C40C /* Trio Watch App Tests */,
 				BDFF7AA02D25FAA80016C40C /* Trio Watch App Tests */,
 				DD09D6492D2B6253000D82C9 /* Trio Watch Complication */,
 				DD09D6492D2B6253000D82C9 /* Trio Watch Complication */,
+				3818AA48274C267000843DB3 /* Frameworks */,
+				3818AA44274C229000843DB3 /* Packages */,
+				388E595925AD948C0019842D /* Products */,
+				192F0FF5276AC36D0085BE4D /* Recovered References */,
 			);
 			);
 			sourceTree = "<group>";
 			sourceTree = "<group>";
 		};
 		};
@@ -2354,11 +2362,11 @@
 		587A54C82BCDCE0F009D38E2 /* Model */ = {
 		587A54C82BCDCE0F009D38E2 /* Model */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
-				DDE179112C9100FA003CDDB7 /* Classes+Properties */,
 				BDF34F8F2C10CF8C00D51995 /* CoreDataStack.swift */,
 				BDF34F8F2C10CF8C00D51995 /* CoreDataStack.swift */,
-				5825D1622BD405AE00F36E9B /* Helper */,
-				DDD1631D2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodeld */,
 				BD4064D02C4ED26900582F43 /* CoreDataObserver.swift */,
 				BD4064D02C4ED26900582F43 /* CoreDataObserver.swift */,
+				DDD1631D2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodeld */,
+				DDE179112C9100FA003CDDB7 /* Classes+Properties */,
+				5825D1622BD405AE00F36E9B /* Helper */,
 			);
 			);
 			path = Model;
 			path = Model;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -2931,6 +2939,14 @@
 			path = View;
 			path = View;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
 		};
 		};
+		DDA9AC072D67291600E6F1A9 /* AppVersionChecker */ = {
+			isa = PBXGroup;
+			children = (
+				DDA9AC082D672CEB00E6F1A9 /* AppVersionChecker.swift */,
+			);
+			path = AppVersionChecker;
+			sourceTree = "<group>";
+		};
 		DDC9B9962CFD2332003E7721 /* Nightscout */ = {
 		DDC9B9962CFD2332003E7721 /* Nightscout */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
@@ -3288,8 +3304,9 @@
 		388E595025AD948C0019842D /* Project object */ = {
 		388E595025AD948C0019842D /* Project object */ = {
 			isa = PBXProject;
 			isa = PBXProject;
 			attributes = {
 			attributes = {
+				BuildIndependentTargetsInParallel = YES;
 				LastSwiftUpdateCheck = 1620;
 				LastSwiftUpdateCheck = 1620;
-				LastUpgradeCheck = 1240;
+				LastUpgradeCheck = 1620;
 				TargetAttributes = {
 				TargetAttributes = {
 					388E595725AD948C0019842D = {
 					388E595725AD948C0019842D = {
 						CreatedOnToolsVersion = 12.3;
 						CreatedOnToolsVersion = 12.3;
@@ -3391,6 +3408,7 @@
 			isa = PBXResourcesBuildPhase;
 			isa = PBXResourcesBuildPhase;
 			buildActionMask = 2147483647;
 			buildActionMask = 2147483647;
 			files = (
 			files = (
+				DD498F2B2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */,
 				6B1A8D242B14D91700E76752 /* Assets.xcassets in Resources */,
 				6B1A8D242B14D91700E76752 /* Assets.xcassets in Resources */,
 			);
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 			runOnlyForDeploymentPostprocessing = 0;
@@ -3399,6 +3417,7 @@
 			isa = PBXResourcesBuildPhase;
 			isa = PBXResourcesBuildPhase;
 			buildActionMask = 2147483647;
 			buildActionMask = 2147483647;
 			files = (
 			files = (
+				DD498F2D2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */,
 				DD09D6442D2B553A000D82C9 /* Assets.xcassets in Resources */,
 				DD09D6442D2B553A000D82C9 /* Assets.xcassets in Resources */,
 			);
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 			runOnlyForDeploymentPostprocessing = 0;
@@ -3408,6 +3427,7 @@
 			buildActionMask = 2147483647;
 			buildActionMask = 2147483647;
 			files = (
 			files = (
 				BDFF7A872D25F97D0016C40C /* Assets.xcassets in Resources */,
 				BDFF7A872D25F97D0016C40C /* Assets.xcassets in Resources */,
+				DD498F2C2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */,
 			);
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 			runOnlyForDeploymentPostprocessing = 0;
 		};
 		};
@@ -3423,6 +3443,7 @@
 /* Begin PBXShellScriptBuildPhase section */
 /* Begin PBXShellScriptBuildPhase section */
 		3811DEF525CA169200A708ED /* Swiftformat */ = {
 		3811DEF525CA169200A708ED /* Swiftformat */ = {
 			isa = PBXShellScriptBuildPhase;
 			isa = PBXShellScriptBuildPhase;
+			alwaysOutOfDate = 1;
 			buildActionMask = 12;
 			buildActionMask = 12;
 			files = (
 			files = (
 			);
 			);
@@ -3460,6 +3481,7 @@
 		};
 		};
 		DD88C8DF2C4D583900F2D558 /* Run Script: Capture Build Details */ = {
 		DD88C8DF2C4D583900F2D558 /* Run Script: Capture Build Details */ = {
 			isa = PBXShellScriptBuildPhase;
 			isa = PBXShellScriptBuildPhase;
+			alwaysOutOfDate = 1;
 			buildActionMask = 2147483647;
 			buildActionMask = 2147483647;
 			files = (
 			files = (
 			);
 			);
@@ -3621,6 +3643,7 @@
 				DDA6E3572D25988500C2988C /* ContactImageHelpView.swift in Sources */,
 				DDA6E3572D25988500C2988C /* ContactImageHelpView.swift in Sources */,
 				38FE826D25CC8461001FF17A /* NightscoutAPI.swift in Sources */,
 				38FE826D25CC8461001FF17A /* NightscoutAPI.swift in Sources */,
 				388358C825EEF6D200E024B2 /* BasalProfileEntry.swift in Sources */,
 				388358C825EEF6D200E024B2 /* BasalProfileEntry.swift in Sources */,
+				DDA9AC092D672CF100E6F1A9 /* AppVersionChecker.swift in Sources */,
 				3811DE0B25C9D32F00A708ED /* BaseView.swift in Sources */,
 				3811DE0B25C9D32F00A708ED /* BaseView.swift in Sources */,
 				3811DE3225C9D49500A708ED /* HomeDataFlow.swift in Sources */,
 				3811DE3225C9D49500A708ED /* HomeDataFlow.swift in Sources */,
 				DD32CFA22CC824E2003686D6 /* TrioRemoteControl+Helpers.swift in Sources */,
 				DD32CFA22CC824E2003686D6 /* TrioRemoteControl+Helpers.swift in Sources */,
@@ -3923,7 +3946,6 @@
 				195D80B92AF697F700D25097 /* DynamicSettingsProvider.swift in Sources */,
 				195D80B92AF697F700D25097 /* DynamicSettingsProvider.swift in Sources */,
 				DD09D47D2C5986DA003FEA5D /* CalendarEventSettingsProvider.swift in Sources */,
 				DD09D47D2C5986DA003FEA5D /* CalendarEventSettingsProvider.swift in Sources */,
 				DD09D47B2C5986D1003FEA5D /* CalendarEventSettingsDataFlow.swift in Sources */,
 				DD09D47B2C5986D1003FEA5D /* CalendarEventSettingsDataFlow.swift in Sources */,
-				6BCF84DD2B16843A003AD46E /* LiveActitiyAttributes.swift in Sources */,
 				DD1745202C55523E00211FAC /* SMBSettingsDataFlow.swift in Sources */,
 				DD1745202C55523E00211FAC /* SMBSettingsDataFlow.swift in Sources */,
 				D6D02515BBFBE64FEBE89856 /* DataTableRootView.swift in Sources */,
 				D6D02515BBFBE64FEBE89856 /* DataTableRootView.swift in Sources */,
 				DD1745292C55642100211FAC /* SettingInputSection.swift in Sources */,
 				DD1745292C55642100211FAC /* SettingInputSection.swift in Sources */,
@@ -3940,7 +3962,6 @@
 				B7C465E9472624D8A2BE2A6A /* (null) in Sources */,
 				B7C465E9472624D8A2BE2A6A /* (null) in Sources */,
 				71D44AAB2CA5F5EA0036EE9E /* AlertPermissionsChecker.swift in Sources */,
 				71D44AAB2CA5F5EA0036EE9E /* AlertPermissionsChecker.swift in Sources */,
 				320D030F724170A637F06D50 /* (null) in Sources */,
 				320D030F724170A637F06D50 /* (null) in Sources */,
-				CE94598729E9E4110047C9C6 /* WatchConfigRootView.swift in Sources */,
 				19E1F7E829D082D0005C8D20 /* IconConfigDataFlow.swift in Sources */,
 				19E1F7E829D082D0005C8D20 /* IconConfigDataFlow.swift in Sources */,
 				5A2325522BFCBF55003518CA /* NightscoutUploadView.swift in Sources */,
 				5A2325522BFCBF55003518CA /* NightscoutUploadView.swift in Sources */,
 				E3A08AAE59538BC8A8ABE477 /* GlucoseNotificationSettingsDataFlow.swift in Sources */,
 				E3A08AAE59538BC8A8ABE477 /* GlucoseNotificationSettingsDataFlow.swift in Sources */,
@@ -4006,7 +4027,6 @@
 			buildActionMask = 2147483647;
 			buildActionMask = 2147483647;
 			files = (
 			files = (
 				6BCF84DE2B16843A003AD46E /* LiveActitiyAttributes.swift in Sources */,
 				6BCF84DE2B16843A003AD46E /* LiveActitiyAttributes.swift in Sources */,
-				6BCF84DE2B16843A003AD46E /* LiveActitiyAttributes.swift in Sources */,
 				DDCEBF5B2CC1B76400DF4C36 /* LiveActivity+Helper.swift in Sources */,
 				DDCEBF5B2CC1B76400DF4C36 /* LiveActivity+Helper.swift in Sources */,
 				6B1A8D1E2B14D91600E76752 /* LiveActivityBundle.swift in Sources */,
 				6B1A8D1E2B14D91600E76752 /* LiveActivityBundle.swift in Sources */,
 				6B1A8D202B14D91600E76752 /* LiveActivity.swift in Sources */,
 				6B1A8D202B14D91600E76752 /* LiveActivity.swift in Sources */,

+ 1 - 1
Trio.xcodeproj/xcshareddata/xcschemes/Trio Watch App.xcscheme

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
 <Scheme
-   LastUpgradeVersion = "1600"
+   LastUpgradeVersion = "1620"
    version = "1.7">
    version = "1.7">
    <BuildAction
    <BuildAction
       parallelizeBuildables = "YES"
       parallelizeBuildables = "YES"

+ 1 - 1
Trio.xcodeproj/xcshareddata/xcschemes/Trio Watch Complication Extension.xcscheme

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
 <Scheme
-   LastUpgradeVersion = "1600"
+   LastUpgradeVersion = "1620"
    version = "2.0">
    version = "2.0">
    <BuildAction
    <BuildAction
       parallelizeBuildables = "YES"
       parallelizeBuildables = "YES"

+ 2 - 2
Trio.xcodeproj/xcshareddata/xcschemes/Trio.xcscheme

@@ -1,9 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
 <Scheme
-   LastUpgradeVersion = "1240"
+   LastUpgradeVersion = "1620"
    version = "1.3">
    version = "1.3">
    <BuildAction
    <BuildAction
-      parallelizeBuildables = "NO"
+      parallelizeBuildables = "YES"
       buildImplicitDependencies = "YES">
       buildImplicitDependencies = "YES">
       <BuildActionEntries>
       <BuildActionEntries>
          <BuildActionEntry
          <BuildActionEntry

+ 325 - 298
Trio/Sources/APS/APSManager.swift

@@ -20,8 +20,8 @@ protocol APSManager {
     var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> { get }
     var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> { get }
     var isManualTempBasal: Bool { get }
     var isManualTempBasal: Bool { get }
     func enactTempBasal(rate: Double, duration: TimeInterval) async
     func enactTempBasal(rate: Double, duration: TimeInterval) async
-    func determineBasal() async -> Bool
-    func determineBasalSync() async
+    func determineBasal() async throws
+    func determineBasalSync() async throws
     func simulateDetermineBasal(simulatedCarbsAmount: Decimal, simulatedBolusAmount: Decimal) async -> Determination?
     func simulateDetermineBasal(simulatedCarbsAmount: Decimal, simulatedBolusAmount: Decimal) async -> Determination?
     func roundBolus(amount: Decimal) -> Decimal
     func roundBolus(amount: Decimal) -> Decimal
     var lastError: CurrentValueSubject<Error?, Never> { get }
     var lastError: CurrentValueSubject<Error?, Never> { get }
@@ -130,7 +130,14 @@ final class BaseAPSManager: APSManager, Injectable {
             let wasParsed = storage.parseOnFileSettingsToMgdL()
             let wasParsed = storage.parseOnFileSettingsToMgdL()
             if wasParsed {
             if wasParsed {
                 Task {
                 Task {
-                    await openAPS.createProfiles()
+                    do {
+                        try await openAPS.createProfiles()
+                    } catch {
+                        debug(
+                            .apsManager,
+                            "\(DebuggingIdentifiers.failed) Error creating profiles: \(error.localizedDescription)"
+                        )
+                    }
                 }
                 }
             }
             }
         }
         }
@@ -184,106 +191,134 @@ final class BaseAPSManager: APSManager, Injectable {
 
 
     // Loop entry point
     // Loop entry point
     private func loop() {
     private func loop() {
-        Task {
-            // check the last start of looping is more the loopInterval but the previous loop was completed
-            if lastLoopDate > lastLoopStartDate {
-                guard lastLoopStartDate.addingTimeInterval(Config.loopInterval) < Date() else {
-                    debug(.apsManager, "too close to do a loop : \(lastLoopStartDate)")
-                    return
+        Task { [weak self] in
+            guard let self else { return }
+
+            // Check if we can start a new loop
+            guard await self.canStartNewLoop() else { return }
+
+            // Setup loop and background task
+            var (loopStatRecord, backgroundTask) = await self.setupLoop()
+
+            do {
+                // Execute loop logic
+                try await self.executeLoop(loopStatRecord: &loopStatRecord)
+
+                // Upload data to Nightscout if available
+                if let nightscoutManager = self.nightscout {
+                    await nightscoutManager.uploadCarbs()
+                    await nightscoutManager.uploadPumpHistory()
+                    await nightscoutManager.uploadOverrides()
+                    await nightscoutManager.uploadTempTargets()
                 }
                 }
+            } catch {
+                var updatedStats = loopStatRecord
+                updatedStats.end = Date()
+                updatedStats.duration = roundDouble((updatedStats.end! - updatedStats.start).timeInterval / 60, 2)
+                updatedStats.loopStatus = error.localizedDescription
+                await loopCompleted(error: error, loopStatRecord: updatedStats)
+                debug(.apsManager, "\(DebuggingIdentifiers.failed) Failed to complete Loop: \(error.localizedDescription)")
             }
             }
 
 
-            guard !isLooping.value else {
-                warning(.apsManager, "Loop already in progress. Skip recommendation.")
-                return
+            // Cleanup background task
+            if let backgroundTask = backgroundTask {
+                await UIApplication.shared.endBackgroundTask(backgroundTask)
+                self.backGroundTaskID = .invalid
             }
             }
+        }
+    }
 
 
-            // start background time extension
-            backGroundTaskID = await UIApplication.shared.beginBackgroundTask(withName: "Loop starting") {
-                guard let backgroundTask = self.backGroundTaskID else { return }
-                Task {
-                    UIApplication.shared.endBackgroundTask(backgroundTask)
-                }
-                self.backGroundTaskID = .invalid
+    private func canStartNewLoop() async -> Bool {
+        // Check if too soon for next loop
+        if lastLoopDate > lastLoopStartDate {
+            guard lastLoopStartDate.addingTimeInterval(Config.loopInterval) < Date() else {
+                debug(.apsManager, "Not enough time have passed since last loop at : \(lastLoopStartDate)")
+                return false
             }
             }
+        }
 
 
-            lastLoopStartDate = Date()
+        // Check if loop already in progress
+        guard !isLooping.value else {
+            warning(.apsManager, "Loop already in progress. Skip recommendation.")
+            return false
+        }
 
 
-            var previousLoop = [LoopStatRecord]()
-            var interval: Double?
+        return true
+    }
 
 
-            do {
-                try await privateContext.perform {
-                    let requestStats = LoopStatRecord.fetchRequest() as NSFetchRequest<LoopStatRecord>
-                    let sortStats = NSSortDescriptor(key: "end", ascending: false)
-                    requestStats.sortDescriptors = [sortStats]
-                    requestStats.fetchLimit = 1
-                    previousLoop = try self.privateContext.fetch(requestStats)
-
-                    if (previousLoop.first?.end ?? .distantFuture) < self.lastLoopStartDate {
-                        interval = self.roundDouble(
-                            (self.lastLoopStartDate - (previousLoop.first?.end ?? Date())).timeInterval / 60,
-                            1
-                        )
-                    }
-                }
-            } catch let error as NSError {
-                debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch the last loop with error: \(error.userInfo)"
-                )
+    private func setupLoop() async -> (LoopStats, UIBackgroundTaskIdentifier?) {
+        // Start background task
+        let backgroundTask = await UIApplication.shared.beginBackgroundTask(withName: "Loop starting") { [weak self] in
+            guard let self, let backgroundTask = self.backGroundTaskID else { return }
+            Task {
+                UIApplication.shared.endBackgroundTask(backgroundTask)
             }
             }
+            self.backGroundTaskID = .invalid
+        }
+        backGroundTaskID = backgroundTask
 
 
-            var loopStatRecord = LoopStats(
-                start: lastLoopStartDate,
-                loopStatus: "Starting",
-                interval: interval
-            )
+        // Set loop start time
+        lastLoopStartDate = Date()
 
 
-            isLooping.send(true)
+        // Calculate interval from previous loop
+        let interval = await calculateLoopInterval()
 
 
-            do {
-                if await !determineBasal() {
-                    throw APSError.apsError(message: "Determine basal failed")
-                }
+        // Create initial loop stats record
+        let loopStatRecord = LoopStats(
+            start: lastLoopStartDate,
+            loopStatus: "Starting",
+            interval: interval
+        )
 
 
-                // Open loop completed
-                guard settings.closedLoop else {
-                    loopStatRecord.end = Date()
-                    loopStatRecord.duration = roundDouble((loopStatRecord.end! - loopStatRecord.start).timeInterval / 60, 2)
-                    loopStatRecord.loopStatus = "Success"
-                    await loopCompleted(loopStatRecord: loopStatRecord)
-                    return
-                }
+        isLooping.send(true)
 
 
-                // Closed loop - enact Determination
-                try await enactDetermination()
-                loopStatRecord.end = Date()
-                loopStatRecord.duration = roundDouble((loopStatRecord.end! - loopStatRecord.start).timeInterval / 60, 2)
-                loopStatRecord.loopStatus = "Success"
-                await loopCompleted(loopStatRecord: loopStatRecord)
-            } catch {
-                loopStatRecord.end = Date()
-                loopStatRecord.duration = roundDouble((loopStatRecord.end! - loopStatRecord.start).timeInterval / 60, 2)
-                loopStatRecord.loopStatus = error.localizedDescription
-                await loopCompleted(error: error, loopStatRecord: loopStatRecord)
-            }
+        return (loopStatRecord, backgroundTask)
+    }
 
 
-            if let nightscoutManager = nightscout {
-                await nightscoutManager.uploadCarbs()
-                await nightscoutManager.uploadPumpHistory()
-                await nightscoutManager.uploadOverrides()
-                await nightscoutManager.uploadTempTargets()
-            }
+    private func executeLoop(loopStatRecord: inout LoopStats) async throws {
+        try await determineBasal()
 
 
-            // End background task after all the operations are completed
-            if let backgroundTask = self.backGroundTaskID {
-                await UIApplication.shared.endBackgroundTask(backgroundTask)
-                self.backGroundTaskID = .invalid
+        // Handle open loop
+        guard settings.closedLoop else {
+            loopStatRecord.end = Date()
+            loopStatRecord.duration = roundDouble((loopStatRecord.end! - loopStatRecord.start).timeInterval / 60, 2)
+            loopStatRecord.loopStatus = "Success"
+            await loopCompleted(loopStatRecord: loopStatRecord)
+            return
+        }
+
+        // Handle closed loop
+        try await enactDetermination()
+        loopStatRecord.end = Date()
+        loopStatRecord.duration = roundDouble((loopStatRecord.end! - loopStatRecord.start).timeInterval / 60, 2)
+        loopStatRecord.loopStatus = "Success"
+        await loopCompleted(loopStatRecord: loopStatRecord)
+    }
+
+    private func calculateLoopInterval() async -> Double? {
+        do {
+            return try await privateContext.perform {
+                let requestStats = LoopStatRecord.fetchRequest() as NSFetchRequest<LoopStatRecord>
+                let sortStats = NSSortDescriptor(key: "end", ascending: false)
+                requestStats.sortDescriptors = [sortStats]
+                requestStats.fetchLimit = 1
+                let previousLoop = try self.privateContext.fetch(requestStats)
+
+                if (previousLoop.first?.end ?? .distantFuture) < self.lastLoopStartDate {
+                    return self.roundDouble(
+                        (self.lastLoopStartDate - (previousLoop.first?.end ?? Date())).timeInterval / 60,
+                        1
+                    )
+                }
+                return nil
             }
             }
+        } catch {
+            debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch the last loop with error: \(error)")
+            return nil
         }
         }
     }
     }
 
 
-//     Loop exit point
+    // Loop exit point
     private func loopCompleted(error: Error? = nil, loopStatRecord: LoopStats) async {
     private func loopCompleted(error: Error? = nil, loopStatRecord: LoopStats) async {
         isLooping.send(false)
         isLooping.send(false)
 
 
@@ -346,14 +381,16 @@ final class BaseAPSManager: APSManager, Injectable {
         return false
         return false
     }
     }
 
 
-    func determineBasal() async -> Bool {
+    func determineBasal() async throws {
         debug(.apsManager, "Start determine basal")
         debug(.apsManager, "Start determine basal")
 
 
         // Fetch glucose asynchronously
         // Fetch glucose asynchronously
-        let glucose = await fetchGlucose(predicate: NSPredicate.predicateForOneHourAgo, fetchLimit: 6)
+        let glucose = try await fetchGlucose(predicate: NSPredicate.predicateForOneHourAgo, fetchLimit: 6)
 
 
         // Perform the context-related checks and actions
         // Perform the context-related checks and actions
-        let isValidGlucoseData = await privateContext.perform {
+        let isValidGlucoseData = await privateContext.perform { [weak self] in
+            guard let self else { return false }
+
             guard glucose.count > 2 else {
             guard glucose.count > 2 else {
                 debug(.apsManager, "Not enough glucose data")
                 debug(.apsManager, "Not enough glucose data")
                 self.processError(APSError.glucoseError(message: "Not enough glucose data"))
                 self.processError(APSError.glucoseError(message: "Not enough glucose data"))
@@ -379,7 +416,7 @@ final class BaseAPSManager: APSManager, Injectable {
         guard isValidGlucoseData else {
         guard isValidGlucoseData else {
             debug(.apsManager, "Glucose validation failed")
             debug(.apsManager, "Glucose validation failed")
             processError(APSError.glucoseError(message: "Glucose validation failed"))
             processError(APSError.glucoseError(message: "Glucose validation failed"))
-            return false
+            return
         }
         }
 
 
         do {
         do {
@@ -390,32 +427,30 @@ final class BaseAPSManager: APSManager, Injectable {
             async let autosenseResult = autosense()
             async let autosenseResult = autosense()
 
 
             _ = try await autosenseResult
             _ = try await autosenseResult
-            await openAPS.createProfiles()
+            try await openAPS.createProfiles()
             let determination = try await openAPS.determineBasal(currentTemp: await currentTemp, clock: now)
             let determination = try await openAPS.determineBasal(currentTemp: await currentTemp, clock: now)
 
 
             if let determination = determination {
             if let determination = determination {
-                DispatchQueue.main.async {
+                // Capture weak self in closure
+                await MainActor.run { [weak self] in
+                    guard let self else { return }
                     self.broadcaster.notify(DeterminationObserver.self, on: .main) {
                     self.broadcaster.notify(DeterminationObserver.self, on: .main) {
                         $0.determinationDidUpdate(determination)
                         $0.determinationDidUpdate(determination)
                     }
                     }
                 }
                 }
-                return true
-            } else {
-                return false
             }
             }
         } catch {
         } catch {
-            debug(.apsManager, "Error determining basal: \(error)")
-            return false
+            throw APSError.apsError(message: "Error determining basal: \(error.localizedDescription)")
         }
         }
     }
     }
 
 
-    func determineBasalSync() async {
-        _ = await determineBasal()
+    func determineBasalSync() async throws {
+        _ = try await determineBasal()
     }
     }
 
 
     func simulateDetermineBasal(simulatedCarbsAmount: Decimal, simulatedBolusAmount: Decimal) async -> Determination? {
     func simulateDetermineBasal(simulatedCarbsAmount: Decimal, simulatedBolusAmount: Decimal) async -> Determination? {
         do {
         do {
-            let temp = await fetchCurrentTempBasal(date: Date.now)
+            let temp = try await fetchCurrentTempBasal(date: Date.now)
             return try await openAPS.determineBasal(
             return try await openAPS.determineBasal(
                 currentTemp: temp,
                 currentTemp: temp,
                 clock: Date(),
                 clock: Date(),
@@ -447,12 +482,14 @@ final class BaseAPSManager: APSManager, Injectable {
 
 
         if let error = verifyStatus() {
         if let error = verifyStatus() {
             processError(error)
             processError(error)
-            processQueue.async {
-                self.broadcaster.notify(BolusFailureObserver.self, on: self.processQueue) {
+            // Capture broadcaster and queue before async context
+            let broadcaster = self.broadcaster
+            Task { @MainActor in
+                broadcaster?.notify(BolusFailureObserver.self, on: .main) {
                     $0.bolusDidFail()
                     $0.bolusDidFail()
                 }
                 }
             }
             }
-            callback?(false, "Error! Failed to enact bolus.")
+            callback?(false, String(localized: "Error! Failed to enact bolus.", comment: "Error message for enacting a bolus"))
             return
             return
         }
         }
 
 
@@ -466,21 +503,26 @@ final class BaseAPSManager: APSManager, Injectable {
             try await pump.enactBolus(units: roundedAmount, automatic: isSMB)
             try await pump.enactBolus(units: roundedAmount, automatic: isSMB)
             debug(.apsManager, "Bolus succeeded")
             debug(.apsManager, "Bolus succeeded")
             if !isSMB {
             if !isSMB {
-                await determineBasalSync()
+                try await determineBasalSync()
             }
             }
             bolusProgress.send(0)
             bolusProgress.send(0)
-            callback?(true, "Bolus enacted successfully.")
+            callback?(true, String(localized: "Bolus enacted successfully.", comment: "Success message for enacting a bolus"))
         } catch {
         } catch {
             warning(.apsManager, "Bolus failed with error: \(error.localizedDescription)")
             warning(.apsManager, "Bolus failed with error: \(error.localizedDescription)")
             processError(APSError.pumpError(error))
             processError(APSError.pumpError(error))
             if !isSMB {
             if !isSMB {
-                processQueue.async {
-                    self.broadcaster.notify(BolusFailureObserver.self, on: self.processQueue) {
+                // Use MainActor to handle broadcaster notification
+                let broadcaster = self.broadcaster
+                Task { @MainActor in
+                    broadcaster?.notify(BolusFailureObserver.self, on: .main) {
                         $0.bolusDidFail()
                         $0.bolusDidFail()
                     }
                     }
                 }
                 }
             }
             }
-            callback?(false, "Error! Failed to enact bolus.")
+            callback?(
+                false,
+                String(localized: "Error! Failed to enact bolus.", comment: "Error message for failing to enact a bolus")
+            )
         }
         }
     }
     }
 
 
@@ -490,11 +532,14 @@ final class BaseAPSManager: APSManager, Injectable {
         do {
         do {
             _ = try await pump.cancelBolus()
             _ = try await pump.cancelBolus()
             debug(.apsManager, "Bolus cancelled")
             debug(.apsManager, "Bolus cancelled")
-            callback?(true, "Bolus cancelled successfully.")
+            callback?(true, String(localized: "Bolus cancelled successfully.", comment: "Success message for canceling a bolus"))
         } catch {
         } catch {
             debug(.apsManager, "Bolus cancellation failed with error: \(error.localizedDescription)")
             debug(.apsManager, "Bolus cancellation failed with error: \(error.localizedDescription)")
             processError(APSError.pumpError(error))
             processError(APSError.pumpError(error))
-            callback?(false, "Error! Bolus cancellation failed.")
+            callback?(
+                false,
+                String(localized: "Error! Bolus cancellation failed.", comment: "Error message for canceling a bolus")
+            )
         }
         }
         bolusReporter?.removeObserver(self)
         bolusReporter?.removeObserver(self)
         bolusReporter = nil
         bolusReporter = nil
@@ -528,8 +573,8 @@ final class BaseAPSManager: APSManager, Injectable {
         }
         }
     }
     }
 
 
-    private func fetchCurrentTempBasal(date: Date) async -> TempBasal {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchCurrentTempBasal(date: Date) async throws -> TempBasal {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: PumpEventStored.self,
             ofType: PumpEventStored.self,
             onContext: privateContext,
             onContext: privateContext,
             predicate: NSPredicate.recentPumpHistory,
             predicate: NSPredicate.recentPumpHistory,
@@ -568,7 +613,7 @@ final class BaseAPSManager: APSManager, Injectable {
     }
     }
 
 
     private func enactDetermination() async throws {
     private func enactDetermination() async throws {
-        guard let determinationID = await determinationStorage
+        guard let determinationID = try await determinationStorage
             .fetchLastDeterminationObjectID(predicate: NSPredicate.predicateFor30MinAgoForDetermination).first
             .fetchLastDeterminationObjectID(predicate: NSPredicate.predicateFor30MinAgoForDetermination).first
         else {
         else {
             throw APSError.apsError(message: "Determination not found")
             throw APSError.apsError(message: "Determination not found")
@@ -623,35 +668,39 @@ final class BaseAPSManager: APSManager, Injectable {
     }
     }
 
 
     private func reportEnacted(wasEnacted: Bool) async {
     private func reportEnacted(wasEnacted: Bool) async {
-        guard let determinationID = await determinationStorage
-            .fetchLastDeterminationObjectID(predicate: NSPredicate.predicateFor30MinAgoForDetermination).first
-        else {
-            return
-        }
-        await privateContext.perform {
-            if let determinationUpdated = self.privateContext.object(with: determinationID) as? OrefDetermination {
+        do {
+            guard let determinationID = try await determinationStorage
+                .fetchLastDeterminationObjectID(predicate: NSPredicate.predicateFor30MinAgoForDetermination).first
+            else {
+                debug(.apsManager, "No determination found to report enacted status")
+                return
+            }
+
+            try await privateContext.perform {
+                guard let determinationUpdated = try self.privateContext
+                    .existingObject(with: determinationID) as? OrefDetermination
+                else {
+                    debug(.apsManager, "Could not find determination object in context")
+                    return
+                }
+
                 determinationUpdated.timestamp = Date()
                 determinationUpdated.timestamp = Date()
                 determinationUpdated.enacted = wasEnacted
                 determinationUpdated.enacted = wasEnacted
                 determinationUpdated.isUploadedToNS = false
                 determinationUpdated.isUploadedToNS = false
 
 
-                do {
-                    guard self.privateContext.hasChanges else { return }
-                    try self.privateContext.save()
-                    debugPrint("Update successful in reportEnacted() \(DebuggingIdentifiers.succeeded)")
-                } catch {
-                    debugPrint(
-                        "Failed  \(DebuggingIdentifiers.succeeded) to save context in reportEnacted(): \(error.localizedDescription)"
-                    )
-                }
-
+                guard self.privateContext.hasChanges else { return }
+                try self.privateContext.save()
                 debug(.apsManager, "Determination enacted. Enacted: \(wasEnacted)")
                 debug(.apsManager, "Determination enacted. Enacted: \(wasEnacted)")
 
 
                 Task.detached(priority: .low) {
                 Task.detached(priority: .low) {
                     await self.statistics()
                     await self.statistics()
                 }
                 }
-            } else {
-                debugPrint("Failed to update OrefDetermination in reportEnacted()")
             }
             }
+        } catch {
+            debug(
+                .apsManager,
+                "\(DebuggingIdentifiers.failed) Error reporting enacted status: \(error.localizedDescription)"
+            )
         }
         }
     }
     }
 
 
@@ -811,8 +860,8 @@ final class BaseAPSManager: APSManager, Injectable {
     }
     }
 
 
     // fetch glucose for time interval
     // fetch glucose for time interval
-    func fetchGlucose(predicate: NSPredicate, fetchLimit: Int? = nil, batchSize: Int? = nil) async -> [GlucoseStored] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func fetchGlucose(predicate: NSPredicate, fetchLimit: Int? = nil, batchSize: Int? = nil) async throws -> [GlucoseStored] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             ofType: GlucoseStored.self,
             onContext: privateContext,
             onContext: privateContext,
             predicate: predicate,
             predicate: predicate,
@@ -822,9 +871,9 @@ final class BaseAPSManager: APSManager, Injectable {
             batchSize: batchSize
             batchSize: batchSize
         )
         )
 
 
-        return await privateContext.perform {
+        return try await privateContext.perform {
             guard let glucoseResults = results as? [GlucoseStored] else {
             guard let glucoseResults = results as? [GlucoseStored] else {
-                return []
+                throw CoreDataError.fetchError(function: #function, file: #file)
             }
             }
 
 
             return glucoseResults
             return glucoseResults
@@ -849,7 +898,7 @@ final class BaseAPSManager: APSManager, Injectable {
             async let carbTotal = carbsForStats()
             async let carbTotal = carbsForStats()
             async let preferences = settingsManager.preferences
             async let preferences = settingsManager.preferences
 
 
-            let loopStats = await loopStats(oneDayGlucose: await glucoseStats.oneDayGlucose.readings)
+            let loopStats = await loopStats(oneDayGlucose: Double(rawValue: (await glucoseStats?.oneDayGlucose.readings)!) ?? 0.0)
 
 
             // Only save and upload once per day
             // Only save and upload once per day
             guard (-1 * (await lastLoopForStats ?? .distantPast).timeIntervalSinceNow.hours) > 22 else { return }
             guard (-1 * (await lastLoopForStats ?? .distantPast).timeIntervalSinceNow.hours) > 22 else { return }
@@ -899,7 +948,7 @@ final class BaseAPSManager: APSManager, Injectable {
                 scheduled_basal: 0,
                 scheduled_basal: 0,
                 total_average: 0
                 total_average: 0
             )
             )
-            let processedGlucoseStats = await glucoseStats
+            guard let processedGlucoseStats = await glucoseStats else { return }
             let hbA1cDisplayUnit = processedGlucoseStats.hbA1cDisplayUnit
             let hbA1cDisplayUnit = processedGlucoseStats.hbA1cDisplayUnit
 
 
             let dailystat = await Statistics(
             let dailystat = await Statistics(
@@ -918,7 +967,7 @@ final class BaseAPSManager: APSManager, Injectable {
                 insulinType: insulin_type.rawValue,
                 insulinType: insulin_type.rawValue,
                 peakActivityTime: iPa,
                 peakActivityTime: iPa,
                 Carbs_24h: await carbTotal,
                 Carbs_24h: await carbTotal,
-                GlucoseStorage_Days: Decimal(roundDouble(processedGlucoseStats.numberofDays, 1)),
+                GlucoseStorage_Days: Decimal(roundDouble(Double(rawValue: processedGlucoseStats.numberofDays) ?? 0.0, 1)),
                 Statistics: Stats(
                 Statistics: Stats(
                     Distribution: processedGlucoseStats.TimeInRange,
                     Distribution: processedGlucoseStats.TimeInRange,
                     Glucose: processedGlucoseStats.avg,
                     Glucose: processedGlucoseStats.avg,
@@ -1078,175 +1127,153 @@ final class BaseAPSManager: APSManager, Injectable {
         return (currentTDD, tddTotalAverage)
         return (currentTDD, tddTotalAverage)
     }
     }
 
 
-    private func glucoseForStats() async
-        -> (
-            oneDayGlucose: (
-                ifcc: Double,
-                ngsp: Double,
-                average: Double,
-                median: Double,
-                sd: Double,
-                cv: Double,
-                readings: Double
-            ),
-            hbA1cDisplayUnit: HbA1cDisplayUnit,
-            numberofDays: Double,
-            TimeInRange: TIRs,
-            avg: Averages,
-            hbs: Durations,
-            variance: Variance
-        )
-    {
-        // Get the Glucose Values
-        let glucose24h = await fetchGlucose(predicate: NSPredicate.predicateForOneDayAgo, fetchLimit: 288, batchSize: 50)
-        let glucoseOneWeek = await fetchGlucose(
-            predicate: NSPredicate.predicateForOneWeek,
-            fetchLimit: 288 * 7,
-            batchSize: 250
-        )
-        let glucoseOneMonth = await fetchGlucose(
-            predicate: NSPredicate.predicateForOneMonth,
-            fetchLimit: 288 * 7 * 30,
-            batchSize: 500
-        )
-        let glucoseThreeMonths = await fetchGlucose(
-            predicate: NSPredicate.predicateForThreeMonths,
-            fetchLimit: 288 * 7 * 30 * 3,
-            batchSize: 1000
-        )
+    private func glucoseForStats() async -> (
+        oneDayGlucose: (ifcc: Double, ngsp: Double, average: Double, median: Double, sd: Double, cv: Double, readings: Double),
+        hbA1cDisplayUnit: HbA1cDisplayUnit,
+        numberofDays: Double,
+        TimeInRange: TIRs,
+        avg: Averages,
+        hbs: Durations,
+        variance: Variance
+    )? {
+        do {
+            // Get the Glucose Values
+            let glucose24h = try await fetchGlucose(predicate: NSPredicate.predicateForOneDayAgo, fetchLimit: 288, batchSize: 50)
+            let glucoseOneWeek = try await fetchGlucose(
+                predicate: NSPredicate.predicateForOneWeek,
+                fetchLimit: 288 * 7,
+                batchSize: 250
+            )
+            let glucoseOneMonth = try await fetchGlucose(
+                predicate: NSPredicate.predicateForOneMonth,
+                fetchLimit: 288 * 7 * 30,
+                batchSize: 500
+            )
+            let glucoseThreeMonths = try await fetchGlucose(
+                predicate: NSPredicate.predicateForThreeMonths,
+                fetchLimit: 288 * 7 * 30 * 3,
+                batchSize: 1000
+            )
 
 
-        var result: (
-            oneDayGlucose: (
-                ifcc: Double,
-                ngsp: Double,
-                average: Double,
-                median: Double,
-                sd: Double,
-                cv: Double,
-                readings: Double
-            ),
-            hbA1cDisplayUnit: HbA1cDisplayUnit,
-            numberofDays: Double,
-            TimeInRange: TIRs,
-            avg: Averages,
-            hbs: Durations,
-            variance: Variance
-        )?
+            return await privateContext.perform {
+                let units = self.settingsManager.settings.units
+
+                // First date
+                let previous = glucoseThreeMonths.last?.date ?? Date()
+                // Last date (recent)
+                let current = glucoseThreeMonths.first?.date ?? Date()
+                // Total time in days
+                let numberOfDays = (current - previous).timeInterval / 8.64E4
+
+                // Get glucose computations for every case
+                let oneDayGlucose = self.glucoseStats(glucose24h)
+                let sevenDaysGlucose = self.glucoseStats(glucoseOneWeek)
+                let thirtyDaysGlucose = self.glucoseStats(glucoseOneMonth)
+                let totalDaysGlucose = self.glucoseStats(glucoseThreeMonths)
+
+                let median = Durations(
+                    day: self.roundDecimal(Decimal(oneDayGlucose.median), 1),
+                    week: self.roundDecimal(Decimal(sevenDaysGlucose.median), 1),
+                    month: self.roundDecimal(Decimal(thirtyDaysGlucose.median), 1),
+                    total: self.roundDecimal(Decimal(totalDaysGlucose.median), 1)
+                )
 
 
-        await privateContext.perform {
-            let units = self.settingsManager.settings.units
-
-            // First date
-            let previous = glucoseThreeMonths.last?.date ?? Date()
-            // Last date (recent)
-            let current = glucoseThreeMonths.first?.date ?? Date()
-            // Total time in days
-            let numberOfDays = (current - previous).timeInterval / 8.64E4
-
-            // Get glucose computations for every case
-            let oneDayGlucose = self.glucoseStats(glucose24h)
-            let sevenDaysGlucose = self.glucoseStats(glucoseOneWeek)
-            let thirtyDaysGlucose = self.glucoseStats(glucoseOneMonth)
-            let totalDaysGlucose = self.glucoseStats(glucoseThreeMonths)
-
-            let median = Durations(
-                day: self.roundDecimal(Decimal(oneDayGlucose.median), 1),
-                week: self.roundDecimal(Decimal(sevenDaysGlucose.median), 1),
-                month: self.roundDecimal(Decimal(thirtyDaysGlucose.median), 1),
-                total: self.roundDecimal(Decimal(totalDaysGlucose.median), 1)
-            )
+                let hbA1cDisplayUnit = self.settingsManager.settings.hbA1cDisplayUnit
+
+                let hbs = Durations(
+                    day: hbA1cDisplayUnit == .mmolMol ?
+                        self.roundDecimal(Decimal(oneDayGlucose.ifcc), 1) :
+                        self.roundDecimal(Decimal(oneDayGlucose.ngsp), 1),
+                    week: hbA1cDisplayUnit == .mmolMol ?
+                        self.roundDecimal(Decimal(sevenDaysGlucose.ifcc), 1) :
+                        self.roundDecimal(Decimal(sevenDaysGlucose.ngsp), 1),
+                    month: hbA1cDisplayUnit == .mmolMol ?
+                        self.roundDecimal(Decimal(thirtyDaysGlucose.ifcc), 1) :
+                        self.roundDecimal(Decimal(thirtyDaysGlucose.ngsp), 1),
+                    total: hbA1cDisplayUnit == .mmolMol ?
+                        self.roundDecimal(Decimal(totalDaysGlucose.ifcc), 1) :
+                        self.roundDecimal(Decimal(totalDaysGlucose.ngsp), 1)
+                )
 
 
-            let hbA1cDisplayUnit = self.settingsManager.settings.hbA1cDisplayUnit
-
-            let hbs = Durations(
-                day: hbA1cDisplayUnit == .mmolMol ?
-                    self.roundDecimal(Decimal(oneDayGlucose.ifcc), 1) :
-                    self.roundDecimal(Decimal(oneDayGlucose.ngsp), 1),
-                week: hbA1cDisplayUnit == .mmolMol ?
-                    self.roundDecimal(Decimal(sevenDaysGlucose.ifcc), 1) :
-                    self.roundDecimal(Decimal(sevenDaysGlucose.ngsp), 1),
-                month: hbA1cDisplayUnit == .mmolMol ?
-                    self.roundDecimal(Decimal(thirtyDaysGlucose.ifcc), 1) :
-                    self.roundDecimal(Decimal(thirtyDaysGlucose.ngsp), 1),
-                total: hbA1cDisplayUnit == .mmolMol ?
-                    self.roundDecimal(Decimal(totalDaysGlucose.ifcc), 1) :
-                    self.roundDecimal(Decimal(totalDaysGlucose.ngsp), 1)
-            )
+                var oneDay_: (TIR: Double, hypos: Double, hypers: Double, normal_: Double) = (0.0, 0.0, 0.0, 0.0)
+                var sevenDays_: (TIR: Double, hypos: Double, hypers: Double, normal_: Double) = (0.0, 0.0, 0.0, 0.0)
+                var thirtyDays_: (TIR: Double, hypos: Double, hypers: Double, normal_: Double) = (0.0, 0.0, 0.0, 0.0)
+                var totalDays_: (TIR: Double, hypos: Double, hypers: Double, normal_: Double) = (0.0, 0.0, 0.0, 0.0)
+                // Get TIR computations for every case
+                oneDay_ = self.tir(glucose24h)
+                sevenDays_ = self.tir(glucoseOneWeek)
+                thirtyDays_ = self.tir(glucoseOneMonth)
+                totalDays_ = self.tir(glucoseThreeMonths)
+
+                let tir = Durations(
+                    day: self.roundDecimal(Decimal(oneDay_.TIR), 1),
+                    week: self.roundDecimal(Decimal(sevenDays_.TIR), 1),
+                    month: self.roundDecimal(Decimal(thirtyDays_.TIR), 1),
+                    total: self.roundDecimal(Decimal(totalDays_.TIR), 1)
+                )
+                let hypo = Durations(
+                    day: Decimal(oneDay_.hypos),
+                    week: Decimal(sevenDays_.hypos),
+                    month: Decimal(thirtyDays_.hypos),
+                    total: Decimal(totalDays_.hypos)
+                )
+                let hyper = Durations(
+                    day: Decimal(oneDay_.hypers),
+                    week: Decimal(sevenDays_.hypers),
+                    month: Decimal(thirtyDays_.hypers),
+                    total: Decimal(totalDays_.hypers)
+                )
+                let normal = Durations(
+                    day: Decimal(oneDay_.normal_),
+                    week: Decimal(sevenDays_.normal_),
+                    month: Decimal(thirtyDays_.normal_),
+                    total: Decimal(totalDays_.normal_)
+                )
+                let range = Threshold(
+                    low: units == .mmolL ? self.roundDecimal(self.settingsManager.settings.low.asMmolL, 1) :
+                        self.roundDecimal(self.settingsManager.settings.low, 0),
+                    high: units == .mmolL ? self.roundDecimal(self.settingsManager.settings.high.asMmolL, 1) :
+                        self.roundDecimal(self.settingsManager.settings.high, 0)
+                )
+                let TimeInRange = TIRs(
+                    TIR: tir,
+                    Hypos: hypo,
+                    Hypers: hyper,
+                    Threshold: range,
+                    Euglycemic: normal
+                )
+                let avgs = Durations(
+                    day: self.roundDecimal(Decimal(oneDayGlucose.average), 1),
+                    week: self.roundDecimal(Decimal(sevenDaysGlucose.average), 1),
+                    month: self.roundDecimal(Decimal(thirtyDaysGlucose.average), 1),
+                    total: self.roundDecimal(Decimal(totalDaysGlucose.average), 1)
+                )
+                let avg = Averages(Average: avgs, Median: median)
+                // Standard Deviations
+                let standardDeviations = Durations(
+                    day: self.roundDecimal(Decimal(oneDayGlucose.sd), 1),
+                    week: self.roundDecimal(Decimal(sevenDaysGlucose.sd), 1),
+                    month: self.roundDecimal(Decimal(thirtyDaysGlucose.sd), 1),
+                    total: self.roundDecimal(Decimal(totalDaysGlucose.sd), 1)
+                )
+                // CV = standard deviation / sample mean x 100
+                let cvs = Durations(
+                    day: self.roundDecimal(Decimal(oneDayGlucose.cv), 1),
+                    week: self.roundDecimal(Decimal(sevenDaysGlucose.cv), 1),
+                    month: self.roundDecimal(Decimal(thirtyDaysGlucose.cv), 1),
+                    total: self.roundDecimal(Decimal(totalDaysGlucose.cv), 1)
+                )
+                let variance = Variance(SD: standardDeviations, CV: cvs)
 
 
-            var oneDay_: (TIR: Double, hypos: Double, hypers: Double, normal_: Double) = (0.0, 0.0, 0.0, 0.0)
-            var sevenDays_: (TIR: Double, hypos: Double, hypers: Double, normal_: Double) = (0.0, 0.0, 0.0, 0.0)
-            var thirtyDays_: (TIR: Double, hypos: Double, hypers: Double, normal_: Double) = (0.0, 0.0, 0.0, 0.0)
-            var totalDays_: (TIR: Double, hypos: Double, hypers: Double, normal_: Double) = (0.0, 0.0, 0.0, 0.0)
-            // Get TIR computations for every case
-            oneDay_ = self.tir(glucose24h)
-            sevenDays_ = self.tir(glucoseOneWeek)
-            thirtyDays_ = self.tir(glucoseOneMonth)
-            totalDays_ = self.tir(glucoseThreeMonths)
-
-            let tir = Durations(
-                day: self.roundDecimal(Decimal(oneDay_.TIR), 1),
-                week: self.roundDecimal(Decimal(sevenDays_.TIR), 1),
-                month: self.roundDecimal(Decimal(thirtyDays_.TIR), 1),
-                total: self.roundDecimal(Decimal(totalDays_.TIR), 1)
-            )
-            let hypo = Durations(
-                day: Decimal(oneDay_.hypos),
-                week: Decimal(sevenDays_.hypos),
-                month: Decimal(thirtyDays_.hypos),
-                total: Decimal(totalDays_.hypos)
-            )
-            let hyper = Durations(
-                day: Decimal(oneDay_.hypers),
-                week: Decimal(sevenDays_.hypers),
-                month: Decimal(thirtyDays_.hypers),
-                total: Decimal(totalDays_.hypers)
-            )
-            let normal = Durations(
-                day: Decimal(oneDay_.normal_),
-                week: Decimal(sevenDays_.normal_),
-                month: Decimal(thirtyDays_.normal_),
-                total: Decimal(totalDays_.normal_)
-            )
-            let range = Threshold(
-                low: units == .mmolL ? self.roundDecimal(self.settingsManager.settings.low.asMmolL, 1) :
-                    self.roundDecimal(self.settingsManager.settings.low, 0),
-                high: units == .mmolL ? self.roundDecimal(self.settingsManager.settings.high.asMmolL, 1) :
-                    self.roundDecimal(self.settingsManager.settings.high, 0)
-            )
-            let TimeInRange = TIRs(
-                TIR: tir,
-                Hypos: hypo,
-                Hypers: hyper,
-                Threshold: range,
-                Euglycemic: normal
-            )
-            let avgs = Durations(
-                day: self.roundDecimal(Decimal(oneDayGlucose.average), 1),
-                week: self.roundDecimal(Decimal(sevenDaysGlucose.average), 1),
-                month: self.roundDecimal(Decimal(thirtyDaysGlucose.average), 1),
-                total: self.roundDecimal(Decimal(totalDaysGlucose.average), 1)
-            )
-            let avg = Averages(Average: avgs, Median: median)
-            // Standard Deviations
-            let standardDeviations = Durations(
-                day: self.roundDecimal(Decimal(oneDayGlucose.sd), 1),
-                week: self.roundDecimal(Decimal(sevenDaysGlucose.sd), 1),
-                month: self.roundDecimal(Decimal(thirtyDaysGlucose.sd), 1),
-                total: self.roundDecimal(Decimal(totalDaysGlucose.sd), 1)
-            )
-            // CV = standard deviation / sample mean x 100
-            let cvs = Durations(
-                day: self.roundDecimal(Decimal(oneDayGlucose.cv), 1),
-                week: self.roundDecimal(Decimal(sevenDaysGlucose.cv), 1),
-                month: self.roundDecimal(Decimal(thirtyDaysGlucose.cv), 1),
-                total: self.roundDecimal(Decimal(totalDaysGlucose.cv), 1)
+                return (oneDayGlucose, hbA1cDisplayUnit, numberOfDays, TimeInRange, avg, hbs, variance)
+            }
+        } catch {
+            debug(
+                .apsManager,
+                "\(DebuggingIdentifiers.failed) Error fetching glucose for stats: \(error.localizedDescription)"
             )
             )
-            let variance = Variance(SD: standardDeviations, CV: cvs)
-
-            result = (oneDayGlucose, hbA1cDisplayUnit, numberOfDays, TimeInRange, avg, hbs, variance)
+            return nil
         }
         }
-
-        return result!
     }
     }
 
 
     private func loopStats(loopStatRecord: LoopStats) {
     private func loopStats(loopStatRecord: LoopStats) {

+ 2 - 2
Trio/Sources/APS/CGM/PluginSource.swift

@@ -159,9 +159,9 @@ extension PluginSource: CGMManagerDelegate {
         UUID().uuidString
         UUID().uuidString
     }
     }
 
 
-    func cgmManager(_ cgmManager: CGMManager, didUpdate status: CGMManagerStatus) {
+    func cgmManager(_: CGMManager, didUpdate status: CGMManagerStatus) {
         debug(.deviceManager, "CGM Manager did update state to \(status)")
         debug(.deviceManager, "CGM Manager did update state to \(status)")
-        
+
         processQueue.async {
         processQueue.async {
             if self.cgmHasValidSensorSession != status.hasValidSensorSession {
             if self.cgmHasValidSensorSession != status.hasValidSensorSession {
                 self.cgmHasValidSensorSession = status.hasValidSensorSession
                 self.cgmHasValidSensorSession = status.hasValidSensorSession

+ 15 - 9
Trio/Sources/APS/DeviceDataManager.swift

@@ -50,7 +50,7 @@ private let staticPumpManagersByIdentifier: [String: PumpManagerUI.Type] = [
 private let accessLock = NSRecursiveLock(label: "BaseDeviceDataManager.accessLock")
 private let accessLock = NSRecursiveLock(label: "BaseDeviceDataManager.accessLock")
 
 
 final class BaseDeviceDataManager: DeviceDataManager, Injectable {
 final class BaseDeviceDataManager: DeviceDataManager, Injectable {
-    private let processQueue = DispatchQueue.markedQueue(label: "BaseDeviceDataManager.processQueue")
+    private let processQueue = DispatchQueue.markedQueue(label: "BaseDeviceDataManager.processQueue", qos: .userInitiated)
     @Injected() private var pumpHistoryStorage: PumpHistoryStorage!
     @Injected() private var pumpHistoryStorage: PumpHistoryStorage!
     @Injected() var alertHistoryStorage: AlertHistoryStorage!
     @Injected() var alertHistoryStorage: AlertHistoryStorage!
     @Injected() private var storage: FileStorage!
     @Injected() private var storage: FileStorage!
@@ -512,15 +512,21 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
     ) {
     ) {
         dispatchPrecondition(condition: .onQueue(processQueue))
         dispatchPrecondition(condition: .onQueue(processQueue))
 
 
-        // filter buggy TBRs > maxBasal from MDT
-        let events = events.filter {
-            // type is optional...
-            guard let type = $0.type, type == .tempBasal else { return true }
-            return $0.dose?.unitsPerHour ?? 0 <= Double(settingsManager.pumpSettings.maxBasal)
+        Task {
+            do {
+                // filter buggy TBRs > maxBasal from MDT
+                let events = events.filter {
+                    // type is optional...
+                    guard let type = $0.type, type == .tempBasal else { return true }
+                    return $0.dose?.unitsPerHour ?? 0 <= Double(settingsManager.pumpSettings.maxBasal)
+                }
+                try await pumpHistoryStorage.storePumpEvents(events)
+                lastEventDate = events.last?.date
+                completion(nil)
+            } catch {
+                debug(.deviceManager, "\(DebuggingIdentifiers.failed) Failed to store pump events: \(error)")
+            }
         }
         }
-        pumpHistoryStorage.storePumpEvents(events)
-        lastEventDate = events.last?.date
-        completion(nil)
     }
     }
 
 
     func pumpManager(
     func pumpManager(

+ 58 - 85
Trio/Sources/APS/FetchGlucoseManager.swift

@@ -8,8 +8,6 @@ import Swinject
 import UIKit
 import UIKit
 
 
 protocol FetchGlucoseManager: SourceInfoProvider {
 protocol FetchGlucoseManager: SourceInfoProvider {
-    func updateGlucoseStore(newBloodGlucose: [BloodGlucose])
-    func refreshCGM()
     func updateGlucoseSource(cgmGlucoseSourceType: CGMType, cgmGlucosePluginId: String, newManager: CGMManagerUI?)
     func updateGlucoseSource(cgmGlucoseSourceType: CGMType, cgmGlucosePluginId: String, newManager: CGMManagerUI?)
     func deleteGlucoseSource()
     func deleteGlucoseSource()
     func removeCalibrations()
     func removeCalibrations()
@@ -77,6 +75,44 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         subscribe()
         subscribe()
     }
     }
 
 
+    /// The function used to start the timer sync - Function of the variable defined in config
+    private func subscribe() {
+        timer.publisher
+            .receive(on: processQueue)
+            .flatMap { [self] _ -> AnyPublisher<[BloodGlucose], Never> in
+                debug(.nightscout, "FetchGlucoseManager timer heartbeat")
+                if let glucoseSource = self.glucoseSource {
+                    return glucoseSource.fetch(self.timer).eraseToAnyPublisher()
+                } else {
+                    return Empty(completeImmediately: false).eraseToAnyPublisher()
+                }
+            }
+            .sink { glucose in
+                debug(.nightscout, "FetchGlucoseManager callback sensor")
+                Publishers.CombineLatest(
+                    Just(glucose),
+                    Just(self.glucoseStorage.syncDate())
+                )
+                .eraseToAnyPublisher()
+                .sink { newGlucose, syncDate in
+                    Task {
+                        do {
+                            try await self.glucoseStoreAndHeartDecision(
+                                syncDate: syncDate,
+                                glucose: newGlucose
+                            )
+                        } catch {
+                            debug(.deviceManager, "Failed to store glucose: \(error.localizedDescription)")
+                        }
+                    }
+                }
+                .store(in: &self.lifetime)
+            }
+            .store(in: &lifetime)
+        timer.fire()
+        timer.resume()
+    }
+
     var glucoseSource: GlucoseSource!
     var glucoseSource: GlucoseSource!
 
 
     func removeCalibrations() {
     func removeCalibrations() {
@@ -171,32 +207,8 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         return Manager.init(rawState: rawState)
         return Manager.init(rawState: rawState)
     }
     }
 
 
-    /// function called when a callback is fired by CGM BLE - no more used
-    public func updateGlucoseStore(newBloodGlucose: [BloodGlucose]) {
-        let syncDate = glucoseStorage.syncDate()
-        debug(.deviceManager, "CGM BLE FETCHGLUCOSE  : SyncDate is \(syncDate)")
-        glucoseStoreAndHeartDecision(syncDate: syncDate, glucose: newBloodGlucose)
-    }
-
-    /// function to try to force the refresh of the CGM - generally provide by the pump heartbeat
-    public func refreshCGM() {
-        debug(.deviceManager, "refreshCGM by pump")
-
-        Publishers.CombineLatest(
-            Just(glucoseStorage.syncDate()),
-            glucoseSource.fetchIfNeeded()
-        )
-        .eraseToAnyPublisher()
-        .receive(on: processQueue)
-        .sink { syncDate, glucose in
-            debug(.nightscout, "refreshCGM FETCHGLUCOSE : SyncDate is \(syncDate)")
-            self.glucoseStoreAndHeartDecision(syncDate: syncDate, glucose: glucose)
-        }
-        .store(in: &lifetime)
-    }
-
-    private func fetchGlucose() -> [GlucoseStored]? {
-        CoreDataStack.shared.fetchEntities(
+    private func fetchGlucose() async throws -> [GlucoseStored]? {
+        try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             ofType: GlucoseStored.self,
             onContext: context,
             onContext: context,
             predicate: NSPredicate.predicateFor30MinAgo,
             predicate: NSPredicate.predicateFor30MinAgo,
@@ -206,9 +218,13 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         ) as? [GlucoseStored]
         ) as? [GlucoseStored]
     }
     }
 
 
-    private func processGlucose() -> [BloodGlucose] {
-        context.performAndWait {
-            guard let results = fetchGlucose() else { return [] }
+    private func processGlucose() async throws -> [BloodGlucose] {
+        let results = try await fetchGlucose()
+
+        return try await context.perform {
+            guard let results else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
             return results.map { result in
             return results.map { result in
                 BloodGlucose(
                 BloodGlucose(
                     sgv: Int(result.glucose),
                     sgv: Int(result.glucose),
@@ -225,7 +241,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         }
         }
     }
     }
 
 
-    private func glucoseStoreAndHeartDecision(syncDate: Date, glucose: [BloodGlucose]) {
+    private func glucoseStoreAndHeartDecision(syncDate: Date, glucose: [BloodGlucose]) async throws {
         // calibration add if required only for sensor
         // calibration add if required only for sensor
         let newGlucose = overcalibrate(entries: glucose)
         let newGlucose = overcalibrate(entries: glucose)
 
 
@@ -234,37 +250,33 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
 
 
         // start background time extension
         // start background time extension
         var backGroundFetchBGTaskID: UIBackgroundTaskIdentifier?
         var backGroundFetchBGTaskID: UIBackgroundTaskIdentifier?
-        backGroundFetchBGTaskID = UIApplication.shared.beginBackgroundTask(withName: "save BG starting") {
+        backGroundFetchBGTaskID = await UIApplication.shared.beginBackgroundTask(withName: "save BG starting") {
             guard let bg = backGroundFetchBGTaskID else { return }
             guard let bg = backGroundFetchBGTaskID else { return }
             UIApplication.shared.endBackgroundTask(bg)
             UIApplication.shared.endBackgroundTask(bg)
             backGroundFetchBGTaskID = .invalid
             backGroundFetchBGTaskID = .invalid
         }
         }
 
 
-        guard newGlucose.isNotEmpty else {
+        defer {
             if let backgroundTask = backGroundFetchBGTaskID {
             if let backgroundTask = backGroundFetchBGTaskID {
-                UIApplication.shared.endBackgroundTask(backgroundTask)
+                Task {
+                    await UIApplication.shared.endBackgroundTask(backgroundTask)
+                }
                 backGroundFetchBGTaskID = .invalid
                 backGroundFetchBGTaskID = .invalid
             }
             }
-            return
         }
         }
 
 
+        guard newGlucose.isNotEmpty else { return }
+
         filteredByDate = newGlucose.filter { $0.dateString > syncDate }
         filteredByDate = newGlucose.filter { $0.dateString > syncDate }
         filtered = glucoseStorage.filterTooFrequentGlucose(filteredByDate, at: syncDate)
         filtered = glucoseStorage.filterTooFrequentGlucose(filteredByDate, at: syncDate)
 
 
-        guard filtered.isNotEmpty else {
-            // end of the Background tasks
-            if let backgroundTask = backGroundFetchBGTaskID {
-                UIApplication.shared.endBackgroundTask(backgroundTask)
-                backGroundFetchBGTaskID = .invalid
-            }
-            return
-        }
+        guard filtered.isNotEmpty else { return }
         debug(.deviceManager, "New glucose found")
         debug(.deviceManager, "New glucose found")
 
 
         // filter the data if it is the case
         // filter the data if it is the case
         if settingsManager.settings.smoothGlucose {
         if settingsManager.settings.smoothGlucose {
             // limited to 30 min of old glucose data
             // limited to 30 min of old glucose data
-            let oldGlucoseValues = processGlucose()
+            let oldGlucoseValues = try await processGlucose()
 
 
             var smoothedValues = oldGlucoseValues + filtered
             var smoothedValues = oldGlucoseValues + filtered
             // smooth with 3 repeats
             // smooth with 3 repeats
@@ -275,47 +287,8 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
             filtered = smoothedValues.filter { $0.dateString > syncDate }
             filtered = smoothedValues.filter { $0.dateString > syncDate }
         }
         }
 
 
-        glucoseStorage.storeGlucose(filtered)
-
+        try await glucoseStorage.storeGlucose(filtered)
         deviceDataManager.heartbeat(date: Date())
         deviceDataManager.heartbeat(date: Date())
-
-        // End of the Background tasks
-        if let backgroundTask = backGroundFetchBGTaskID {
-            UIApplication.shared.endBackgroundTask(backgroundTask)
-            backGroundFetchBGTaskID = .invalid
-        }
-    }
-
-    /// The function used to start the timer sync - Function of the variable defined in config
-    private func subscribe() {
-        timer.publisher
-            .receive(on: processQueue)
-            .flatMap { [self] _ -> AnyPublisher<[BloodGlucose], Never> in
-                debug(.nightscout, "FetchGlucoseManager timer heartbeat")
-                if let glucoseSource = self.glucoseSource {
-                    return glucoseSource.fetch(self.timer).eraseToAnyPublisher()
-                } else {
-                    return Empty(completeImmediately: false).eraseToAnyPublisher()
-                }
-            }
-            .sink { glucose in
-                debug(.nightscout, "FetchGlucoseManager callback sensor")
-                Publishers.CombineLatest(
-                    Just(glucose),
-                    Just(self.glucoseStorage.syncDate())
-                )
-                .eraseToAnyPublisher()
-                .sink { newGlucose, syncDate in
-                    self.glucoseStoreAndHeartDecision(
-                        syncDate: syncDate,
-                        glucose: newGlucose
-                    )
-                }
-                .store(in: &self.lifetime)
-            }
-            .store(in: &lifetime)
-        timer.fire()
-        timer.resume()
     }
     }
 
 
     func sourceInfo() -> [String: Any]? {
     func sourceInfo() -> [String: Any]? {

+ 39 - 35
Trio/Sources/APS/FetchTreatmentsManager.swift

@@ -30,48 +30,52 @@ final class BaseFetchTreatmentsManager: FetchTreatmentsManager, Injectable {
                 debug(.nightscout, "Start fetching carbs and temptargets")
                 debug(.nightscout, "Start fetching carbs and temptargets")
 
 
                 Task {
                 Task {
-                    // Fetch carbs and temp targets concurrently
-                    async let carbs = self.nightscoutManager.fetchCarbs()
-                    async let tempTargets = self.nightscoutManager.fetchTempTargets()
+                    do {
+                        // Fetch carbs and temp targets concurrently
+                        async let carbs = self.nightscoutManager.fetchCarbs()
+                        async let tempTargets = self.nightscoutManager.fetchTempTargets()
 
 
-                    // Filter and store if not from "Trio"
-                    let filteredCarbs = await carbs.filter { $0.enteredBy != CarbsEntry.local }
-                    if filteredCarbs.isNotEmpty {
-                        await self.carbsStorage.storeCarbs(filteredCarbs, areFetchedFromRemote: true)
-                    }
+                        // Filter and store if not from "Trio"
+                        let filteredCarbs = await carbs.filter { $0.enteredBy != CarbsEntry.local }
+                        if filteredCarbs.isNotEmpty {
+                            try await self.carbsStorage.storeCarbs(filteredCarbs, areFetchedFromRemote: true)
+                        }
 
 
-                    // Filter and store if not from Trio
-                    let filteredTargets = await tempTargets.filter { $0.enteredBy != TempTarget.local }
-                    if filteredTargets.isNotEmpty {
-                        // Sort temp targets by creation date
-                        let sortedTargets = filteredTargets.sorted { $0.createdAt < $1.createdAt }
+                        // Filter and store if not from Trio
+                        let filteredTargets = await tempTargets.filter { $0.enteredBy != TempTarget.local }
+                        if filteredTargets.isNotEmpty {
+                            // Sort temp targets by creation date
+                            let sortedTargets = filteredTargets.sorted { $0.createdAt < $1.createdAt }
 
 
-                        // Iterate and store each temp target
-                        for (index, tempTarget) in sortedTargets.enumerated() {
-                            // Skip saving if a Temp Target with the same date already exists or it's a cancel target
-                            guard await !self.tempTargetsStorage.existsTempTarget(with: tempTarget.createdAt),
-                                  tempTarget.reason != TempTarget.cancel
-                            else {
-                                debug(
-                                    .nightscout,
-                                    "Skipping temp target with date: \(tempTarget.date ?? Date.distantPast)"
-                                )
-                                continue
-                            }
+                            // Iterate and store each temp target
+                            for (index, tempTarget) in sortedTargets.enumerated() {
+                                // Skip saving if a Temp Target with the same date already exists or it's a cancel target
+                                guard await !self.tempTargetsStorage.existsTempTarget(with: tempTarget.createdAt),
+                                      tempTarget.reason != TempTarget.cancel
+                                else {
+                                    debug(
+                                        .nightscout,
+                                        "Skipping temp target with date: \(tempTarget.date ?? Date.distantPast)"
+                                    )
+                                    continue
+                                }
 
 
-                            // Create a mutable copy and set enabled for the last temp target
-                            var mutableTempTarget = tempTarget
-                            mutableTempTarget.enabled = (index == sortedTargets.count - 1)
+                                // Create a mutable copy and set enabled for the last temp target
+                                var mutableTempTarget = tempTarget
+                                mutableTempTarget.enabled = (index == sortedTargets.count - 1)
 
 
-                            // Save to Core Data
-                            await self.tempTargetsStorage.storeTempTarget(tempTarget: mutableTempTarget)
-                        }
+                                // Save to Core Data
+                                try await self.tempTargetsStorage.storeTempTarget(tempTarget: mutableTempTarget)
+                            }
 
 
-                        // Save the temp targets to JSON so that they get used by oref
-                        self.tempTargetsStorage.saveTempTargetsToStorage(sortedTargets)
+                            // Save the temp targets to JSON so that they get used by oref
+                            self.tempTargetsStorage.saveTempTargetsToStorage(sortedTargets)
 
 
-                        // Update Adjustments View
-                        Foundation.NotificationCenter.default.post(name: .didUpdateTempTargetConfiguration, object: nil)
+                            // Update Adjustments View
+                            Foundation.NotificationCenter.default.post(name: .didUpdateTempTargetConfiguration, object: nil)
+                        }
+                    } catch {
+                        debug(.default, "\(DebuggingIdentifiers.failed) error in \(#file) \(#function): \(error)")
                     }
                     }
                 }
                 }
             }
             }

+ 35 - 34
Trio/Sources/APS/OpenAPS/OpenAPS.swift

@@ -100,8 +100,8 @@ final class OpenAPS {
     }
     }
 
 
     // fetch glucose to pass it to the meal function and to determine basal
     // fetch glucose to pass it to the meal function and to determine basal
-    private func fetchAndProcessGlucose() async -> String {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchAndProcessGlucose() async throws -> String {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             ofType: GlucoseStored.self,
             onContext: context,
             onContext: context,
             predicate: NSPredicate.predicateForOneDayAgoInMinutes,
             predicate: NSPredicate.predicateForOneDayAgoInMinutes,
@@ -111,9 +111,9 @@ final class OpenAPS {
             batchSize: 24
             batchSize: 24
         )
         )
 
 
-        return await context.perform {
+        return try await context.perform {
             guard let glucoseResults = results as? [GlucoseStored] else {
             guard let glucoseResults = results as? [GlucoseStored] else {
-                return ""
+                throw CoreDataError.fetchError(function: #function, file: #file)
             }
             }
 
 
             // convert to JSON
             // convert to JSON
@@ -121,8 +121,8 @@ final class OpenAPS {
         }
         }
     }
     }
 
 
-    private func fetchAndProcessCarbs(additionalCarbs: Decimal? = nil) async -> String {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchAndProcessCarbs(additionalCarbs: Decimal? = nil) async throws -> String {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
             ofType: CarbEntryStored.self,
             onContext: context,
             onContext: context,
             predicate: NSPredicate.predicateForOneDayAgo,
             predicate: NSPredicate.predicateForOneDayAgo,
@@ -130,9 +130,9 @@ final class OpenAPS {
             ascending: false
             ascending: false
         )
         )
 
 
-        let json = await context.perform {
+        let json = try await context.perform {
             guard let carbResults = results as? [CarbEntryStored] else {
             guard let carbResults = results as? [CarbEntryStored] else {
-                return ""
+                throw CoreDataError.fetchError(function: #function, file: #file)
             }
             }
 
 
             var jsonArray = self.jsonConverter.convertToJSON(carbResults)
             var jsonArray = self.jsonConverter.convertToJSON(carbResults)
@@ -170,8 +170,8 @@ final class OpenAPS {
         return json
         return json
     }
     }
 
 
-    private func fetchPumpHistoryObjectIDs() async -> [NSManagedObjectID]? {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchPumpHistoryObjectIDs() async throws -> [NSManagedObjectID]? {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: PumpEventStored.self,
             ofType: PumpEventStored.self,
             onContext: context,
             onContext: context,
             predicate: NSPredicate.pumpHistoryLast1440Minutes,
             predicate: NSPredicate.pumpHistoryLast1440Minutes,
@@ -180,9 +180,9 @@ final class OpenAPS {
             batchSize: 50
             batchSize: 50
         )
         )
 
 
-        return await context.perform {
+        return try await context.perform {
             guard let pumpEventResults = results as? [PumpEventStored] else {
             guard let pumpEventResults = results as? [PumpEventStored] else {
-                return nil
+                throw CoreDataError.fetchError(function: #function, file: #file)
             }
             }
 
 
             return pumpEventResults.map(\.objectID)
             return pumpEventResults.map(\.objectID)
@@ -302,10 +302,10 @@ final class OpenAPS {
             reservoir,
             reservoir,
             preferences
             preferences
         ) = await (
         ) = await (
-            parsePumpHistory(await pumpHistoryObjectIDs, simulatedBolusAmount: simulatedBolusAmount),
-            carbs,
-            glucose,
-            oref2,
+            try parsePumpHistory(await pumpHistoryObjectIDs, simulatedBolusAmount: simulatedBolusAmount),
+            try carbs,
+            try glucose,
+            try oref2,
             profileAsync,
             profileAsync,
             basalAsync,
             basalAsync,
             autosenseAsync,
             autosenseAsync,
@@ -366,12 +366,12 @@ final class OpenAPS {
 
 
             return determination
             return determination
         } else {
         } else {
-            return nil
+            throw APSError.apsError(message: "Determination is nil")
         }
         }
     }
     }
 
 
-    func oref2() async -> RawJSON {
-        await context.perform {
+    func oref2() async throws -> RawJSON {
+        try await context.perform {
             // Retrieve user preferences
             // Retrieve user preferences
             let userPreferences = self.storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self)
             let userPreferences = self.storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self)
             let weightPercentage = userPreferences?.weightPercentage ?? 1.0
             let weightPercentage = userPreferences?.weightPercentage ?? 1.0
@@ -381,10 +381,10 @@ final class OpenAPS {
             // Fetch historical events for Total Daily Dose (TDD) calculation
             // Fetch historical events for Total Daily Dose (TDD) calculation
             let tenDaysAgo = Date().addingTimeInterval(-10.days.timeInterval)
             let tenDaysAgo = Date().addingTimeInterval(-10.days.timeInterval)
             let twoHoursAgo = Date().addingTimeInterval(-2.hours.timeInterval)
             let twoHoursAgo = Date().addingTimeInterval(-2.hours.timeInterval)
-            let historicalTDDData = self.fetchHistoricalTDDData(from: tenDaysAgo)
+            let historicalTDDData = try self.fetchHistoricalTDDData(from: tenDaysAgo)
 
 
             // Fetch the last active Override
             // Fetch the last active Override
-            let activeOverrides = self.fetchActiveOverrides()
+            let activeOverrides = try self.fetchActiveOverrides()
             let isOverrideActive = activeOverrides.first?.enabled ?? false
             let isOverrideActive = activeOverrides.first?.enabled ?? false
             let overridePercentage = Decimal(activeOverrides.first?.percentage ?? 100)
             let overridePercentage = Decimal(activeOverrides.first?.percentage ?? 100)
             let isOverrideIndefinite = activeOverrides.first?.indefinite ?? true
             let isOverrideIndefinite = activeOverrides.first?.indefinite ?? true
@@ -448,9 +448,9 @@ final class OpenAPS {
 
 
         // Await the results of asynchronous tasks
         // Await the results of asynchronous tasks
         let (pumpHistoryJSON, carbsAsJSON, glucoseAsJSON, profile, basalProfile, tempTargets) = await (
         let (pumpHistoryJSON, carbsAsJSON, glucoseAsJSON, profile, basalProfile, tempTargets) = await (
-            parsePumpHistory(await pumpHistoryObjectIDs),
-            carbs,
-            glucose,
+            try parsePumpHistory(await pumpHistoryObjectIDs),
+            try carbs,
+            try glucose,
             getProfile,
             getProfile,
             getBasalProfile,
             getBasalProfile,
             getTempTargets
             getTempTargets
@@ -477,7 +477,7 @@ final class OpenAPS {
         }
         }
     }
     }
 
 
-    func createProfiles() async {
+    func createProfiles() async throws {
         debug(.openAPS, "Start creating pump profile and user profile")
         debug(.openAPS, "Start creating pump profile and user profile")
 
 
         // Load required settings and profiles asynchronously
         // Load required settings and profiles asynchronously
@@ -507,9 +507,9 @@ final class OpenAPS {
         var adjustedPreferences = preferences
         var adjustedPreferences = preferences
 
 
         // Check for active Temp Targets and adjust HBT if necessary
         // Check for active Temp Targets and adjust HBT if necessary
-        await context.perform {
+        try await context.perform {
             // Check if a Temp Target is active and if its HBT differs from user preferences
             // Check if a Temp Target is active and if its HBT differs from user preferences
-            if let activeTempTarget = self.fetchActiveTempTargets().first,
+            if let activeTempTarget = try self.fetchActiveTempTargets().first,
                activeTempTarget.enabled,
                activeTempTarget.enabled,
                let activeHBT = activeTempTarget.halfBasalTarget?.decimalValue,
                let activeHBT = activeTempTarget.halfBasalTarget?.decimalValue,
                activeHBT != defaultHalfBasalTarget
                activeHBT != defaultHalfBasalTarget
@@ -555,11 +555,12 @@ final class OpenAPS {
                 .apsManager,
                 .apsManager,
                 "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to create pump profile and normal profile: \(error)"
                 "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to create pump profile and normal profile: \(error)"
             )
             )
+            throw error
         }
         }
     }
     }
 
 
     private func iob(pumphistory: JSON, profile: JSON, clock: JSON, autosens: JSON) async throws -> RawJSON {
     private func iob(pumphistory: JSON, profile: JSON, clock: JSON, autosens: JSON) async throws -> RawJSON {
-        await withCheckedContinuation { continuation in
+        try await withCheckedThrowingContinuation { continuation in
             jsWorker.inCommonContext { worker in
             jsWorker.inCommonContext { worker in
                 worker.evaluateBatch(scripts: [
                 worker.evaluateBatch(scripts: [
                     Script(name: Prepare.log),
                     Script(name: Prepare.log),
@@ -801,8 +802,8 @@ final class OpenAPS {
 
 
 // Non-Async fetch methods for oref2
 // Non-Async fetch methods for oref2
 extension OpenAPS {
 extension OpenAPS {
-    func fetchActiveTempTargets() -> [TempTargetStored] {
-        CoreDataStack.shared.fetchEntities(
+    func fetchActiveTempTargets() throws -> [TempTargetStored] {
+        try CoreDataStack.shared.fetchEntities(
             ofType: TempTargetStored.self,
             ofType: TempTargetStored.self,
             onContext: context,
             onContext: context,
             predicate: NSPredicate.lastActiveTempTarget,
             predicate: NSPredicate.lastActiveTempTarget,
@@ -812,8 +813,8 @@ extension OpenAPS {
         ) as? [TempTargetStored] ?? []
         ) as? [TempTargetStored] ?? []
     }
     }
 
 
-    func fetchActiveOverrides() -> [OverrideStored] {
-        CoreDataStack.shared.fetchEntities(
+    func fetchActiveOverrides() throws -> [OverrideStored] {
+        try CoreDataStack.shared.fetchEntities(
             ofType: OverrideStored.self,
             ofType: OverrideStored.self,
             onContext: context,
             onContext: context,
             predicate: NSPredicate.lastActiveOverride,
             predicate: NSPredicate.lastActiveOverride,
@@ -823,8 +824,8 @@ extension OpenAPS {
         ) as? [OverrideStored] ?? []
         ) as? [OverrideStored] ?? []
     }
     }
 
 
-    func fetchHistoricalTDDData(from date: Date) -> [[String: Any]] {
-        CoreDataStack.shared.fetchEntities(
+    func fetchHistoricalTDDData(from date: Date) throws -> [[String: Any]] {
+        try CoreDataStack.shared.fetchEntities(
             ofType: OrefDetermination.self,
             ofType: OrefDetermination.self,
             onContext: context,
             onContext: context,
             predicate: NSPredicate(format: "timestamp > %@ AND totalDailyDose > 0", date as NSDate),
             predicate: NSPredicate(format: "timestamp > %@ AND totalDailyDose > 0", date as NSDate),

+ 31 - 29
Trio/Sources/APS/Storage/CarbsStorage.swift

@@ -10,14 +10,14 @@ protocol CarbsObserver {
 
 
 protocol CarbsStorage {
 protocol CarbsStorage {
     var updatePublisher: AnyPublisher<Void, Never> { get }
     var updatePublisher: AnyPublisher<Void, Never> { get }
-    func storeCarbs(_ carbs: [CarbsEntry], areFetchedFromRemote: Bool) async
+    func storeCarbs(_ carbs: [CarbsEntry], areFetchedFromRemote: Bool) async throws
     func deleteCarbsEntryStored(_ treatmentObjectID: NSManagedObjectID) async
     func deleteCarbsEntryStored(_ treatmentObjectID: NSManagedObjectID) async
     func syncDate() -> Date
     func syncDate() -> Date
     func recent() -> [CarbsEntry]
     func recent() -> [CarbsEntry]
-    func getCarbsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
-    func getFPUsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
-    func getCarbsNotYetUploadedToHealth() async -> [CarbsEntry]
-    func getCarbsNotYetUploadedToTidepool() async -> [CarbsEntry]
+    func getCarbsNotYetUploadedToNightscout() async throws -> [NightscoutTreatment]
+    func getFPUsNotYetUploadedToNightscout() async throws -> [NightscoutTreatment]
+    func getCarbsNotYetUploadedToHealth() async throws -> [CarbsEntry]
+    func getCarbsNotYetUploadedToTidepool() async throws -> [CarbsEntry]
 }
 }
 
 
 final class BaseCarbsStorage: CarbsStorage, Injectable {
 final class BaseCarbsStorage: CarbsStorage, Injectable {
@@ -38,11 +38,11 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         injectServices(resolver)
         injectServices(resolver)
     }
     }
 
 
-    func storeCarbs(_ entries: [CarbsEntry], areFetchedFromRemote: Bool) async {
+    func storeCarbs(_ entries: [CarbsEntry], areFetchedFromRemote: Bool) async throws {
         var entriesToStore = entries
         var entriesToStore = entries
 
 
         if areFetchedFromRemote {
         if areFetchedFromRemote {
-            entriesToStore = await filterRemoteEntries(entries: entriesToStore)
+            entriesToStore = try await filterRemoteEntries(entries: entriesToStore)
         }
         }
 
 
         // Check for FPU-only entries (fat/protein without carbs)
         // Check for FPU-only entries (fat/protein without carbs)
@@ -71,9 +71,9 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         await saveCarbEquivalents(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
         await saveCarbEquivalents(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
     }
     }
 
 
-    private func filterRemoteEntries(entries: [CarbsEntry]) async -> [CarbsEntry] {
+    private func filterRemoteEntries(entries: [CarbsEntry]) async throws -> [CarbsEntry] {
         // Fetch only the date property from Core Data
         // Fetch only the date property from Core Data
-        guard let existing24hCarbEntries = await CoreDataStack.shared.fetchEntitiesAsync(
+        guard let existing24hCarbEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
             ofType: CarbEntryStored.self,
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: NSPredicate.predicateForOneDayAgo,
             predicate: NSPredicate.predicateForOneDayAgo,
@@ -339,8 +339,8 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         }
         }
     }
     }
 
 
-    func getCarbsNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getCarbsNotYetUploadedToNightscout() async throws -> [NightscoutTreatment] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
             ofType: CarbEntryStored.self,
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: NSPredicate.carbsNotYetUploadedToNightscout,
             predicate: NSPredicate.carbsNotYetUploadedToNightscout,
@@ -348,9 +348,9 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        return await coredataContext.perform {
+        return try await coredataContext.perform {
             guard let carbEntries = results as? [CarbEntryStored] else {
             guard let carbEntries = results as? [CarbEntryStored] else {
-                return []
+                throw CoreDataError.fetchError(function: #function, file: #file)
             }
             }
 
 
             return carbEntries.map { result in
             return carbEntries.map { result in
@@ -378,8 +378,8 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         }
         }
     }
     }
 
 
-    func getFPUsNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getFPUsNotYetUploadedToNightscout() async throws -> [NightscoutTreatment] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
             ofType: CarbEntryStored.self,
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: NSPredicate.fpusNotYetUploadedToNightscout,
             predicate: NSPredicate.fpusNotYetUploadedToNightscout,
@@ -387,8 +387,10 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        return await coredataContext.perform {
-            guard let fpuEntries = results as? [CarbEntryStored] else { return [] }
+        return try await coredataContext.perform {
+            guard let fpuEntries = results as? [CarbEntryStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             return fpuEntries.map { result in
             return fpuEntries.map { result in
                 NightscoutTreatment(
                 NightscoutTreatment(
@@ -415,8 +417,8 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         }
         }
     }
     }
 
 
-    func getCarbsNotYetUploadedToHealth() async -> [CarbsEntry] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getCarbsNotYetUploadedToHealth() async throws -> [CarbsEntry] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
             ofType: CarbEntryStored.self,
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: NSPredicate.carbsNotYetUploadedToHealth,
             predicate: NSPredicate.carbsNotYetUploadedToHealth,
@@ -424,11 +426,11 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        guard let carbEntries = results as? [CarbEntryStored] else {
-            return []
-        }
+        return try await coredataContext.perform {
+            guard let carbEntries = results as? [CarbEntryStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
-        return await coredataContext.perform {
             return carbEntries.map { result in
             return carbEntries.map { result in
                 CarbsEntry(
                 CarbsEntry(
                     id: result.id?.uuidString,
                     id: result.id?.uuidString,
@@ -446,8 +448,8 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         }
         }
     }
     }
 
 
-    func getCarbsNotYetUploadedToTidepool() async -> [CarbsEntry] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getCarbsNotYetUploadedToTidepool() async throws -> [CarbsEntry] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
             ofType: CarbEntryStored.self,
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: NSPredicate.carbsNotYetUploadedToTidepool,
             predicate: NSPredicate.carbsNotYetUploadedToTidepool,
@@ -455,11 +457,11 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        guard let carbEntries = results as? [CarbEntryStored] else {
-            return []
-        }
+        return try await coredataContext.perform {
+            guard let carbEntries = results as? [CarbEntryStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
-        return await coredataContext.perform {
             return carbEntries.map { result in
             return carbEntries.map { result in
                 CarbsEntry(
                 CarbsEntry(
                     id: result.id?.uuidString,
                     id: result.id?.uuidString,

+ 34 - 27
Trio/Sources/APS/Storage/ContactImageStorage.swift

@@ -26,36 +26,43 @@ final class BaseContactImageStorage: ContactImageStorage, Injectable {
     ///
     ///
     /// - Returns: An array of `ContactImageEntry` objects.
     /// - Returns: An array of `ContactImageEntry` objects.
     func fetchContactImageEntries() async -> [ContactImageEntry] {
     func fetchContactImageEntries() async -> [ContactImageEntry] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: ContactImageEntryStored.self,
-            onContext: backgroundContext,
-            predicate: NSPredicate.all,
-            key: "hasHighContrast",
-            ascending: false
-        )
+        do {
+            let results = try await CoreDataStack.shared.fetchEntitiesAsync(
+                ofType: ContactImageEntryStored.self,
+                onContext: backgroundContext,
+                predicate: NSPredicate.all,
+                key: "hasHighContrast",
+                ascending: false
+            )
 
 
-        return await backgroundContext.perform {
-            guard let fetchedContactImageEntries = results as? [ContactImageEntryStored] else { return [] }
+            return try await backgroundContext.perform {
+                guard let fetchedContactImageEntries = results as? [ContactImageEntryStored]
+                else { throw CoreDataError.fetchError(function: #function, file: #file)
+                }
 
 
-            return fetchedContactImageEntries.compactMap { entry in
-                ContactImageEntry(
-                    name: entry.name ?? String(localized: "No name provided"),
-                    layout: ContactImageLayout(rawValue: entry.layout ?? "Default") ?? .default,
-                    ring: ContactImageLargeRing(rawValue: entry.ring ?? "Hidden") ?? .none,
-                    primary: ContactImageValue(rawValue: entry.primary ?? "Glucose Reading") ?? .glucose,
-                    top: ContactImageValue(rawValue: entry.top ?? "None") ?? .none,
-                    bottom: ContactImageValue(rawValue: entry.bottom ?? "None") ?? .none,
-                    contactId: entry.contactId?.string,
-                    hasHighContrast: entry.hasHighContrast,
-                    ringWidth: ContactImageEntry.RingWidth(rawValue: Int(entry.ringWidth)) ?? .regular,
-                    ringGap: ContactImageEntry.RingGap(rawValue: Int(entry.ringGap)) ?? .small,
-                    fontSize: ContactImageEntry.FontSize(rawValue: Int(entry.fontSize)) ?? .regular,
-                    secondaryFontSize: ContactImageEntry.FontSize(rawValue: Int(entry.fontSizeSecondary)) ?? .small,
-                    fontWeight: Font.Weight.fromString(entry.fontWeight ?? "regular"),
-                    fontWidth: Font.Width.fromString(entry.fontWidth ?? "standard"),
-                    managedObjectID: entry.objectID
-                )
+                return fetchedContactImageEntries.compactMap { entry in
+                    ContactImageEntry(
+                        name: entry.name ?? String(localized: "No name provided"),
+                        layout: ContactImageLayout(rawValue: entry.layout ?? "Default") ?? .default,
+                        ring: ContactImageLargeRing(rawValue: entry.ring ?? "Hidden") ?? .none,
+                        primary: ContactImageValue(rawValue: entry.primary ?? "Glucose Reading") ?? .glucose,
+                        top: ContactImageValue(rawValue: entry.top ?? "None") ?? .none,
+                        bottom: ContactImageValue(rawValue: entry.bottom ?? "None") ?? .none,
+                        contactId: entry.contactId?.string,
+                        hasHighContrast: entry.hasHighContrast,
+                        ringWidth: ContactImageEntry.RingWidth(rawValue: Int(entry.ringWidth)) ?? .regular,
+                        ringGap: ContactImageEntry.RingGap(rawValue: Int(entry.ringGap)) ?? .small,
+                        fontSize: ContactImageEntry.FontSize(rawValue: Int(entry.fontSize)) ?? .regular,
+                        secondaryFontSize: ContactImageEntry.FontSize(rawValue: Int(entry.fontSizeSecondary)) ?? .small,
+                        fontWeight: Font.Weight.fromString(entry.fontWeight ?? "regular"),
+                        fontWidth: Font.Width.fromString(entry.fontWidth ?? "standard"),
+                        managedObjectID: entry.objectID
+                    )
+                }
             }
             }
+        } catch {
+            debug(.default, "\(DebuggingIdentifiers.failed) Error fetching contact image entries: \(error.localizedDescription)")
+            return []
         }
         }
     }
     }
 
 

+ 49 - 15
Trio/Sources/APS/Storage/DeterminationStorage.swift

@@ -4,7 +4,7 @@ import Foundation
 import Swinject
 import Swinject
 
 
 protocol DeterminationStorage {
 protocol DeterminationStorage {
-    func fetchLastDeterminationObjectID(predicate: NSPredicate) async -> [NSManagedObjectID]
+    func fetchLastDeterminationObjectID(predicate: NSPredicate) async throws -> [NSManagedObjectID]
     func getForecastIDs(for determinationID: NSManagedObjectID, in context: NSManagedObjectContext) async -> [NSManagedObjectID]
     func getForecastIDs(for determinationID: NSManagedObjectID, in context: NSManagedObjectContext) async -> [NSManagedObjectID]
     func getForecastValueIDs(for forecastID: NSManagedObjectID, in context: NSManagedObjectContext) async -> [NSManagedObjectID]
     func getForecastValueIDs(for forecastID: NSManagedObjectID, in context: NSManagedObjectContext) async -> [NSManagedObjectID]
     func fetchForecastObjects(
     func fetchForecastObjects(
@@ -12,29 +12,32 @@ protocol DeterminationStorage {
         in context: NSManagedObjectContext
         in context: NSManagedObjectContext
     ) async -> (UUID, Forecast?, [ForecastValue])
     ) async -> (UUID, Forecast?, [ForecastValue])
     func getOrefDeterminationNotYetUploadedToNightscout(_ determinationIds: [NSManagedObjectID]) async -> Determination?
     func getOrefDeterminationNotYetUploadedToNightscout(_ determinationIds: [NSManagedObjectID]) async -> Determination?
+    func fetchForecastHierarchy(for determinationID: NSManagedObjectID, in context: NSManagedObjectContext)
+    async throws -> [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])]
 }
 }
 
 
 final class BaseDeterminationStorage: DeterminationStorage, Injectable {
 final class BaseDeterminationStorage: DeterminationStorage, Injectable {
     private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
     private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
-    private let backgroundContext = CoreDataStack.shared.newTaskContext()
+    private let context = CoreDataStack.shared.newTaskContext()
 
 
     init(resolver: Resolver) {
     init(resolver: Resolver) {
         injectServices(resolver)
         injectServices(resolver)
     }
     }
 
 
-    func fetchLastDeterminationObjectID(predicate: NSPredicate) async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func fetchLastDeterminationObjectID(predicate: NSPredicate) async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OrefDetermination.self,
             ofType: OrefDetermination.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: predicate,
             predicate: predicate,
             key: "deliverAt",
             key: "deliverAt",
             ascending: false,
             ascending: false,
             fetchLimit: 1
             fetchLimit: 1
         )
         )
 
 
-        return await backgroundContext.perform {
-            guard let fetchedResults = results as? [OrefDetermination] else { return [] }
-
+        return try await context.perform {
+            guard let fetchedResults = results as? [OrefDetermination] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
             return fetchedResults.map(\.objectID)
             return fetchedResults.map(\.objectID)
         }
         }
     }
     }
@@ -77,7 +80,7 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
         }
         }
     }
     }
 
 
-    // Fetch forecast value IDs for a given data set
+    // Fetch forecast objects for a given data set
     func fetchForecastObjects(
     func fetchForecastObjects(
         for data: (id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID]),
         for data: (id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID]),
         in context: NSManagedObjectContext
         in context: NSManagedObjectContext
@@ -112,19 +115,19 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
 
 
     // Convert NSSet to array of Ints for Predictions
     // Convert NSSet to array of Ints for Predictions
     func parseForecastValues(ofType type: String, from determinationID: NSManagedObjectID) async -> [Int]? {
     func parseForecastValues(ofType type: String, from determinationID: NSManagedObjectID) async -> [Int]? {
-        let forecastIDs = await getForecastIDs(for: determinationID, in: backgroundContext)
+        let forecastIDs = await getForecastIDs(for: determinationID, in: context)
 
 
         var forecastValuesList: [Int] = []
         var forecastValuesList: [Int] = []
 
 
         for forecastID in forecastIDs {
         for forecastID in forecastIDs {
-            await backgroundContext.perform {
-                if let forecast = try? self.backgroundContext.existingObject(with: forecastID) as? Forecast {
+            await context.perform {
+                if let forecast = try? self.context.existingObject(with: forecastID) as? Forecast {
                     // Filter the forecast based on the type
                     // Filter the forecast based on the type
                     if forecast.type == type {
                     if forecast.type == type {
                         let forecastValueIDs = forecast.forecastValues?.sorted(by: { $0.index < $1.index }).map(\.objectID) ?? []
                         let forecastValueIDs = forecast.forecastValues?.sorted(by: { $0.index < $1.index }).map(\.objectID) ?? []
 
 
                         for forecastValueID in forecastValueIDs {
                         for forecastValueID in forecastValueIDs {
-                            if let forecastValue = try? self.backgroundContext
+                            if let forecastValue = try? self.context
                                 .existingObject(with: forecastValueID) as? ForecastValue
                                 .existingObject(with: forecastValueID) as? ForecastValue
                             {
                             {
                                 let forecastValueInt = Int(forecastValue.value)
                                 let forecastValueInt = Int(forecastValue.value)
@@ -153,9 +156,9 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
             uam: await parseForecastValues(ofType: "uam", from: determinationId)
             uam: await parseForecastValues(ofType: "uam", from: determinationId)
         )
         )
 
 
-        return await backgroundContext.perform {
+        return await context.perform {
             do {
             do {
-                let orefDetermination = try self.backgroundContext.existingObject(with: determinationId) as? OrefDetermination
+                let orefDetermination = try self.context.existingObject(with: determinationId) as? OrefDetermination
 
 
                 // Check if the fetched object is of the expected type
                 // Check if the fetched object is of the expected type
                 if let orefDetermination = orefDetermination {
                 if let orefDetermination = orefDetermination {
@@ -201,4 +204,35 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
             return result
             return result
         }
         }
     }
     }
+
+    func fetchForecastHierarchy(for determinationID: NSManagedObjectID, in context: NSManagedObjectContext)
+    async throws -> [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])]
+    {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: Forecast.self,
+            onContext: context,
+            predicate: NSPredicate(format: "orefDetermination = %@", determinationID),
+            key: "type",
+            ascending: true,
+            relationshipKeyPathsForPrefetching: ["forecastValues"]
+        )
+
+        var result: [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])] = []
+
+        await context.perform {
+            if let forecasts = results as? [Forecast] {
+                for forecast in forecasts {
+                    // Use the helper property that already sorts by index
+                    let sortedValues = forecast.forecastValuesArray
+                    result.append((
+                        id: UUID(),
+                        forecastID: forecast.objectID,
+                        forecastValueIDs: sortedValues.map(\.objectID)
+                    ))
+                }
+            }
+        }
+
+        return result
+    }
 }
 }

+ 225 - 157
Trio/Sources/APS/Storage/GlucoseStorage.swift

@@ -9,20 +9,20 @@ import Swinject
 
 
 protocol GlucoseStorage {
 protocol GlucoseStorage {
     var updatePublisher: AnyPublisher<Void, Never> { get }
     var updatePublisher: AnyPublisher<Void, Never> { get }
-    func storeGlucose(_ glucose: [BloodGlucose])
+    func storeGlucose(_ glucose: [BloodGlucose]) async throws
     func addManualGlucose(glucose: Int)
     func addManualGlucose(glucose: Int)
     func isGlucoseDataFresh(_ glucoseDate: Date?) -> Bool
     func isGlucoseDataFresh(_ glucoseDate: Date?) -> Bool
     func syncDate() -> Date
     func syncDate() -> Date
     func filterTooFrequentGlucose(_ glucose: [BloodGlucose], at: Date) -> [BloodGlucose]
     func filterTooFrequentGlucose(_ glucose: [BloodGlucose], at: Date) -> [BloodGlucose]
     func lastGlucoseDate() -> Date
     func lastGlucoseDate() -> Date
     func isGlucoseFresh() -> Bool
     func isGlucoseFresh() -> Bool
-    func getGlucoseNotYetUploadedToNightscout() async -> [BloodGlucose]
-    func getCGMStateNotYetUploadedToNightscout() async -> [NightscoutTreatment]
-    func getManualGlucoseNotYetUploadedToNightscout() async -> [NightscoutTreatment]
-    func getGlucoseNotYetUploadedToHealth() async -> [BloodGlucose]
-    func getManualGlucoseNotYetUploadedToHealth() async -> [BloodGlucose]
-    func getGlucoseNotYetUploadedToTidepool() async -> [StoredGlucoseSample]
-    func getManualGlucoseNotYetUploadedToTidepool() async -> [StoredGlucoseSample]
+    func getGlucoseNotYetUploadedToNightscout() async throws -> [BloodGlucose]
+    func getCGMStateNotYetUploadedToNightscout() async throws -> [NightscoutTreatment]
+    func getManualGlucoseNotYetUploadedToNightscout() async throws -> [NightscoutTreatment]
+    func getGlucoseNotYetUploadedToHealth() async throws -> [BloodGlucose]
+    func getManualGlucoseNotYetUploadedToHealth() async throws -> [BloodGlucose]
+    func getGlucoseNotYetUploadedToTidepool() async throws -> [StoredGlucoseSample]
+    func getManualGlucoseNotYetUploadedToTidepool() async throws -> [StoredGlucoseSample]
     var alarm: GlucoseAlarm? { get }
     var alarm: GlucoseAlarm? { get }
     func deleteGlucose(_ treatmentObjectID: NSManagedObjectID) async
     func deleteGlucose(_ treatmentObjectID: NSManagedObjectID) async
 }
 }
@@ -60,120 +60,163 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
         return formatter
         return formatter
     }
     }
 
 
-    func storeGlucose(_ glucose: [BloodGlucose]) {
-        processQueue.sync {
-            self.coredataContext.perform {
-                let datesToCheck: Set<Date?> = Set(glucose.compactMap { $0.dateString as Date? })
-                let fetchRequest: NSFetchRequest<NSFetchRequestResult> = GlucoseStored.fetchRequest()
-                fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
-                    NSPredicate(format: "date IN %@", datesToCheck),
-                    NSPredicate.predicateForOneDayAgo
-                ])
-                fetchRequest.propertiesToFetch = ["date"]
-                fetchRequest.resultType = .dictionaryResultType
-
-                var existingDates = Set<Date>()
-                do {
-                    let results = try self.coredataContext.fetch(fetchRequest) as? [NSDictionary]
-                    existingDates = Set(results?.compactMap({ $0["date"] as? Date }) ?? [])
-                } catch {
-                    debugPrint("Failed to fetch existing glucose dates: \(error)")
-                }
+    func storeGlucose(_ glucose: [BloodGlucose]) async throws {
+        try await coredataContext.perform {
+            // Get new glucose values that don't exist yet
+            let newGlucose = self.filterNewGlucoseValues(glucose)
+            guard !newGlucose.isEmpty else { return }
 
 
-                var filteredGlucose = glucose.filter { !existingDates.contains($0.dateString) }
-
-                // prepare batch insert
-                let batchInsert = NSBatchInsertRequest(
-                    entity: GlucoseStored.entity(),
-                    managedObjectHandler: { (managedObject: NSManagedObject) -> Bool in
-                        guard let glucoseEntry = managedObject as? GlucoseStored, !filteredGlucose.isEmpty else {
-                            return true // Stop if there are no more items
-                        }
-                        let entry = filteredGlucose.removeFirst()
-                        glucoseEntry.id = UUID()
-                        glucoseEntry.glucose = Int16(entry.glucose ?? 0)
-                        glucoseEntry.date = entry.dateString
-                        glucoseEntry.direction = entry.direction?.rawValue
-                        glucoseEntry.isUploadedToNS = false /// the value is not uploaded to NS (yet)
-                        glucoseEntry.isUploadedToHealth = false /// the value is not uploaded to Health (yet)
-                        glucoseEntry.isUploadedToTidepool = false /// the value is not uploaded to Tidepool (yet)
-                        return false // Continue processing
-                    }
+            do {
+                // Store glucose values in Core Data
+                try self.storeGlucoseInCoreData(newGlucose)
+            } catch {
+                throw CoreDataError.creationError(
+                    function: #function,
+                    file: #fileID
                 )
                 )
+            }
+
+            // Store CGM state if needed
+            self.storeCGMState(glucose)
+        }
+    }
+
+    private func filterNewGlucoseValues(_ glucose: [BloodGlucose]) -> [BloodGlucose] {
+        let datesToCheck: Set<Date?> = Set(glucose.compactMap { $0.dateString as Date? })
+        let fetchRequest: NSFetchRequest<NSFetchRequestResult> = GlucoseStored.fetchRequest()
+        fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
+            NSPredicate(format: "date IN %@", datesToCheck),
+            NSPredicate.predicateForOneDayAgo
+        ])
+        fetchRequest.propertiesToFetch = ["date"]
+        fetchRequest.resultType = .dictionaryResultType
+
+        var existingDates = Set<Date>()
+        do {
+            let results = try coredataContext.fetch(fetchRequest) as? [NSDictionary]
+            existingDates = Set(results?.compactMap({ $0["date"] as? Date }) ?? [])
+        } catch {
+            debugPrint("Failed to fetch existing glucose dates: \(error)")
+        }
+
+        return glucose.filter { !existingDates.contains($0.dateString) }
+    }
+
+    private func storeGlucoseInCoreData(_ glucose: [BloodGlucose]) throws {
+        if glucose.count > 1 {
+            try storeGlucoseBatch(glucose)
+        } else {
+            try storeGlucoseRegular(glucose)
+        }
+    }
+
+    private func storeGlucoseRegular(_ glucose: [BloodGlucose]) throws {
+        for entry in glucose {
+            let glucoseEntry = GlucoseStored(context: coredataContext)
+            configureGlucoseEntry(glucoseEntry, with: entry)
+        }
 
 
-                // process batch insert
-                do {
-                    try self.coredataContext.execute(batchInsert)
-
-                    // Notify subscribers that there is a new glucose value
-                    // We need to do this because the due to the batch insert there is no ManagedObjectContext notification
-                    self.updateSubject.send(())
-                } catch {
-                    debugPrint(
-                        "Glucose Storage: \(#function) \(DebuggingIdentifiers.failed) failed to execute batch insert: \(error)"
-                    )
+        guard coredataContext.hasChanges else { return }
+        try coredataContext.save()
+    }
+
+    private func storeGlucoseBatch(_ glucose: [BloodGlucose]) throws {
+        var remainingGlucose = glucose
+        let batchInsert = NSBatchInsertRequest(
+            entity: GlucoseStored.entity(),
+            managedObjectHandler: { (managedObject: NSManagedObject) -> Bool in
+                guard let glucoseEntry = managedObject as? GlucoseStored,
+                      !remainingGlucose.isEmpty
+                else {
+                    return true
                 }
                 }
+                let entry = remainingGlucose.removeFirst()
+                self.configureGlucoseEntry(glucoseEntry, with: entry)
+                return false
+            }
+        )
+        try coredataContext.execute(batchInsert)
+        // Only send update for batch insert since regular save triggers CoreData notifications
+        updateSubject.send()
+    }
+
+    private func configureGlucoseEntry(_ entry: GlucoseStored, with glucose: BloodGlucose) {
+        entry.id = UUID()
+        entry.glucose = Int16(glucose.glucose ?? 0)
+        entry.date = glucose.dateString
+        entry.direction = glucose.direction?.rawValue
+        entry.isUploadedToNS = false
+        entry.isUploadedToHealth = false
+        entry.isUploadedToTidepool = false
+    }
 
 
-                debug(.deviceManager, "start storage cgmState")
-                self.storage.transaction { storage in
-                    let file = OpenAPS.Monitor.cgmState
-                    var treatments = storage.retrieve(file, as: [NightscoutTreatment].self) ?? []
-                    var updated = false
-                    for x in glucose {
-                        debug(.deviceManager, "storeGlucose \(x)")
-                        guard let sessionStartDate = x.sessionStartDate else {
-                            continue
-                        }
-                        if let lastTreatment = treatments.last,
-                           let createdAt = lastTreatment.createdAt,
-                           // When a new Dexcom sensor is started, it produces multiple consecutive
-                           // startDates. Disambiguate them by only allowing a session start per minute.
-                           abs(createdAt.timeIntervalSince(sessionStartDate)) < TimeInterval(60)
-                        {
-                            continue
-                        }
-                        var notes = ""
-                        if let t = x.transmitterID {
-                            notes = t
-                        }
-                        if let a = x.activationDate {
-                            notes = "\(notes) activated on \(a)"
-                        }
-                        let treatment = NightscoutTreatment(
-                            duration: nil,
-                            rawDuration: nil,
-                            rawRate: nil,
-                            absolute: nil,
-                            rate: nil,
-                            eventType: .nsSensorChange,
-                            createdAt: sessionStartDate,
-                            enteredBy: NightscoutTreatment.local,
-                            bolus: nil,
-                            insulin: nil,
-                            notes: notes,
-                            carbs: nil,
-                            fat: nil,
-                            protein: nil,
-                            targetTop: nil,
-                            targetBottom: nil
-                        )
-                        debug(.deviceManager, "CGM sensor change \(treatment)")
-                        treatments.append(treatment)
-                        updated = true
-                    }
-                    if updated {
-                        // We have to keep quite a bit of history as sensors start only every 10 days.
-                        storage.save(
-                            treatments.filter
-                                { $0.createdAt != nil && $0.createdAt!.addingTimeInterval(30.days.timeInterval) > Date() },
-                            as: file
-                        )
-                    }
+    private func storeCGMState(_ glucose: [BloodGlucose]) {
+        debug(.deviceManager, "start storage cgmState")
+        storage.transaction { storage in
+            let file = OpenAPS.Monitor.cgmState
+            var treatments = storage.retrieve(file, as: [NightscoutTreatment].self) ?? []
+            var updated = false
+
+            for x in glucose {
+                guard let sessionStartDate = x.sessionStartDate else { continue }
+
+                // Skip if we already have a recent treatment
+                if let lastTreatment = treatments.last,
+                   let createdAt = lastTreatment.createdAt,
+                   abs(createdAt.timeIntervalSince(sessionStartDate)) < TimeInterval(60)
+                {
+                    continue
                 }
                 }
+
+                let notes = createCGMStateNotes(transmitterID: x.transmitterID, activationDate: x.activationDate)
+                let treatment = createCGMStateTreatment(sessionStartDate: sessionStartDate, notes: notes)
+
+                debug(.deviceManager, "CGM sensor change \(treatment)")
+                treatments.append(treatment)
+                updated = true
+            }
+
+            if updated {
+                storage.save(
+                    treatments.filter { $0.createdAt?.addingTimeInterval(30.days.timeInterval) ?? .distantPast > Date() },
+                    as: file
+                )
             }
             }
         }
         }
     }
     }
 
 
+    private func createCGMStateNotes(transmitterID: String?, activationDate: Date?) -> String {
+        var notes = ""
+        if let t = transmitterID {
+            notes = t
+        }
+        if let a = activationDate {
+            notes = "\(notes) activated on \(a)"
+        }
+        return notes
+    }
+
+    private func createCGMStateTreatment(sessionStartDate: Date, notes: String) -> NightscoutTreatment {
+        NightscoutTreatment(
+            duration: nil,
+            rawDuration: nil,
+            rawRate: nil,
+            absolute: nil,
+            rate: nil,
+            eventType: .nsSensorChange,
+            createdAt: sessionStartDate,
+            enteredBy: NightscoutTreatment.local,
+            bolus: nil,
+            insulin: nil,
+            notes: notes,
+            carbs: nil,
+            fat: nil,
+            protein: nil,
+            targetTop: nil,
+            targetBottom: nil
+        )
+    }
+
     func addManualGlucose(glucose: Int) {
     func addManualGlucose(glucose: Int) {
         coredataContext.perform {
         coredataContext.perform {
             let newItem = GlucoseStored(context: self.coredataContext)
             let newItem = GlucoseStored(context: self.coredataContext)
@@ -190,7 +233,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                 try self.coredataContext.save()
                 try self.coredataContext.save()
 
 
                 // Glucose subscribers already listen to the update publisher, so call here to update glucose-related data.
                 // Glucose subscribers already listen to the update publisher, so call here to update glucose-related data.
-                self.updateSubject.send(())
+                self.updateSubject.send()
             } catch let error as NSError {
             } catch let error as NSError {
                 debugPrint(
                 debugPrint(
                     "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save manual glucose to Core Data with error: \(error)"
                     "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save manual glucose to Core Data with error: \(error)"
@@ -205,22 +248,30 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     }
     }
 
 
     func syncDate() -> Date {
     func syncDate() -> Date {
-        let fr = GlucoseStored.fetchRequest()
+        // Optimize fetch request to only get the date
+        let taskContext = CoreDataStack.shared.newTaskContext()
+        let fr = NSFetchRequest<NSDictionary>(entityName: "GlucoseStored")
         fr.predicate = NSPredicate.predicateForOneDayAgo
         fr.predicate = NSPredicate.predicateForOneDayAgo
-        fr.sortDescriptors = [NSSortDescriptor(keyPath: \GlucoseStored.date, ascending: false)]
+        fr.propertiesToFetch = ["date"]
         fr.fetchLimit = 1
         fr.fetchLimit = 1
+        fr.resultType = .dictionaryResultType
+        fr.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)]
 
 
-        var date: Date?
-        coredataContext.performAndWait {
+        var fetchedDate: Date = .distantPast
+
+        taskContext.performAndWait {
             do {
             do {
-                let results = try self.coredataContext.fetch(fr)
-                date = results.first?.date
-            } catch let error as NSError {
-                print("Fetch error: \(DebuggingIdentifiers.failed) \(error.localizedDescription), \(error.userInfo)")
+                if let result = try taskContext.fetch(fr).first,
+                   let date = result["date"] as? Date
+                {
+                    fetchedDate = date
+                }
+            } catch {
+                debugPrint("Fetch error: \(DebuggingIdentifiers.failed) \(error.localizedDescription)")
             }
             }
         }
         }
 
 
-        return date ?? .distantPast
+        return fetchedDate
     }
     }
 
 
     func lastGlucoseDate() -> Date {
     func lastGlucoseDate() -> Date {
@@ -262,9 +313,9 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
         return filtered
         return filtered
     }
     }
 
 
-    func fetchLatestGlucose() -> GlucoseStored? {
+    func fetchLatestGlucose() throws -> GlucoseStored? {
         let predicate = NSPredicate.predicateFor20MinAgo
         let predicate = NSPredicate.predicateFor20MinAgo
-        return (CoreDataStack.shared.fetchEntities(
+        return (try CoreDataStack.shared.fetchEntities(
             ofType: GlucoseStored.self,
             ofType: GlucoseStored.self,
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: predicate,
             predicate: predicate,
@@ -276,8 +327,8 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
 
     // Fetch glucose that is not uploaded to Nightscout yet
     // Fetch glucose that is not uploaded to Nightscout yet
     /// - Returns: Array of BloodGlucose to ensure the correct format for the NS Upload
     /// - Returns: Array of BloodGlucose to ensure the correct format for the NS Upload
-    func getGlucoseNotYetUploadedToNightscout() async -> [BloodGlucose] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getGlucoseNotYetUploadedToNightscout() async throws -> [BloodGlucose] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             ofType: GlucoseStored.self,
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: NSPredicate.glucoseNotYetUploadedToNightscout,
             predicate: NSPredicate.glucoseNotYetUploadedToNightscout,
@@ -285,8 +336,10 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        return await coredataContext.perform {
-            guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+        return try await coredataContext.perform {
+            guard let fetchedResults = results as? [GlucoseStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             return fetchedResults.map { result in
             return fetchedResults.map { result in
                 BloodGlucose(
                 BloodGlucose(
@@ -307,8 +360,8 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
 
     // Fetch manual glucose that is not uploaded to Nightscout yet
     // Fetch manual glucose that is not uploaded to Nightscout yet
     /// - Returns: Array of NightscoutTreatment to ensure the correct format for the NS Upload
     /// - Returns: Array of NightscoutTreatment to ensure the correct format for the NS Upload
-    func getManualGlucoseNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getManualGlucoseNotYetUploadedToNightscout() async throws -> [NightscoutTreatment] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             ofType: GlucoseStored.self,
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToNightscout,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToNightscout,
@@ -316,9 +369,11 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+        return try await coredataContext.perform {
+            guard let fetchedResults = results as? [GlucoseStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
-        return await coredataContext.perform {
             return fetchedResults.map { result in
             return fetchedResults.map { result in
                 NightscoutTreatment(
                 NightscoutTreatment(
                     duration: nil,
                     duration: nil,
@@ -361,8 +416,8 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
 
     // Fetch glucose that is not uploaded to Nightscout yet
     // Fetch glucose that is not uploaded to Nightscout yet
     /// - Returns: Array of BloodGlucose to ensure the correct format for the NS Upload
     /// - Returns: Array of BloodGlucose to ensure the correct format for the NS Upload
-    func getGlucoseNotYetUploadedToHealth() async -> [BloodGlucose] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getGlucoseNotYetUploadedToHealth() async throws -> [BloodGlucose] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             ofType: GlucoseStored.self,
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: NSPredicate.glucoseNotYetUploadedToHealth,
             predicate: NSPredicate.glucoseNotYetUploadedToHealth,
@@ -370,9 +425,11 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+        return try await coredataContext.perform {
+            guard let fetchedResults = results as? [GlucoseStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
-        return await coredataContext.perform {
             return fetchedResults.map { result in
             return fetchedResults.map { result in
                 BloodGlucose(
                 BloodGlucose(
                     _id: result.id?.uuidString ?? UUID().uuidString,
                     _id: result.id?.uuidString ?? UUID().uuidString,
@@ -391,8 +448,8 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
 
     // Fetch manual glucose that is not uploaded to Nightscout yet
     // Fetch manual glucose that is not uploaded to Nightscout yet
     /// - Returns: Array of NightscoutTreatment to ensure the correct format for the NS Upload
     /// - Returns: Array of NightscoutTreatment to ensure the correct format for the NS Upload
-    func getManualGlucoseNotYetUploadedToHealth() async -> [BloodGlucose] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getManualGlucoseNotYetUploadedToHealth() async throws -> [BloodGlucose] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             ofType: GlucoseStored.self,
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToHealth,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToHealth,
@@ -400,9 +457,11 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+        return try await coredataContext.perform {
+            guard let fetchedResults = results as? [GlucoseStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
-        return await coredataContext.perform {
             return fetchedResults.map { result in
             return fetchedResults.map { result in
                 BloodGlucose(
                 BloodGlucose(
                     _id: result.id?.uuidString ?? UUID().uuidString,
                     _id: result.id?.uuidString ?? UUID().uuidString,
@@ -421,8 +480,8 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
 
     // Fetch glucose that is not uploaded to Tidepool yet
     // Fetch glucose that is not uploaded to Tidepool yet
     /// - Returns: Array of StoredGlucoseSample to ensure the correct format for Tidepool upload
     /// - Returns: Array of StoredGlucoseSample to ensure the correct format for Tidepool upload
-    func getGlucoseNotYetUploadedToTidepool() async -> [StoredGlucoseSample] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getGlucoseNotYetUploadedToTidepool() async throws -> [StoredGlucoseSample] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             ofType: GlucoseStored.self,
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: NSPredicate.glucoseNotYetUploadedToTidepool,
             predicate: NSPredicate.glucoseNotYetUploadedToTidepool,
@@ -430,9 +489,11 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+        return try await coredataContext.perform {
+            guard let fetchedResults = results as? [GlucoseStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
-        return await coredataContext.perform {
             return fetchedResults.map { result in
             return fetchedResults.map { result in
                 BloodGlucose(
                 BloodGlucose(
                     _id: result.id?.uuidString ?? UUID().uuidString,
                     _id: result.id?.uuidString ?? UUID().uuidString,
@@ -452,8 +513,8 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
 
     // Fetch manual glucose that is not uploaded to Tidepool yet
     // Fetch manual glucose that is not uploaded to Tidepool yet
     /// - Returns: Array of StoredGlucoseSample to ensure the correct format for the Tidepool upload
     /// - Returns: Array of StoredGlucoseSample to ensure the correct format for the Tidepool upload
-    func getManualGlucoseNotYetUploadedToTidepool() async -> [StoredGlucoseSample] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getManualGlucoseNotYetUploadedToTidepool() async throws -> [StoredGlucoseSample] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             ofType: GlucoseStored.self,
             onContext: coredataContext,
             onContext: coredataContext,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToTidepool,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToTidepool,
@@ -461,9 +522,11 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+        return try await coredataContext.perform {
+            guard let fetchedResults = results as? [GlucoseStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
-        return await coredataContext.perform {
             return fetchedResults.map { result in
             return fetchedResults.map { result in
                 BloodGlucose(
                 BloodGlucose(
                     _id: result.id?.uuidString ?? UUID().uuidString,
                     _id: result.id?.uuidString ?? UUID().uuidString,
@@ -510,19 +573,24 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     var alarm: GlucoseAlarm? {
     var alarm: GlucoseAlarm? {
         /// glucose can not be older than 20 minutes due to the predicate in the fetch request
         /// glucose can not be older than 20 minutes due to the predicate in the fetch request
         coredataContext.performAndWait {
         coredataContext.performAndWait {
-            guard let glucose = fetchLatestGlucose() else { return nil }
+            do {
+                guard let glucose = try fetchLatestGlucose() else { return nil }
 
 
-            let glucoseValue = glucose.glucose
+                let glucoseValue = glucose.glucose
 
 
-            if Decimal(glucoseValue) <= settingsManager.settings.lowGlucose {
-                return .low
-            }
+                if Decimal(glucoseValue) <= settingsManager.settings.lowGlucose {
+                    return .low
+                }
 
 
-            if Decimal(glucoseValue) >= settingsManager.settings.highGlucose {
-                return .high
-            }
+                if Decimal(glucoseValue) >= settingsManager.settings.highGlucose {
+                    return .high
+                }
 
 
-            return nil
+                return nil
+            } catch {
+                debugPrint("Error fetching latest glucose: \(error)")
+                return nil
+            }
         }
         }
     }
     }
 }
 }

+ 55 - 47
Trio/Sources/APS/Storage/OverrideStorage.swift

@@ -3,17 +3,17 @@ import Foundation
 import Swinject
 import Swinject
 
 
 protocol OverrideStorage {
 protocol OverrideStorage {
-    func fetchLastCreatedOverride() async -> [NSManagedObjectID]
-    func loadLatestOverrideConfigurations(fetchLimit: Int) async -> [NSManagedObjectID]
-    func fetchForOverridePresets() async -> [NSManagedObjectID]
+    func fetchLastCreatedOverride() async throws -> [NSManagedObjectID]
+    func loadLatestOverrideConfigurations(fetchLimit: Int) async throws -> [NSManagedObjectID]
+    func fetchForOverridePresets() async throws -> [NSManagedObjectID]
     func calculateTarget(override: OverrideStored) -> Decimal
     func calculateTarget(override: OverrideStored) -> Decimal
-    func storeOverride(override: Override) async
+    func storeOverride(override: Override) async throws
     func copyRunningOverride(_ override: OverrideStored) async -> NSManagedObjectID
     func copyRunningOverride(_ override: OverrideStored) async -> NSManagedObjectID
     func deleteOverridePreset(_ objectID: NSManagedObjectID) async
     func deleteOverridePreset(_ objectID: NSManagedObjectID) async
-    func getOverridesNotYetUploadedToNightscout() async -> [NightscoutExercise]
-    func getOverrideRunsNotYetUploadedToNightscout() async -> [NightscoutExercise]
-    func getPresetOverridesForNightscout() async -> [NightscoutPresetOverride]
-    func fetchLatestActiveOverride() async -> NSManagedObjectID?
+    func getOverridesNotYetUploadedToNightscout() async throws -> [NightscoutExercise]
+    func getOverrideRunsNotYetUploadedToNightscout() async throws -> [NightscoutExercise]
+    func getPresetOverridesForNightscout() async throws -> [NightscoutPresetOverride]
+    func fetchLatestActiveOverride() async throws -> NSManagedObjectID?
 }
 }
 
 
 final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
 final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
@@ -34,8 +34,8 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
         return dateFormatter
         return dateFormatter
     }
     }
 
 
-    func fetchLastCreatedOverride() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func fetchLastCreatedOverride() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
             ofType: OverrideStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: NSPredicate(
             predicate: NSPredicate(
@@ -47,15 +47,17 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
             fetchLimit: 1
             fetchLimit: 1
         )
         )
 
 
-        return await backgroundContext.perform {
-            guard let fetchedResults = results as? [OverrideStored] else { return [] }
+        return try await backgroundContext.perform {
+            guard let fetchedResults = results as? [OverrideStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             return fetchedResults.map(\.objectID)
             return fetchedResults.map(\.objectID)
         }
         }
     }
     }
 
 
-    func loadLatestOverrideConfigurations(fetchLimit: Int) async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func loadLatestOverrideConfigurations(fetchLimit: Int) async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
             ofType: OverrideStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: NSPredicate.lastActiveOverride,
             predicate: NSPredicate.lastActiveOverride,
@@ -64,16 +66,18 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
             fetchLimit: fetchLimit
             fetchLimit: fetchLimit
         )
         )
 
 
-        return await backgroundContext.perform {
-            guard let fetchedResults = results as? [OverrideStored] else { return [] }
+        return try await backgroundContext.perform {
+            guard let fetchedResults = results as? [OverrideStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             return fetchedResults.map(\.objectID)
             return fetchedResults.map(\.objectID)
         }
         }
     }
     }
 
 
     /// Returns the NSManagedObjectID of the Override Presets
     /// Returns the NSManagedObjectID of the Override Presets
-    func fetchForOverridePresets() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func fetchForOverridePresets() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
             ofType: OverrideStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: NSPredicate.allOverridePresets,
             predicate: NSPredicate.allOverridePresets,
@@ -81,8 +85,10 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
             ascending: true
             ascending: true
         )
         )
 
 
-        return await backgroundContext.perform {
-            guard let fetchedResults = results as? [OverrideStored] else { return [] }
+        return try await backgroundContext.perform {
+            guard let fetchedResults = results as? [OverrideStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             return fetchedResults.map(\.objectID)
             return fetchedResults.map(\.objectID)
         }
         }
@@ -95,14 +101,14 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
         return overrideTarget.decimalValue
         return overrideTarget.decimalValue
     }
     }
 
 
-    func storeOverride(override: Override) async {
+    func storeOverride(override: Override) async throws {
         var presetCount = -1
         var presetCount = -1
         if override.isPreset {
         if override.isPreset {
-            let presets = await fetchForOverridePresets()
+            let presets = try await fetchForOverridePresets()
             presetCount = presets.count
             presetCount = presets.count
         }
         }
 
 
-        await backgroundContext.perform {
+        try await backgroundContext.perform {
             let newOverride = OverrideStored(context: self.backgroundContext)
             let newOverride = OverrideStored(context: self.backgroundContext)
 
 
             // override key meta data
             // override key meta data
@@ -151,14 +157,8 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
                 newOverride.smbIsScheduledOff = false
                 newOverride.smbIsScheduledOff = false
             }
             }
 
 
-            do {
-                guard self.backgroundContext.hasChanges else { return }
-                try self.backgroundContext.save()
-            } catch let error as NSError {
-                debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save Override Preset to Core Data with error: \(error.userInfo)"
-                )
-            }
+            guard self.backgroundContext.hasChanges else { return }
+            try self.backgroundContext.save()
         }
         }
     }
     }
 
 
@@ -206,8 +206,8 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
         await CoreDataStack.shared.deleteObject(identifiedBy: objectID)
         await CoreDataStack.shared.deleteObject(identifiedBy: objectID)
     }
     }
 
 
-    func getOverridesNotYetUploadedToNightscout() async -> [NightscoutExercise] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getOverridesNotYetUploadedToNightscout() async throws -> [NightscoutExercise] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
             ofType: OverrideStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: NSPredicate.lastActiveAdjustmentNotYetUploadedToNightscout,
             predicate: NSPredicate.lastActiveAdjustmentNotYetUploadedToNightscout,
@@ -215,8 +215,10 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        return await backgroundContext.perform {
-            guard let fetchedOverrides = results as? [OverrideStored] else { return [] }
+        return try await backgroundContext.perform {
+            guard let fetchedOverrides = results as? [OverrideStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             return fetchedOverrides.map { override in
             return fetchedOverrides.map { override in
                 let duration = override.indefinite ? 43200 : override.duration ?? 0 // 43200 min = 30 days
                 let duration = override.indefinite ? 43200 : override.duration ?? 0 // 43200 min = 30 days
@@ -232,8 +234,8 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
         }
         }
     }
     }
 
 
-    func getOverrideRunsNotYetUploadedToNightscout() async -> [NightscoutExercise] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getOverrideRunsNotYetUploadedToNightscout() async throws -> [NightscoutExercise] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideRunStored.self,
             ofType: OverrideRunStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: NSPredicate(
             predicate: NSPredicate(
@@ -245,8 +247,10 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        return await backgroundContext.perform {
-            guard let fetchedOverrideRuns = results as? [OverrideRunStored] else { return [] }
+        return try await backgroundContext.perform {
+            guard let fetchedOverrideRuns = results as? [OverrideRunStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             return fetchedOverrideRuns.map { overrideRun in
             return fetchedOverrideRuns.map { overrideRun in
                 var durationInMinutes = (overrideRun.endDate?.timeIntervalSince(overrideRun.startDate ?? Date()) ?? 1) / 60
                 var durationInMinutes = (overrideRun.endDate?.timeIntervalSince(overrideRun.startDate ?? Date()) ?? 1) / 60
@@ -263,8 +267,8 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
         }
         }
     }
     }
 
 
-    func getPresetOverridesForNightscout() async -> [NightscoutPresetOverride] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getPresetOverridesForNightscout() async throws -> [NightscoutPresetOverride] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
             ofType: OverrideStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: NSPredicate.allOverridePresets,
             predicate: NSPredicate.allOverridePresets,
@@ -272,8 +276,10 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
             ascending: true
             ascending: true
         )
         )
 
 
-        return await backgroundContext.perform {
-            guard let fetchedResults = results as? [OverrideStored] else { return [] }
+        return try await backgroundContext.perform {
+            guard let fetchedResults = results as? [OverrideStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             return fetchedResults.map { overrideStored in
             return fetchedResults.map { overrideStored in
                 let duration = overrideStored.duration as? Decimal != 0 ? overrideStored.duration as? Decimal : nil
                 let duration = overrideStored.duration as? Decimal != 0 ? overrideStored.duration as? Decimal : nil
@@ -290,8 +296,8 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
         }
         }
     }
     }
 
 
-    func fetchLatestActiveOverride() async -> NSManagedObjectID? {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func fetchLatestActiveOverride() async throws -> NSManagedObjectID? {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
             ofType: OverrideStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: NSPredicate.lastActiveOverride,
             predicate: NSPredicate.lastActiveOverride,
@@ -300,10 +306,12 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
             fetchLimit: 1
             fetchLimit: 1
         )
         )
 
 
-        return await backgroundContext.perform {
+        return try await backgroundContext.perform {
             guard let fetchedResults = results as? [OverrideStored],
             guard let fetchedResults = results as? [OverrideStored],
                   let latestOverride = fetchedResults.first
                   let latestOverride = fetchedResults.first
-            else { return nil }
+            else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             return latestOverride.objectID
             return latestOverride.objectID
         }
         }

+ 187 - 206
Trio/Sources/APS/Storage/PumpHistoryStorage.swift

@@ -11,13 +11,11 @@ protocol PumpHistoryObserver {
 
 
 protocol PumpHistoryStorage {
 protocol PumpHistoryStorage {
     var updatePublisher: AnyPublisher<Void, Never> { get }
     var updatePublisher: AnyPublisher<Void, Never> { get }
-    func storePumpEvents(_ events: [NewPumpEvent])
+    func storePumpEvents(_ events: [NewPumpEvent]) async throws
     func storeExternalInsulinEvent(amount: Decimal, timestamp: Date) async
     func storeExternalInsulinEvent(amount: Decimal, timestamp: Date) async
-    func recent() -> [PumpHistoryEvent]
-    func getPumpHistoryNotYetUploadedToNightscout() async -> [NightscoutTreatment]
-    func getPumpHistoryNotYetUploadedToHealth() async -> [PumpHistoryEvent]
-    func getPumpHistoryNotYetUploadedToTidepool() async -> [PumpHistoryEvent]
-    func deleteInsulin(at date: Date)
+    func getPumpHistoryNotYetUploadedToNightscout() async throws -> [NightscoutTreatment]
+    func getPumpHistoryNotYetUploadedToHealth() async throws -> [PumpHistoryEvent]
+    func getPumpHistoryNotYetUploadedToTidepool() async throws -> [PumpHistoryEvent]
 }
 }
 
 
 final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
 final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
@@ -27,6 +25,7 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
     @Injected() private var settings: SettingsManager!
     @Injected() private var settings: SettingsManager!
 
 
     private let updateSubject = PassthroughSubject<Void, Never>()
     private let updateSubject = PassthroughSubject<Void, Never>()
+    private let context = CoreDataStack.shared.newTaskContext()
 
 
     var updatePublisher: AnyPublisher<Void, Never> {
     var updatePublisher: AnyPublisher<Void, Never> {
         updateSubject.eraseToAnyPublisher()
         updateSubject.eraseToAnyPublisher()
@@ -39,190 +38,184 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
     typealias PumpEvent = PumpEventStored.EventType
     typealias PumpEvent = PumpEventStored.EventType
     typealias TempType = PumpEventStored.TempType
     typealias TempType = PumpEventStored.TempType
 
 
-    private let context = CoreDataStack.shared.newTaskContext()
-
     private func roundDose(_ dose: Double, toIncrement increment: Double) -> Decimal {
     private func roundDose(_ dose: Double, toIncrement increment: Double) -> Decimal {
         let roundedValue = (dose / increment).rounded() * increment
         let roundedValue = (dose / increment).rounded() * increment
         return Decimal(roundedValue)
         return Decimal(roundedValue)
     }
     }
 
 
-    func storePumpEvents(_ events: [NewPumpEvent]) {
-        processQueue.async {
-            self.context.perform {
-                for event in events {
-                    // Fetch to filter out duplicates
-                    // TODO: - move this to the Core Data Class
-
-                    let existingEvents: [PumpEventStored] = CoreDataStack.shared.fetchEntities(
-                        ofType: PumpEventStored.self,
-                        onContext: self.context,
-                        predicate: NSPredicate.duplicateInLastHour(event.date),
-                        key: "timestamp",
-                        ascending: false,
-                        batchSize: 50
-                    ) as? [PumpEventStored] ?? []
-
-                    switch event.type {
-                    case .bolus:
-
-                        guard let dose = event.dose else { continue }
-                        let amount = self.roundDose(
-                            dose.unitsInDeliverableIncrements,
-                            toIncrement: Double(self.settings.preferences.bolusIncrement)
-                        )
+    func storePumpEvents(_ events: [NewPumpEvent]) async throws {
+        try await context.perform {
+            for event in events {
+                let existingEvents: [PumpEventStored] = try CoreDataStack.shared.fetchEntities(
+                    ofType: PumpEventStored.self,
+                    onContext: self.context,
+                    predicate: NSPredicate.duplicateInLastHour(event.date),
+                    key: "timestamp",
+                    ascending: false,
+                    batchSize: 50
+                ) as? [PumpEventStored] ?? []
+
+                switch event.type {
+                case .bolus:
 
 
-                        guard existingEvents.isEmpty else {
-                            // Duplicate found, do not store the event
-                            print("Duplicate event found with timestamp: \(event.date)")
-
-                            if let existingEvent = existingEvents.first(where: { $0.type == EventType.bolus.rawValue }) {
-                                if existingEvent.timestamp == event.date {
-                                    if let existingAmount = existingEvent.bolus?.amount, amount < existingAmount as Decimal {
-                                        // Update existing event with new smaller value
-                                        existingEvent.bolus?.amount = amount as NSDecimalNumber
-                                        existingEvent.bolus?.isSMB = dose.automatic ?? true
-                                        existingEvent.isUploadedToNS = false
-                                        existingEvent.isUploadedToHealth = false
-                                        existingEvent.isUploadedToTidepool = false
-
-                                        print("Updated existing event with smaller value: \(amount)")
-                                    }
+                    guard let dose = event.dose else { continue }
+                    let amount = self.roundDose(
+                        dose.unitsInDeliverableIncrements,
+                        toIncrement: Double(self.settings.preferences.bolusIncrement)
+                    )
+
+                    guard existingEvents.isEmpty else {
+                        // Duplicate found, do not store the event
+                        print("Duplicate event found with timestamp: \(event.date)")
+
+                        if let existingEvent = existingEvents.first(where: { $0.type == EventType.bolus.rawValue }) {
+                            if existingEvent.timestamp == event.date {
+                                if let existingAmount = existingEvent.bolus?.amount, amount < existingAmount as Decimal {
+                                    // Update existing event with new smaller value
+                                    existingEvent.bolus?.amount = amount as NSDecimalNumber
+                                    existingEvent.bolus?.isSMB = dose.automatic ?? true
+                                    existingEvent.isUploadedToNS = false
+                                    existingEvent.isUploadedToHealth = false
+                                    existingEvent.isUploadedToTidepool = false
+
+                                    print("Updated existing event with smaller value: \(amount)")
                                 }
                                 }
                             }
                             }
-                            continue
                         }
                         }
+                        continue
+                    }
 
 
-                        let newPumpEvent = PumpEventStored(context: self.context)
-                        newPumpEvent.id = UUID().uuidString
-                        // restrict entry to now or past
-                        newPumpEvent.timestamp = event.date > Date() ? Date() : event.date
-                        newPumpEvent.type = PumpEvent.bolus.rawValue
-                        newPumpEvent.isUploadedToNS = false
-                        newPumpEvent.isUploadedToHealth = false
-                        newPumpEvent.isUploadedToTidepool = false
-
-                        let newBolusEntry = BolusStored(context: self.context)
-                        newBolusEntry.pumpEvent = newPumpEvent
-                        newBolusEntry.amount = NSDecimalNumber(decimal: amount)
-                        newBolusEntry.isExternal = dose.manuallyEntered
-                        newBolusEntry.isSMB = dose.automatic ?? true
-
-                    case .tempBasal:
-                        guard let dose = event.dose else { continue }
-
-                        guard existingEvents.isEmpty else {
-                            // Duplicate found, do not store the event
-                            print("Duplicate event found with timestamp: \(event.date)")
-                            continue
-                        }
+                    let newPumpEvent = PumpEventStored(context: self.context)
+                    newPumpEvent.id = UUID().uuidString
+                    // restrict entry to now or past
+                    newPumpEvent.timestamp = event.date > Date() ? Date() : event.date
+                    newPumpEvent.type = PumpEvent.bolus.rawValue
+                    newPumpEvent.isUploadedToNS = false
+                    newPumpEvent.isUploadedToHealth = false
+                    newPumpEvent.isUploadedToTidepool = false
+
+                    let newBolusEntry = BolusStored(context: self.context)
+                    newBolusEntry.pumpEvent = newPumpEvent
+                    newBolusEntry.amount = NSDecimalNumber(decimal: amount)
+                    newBolusEntry.isExternal = dose.manuallyEntered
+                    newBolusEntry.isSMB = dose.automatic ?? true
+
+                case .tempBasal:
+                    guard let dose = event.dose else { continue }
+
+                    guard existingEvents.isEmpty else {
+                        // Duplicate found, do not store the event
+                        print("Duplicate event found with timestamp: \(event.date)")
+                        continue
+                    }
 
 
-                        let rate = Decimal(dose.unitsPerHour)
-                        let minutes = (dose.endDate - dose.startDate).timeInterval / 60
-                        let delivered = dose.deliveredUnits
-                        let date = event.date
-
-                        let isCancel = delivered != nil
-                        guard !isCancel else { continue }
-
-                        let newPumpEvent = PumpEventStored(context: self.context)
-                        newPumpEvent.id = UUID().uuidString
-                        newPumpEvent.timestamp = date
-                        newPumpEvent.type = PumpEvent.tempBasal.rawValue
-                        newPumpEvent.isUploadedToNS = false
-                        newPumpEvent.isUploadedToHealth = false
-                        newPumpEvent.isUploadedToTidepool = false
-
-                        let newTempBasal = TempBasalStored(context: self.context)
-                        newTempBasal.pumpEvent = newPumpEvent
-                        newTempBasal.duration = Int16(round(minutes))
-                        newTempBasal.rate = rate as NSDecimalNumber
-                        newTempBasal.tempType = TempType.absolute.rawValue
-
-                    case .suspend:
-                        guard existingEvents.isEmpty else {
-                            // Duplicate found, do not store the event
-                            print("Duplicate event found with timestamp: \(event.date)")
-                            continue
-                        }
-                        let newPumpEvent = PumpEventStored(context: self.context)
-                        newPumpEvent.id = UUID().uuidString
-                        newPumpEvent.timestamp = event.date
-                        newPumpEvent.type = PumpEvent.pumpSuspend.rawValue
-                        newPumpEvent.isUploadedToNS = false
-                        newPumpEvent.isUploadedToHealth = false
-                        newPumpEvent.isUploadedToTidepool = false
-
-                    case .resume:
-                        guard existingEvents.isEmpty else {
-                            // Duplicate found, do not store the event
-                            print("Duplicate event found with timestamp: \(event.date)")
-                            continue
-                        }
-                        let newPumpEvent = PumpEventStored(context: self.context)
-                        newPumpEvent.id = UUID().uuidString
-                        newPumpEvent.timestamp = event.date
-                        newPumpEvent.type = PumpEvent.pumpResume.rawValue
-                        newPumpEvent.isUploadedToNS = false
-                        newPumpEvent.isUploadedToHealth = false
-                        newPumpEvent.isUploadedToTidepool = false
-
-                    case .rewind:
-                        guard existingEvents.isEmpty else {
-                            // Duplicate found, do not store the event
-                            print("Duplicate event found with timestamp: \(event.date)")
-                            continue
-                        }
-                        let newPumpEvent = PumpEventStored(context: self.context)
-                        newPumpEvent.id = UUID().uuidString
-                        newPumpEvent.timestamp = event.date
-                        newPumpEvent.type = PumpEvent.rewind.rawValue
-                        newPumpEvent.isUploadedToNS = false
-                        newPumpEvent.isUploadedToHealth = false
-                        newPumpEvent.isUploadedToTidepool = false
-
-                    case .prime:
-                        guard existingEvents.isEmpty else {
-                            // Duplicate found, do not store the event
-                            print("Duplicate event found with timestamp: \(event.date)")
-                            continue
-                        }
-                        let newPumpEvent = PumpEventStored(context: self.context)
-                        newPumpEvent.id = UUID().uuidString
-                        newPumpEvent.timestamp = event.date
-                        newPumpEvent.type = PumpEvent.prime.rawValue
-                        newPumpEvent.isUploadedToNS = false
-                        newPumpEvent.isUploadedToHealth = false
-                        newPumpEvent.isUploadedToTidepool = false
-
-                    case .alarm:
-                        guard existingEvents.isEmpty else {
-                            // Duplicate found, do not store the event
-                            print("Duplicate event found with timestamp: \(event.date)")
-                            continue
-                        }
-                        let newPumpEvent = PumpEventStored(context: self.context)
-                        newPumpEvent.id = UUID().uuidString
-                        newPumpEvent.timestamp = event.date
-                        newPumpEvent.type = PumpEvent.pumpAlarm.rawValue
-                        newPumpEvent.isUploadedToNS = false
-                        newPumpEvent.isUploadedToHealth = false
-                        newPumpEvent.isUploadedToTidepool = false
-                        newPumpEvent.note = event.title
-
-                    default:
+                    let rate = Decimal(dose.unitsPerHour)
+                    let minutes = (dose.endDate - dose.startDate).timeInterval / 60
+                    let delivered = dose.deliveredUnits
+                    let date = event.date
+
+                    let isCancel = delivered != nil
+                    guard !isCancel else { continue }
+
+                    let newPumpEvent = PumpEventStored(context: self.context)
+                    newPumpEvent.id = UUID().uuidString
+                    newPumpEvent.timestamp = date
+                    newPumpEvent.type = PumpEvent.tempBasal.rawValue
+                    newPumpEvent.isUploadedToNS = false
+                    newPumpEvent.isUploadedToHealth = false
+                    newPumpEvent.isUploadedToTidepool = false
+
+                    let newTempBasal = TempBasalStored(context: self.context)
+                    newTempBasal.pumpEvent = newPumpEvent
+                    newTempBasal.duration = Int16(round(minutes))
+                    newTempBasal.rate = rate as NSDecimalNumber
+                    newTempBasal.tempType = TempType.absolute.rawValue
+
+                case .suspend:
+                    guard existingEvents.isEmpty else {
+                        // Duplicate found, do not store the event
+                        print("Duplicate event found with timestamp: \(event.date)")
+                        continue
+                    }
+                    let newPumpEvent = PumpEventStored(context: self.context)
+                    newPumpEvent.id = UUID().uuidString
+                    newPumpEvent.timestamp = event.date
+                    newPumpEvent.type = PumpEvent.pumpSuspend.rawValue
+                    newPumpEvent.isUploadedToNS = false
+                    newPumpEvent.isUploadedToHealth = false
+                    newPumpEvent.isUploadedToTidepool = false
+
+                case .resume:
+                    guard existingEvents.isEmpty else {
+                        // Duplicate found, do not store the event
+                        print("Duplicate event found with timestamp: \(event.date)")
+                        continue
+                    }
+                    let newPumpEvent = PumpEventStored(context: self.context)
+                    newPumpEvent.id = UUID().uuidString
+                    newPumpEvent.timestamp = event.date
+                    newPumpEvent.type = PumpEvent.pumpResume.rawValue
+                    newPumpEvent.isUploadedToNS = false
+                    newPumpEvent.isUploadedToHealth = false
+                    newPumpEvent.isUploadedToTidepool = false
+
+                case .rewind:
+                    guard existingEvents.isEmpty else {
+                        // Duplicate found, do not store the event
+                        print("Duplicate event found with timestamp: \(event.date)")
+                        continue
+                    }
+                    let newPumpEvent = PumpEventStored(context: self.context)
+                    newPumpEvent.id = UUID().uuidString
+                    newPumpEvent.timestamp = event.date
+                    newPumpEvent.type = PumpEvent.rewind.rawValue
+                    newPumpEvent.isUploadedToNS = false
+                    newPumpEvent.isUploadedToHealth = false
+                    newPumpEvent.isUploadedToTidepool = false
+
+                case .prime:
+                    guard existingEvents.isEmpty else {
+                        // Duplicate found, do not store the event
+                        print("Duplicate event found with timestamp: \(event.date)")
+                        continue
+                    }
+                    let newPumpEvent = PumpEventStored(context: self.context)
+                    newPumpEvent.id = UUID().uuidString
+                    newPumpEvent.timestamp = event.date
+                    newPumpEvent.type = PumpEvent.prime.rawValue
+                    newPumpEvent.isUploadedToNS = false
+                    newPumpEvent.isUploadedToHealth = false
+                    newPumpEvent.isUploadedToTidepool = false
+
+                case .alarm:
+                    guard existingEvents.isEmpty else {
+                        // Duplicate found, do not store the event
+                        print("Duplicate event found with timestamp: \(event.date)")
                         continue
                         continue
                     }
                     }
+                    let newPumpEvent = PumpEventStored(context: self.context)
+                    newPumpEvent.id = UUID().uuidString
+                    newPumpEvent.timestamp = event.date
+                    newPumpEvent.type = PumpEvent.pumpAlarm.rawValue
+                    newPumpEvent.isUploadedToNS = false
+                    newPumpEvent.isUploadedToHealth = false
+                    newPumpEvent.isUploadedToTidepool = false
+                    newPumpEvent.note = event.title
+
+                default:
+                    continue
                 }
                 }
+            }
 
 
-                do {
-                    guard self.context.hasChanges else { return }
-                    try self.context.save()
+            do {
+                guard self.context.hasChanges else { return }
+                try self.context.save()
 
 
-                    self.updateSubject.send(())
-                    debugPrint("\(DebuggingIdentifiers.succeeded) stored pump events in Core Data")
-                } catch let error as NSError {
-                    debugPrint("\(DebuggingIdentifiers.failed) failed to store pump events with error: \(error.userInfo)")
-                }
+                self.updateSubject.send(())
+                debugPrint("\(DebuggingIdentifiers.succeeded) stored pump events in Core Data")
+            } catch let error as NSError {
+                debugPrint("\(DebuggingIdentifiers.failed) failed to store pump events with error: \(error.userInfo)")
+                throw error
             }
             }
         }
         }
     }
     }
@@ -258,24 +251,6 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
         }
         }
     }
     }
 
 
-    func recent() -> [PumpHistoryEvent] {
-        storage.retrieve(OpenAPS.Monitor.pumpHistory, as: [PumpHistoryEvent].self)?.reversed() ?? []
-    }
-
-    func deleteInsulin(at date: Date) {
-        processQueue.sync {
-            var allValues = storage.retrieve(OpenAPS.Monitor.pumpHistory, as: [PumpHistoryEvent].self) ?? []
-            guard let entryIndex = allValues.firstIndex(where: { $0.timestamp == date }) else {
-                return
-            }
-            allValues.remove(at: entryIndex)
-            storage.save(allValues, as: OpenAPS.Monitor.pumpHistory)
-            broadcaster.notify(PumpHistoryObserver.self, on: processQueue) {
-                $0.pumpHistoryDidUpdate(allValues)
-            }
-        }
-    }
-
     func determineBolusEventType(for event: PumpEventStored) -> PumpEventStored.EventType {
     func determineBolusEventType(for event: PumpEventStored) -> PumpEventStored.EventType {
         if event.bolus!.isSMB {
         if event.bolus!.isSMB {
             return .smb
             return .smb
@@ -286,8 +261,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
         return PumpEventStored.EventType(rawValue: event.type!) ?? PumpEventStored.EventType.bolus
         return PumpEventStored.EventType(rawValue: event.type!) ?? PumpEventStored.EventType.bolus
     }
     }
 
 
-    func getPumpHistoryNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getPumpHistoryNotYetUploadedToNightscout() async throws -> [NightscoutTreatment] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: PumpEventStored.self,
             ofType: PumpEventStored.self,
             onContext: context,
             onContext: context,
             predicate: NSPredicate.pumpEventsNotYetUploadedToNightscout,
             predicate: NSPredicate.pumpEventsNotYetUploadedToNightscout,
@@ -295,8 +270,10 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        return await context.perform { [self] in
-            guard let fetchedPumpEvents = results as? [PumpEventStored] else { return [] }
+        return try await context.perform { [self] in
+            guard let fetchedPumpEvents = results as? [PumpEventStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             return fetchedPumpEvents.map { event in
             return fetchedPumpEvents.map { event in
                 switch event.type {
                 switch event.type {
@@ -445,8 +422,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
         }
         }
     }
     }
 
 
-    func getPumpHistoryNotYetUploadedToHealth() async -> [PumpHistoryEvent] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getPumpHistoryNotYetUploadedToHealth() async throws -> [PumpHistoryEvent] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: PumpEventStored.self,
             ofType: PumpEventStored.self,
             onContext: context,
             onContext: context,
             predicate: NSPredicate.pumpEventsNotYetUploadedToHealth,
             predicate: NSPredicate.pumpEventsNotYetUploadedToHealth,
@@ -454,10 +431,12 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        guard let fetchedPumpEvents = results as? [PumpEventStored] else { return [] }
+        return try await context.perform {
+            guard let fetchedPumpEvents = results as? [PumpEventStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
-        return await context.perform {
-            fetchedPumpEvents.map { event in
+            return fetchedPumpEvents.map { event in
                 switch event.type {
                 switch event.type {
                 case PumpEvent.bolus.rawValue:
                 case PumpEvent.bolus.rawValue:
                     return PumpHistoryEvent(
                     return PumpHistoryEvent(
@@ -487,8 +466,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
         }
         }
     }
     }
 
 
-    func getPumpHistoryNotYetUploadedToTidepool() async -> [PumpHistoryEvent] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getPumpHistoryNotYetUploadedToTidepool() async throws -> [PumpHistoryEvent] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: PumpEventStored.self,
             ofType: PumpEventStored.self,
             onContext: context,
             onContext: context,
             predicate: NSPredicate.pumpEventsNotYetUploadedToTidepool,
             predicate: NSPredicate.pumpEventsNotYetUploadedToTidepool,
@@ -496,10 +475,12 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        guard let fetchedPumpEvents = results as? [PumpEventStored] else { return [] }
+        return try await context.perform {
+            guard let fetchedPumpEvents = results as? [PumpEventStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
-        return await context.perform {
-            fetchedPumpEvents.map { event in
+            return fetchedPumpEvents.map { event in
                 switch event.type {
                 switch event.type {
                 case PumpEvent.bolus.rawValue:
                 case PumpEvent.bolus.rawValue:
                     return PumpHistoryEvent(
                     return PumpHistoryEvent(

+ 49 - 38
Trio/Sources/APS/Storage/TempTargetsStorage.swift

@@ -8,18 +8,18 @@ protocol TempTargetsObserver {
 }
 }
 
 
 protocol TempTargetsStorage {
 protocol TempTargetsStorage {
-    func storeTempTarget(tempTarget: TempTarget) async
+    func storeTempTarget(tempTarget: TempTarget) async throws
     func saveTempTargetsToStorage(_ targets: [TempTarget])
     func saveTempTargetsToStorage(_ targets: [TempTarget])
-    func fetchForTempTargetPresets() async -> [NSManagedObjectID]
-    func fetchScheduledTempTargets() async -> [NSManagedObjectID]
-    func fetchScheduledTempTarget(for targetDate: Date) async -> [NSManagedObjectID]
+    func fetchForTempTargetPresets() async throws -> [NSManagedObjectID]
+    func fetchScheduledTempTargets() async throws -> [NSManagedObjectID]
+    func fetchScheduledTempTarget(for targetDate: Date) async throws -> [NSManagedObjectID]
     func copyRunningTempTarget(_ tempTarget: TempTargetStored) async -> NSManagedObjectID
     func copyRunningTempTarget(_ tempTarget: TempTargetStored) async -> NSManagedObjectID
     func deleteTempTargetPreset(_ objectID: NSManagedObjectID) async
     func deleteTempTargetPreset(_ objectID: NSManagedObjectID) async
-    func loadLatestTempTargetConfigurations(fetchLimit: Int) async -> [NSManagedObjectID]
+    func loadLatestTempTargetConfigurations(fetchLimit: Int) async throws -> [NSManagedObjectID]
     func syncDate() -> Date
     func syncDate() -> Date
     func recent() -> [TempTarget]
     func recent() -> [TempTarget]
-    func getTempTargetsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
-    func getTempTargetRunsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
+    func getTempTargetsNotYetUploadedToNightscout() async throws -> [NightscoutTreatment]
+    func getTempTargetRunsNotYetUploadedToNightscout() async throws -> [NightscoutTreatment]
     func presets() -> [TempTarget]
     func presets() -> [TempTarget]
     func current() -> TempTarget?
     func current() -> TempTarget?
     func existsTempTarget(with date: Date) async -> Bool
     func existsTempTarget(with date: Date) async -> Bool
@@ -38,8 +38,8 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
         injectServices(resolver)
         injectServices(resolver)
     }
     }
 
 
-    func loadLatestTempTargetConfigurations(fetchLimit: Int) async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func loadLatestTempTargetConfigurations(fetchLimit: Int) async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetStored.self,
             ofType: TempTargetStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: NSPredicate.lastActiveTempTarget,
             predicate: NSPredicate.lastActiveTempTarget,
@@ -48,16 +48,18 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
             fetchLimit: fetchLimit
             fetchLimit: fetchLimit
         )
         )
 
 
-        guard let fetchedResults = results as? [TempTargetStored] else { return [] }
+        return try await backgroundContext.perform {
+            guard let fetchedResults = results as? [TempTargetStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
-        return await backgroundContext.perform {
             return fetchedResults.map(\.objectID)
             return fetchedResults.map(\.objectID)
         }
         }
     }
     }
 
 
     /// Returns the NSManagedObjectID of the Temp Target Presets
     /// Returns the NSManagedObjectID of the Temp Target Presets
-    func fetchForTempTargetPresets() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func fetchForTempTargetPresets() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetStored.self,
             ofType: TempTargetStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: NSPredicate.allTempTargetPresets,
             predicate: NSPredicate.allTempTargetPresets,
@@ -65,17 +67,19 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
             ascending: true
             ascending: true
         )
         )
 
 
-        guard let fetchedResults = results as? [TempTargetStored] else { return [] }
+        return try await backgroundContext.perform {
+            guard let fetchedResults = results as? [TempTargetStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
-        return await backgroundContext.perform {
             return fetchedResults.map(\.objectID)
             return fetchedResults.map(\.objectID)
         }
         }
     }
     }
 
 
-    func fetchScheduledTempTargets() async -> [NSManagedObjectID] {
+    func fetchScheduledTempTargets() async throws -> [NSManagedObjectID] {
         let scheduledTempTargets = NSPredicate(format: "date > %@", Date() as NSDate)
         let scheduledTempTargets = NSPredicate(format: "date > %@", Date() as NSDate)
 
 
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetStored.self,
             ofType: TempTargetStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: scheduledTempTargets,
             predicate: scheduledTempTargets,
@@ -83,17 +87,19 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        guard let fetchedResults = results as? [TempTargetStored] else { return [] }
+        return try await backgroundContext.perform {
+            guard let fetchedResults = results as? [TempTargetStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
-        return await backgroundContext.perform {
             return fetchedResults.map(\.objectID)
             return fetchedResults.map(\.objectID)
         }
         }
     }
     }
 
 
-    func fetchScheduledTempTarget(for targetDate: Date) async -> [NSManagedObjectID] {
+    func fetchScheduledTempTarget(for targetDate: Date) async throws -> [NSManagedObjectID] {
         let predicate = NSPredicate(format: "date == %@", targetDate as NSDate)
         let predicate = NSPredicate(format: "date == %@", targetDate as NSDate)
 
 
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetStored.self,
             ofType: TempTargetStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: predicate,
             predicate: predicate,
@@ -102,21 +108,23 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
             fetchLimit: 1
             fetchLimit: 1
         )
         )
 
 
-        guard let fetchedResults = results as? [TempTargetStored] else { return [] }
+        return try await backgroundContext.perform {
+            guard let fetchedResults = results as? [TempTargetStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
-        return await backgroundContext.perform {
-            fetchedResults.map(\.objectID)
+            return fetchedResults.map(\.objectID)
         }
         }
     }
     }
 
 
-    func storeTempTarget(tempTarget: TempTarget) async {
+    func storeTempTarget(tempTarget: TempTarget) async throws {
         var presetCount = -1
         var presetCount = -1
         if tempTarget.isPreset == true {
         if tempTarget.isPreset == true {
-            let presets = await fetchForTempTargetPresets()
+            let presets = try await fetchForTempTargetPresets()
             presetCount = presets.count
             presetCount = presets.count
         }
         }
 
 
-        await backgroundContext.perform {
+        try await backgroundContext.perform {
             let newTempTarget = TempTargetStored(context: self.backgroundContext)
             let newTempTarget = TempTargetStored(context: self.backgroundContext)
             newTempTarget.date = tempTarget.createdAt
             newTempTarget.date = tempTarget.createdAt
             newTempTarget.id = UUID()
             newTempTarget.id = UUID()
@@ -144,9 +152,8 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
                 guard self.backgroundContext.hasChanges else { return }
                 guard self.backgroundContext.hasChanges else { return }
                 try self.backgroundContext.save()
                 try self.backgroundContext.save()
             } catch let error as NSError {
             } catch let error as NSError {
-                debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save Temp Target to Core Data with error: \(error.userInfo)"
-                )
+                debug(.default, "\(DebuggingIdentifiers.failed) Failed to save new temp target with error: \(error.userInfo)")
+                throw error
             }
             }
         }
         }
     }
     }
@@ -242,8 +249,8 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
         return last
         return last
     }
     }
 
 
-    func getTempTargetsNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getTempTargetsNotYetUploadedToNightscout() async throws -> [NightscoutTreatment] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetStored.self,
             ofType: TempTargetStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: NSPredicate.lastActiveAdjustmentNotYetUploadedToNightscout,
             predicate: NSPredicate.lastActiveAdjustmentNotYetUploadedToNightscout,
@@ -251,8 +258,10 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        return await backgroundContext.perform {
-            guard let fetchedTempTargets = results as? [TempTargetStored] else { return [] }
+        return try await backgroundContext.perform {
+            guard let fetchedTempTargets = results as? [TempTargetStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             return fetchedTempTargets.map { tempTarget in
             return fetchedTempTargets.map { tempTarget in
                 NightscoutTreatment(
                 NightscoutTreatment(
@@ -277,8 +286,8 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
         }
         }
     }
     }
 
 
-    func getTempTargetRunsNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func getTempTargetRunsNotYetUploadedToNightscout() async throws -> [NightscoutTreatment] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetRunStored.self,
             ofType: TempTargetRunStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: NSPredicate(
             predicate: NSPredicate(
@@ -290,8 +299,10 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        return await backgroundContext.perform {
-            guard let fetchedTempTargetRuns = results as? [TempTargetRunStored] else { return [] }
+        return try await backgroundContext.perform {
+            guard let fetchedTempTargetRuns = results as? [TempTargetRunStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             return fetchedTempTargetRuns.map { tempTargetRun in
             return fetchedTempTargetRuns.map { tempTargetRun in
                 var durationInMinutes = (tempTargetRun.endDate?.timeIntervalSince(tempTargetRun.startDate ?? Date()) ?? 1) / 60
                 var durationInMinutes = (tempTargetRun.endDate?.timeIntervalSince(tempTargetRun.startDate ?? Date()) ?? 1) / 60

+ 17 - 3
Trio/Sources/Application/AppDelegate.swift

@@ -23,8 +23,15 @@ class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject, UNUserNoti
             let pushMessage = try JSONDecoder().decode(PushMessage.self, from: jsonData)
             let pushMessage = try JSONDecoder().decode(PushMessage.self, from: jsonData)
 
 
             Task {
             Task {
-                await TrioRemoteControl.shared.handleRemoteNotification(pushMessage: pushMessage)
-                completionHandler(.newData)
+                do {
+                    try await TrioRemoteControl.shared.handleRemoteNotification(pushMessage: pushMessage)
+                    completionHandler(.newData)
+                } catch {
+                    debug(
+                        .default,
+                        "\(DebuggingIdentifiers.failed) failed to handle remote notification with error: \(error.localizedDescription)"
+                    )
+                }
             }
             }
         } catch {
         } catch {
             debug(.remoteControl, "Error decoding push message: \(error.localizedDescription)")
             debug(.remoteControl, "Error decoding push message: \(error.localizedDescription)")
@@ -40,7 +47,14 @@ class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject, UNUserNoti
         let token = tokenParts.joined()
         let token = tokenParts.joined()
 
 
         Task {
         Task {
-            await TrioRemoteControl.shared.handleAPNSChanges(deviceToken: token)
+            do {
+                try await TrioRemoteControl.shared.handleAPNSChanges(deviceToken: token)
+            } catch {
+                debug(
+                    .remoteControl,
+                    "\(DebuggingIdentifiers.failed) failed to register for remote notifications: \(error.localizedDescription)"
+                )
+            }
         }
         }
     }
     }
 
 

+ 15 - 10
Trio/Sources/Application/TrioApp.swift

@@ -75,11 +75,11 @@ import Swinject
         // Setup up the Core Data Stack
         // Setup up the Core Data Stack
         coreDataStack = CoreDataStack.shared
         coreDataStack = CoreDataStack.shared
 
 
+        // Explicitly initialize Core Data Stack
         do {
         do {
-            // Explicitly initialize Core Data Stacak
             try coreDataStack.initializeStack()
             try coreDataStack.initializeStack()
 
 
-            // Load services
+            // Only load services after successful Core Data initialization
             loadServices()
             loadServices()
 
 
             // Fix bug in iOS 18 related to the translucent tab bar
             // Fix bug in iOS 18 related to the translucent tab bar
@@ -88,12 +88,9 @@ import Swinject
             // Clear the persistentHistory and the NSManagedObjects that are older than 90 days every time the app starts
             // Clear the persistentHistory and the NSManagedObjects that are older than 90 days every time the app starts
             cleanupOldData()
             cleanupOldData()
         } catch {
         } catch {
-            debug(
-                .coreData,
-                "Failed to initialize Core Data Stack: \(error.localizedDescription)"
-            )
-            // Handle initialization failure
-            fatalError("Core Data Stack initialization failed: \(error.localizedDescription)")
+            debug(.coreData, "\(DebuggingIdentifiers.failed) Failed to initialize Core Data Stack: \(error.localizedDescription)")
+
+            fatalError("Core Data Stack initialization failed")
         }
         }
     }
     }
 
 
@@ -113,6 +110,14 @@ import Swinject
             if newScenePhase == .background {
             if newScenePhase == .background {
                 coreDataStack.save()
                 coreDataStack.save()
             }
             }
+
+            if newScenePhase == .active {
+                if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
+                   let rootVC = windowScene.windows.first(where: { $0.isKeyWindow })?.rootViewController
+                {
+                    AppVersionChecker.shared.checkAndNotifyVersionStatus(in: rootVC)
+                }
+            }
         }
         }
         .backgroundTask(.appRefresh("com.trio.cleanup")) {
         .backgroundTask(.appRefresh("com.trio.cleanup")) {
             await scheduleDatabaseCleaning()
             await scheduleDatabaseCleaning()
@@ -146,9 +151,9 @@ import Swinject
         request.earliestBeginDate = .now.addingTimeInterval(7 * 24 * 60 * 60) // 7 days
         request.earliestBeginDate = .now.addingTimeInterval(7 * 24 * 60 * 60) // 7 days
         do {
         do {
             try BGTaskScheduler.shared.submit(request)
             try BGTaskScheduler.shared.submit(request)
-            debugPrint("Task scheduled successfully")
+            debug(.coreData, "Task for cleaning database scheduled successfully")
         } catch {
         } catch {
-            debugPrint("Failed to schedule tasks")
+            debug(.coreData, "Failed to schedule tasks for cleaning database: \(error.localizedDescription)")
         }
         }
     }
     }
 
 

+ 1 - 1
Trio/Sources/Helpers/MainChartHelper.swift

@@ -50,7 +50,7 @@ enum MainChartHelper {
     }
     }
 
 
     static func bolusOffset(units: GlucoseUnits) -> Decimal {
     static func bolusOffset(units: GlucoseUnits) -> Decimal {
-        units == .mgdL ? 30 : 1.66
+        units == .mgdL ? 20 : (20 / 18)
     }
     }
 
 
     static func calculateDuration(
     static func calculateDuration(

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 228 - 237
Trio/Sources/Localizations/Main/Localizable.xcstrings


+ 4 - 0
Trio/Sources/Logger/Logger.swift

@@ -116,6 +116,7 @@ final class Logger {
     static let bolusState = Logger(category: .bolusState, reporter: baseReporter)
     static let bolusState = Logger(category: .bolusState, reporter: baseReporter)
     static let watchManager = Logger(category: .watchManager, reporter: baseReporter)
     static let watchManager = Logger(category: .watchManager, reporter: baseReporter)
     static let coreData = Logger(category: .coreData, reporter: baseReporter)
     static let coreData = Logger(category: .coreData, reporter: baseReporter)
+    static let storage = Logger(category: .storage, reporter: baseReporter)
 
 
     enum Category: String {
     enum Category: String {
         case `default`
         case `default`
@@ -129,6 +130,7 @@ final class Logger {
         case bolusState
         case bolusState
         case watchManager
         case watchManager
         case coreData
         case coreData
+        case storage
 
 
         var name: String {
         var name: String {
             rawValue.capitalizingFirstLetter()
             rawValue.capitalizingFirstLetter()
@@ -147,6 +149,7 @@ final class Logger {
             case .bolusState: return .bolusState
             case .bolusState: return .bolusState
             case .watchManager: return .watchManager
             case .watchManager: return .watchManager
             case .coreData: return .coreData
             case .coreData: return .coreData
+            case .storage: return .storage
             }
             }
         }
         }
 
 
@@ -163,6 +166,7 @@ final class Logger {
                  .openAPS,
                  .openAPS,
                  .remoteControl,
                  .remoteControl,
                  .service,
                  .service,
+                 .storage,
                  .watchManager:
                  .watchManager:
                 return OSLog(subsystem: subsystem, category: name)
                 return OSLog(subsystem: subsystem, category: name)
             }
             }

+ 133 - 91
Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+Overrides.swift

@@ -8,15 +8,16 @@ extension Adjustments.StateModel {
     /// Enacts an Override Preset by enabling it and disabling others.
     /// Enacts an Override Preset by enabling it and disabling others.
     @MainActor func enactOverridePreset(withID id: NSManagedObjectID) async {
     @MainActor func enactOverridePreset(withID id: NSManagedObjectID) async {
         do {
         do {
-            let overrideToEnact = try viewContext.existingObject(with: id) as? OverrideStored
-            overrideToEnact?.enabled = true
-            overrideToEnact?.date = Date()
-            overrideToEnact?.isUploadedToNS = false
-            isOverrideEnabled = true
-
-            await disableAllActiveOverrides(except: id, createOverrideRunEntry: currentActiveOverride != nil)
+            guard let overrideToEnact = try viewContext.existingObject(with: id) as? OverrideStored else { return }
+            /// Wait for currently active override to be disabled before storing the new one
+            await disableAllActiveOverrides(createOverrideRunEntry: currentActiveOverride != nil)
             await resetStateVariables()
             await resetStateVariables()
 
 
+            overrideToEnact.enabled = true
+            overrideToEnact.date = Date()
+            overrideToEnact.isUploadedToNS = false
+            isOverrideEnabled = true
+
             guard viewContext.hasChanges else { return }
             guard viewContext.hasChanges else { return }
             try viewContext.save()
             try viewContext.save()
 
 
@@ -29,19 +30,22 @@ extension Adjustments.StateModel {
     // MARK: - Disable Overrides
     // MARK: - Disable Overrides
 
 
     /// Disables all active Overrides, optionally creating a run entry.
     /// Disables all active Overrides, optionally creating a run entry.
-    @MainActor func disableAllActiveOverrides(except overrideID: NSManagedObjectID? = nil, createOverrideRunEntry: Bool) async {
-        // Get ALL NSManagedObject IDs of ALL active Override to cancel every single Override
-        let ids = await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0)
+    @MainActor func disableAllActiveOverrides(
+        except overrideID: NSManagedObjectID? = nil,
+        createOverrideRunEntry: Bool
+    ) async {
+        do {
+            // Get ALL NSManagedObject IDs of ALL active Override to cancel every single Override
+            let ids = try await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0)
 
 
-        await viewContext.perform {
-            do {
+            try await viewContext.perform {
                 // Fetch the existing OverrideStored objects from the context
                 // Fetch the existing OverrideStored objects from the context
                 let results = try ids.compactMap { id in
                 let results = try ids.compactMap { id in
                     try self.viewContext.existingObject(with: id) as? OverrideStored
                     try self.viewContext.existingObject(with: id) as? OverrideStored
                 }
                 }
                 guard !results.isEmpty else { return }
                 guard !results.isEmpty else { return }
 
 
-                // Check if we also need to create a corresponding OverrideRunStored entry, i.e. when the User uses the Cancel Button in Override View
+                // Check if we also need to create a corresponding OverrideRunStored entry
                 if createOverrideRunEntry {
                 if createOverrideRunEntry {
                     // Use the first override to create a new OverrideRunStored entry
                     // Use the first override to create a new OverrideRunStored entry
                     if let canceledOverride = results.first {
                     if let canceledOverride = results.first {
@@ -50,8 +54,9 @@ extension Adjustments.StateModel {
                         newOverrideRunStored.name = canceledOverride.name
                         newOverrideRunStored.name = canceledOverride.name
                         newOverrideRunStored.startDate = canceledOverride.date ?? .distantPast
                         newOverrideRunStored.startDate = canceledOverride.date ?? .distantPast
                         newOverrideRunStored.endDate = Date()
                         newOverrideRunStored.endDate = Date()
-                        newOverrideRunStored
-                            .target = NSDecimalNumber(decimal: self.overrideStorage.calculateTarget(override: canceledOverride))
+                        newOverrideRunStored.target = NSDecimalNumber(
+                            decimal: self.overrideStorage.calculateTarget(override: canceledOverride)
+                        )
                         newOverrideRunStored.override = canceledOverride
                         newOverrideRunStored.override = canceledOverride
                         newOverrideRunStored.isUploadedToNS = false
                         newOverrideRunStored.isUploadedToNS = false
                     }
                     }
@@ -67,11 +72,12 @@ extension Adjustments.StateModel {
                     try self.viewContext.save()
                     try self.viewContext.save()
                     self.updateLatestOverrideConfiguration()
                     self.updateLatestOverrideConfiguration()
                 }
                 }
-            } catch {
-                debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active Overrides: \(error.localizedDescription)"
-                )
             }
             }
+        } catch {
+            debug(
+                .default,
+                "\(DebuggingIdentifiers.failed) Failed to disable active overrides: \(error.localizedDescription)"
+            )
         }
         }
     }
     }
 
 
@@ -79,74 +85,89 @@ extension Adjustments.StateModel {
 
 
     /// Saves a custom Override and activates it.
     /// Saves a custom Override and activates it.
     func saveCustomOverride() async {
     func saveCustomOverride() async {
-        let override = Override(
-            name: overrideName,
-            enabled: true,
-            date: Date(),
-            duration: overrideDuration,
-            indefinite: indefinite,
-            percentage: overridePercentage,
-            smbIsOff: smbIsOff,
-            isPreset: isPreset,
-            id: id,
-            overrideTarget: shouldOverrideTarget,
-            target: target,
-            advancedSettings: advancedSettings,
-            isfAndCr: isfAndCr,
-            isf: isf,
-            cr: cr,
-            smbIsScheduledOff: smbIsScheduledOff,
-            start: start,
-            end: end,
-            smbMinutes: smbMinutes,
-            uamMinutes: uamMinutes
-        )
-
-        // First disable all Overrides
-        await disableAllActiveOverrides(createOverrideRunEntry: true)
-
-        // Then save and activate a new custom Override
-        await overrideStorage.storeOverride(override: override)
-
-        // Reset State variables
-        await resetStateVariables()
-
-        // Update View
-        updateLatestOverrideConfiguration()
+        do {
+            let override = Override(
+                name: overrideName,
+                enabled: true,
+                date: Date(),
+                duration: overrideDuration,
+                indefinite: indefinite,
+                percentage: overridePercentage,
+                smbIsOff: smbIsOff,
+                isPreset: isPreset,
+                id: id,
+                overrideTarget: shouldOverrideTarget,
+                target: target,
+                advancedSettings: advancedSettings,
+                isfAndCr: isfAndCr,
+                isf: isf,
+                cr: cr,
+                smbIsScheduledOff: smbIsScheduledOff,
+                start: start,
+                end: end,
+                smbMinutes: smbMinutes,
+                uamMinutes: uamMinutes
+            )
+
+            // First disable all Overrides
+            await disableAllActiveOverrides(createOverrideRunEntry: true)
+
+            // Then save and activate a new custom Override
+            try await overrideStorage.storeOverride(override: override)
+
+            // Reset State variables
+            await resetStateVariables()
+
+            // Update View
+            updateLatestOverrideConfiguration()
+        } catch {
+            debug(
+                .default,
+                "\(DebuggingIdentifiers.failed) Failed to save custom override: \(error.localizedDescription)"
+            )
+        }
     }
     }
 
 
     /// Saves an Override Preset without activating it.
     /// Saves an Override Preset without activating it.
     /// `enabled` has to be false
     /// `enabled` has to be false
     /// `isPreset` has to be true
     /// `isPreset` has to be true
     func saveOverridePreset() async {
     func saveOverridePreset() async {
-        let preset = Override(
-            name: overrideName,
-            enabled: false,
-            date: Date(),
-            duration: overrideDuration,
-            indefinite: indefinite,
-            percentage: overridePercentage,
-            smbIsOff: smbIsOff,
-            isPreset: true,
-            id: id,
-            overrideTarget: shouldOverrideTarget,
-            target: target,
-            advancedSettings: advancedSettings,
-            isfAndCr: isfAndCr,
-            isf: isf,
-            cr: cr,
-            smbIsScheduledOff: smbIsScheduledOff,
-            start: start,
-            end: end,
-            smbMinutes: smbMinutes,
-            uamMinutes: uamMinutes
-        )
-
-        async let storeOverride: () = overrideStorage.storeOverride(override: preset)
-        async let resetState: () = resetStateVariables()
-        _ = await (storeOverride, resetState)
-        setupOverridePresetsArray()
-        await nightscoutManager.uploadProfiles()
+        do {
+            let preset = Override(
+                name: overrideName,
+                enabled: false,
+                date: Date(),
+                duration: overrideDuration,
+                indefinite: indefinite,
+                percentage: overridePercentage,
+                smbIsOff: smbIsOff,
+                isPreset: true,
+                id: id,
+                overrideTarget: shouldOverrideTarget,
+                target: target,
+                advancedSettings: advancedSettings,
+                isfAndCr: isfAndCr,
+                isf: isf,
+                cr: cr,
+                smbIsScheduledOff: smbIsScheduledOff,
+                start: start,
+                end: end,
+                smbMinutes: smbMinutes,
+                uamMinutes: uamMinutes
+            )
+
+            async let storeOverride: () = overrideStorage.storeOverride(override: preset)
+            async let resetState: () = resetStateVariables()
+            _ = try await (storeOverride, resetState)
+
+            setupOverridePresetsArray()
+            try await nightscoutManager.uploadProfiles()
+        } catch {
+            debug(
+                .default,
+                "\(DebuggingIdentifiers.failed) Failed to save override preset: \(error.localizedDescription)"
+            )
+        }
     }
     }
 
 
     // MARK: - Override Preset Management
     // MARK: - Override Preset Management
@@ -154,8 +175,15 @@ extension Adjustments.StateModel {
     /// Sets up the array of Override Presets for UI display.
     /// Sets up the array of Override Presets for UI display.
     func setupOverridePresetsArray() {
     func setupOverridePresetsArray() {
         Task {
         Task {
-            let ids = await overrideStorage.fetchForOverridePresets()
-            await updateOverridePresetsArray(with: ids)
+            do {
+                let ids = try await overrideStorage.fetchForOverridePresets()
+                await updateOverridePresetsArray(with: ids)
+            } catch {
+                debug(
+                    .default,
+                    "\(DebuggingIdentifiers.failed) Failed to setup override presets: \(error.localizedDescription)"
+                )
+            }
         }
         }
     }
     }
 
 
@@ -175,9 +203,16 @@ extension Adjustments.StateModel {
 
 
     /// Deletes an Override Preset and updates the view.
     /// Deletes an Override Preset and updates the view.
     func invokeOverridePresetDeletion(_ objectID: NSManagedObjectID) async {
     func invokeOverridePresetDeletion(_ objectID: NSManagedObjectID) async {
-        await overrideStorage.deleteOverridePreset(objectID)
-        setupOverridePresetsArray()
-        await nightscoutManager.uploadProfiles()
+        do {
+            await overrideStorage.deleteOverridePreset(objectID)
+            setupOverridePresetsArray()
+            try await nightscoutManager.uploadProfiles()
+        } catch {
+            debug(
+                .default,
+                "\(DebuggingIdentifiers.failed) Failed to delete override preset: \(error.localizedDescription)"
+            )
+        }
     }
     }
 
 
     // MARK: - Update Latest Override Configuration
     // MARK: - Update Latest Override Configuration
@@ -188,13 +223,20 @@ extension Adjustments.StateModel {
     /// This also needs to be called when we cancel an Override via the Home View to update the State of the Button for this case
     /// This also needs to be called when we cancel an Override via the Home View to update the State of the Button for this case
     func updateLatestOverrideConfiguration() {
     func updateLatestOverrideConfiguration() {
         Task { [weak self] in
         Task { [weak self] in
-            guard let self = self else { return }
+            do {
+                guard let self = self else { return }
 
 
-            let id = await self.overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 1)
+                let id = try await self.overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 1)
 
 
-            // execute sequentially instead of concurrently
-            await self.updateLatestOverrideConfigurationOfState(from: id)
-            await self.setCurrentOverride(from: id)
+                // execute sequentially instead of concurrently
+                await self.updateLatestOverrideConfigurationOfState(from: id)
+                await self.setCurrentOverride(from: id)
+            } catch {
+                debug(
+                    .default,
+                    "\(DebuggingIdentifiers.failed) Failed to update override configuration: \(error.localizedDescription)"
+                )
+            }
         }
         }
     }
     }
 
 

+ 90 - 70
Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+TempTargets.swift

@@ -11,10 +11,17 @@ extension Adjustments.StateModel {
     /// This also needs to be called when we cancel an Temp Target via the Home View to update the State of the Button for this case
     /// This also needs to be called when we cancel an Temp Target via the Home View to update the State of the Button for this case
     func updateLatestTempTargetConfiguration() {
     func updateLatestTempTargetConfiguration() {
         Task {
         Task {
-            let id = await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 1)
-            async let updateState: () = updateLatestTempTargetConfigurationOfState(from: id)
-            async let setTempTarget: () = setCurrentTempTarget(from: id)
-            _ = await (updateState, setTempTarget)
+            do {
+                let id = try await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 1)
+                async let updateState: () = updateLatestTempTargetConfigurationOfState(from: id)
+                async let setTempTarget: () = setCurrentTempTarget(from: id)
+                _ = await (updateState, setTempTarget)
+            } catch {
+                debug(
+                    .default,
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to load latest temp target configuration with error: \(error.localizedDescription)"
+                )
+            }
         }
         }
     }
     }
 
 
@@ -58,13 +65,20 @@ extension Adjustments.StateModel {
 
 
     /// Sets up Temp Targets using fetch and update functions.
     /// Sets up Temp Targets using fetch and update functions.
     func setupTempTargets(
     func setupTempTargets(
-        fetchFunction: @escaping () async -> [NSManagedObjectID],
+        fetchFunction: @escaping () async throws -> [NSManagedObjectID],
         updateFunction: @escaping @MainActor([TempTargetStored]) -> Void
         updateFunction: @escaping @MainActor([TempTargetStored]) -> Void
     ) {
     ) {
         Task {
         Task {
-            let ids = await fetchFunction()
-            let tempTargetObjects = await fetchTempTargetObjects(for: ids)
-            await updateFunction(tempTargetObjects)
+            do {
+                let ids = try await fetchFunction()
+                let tempTargetObjects = await fetchTempTargetObjects(for: ids)
+                await updateFunction(tempTargetObjects)
+            } catch {
+                debug(
+                    .default,
+                    "\(DebuggingIdentifiers.failed) Failed to setup temp targets: \(error.localizedDescription)"
+                )
+            }
         }
         }
     }
     }
 
 
@@ -83,7 +97,7 @@ extension Adjustments.StateModel {
     /// Sets up the Temp Target presets array for the view.
     /// Sets up the Temp Target presets array for the view.
     func setupTempTargetPresetsArray() {
     func setupTempTargetPresetsArray() {
         setupTempTargets(
         setupTempTargets(
-            fetchFunction: tempTargetStorage.fetchForTempTargetPresets,
+            fetchFunction: { try await self.tempTargetStorage.fetchForTempTargetPresets() },
             updateFunction: { tempTargets in
             updateFunction: { tempTargets in
                 self.tempTargetPresets = tempTargets
                 self.tempTargetPresets = tempTargets
             }
             }
@@ -93,7 +107,7 @@ extension Adjustments.StateModel {
     /// Sets up the scheduled Temp Targets array for the view.
     /// Sets up the scheduled Temp Targets array for the view.
     func setupScheduledTempTargetsArray() {
     func setupScheduledTempTargetsArray() {
         setupTempTargets(
         setupTempTargets(
-            fetchFunction: tempTargetStorage.fetchScheduledTempTargets,
+            fetchFunction: { try await self.tempTargetStorage.fetchScheduledTempTargets() },
             updateFunction: { tempTargets in
             updateFunction: { tempTargets in
                 self.scheduledTempTargets = tempTargets
                 self.scheduledTempTargets = tempTargets
             }
             }
@@ -108,16 +122,16 @@ extension Adjustments.StateModel {
     }
     }
 
 
     /// Saves a Temp Target based on whether it is scheduled or custom.
     /// Saves a Temp Target based on whether it is scheduled or custom.
-    func invokeSaveOfCustomTempTargets() async {
+    func invokeSaveOfCustomTempTargets() async throws {
         if date > Date() {
         if date > Date() {
-            await saveScheduledTempTarget()
+            try await saveScheduledTempTarget()
         } else {
         } else {
-            await saveCustomTempTarget()
+            try await saveCustomTempTarget()
         }
         }
     }
     }
 
 
     /// Saves a scheduled Temp Target and activates it at the specified date.
     /// Saves a scheduled Temp Target and activates it at the specified date.
-    func saveScheduledTempTarget() async {
+    func saveScheduledTempTarget() async throws {
         let date = self.date
         let date = self.date
         guard date > Date() else { return }
         guard date > Date() else { return }
 
 
@@ -133,38 +147,45 @@ extension Adjustments.StateModel {
             enabled: false,
             enabled: false,
             halfBasalTarget: halfBasalTarget
             halfBasalTarget: halfBasalTarget
         )
         )
-        await tempTargetStorage.storeTempTarget(tempTarget: tempTarget)
+        try await tempTargetStorage.storeTempTarget(tempTarget: tempTarget)
         setupScheduledTempTargetsArray()
         setupScheduledTempTargetsArray()
-
-        Task {
-            await waitUntilDate(date)
-            await disableAllActiveTempTargets(createTempTargetRunEntry: true)
-            await enableScheduledTempTarget(for: date)
-            tempTargetStorage.saveTempTargetsToStorage([tempTarget])
-        }
+        await waitUntilDate(date)
+        await disableAllActiveTempTargets(createTempTargetRunEntry: true)
+        await enableScheduledTempTarget(for: date)
+        tempTargetStorage.saveTempTargetsToStorage([tempTarget])
     }
     }
 
 
     /// Enables a scheduled Temp Target for a specific date.
     /// Enables a scheduled Temp Target for a specific date.
     func enableScheduledTempTarget(for date: Date) async {
     func enableScheduledTempTarget(for date: Date) async {
-        let ids = await tempTargetStorage.fetchScheduledTempTarget(for: date)
-        guard let firstID = ids.first else {
-            debugPrint("No Temp Target found for the specified date.")
-            return
-        }
-        await setCurrentTempTarget(from: ids)
-
-        await MainActor.run {
-            do {
-                if let tempTarget = try viewContext.existingObject(with: firstID) as? TempTargetStored {
-                    tempTarget.enabled = true
-                    try viewContext.save()
-                    isTempTargetEnabled = true
+        do {
+            let ids = try await tempTargetStorage.fetchScheduledTempTarget(for: date)
+            guard let firstID = ids.first else {
+                debug(.default, "No Temp Target found for the specified date.")
+                return
+            }
+            await setCurrentTempTarget(from: ids)
+
+            try await MainActor.run {
+                guard let tempTarget = try viewContext.existingObject(with: firstID) as? TempTargetStored else {
+                    throw NSError(
+                        domain: "TempTarget",
+                        code: -1,
+                        userInfo: [NSLocalizedDescriptionKey: "Failed to find temp target"]
+                    )
                 }
                 }
-            } catch {
-                debugPrint("Failed to enable the Temp Target: \(error.localizedDescription)")
+
+                tempTarget.enabled = true
+                try viewContext.save()
+                isTempTargetEnabled = true
             }
             }
+
+            setupScheduledTempTargetsArray()
+        } catch {
+            debug(
+                .default,
+                "\(DebuggingIdentifiers.failed) Failed to enable scheduled temp target: \(error.localizedDescription)"
+            )
         }
         }
-        setupScheduledTempTargetsArray()
     }
     }
 
 
     /// Waits until a target date before proceeding.
     /// Waits until a target date before proceeding.
@@ -177,7 +198,7 @@ extension Adjustments.StateModel {
     }
     }
 
 
     /// Saves a custom Temp Target and disables existing ones.
     /// Saves a custom Temp Target and disables existing ones.
-    func saveCustomTempTarget() async {
+    func saveCustomTempTarget() async throws {
         await disableAllActiveTempTargets(createTempTargetRunEntry: true)
         await disableAllActiveTempTargets(createTempTargetRunEntry: true)
         let tempTarget = TempTarget(
         let tempTarget = TempTarget(
             name: tempTargetName,
             name: tempTargetName,
@@ -192,7 +213,7 @@ extension Adjustments.StateModel {
             enabled: true,
             enabled: true,
             halfBasalTarget: halfBasalTarget
             halfBasalTarget: halfBasalTarget
         )
         )
-        await tempTargetStorage.storeTempTarget(tempTarget: tempTarget)
+        try await tempTargetStorage.storeTempTarget(tempTarget: tempTarget)
         tempTargetStorage.saveTempTargetsToStorage([tempTarget])
         tempTargetStorage.saveTempTargetsToStorage([tempTarget])
         await resetTempTargetState()
         await resetTempTargetState()
         isTempTargetEnabled = true
         isTempTargetEnabled = true
@@ -200,7 +221,7 @@ extension Adjustments.StateModel {
     }
     }
 
 
     /// Creates a new Temp Target preset.
     /// Creates a new Temp Target preset.
-    func saveTempTargetPreset() async {
+    func saveTempTargetPreset() async throws {
         let tempTarget = TempTarget(
         let tempTarget = TempTarget(
             name: tempTargetName,
             name: tempTargetName,
             createdAt: Date(),
             createdAt: Date(),
@@ -213,7 +234,7 @@ extension Adjustments.StateModel {
             enabled: false,
             enabled: false,
             halfBasalTarget: halfBasalTarget
             halfBasalTarget: halfBasalTarget
         )
         )
-        await tempTargetStorage.storeTempTarget(tempTarget: tempTarget)
+        try await tempTargetStorage.storeTempTarget(tempTarget: tempTarget)
         await resetTempTargetState()
         await resetTempTargetState()
         setupTempTargetPresetsArray()
         setupTempTargetPresetsArray()
     }
     }
@@ -221,19 +242,15 @@ extension Adjustments.StateModel {
     /// Enacts a Temp Target preset by enabling it.
     /// Enacts a Temp Target preset by enabling it.
     @MainActor func enactTempTargetPreset(withID id: NSManagedObjectID) async {
     @MainActor func enactTempTargetPreset(withID id: NSManagedObjectID) async {
         do {
         do {
-            let tempTargetToEnact = try viewContext.existingObject(with: id) as? TempTargetStored
-            tempTargetToEnact?.enabled = true
-            tempTargetToEnact?.date = Date()
-            tempTargetToEnact?.isUploadedToNS = false
-            isTempTargetEnabled = true
-
-            async let disableTempTargets: () = disableAllActiveTempTargets(
-                except: id,
-                createTempTargetRunEntry: currentActiveTempTarget != nil
-            )
-            async let resetState: () = resetTempTargetState()
-            _ = await (disableTempTargets, resetState)
+            guard let tempTargetToEnact = try viewContext.existingObject(with: id) as? TempTargetStored else { return }
+            /// Wait for currently active temp target to be disabled before storing the new temp target
+            await disableAllActiveTempTargets(createTempTargetRunEntry: true)
+            await resetTempTargetState()
 
 
+            tempTargetToEnact.enabled = true
+            tempTargetToEnact.date = Date()
+            tempTargetToEnact.isUploadedToNS = false
+            isTempTargetEnabled = true
             if viewContext.hasChanges {
             if viewContext.hasChanges {
                 try viewContext.save()
                 try viewContext.save()
             }
             }
@@ -241,11 +258,11 @@ extension Adjustments.StateModel {
             updateLatestTempTargetConfiguration()
             updateLatestTempTargetConfiguration()
 
 
             let tempTarget = TempTarget(
             let tempTarget = TempTarget(
-                name: tempTargetToEnact?.name,
+                name: tempTargetToEnact.name,
                 createdAt: Date(),
                 createdAt: Date(),
-                targetTop: tempTargetToEnact?.target?.decimalValue,
-                targetBottom: tempTargetToEnact?.target?.decimalValue,
-                duration: tempTargetToEnact?.duration?.decimalValue ?? 0,
+                targetTop: tempTargetToEnact.target?.decimalValue,
+                targetBottom: tempTargetToEnact.target?.decimalValue,
+                duration: tempTargetToEnact.duration?.decimalValue ?? 0,
                 enteredBy: TempTarget.local,
                 enteredBy: TempTarget.local,
                 reason: TempTarget.custom,
                 reason: TempTarget.custom,
                 isPreset: true,
                 isPreset: true,
@@ -259,12 +276,15 @@ extension Adjustments.StateModel {
     }
     }
 
 
     /// Disables all active Temp Targets.
     /// Disables all active Temp Targets.
-    @MainActor func disableAllActiveTempTargets(except id: NSManagedObjectID? = nil, createTempTargetRunEntry: Bool) async {
-        // Get ALL NSManagedObject IDs of ALL active Temp Targets to cancel every single Temp Target
-        let ids = await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 0) // 0 = no fetch limit
+    @MainActor func disableAllActiveTempTargets(
+        except id: NSManagedObjectID? = nil,
+        createTempTargetRunEntry: Bool
+    ) async {
+        do {
+            // Get ALL NSManagedObject IDs of ALL active Temp Targets to cancel every single Temp Target
+            let ids = try await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 0) // 0 = no fetch limit
 
 
-        await viewContext.perform {
-            do {
+            try await viewContext.perform {
                 // Fetch the existing TempTargetStored objects from the context
                 // Fetch the existing TempTargetStored objects from the context
                 let results = try ids.compactMap { id in
                 let results = try ids.compactMap { id in
                     try self.viewContext.existingObject(with: id) as? TempTargetStored
                     try self.viewContext.existingObject(with: id) as? TempTargetStored
@@ -273,7 +293,7 @@ extension Adjustments.StateModel {
                 // If there are no results, return early
                 // If there are no results, return early
                 guard !results.isEmpty else { return }
                 guard !results.isEmpty else { return }
 
 
-                // Check if we also need to create a corresponding TempTargetRunStored entry, i.e. when the User uses the Cancel Button in Temp Target View
+                // Check if we also need to create a corresponding TempTargetRunStored entry
                 if createTempTargetRunEntry {
                 if createTempTargetRunEntry {
                     // Use the first temp target to create a new TempTargetRunStored entry
                     // Use the first temp target to create a new TempTargetRunStored entry
                     if let canceledTempTarget = results.first {
                     if let canceledTempTarget = results.first {
@@ -282,8 +302,7 @@ extension Adjustments.StateModel {
                         newTempTargetRunStored.name = canceledTempTarget.name
                         newTempTargetRunStored.name = canceledTempTarget.name
                         newTempTargetRunStored.startDate = canceledTempTarget.date ?? .distantPast
                         newTempTargetRunStored.startDate = canceledTempTarget.date ?? .distantPast
                         newTempTargetRunStored.endDate = Date()
                         newTempTargetRunStored.endDate = Date()
-                        newTempTargetRunStored
-                            .target = canceledTempTarget.target ?? 0
+                        newTempTargetRunStored.target = canceledTempTarget.target ?? 0
                         newTempTargetRunStored.tempTarget = canceledTempTarget
                         newTempTargetRunStored.tempTarget = canceledTempTarget
                         newTempTargetRunStored.isUploadedToNS = false
                         newTempTargetRunStored.isUploadedToNS = false
                     }
                     }
@@ -303,11 +322,12 @@ extension Adjustments.StateModel {
                     // Update the storage
                     // Update the storage
                     self.tempTargetStorage.saveTempTargetsToStorage([TempTarget.cancel(at: Date().addingTimeInterval(-1))])
                     self.tempTargetStorage.saveTempTargetsToStorage([TempTarget.cancel(at: Date().addingTimeInterval(-1))])
                 }
                 }
-            } catch {
-                debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active TempTargets with error: \(error.localizedDescription)"
-                )
             }
             }
+        } catch {
+            debug(
+                .default,
+                "\(DebuggingIdentifiers.failed) Failed to disable active temp targets: \(error.localizedDescription)"
+            )
         }
         }
     }
     }
 
 

+ 11 - 7
Trio/Sources/Modules/Adjustments/AdjustmentsStateModel.swift

@@ -170,13 +170,17 @@ extension Adjustments {
             for (index, override) in overridePresets.enumerated() {
             for (index, override) in overridePresets.enumerated() {
                 override.orderPosition = Int16(index + 1)
                 override.orderPosition = Int16(index + 1)
             }
             }
-            do {
-                guard viewContext.hasChanges else { return }
-                try viewContext.save()
-                setupOverridePresetsArray()
-                Task { await nightscoutManager.uploadProfiles() }
-            } catch {
-                debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save Override Presets order")
+            Task {
+                do {
+                    guard viewContext.hasChanges else { return }
+                    try viewContext.save()
+                    setupOverridePresetsArray()
+                    try await nightscoutManager.uploadProfiles()
+                } catch {
+                    debugPrint(
+                        "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save Override Presets order or upload profiles"
+                    )
+                }
             }
             }
         }
         }
 
 

+ 23 - 21
Trio/Sources/Modules/Adjustments/View/Overrides/EditOverrideForm.swift

@@ -513,29 +513,31 @@ struct EditOverrideForm: View {
                 Button(action: {
                 Button(action: {
                     saveChanges()
                     saveChanges()
 
 
-                    do {
-                        guard let moc = override.managedObjectContext else { return }
-                        guard moc.hasChanges else { return }
-                        try moc.save()
-                        Task {
-                            await state.nightscoutManager.uploadProfiles()
-                        }
-                        // Disable previous active Override
-                        if let currentActiveOverride = state.currentActiveOverride {
-                            Task {
-                                await state.disableAllActiveOverrides(
-                                    except: currentActiveOverride.objectID,
-                                    createOverrideRunEntry: false
-                                )
-                                // Update View
-                                state.updateLatestOverrideConfiguration()
+                    Task {
+                        do {
+                            guard let moc = override.managedObjectContext else { return }
+                            guard moc.hasChanges else { return }
+                            try moc.save()
+
+                            try await state.nightscoutManager.uploadProfiles()
+
+                            // Disable previous active Override
+                            if let currentActiveOverride = state.currentActiveOverride {
+                                Task {
+                                    await state.disableAllActiveOverrides(
+                                        except: currentActiveOverride.objectID,
+                                        createOverrideRunEntry: false
+                                    )
+                                    // Update View
+                                    state.updateLatestOverrideConfiguration()
+                                }
                             }
                             }
-                        }
 
 
-                        hasChanges = false
-                        presentationMode.wrappedValue.dismiss()
-                    } catch {
-                        debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to edit Override")
+                            hasChanges = false
+                            presentationMode.wrappedValue.dismiss()
+                        } catch {
+                            debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to edit Override")
+                        }
                     }
                     }
                 }, label: {
                 }, label: {
                     Text("Save Override")
                     Text("Save Override")

+ 16 - 8
Trio/Sources/Modules/Adjustments/View/TempTargets/AddTempTargetForm.swift

@@ -249,10 +249,14 @@ struct AddTempTargetForm: View {
                 content: {
                 content: {
                     Button(action: {
                     Button(action: {
                         Task {
                         Task {
-                            if noNameSpecified { state.tempTargetName = "Custom Target" }
-                            didPressSave.toggle()
-                            await state.invokeSaveOfCustomTempTargets()
-                            dismiss()
+                            do {
+                                if noNameSpecified { state.tempTargetName = "Custom Target" }
+                                didPressSave.toggle()
+                                try await state.invokeSaveOfCustomTempTargets()
+                                dismiss()
+                            } catch {
+                                debug(.default, "\(DebuggingIdentifiers.failed) failed to save custom temp target: \(error)")
+                            }
                         }
                         }
                     }, label: {
                     }, label: {
                         Text("Start Temp Target")
                         Text("Start Temp Target")
@@ -266,10 +270,14 @@ struct AddTempTargetForm: View {
             Section {
             Section {
                 Button(action: {
                 Button(action: {
                     Task {
                     Task {
-                        if noNameSpecified { state.tempTargetName = "Custom Target" }
-                        didPressSave.toggle()
-                        await state.saveTempTargetPreset()
-                        dismiss()
+                        do {
+                            if noNameSpecified { state.tempTargetName = "Custom Target" }
+                            didPressSave.toggle()
+                            try await state.saveTempTargetPreset()
+                            dismiss()
+                        } catch {
+                            debug(.default, "\(DebuggingIdentifiers.failed) failed to save temp target preset: \(error)")
+                        }
                     }
                     }
                 }, label: {
                 }, label: {
                     Text("Save as Preset")
                     Text("Save as Preset")

+ 2 - 2
Trio/Sources/Modules/Adjustments/View/TempTargets/EditTempTargetForm.swift

@@ -312,9 +312,9 @@ struct EditTempTargetForm: View {
                         Task {
                         Task {
                             // TODO: - Creating a Run entry is probably needed for Overrides as well and the reason for "jumping" Overrides?
                             // TODO: - Creating a Run entry is probably needed for Overrides as well and the reason for "jumping" Overrides?
                             // Disable previous active Temp Targets
                             // Disable previous active Temp Targets
-                            await state.disableAllActiveOverrides(
+                            await state.disableAllActiveTempTargets(
                                 except: currentActiveTempTarget.objectID,
                                 except: currentActiveTempTarget.objectID,
-                                createOverrideRunEntry: false
+                                createTempTargetRunEntry: false
                             )
                             )
 
 
                             // If the temp target which currently gets edited is enabled, then store it to the Temp Target JSON so that oref uses it
                             // If the temp target which currently gets edited is enabled, then store it to the Temp Target JSON so that oref uses it

+ 9 - 2
Trio/Sources/Modules/AlgorithmAdvancedSettings/AlgorithmAdvancedSettingsStateModel.swift

@@ -66,8 +66,15 @@ extension AlgorithmAdvancedSettings {
                         self.insulinActionCurve = settings.insulinActionCurve
                         self.insulinActionCurve = settings.insulinActionCurve
 
 
                         Task.detached(priority: .low) {
                         Task.detached(priority: .low) {
-                            debug(.nightscout, "Attempting to upload DIA to Nightscout")
-                            await self.nightscout.uploadProfiles()
+                            do {
+                                debug(.nightscout, "Attempting to upload DIA to Nightscout")
+                                try await self.nightscout.uploadProfiles()
+                            } catch {
+                                debug(
+                                    .default,
+                                    "\(DebuggingIdentifiers.failed) failed to upload DIA to Nightscout: \(error.localizedDescription)"
+                                )
+                            }
                         }
                         }
                     } receiveValue: {}
                     } receiveValue: {}
                     .store(in: &lifetime)
                     .store(in: &lifetime)

+ 11 - 4
Trio/Sources/Modules/AutosensSettings/AutosensSettingsStateModel.swift

@@ -41,10 +41,17 @@ extension AutosensSettings {
 
 
         private func setupDeterminationsArray() {
         private func setupDeterminationsArray() {
             Task {
             Task {
-                let ids = await determinationStorage.fetchLastDeterminationObjectID(
-                    predicate: NSPredicate.enactedDetermination
-                )
-                await updateDeterminationsArray(with: ids)
+                do {
+                    let ids = try await determinationStorage.fetchLastDeterminationObjectID(
+                        predicate: NSPredicate.enactedDetermination
+                    )
+                    await updateDeterminationsArray(with: ids)
+                } catch {
+                    debug(
+                        .default,
+                        "\(DebuggingIdentifiers.failed) Error fetching determination IDs: \(error.localizedDescription)"
+                    )
+                }
             }
             }
         }
         }
 
 

+ 6 - 2
Trio/Sources/Modules/BasalProfileEditor/BasalProfileEditorStateModel.swift

@@ -94,8 +94,12 @@ extension BasalProfileEditor {
                         self.initialItems = self.items.map { Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
                         self.initialItems = self.items.map { Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
 
 
                         Task.detached(priority: .low) {
                         Task.detached(priority: .low) {
-                            debug(.nightscout, "Attempting to upload basal rates to Nightscout")
-                            await self.nightscout.uploadProfiles()
+                            do {
+                                debug(.nightscout, "Attempting to upload basal rates to Nightscout")
+                                try await self.nightscout.uploadProfiles()
+                            } catch {
+                                debug(.default, "Failed to upload basal rates to Nightscout: \(error.localizedDescription)")
+                            }
                         }
                         }
                     case .failure:
                     case .failure:
                         // Handle the error, show error message
                         // Handle the error, show error message

+ 26 - 22
Trio/Sources/Modules/Calibrations/CalibrationsStateModel.swift

@@ -36,8 +36,8 @@ extension Calibrations {
         }
         }
 
 
         /// - Returns: An array of NSManagedObjectIDs for glucose readings.
         /// - Returns: An array of NSManagedObjectIDs for glucose readings.
-        private func fetchGlucose() async -> [NSManagedObjectID] {
-            let results = await CoreDataStack.shared.fetchEntitiesAsync(
+        private func fetchGlucose() async throws -> [NSManagedObjectID] {
+            let results = try await CoreDataStack.shared.fetchEntitiesAsync(
                 ofType: GlucoseStored.self,
                 ofType: GlucoseStored.self,
                 onContext: backgroundContext,
                 onContext: backgroundContext,
                 predicate: NSPredicate.predicateFor20MinAgo,
                 predicate: NSPredicate.predicateFor20MinAgo,
@@ -46,9 +46,9 @@ extension Calibrations {
                 fetchLimit: 1 /// We only need the last value
                 fetchLimit: 1 /// We only need the last value
             )
             )
 
 
-            return await backgroundContext.perform {
+            return try await backgroundContext.perform {
                 guard let glucoseResults = results as? [GlucoseStored] else {
                 guard let glucoseResults = results as? [GlucoseStored] else {
-                    return []
+                    throw CoreDataError.fetchError(function: #function, file: #file)
                 }
                 }
 
 
                 return glucoseResults.map(\.objectID)
                 return glucoseResults.map(\.objectID)
@@ -56,28 +56,32 @@ extension Calibrations {
         }
         }
 
 
         @MainActor func addCalibration() async {
         @MainActor func addCalibration() async {
-            defer {
-                UIApplication.shared.endEditing()
-                setupCalibrations()
-            }
+            do {
+                defer {
+                    UIApplication.shared.endEditing()
+                    setupCalibrations()
+                }
 
 
-            var glucose = newCalibration
-            if units == .mmolL {
-                glucose = newCalibration.asMgdL
-            }
+                var glucose = newCalibration
+                if units == .mmolL {
+                    glucose = newCalibration.asMgdL
+                }
 
 
-            let glucoseValuesIds = await fetchGlucose()
-            let glucoseObjects: [GlucoseStored] = await CoreDataStack.shared
-                .getNSManagedObject(with: glucoseValuesIds, context: viewContext)
+                let glucoseValuesIds = try await fetchGlucose()
+                let glucoseObjects: [GlucoseStored] = try await CoreDataStack.shared
+                    .getNSManagedObject(with: glucoseValuesIds, context: viewContext)
 
 
-            if let lastGlucose = glucoseObjects.first {
-                let unfiltered = lastGlucose.glucose
-                let calibration = Calibration(x: Double(unfiltered), y: Double(glucose))
+                if let lastGlucose = glucoseObjects.first {
+                    let unfiltered = lastGlucose.glucose
+                    let calibration = Calibration(x: Double(unfiltered), y: Double(glucose))
 
 
-                calibrationService.addCalibration(calibration)
-            } else {
-                info(.service, "Glucose is stale for calibration")
-                return
+                    calibrationService.addCalibration(calibration)
+                } else {
+                    info(.service, "Glucose is stale for calibration")
+                    return
+                }
+            } catch {
+                debug(.default, "\(DebuggingIdentifiers.failed) Failed to add calibration: \(error.localizedDescription)")
             }
             }
         }
         }
 
 

+ 6 - 2
Trio/Sources/Modules/CarbRatioEditor/CarbRatioEditorStateModel.swift

@@ -70,8 +70,12 @@ extension CarbRatioEditor {
             provider.saveProfile(profile)
             provider.saveProfile(profile)
             initialItems = items.map { Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
             initialItems = items.map { Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
             Task.detached(priority: .low) {
             Task.detached(priority: .low) {
-                debug(.nightscout, "Attempting to upload CRs to Nightscout")
-                await self.nightscout.uploadProfiles()
+                do {
+                    debug(.nightscout, "Attempting to upload CRs to Nightscout")
+                    try await self.nightscout.uploadProfiles()
+                } catch {
+                    debug(.default, "Failed to upload CRs to Nightscout: \(error.localizedDescription)")
+                }
             }
             }
         }
         }
 
 

+ 54 - 40
Trio/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -105,16 +105,20 @@ extension DataTable {
         /// - **Parameter**: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
         /// - **Parameter**: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
         func invokeCarbDeletionTask(_ treatmentObjectID: NSManagedObjectID, isFpuOrComplexMeal: Bool = false) {
         func invokeCarbDeletionTask(_ treatmentObjectID: NSManagedObjectID, isFpuOrComplexMeal: Bool = false) {
             Task {
             Task {
-                await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: isFpuOrComplexMeal)
+                do {
+                    try await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: isFpuOrComplexMeal)
 
 
-                await MainActor.run {
-                    carbEntryDeleted = true
-                    waitForSuggestion = true
+                    await MainActor.run {
+                        carbEntryDeleted = true
+                        waitForSuggestion = true
+                    }
+                } catch {
+                    debug(.default, "\(DebuggingIdentifiers.failed) Failed to delete carbs: \(error.localizedDescription)")
                 }
                 }
             }
             }
         }
         }
 
 
-        func deleteCarbs(_ treatmentObjectID: NSManagedObjectID, isFpuOrComplexMeal: Bool = false) async {
+        func deleteCarbs(_ treatmentObjectID: NSManagedObjectID, isFpuOrComplexMeal: Bool = false) async throws {
             // Delete from Nightscout/Apple Health/Tidepool
             // Delete from Nightscout/Apple Health/Tidepool
             await deleteFromServices(treatmentObjectID, isFPUDeletion: isFpuOrComplexMeal)
             await deleteFromServices(treatmentObjectID, isFPUDeletion: isFpuOrComplexMeal)
 
 
@@ -122,7 +126,7 @@ extension DataTable {
             await carbsStorage.deleteCarbsEntryStored(treatmentObjectID)
             await carbsStorage.deleteCarbsEntryStored(treatmentObjectID)
 
 
             // Perform a determine basal sync to update cob
             // Perform a determine basal sync to update cob
-            await apsManager.determineBasalSync()
+            try await apsManager.determineBasalSync()
         }
         }
 
 
         /// Deletes carb and FPU entries from all connected services (Nightscout, HealthKit, Tidepool)
         /// Deletes carb and FPU entries from all connected services (Nightscout, HealthKit, Tidepool)
@@ -208,16 +212,20 @@ extension DataTable {
         /// - **Parameter**: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
         /// - **Parameter**: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
         func invokeInsulinDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
         func invokeInsulinDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
             Task {
             Task {
-                await invokeInsulinDeletion(treatmentObjectID)
+                do {
+                    try await invokeInsulinDeletion(treatmentObjectID)
 
 
-                await MainActor.run {
-                    insulinEntryDeleted = true
-                    waitForSuggestion = true
+                    await MainActor.run {
+                        insulinEntryDeleted = true
+                        waitForSuggestion = true
+                    }
+                } catch {
+                    debug(.default, "\(DebuggingIdentifiers.failed) Failed to delete insulin entry: \(error)")
                 }
                 }
             }
             }
         }
         }
 
 
-        func invokeInsulinDeletion(_ treatmentObjectID: NSManagedObjectID) async {
+        func invokeInsulinDeletion(_ treatmentObjectID: NSManagedObjectID) async throws {
             do {
             do {
                 let authenticated = try await unlockmanager.unlock()
                 let authenticated = try await unlockmanager.unlock()
 
 
@@ -233,7 +241,7 @@ extension DataTable {
                 await CoreDataStack.shared.deleteObject(identifiedBy: treatmentObjectID)
                 await CoreDataStack.shared.deleteObject(identifiedBy: treatmentObjectID)
 
 
                 // Perform a determine basal sync to update iob
                 // Perform a determine basal sync to update iob
-                await apsManager.determineBasalSync()
+                try await apsManager.determineBasalSync()
             } catch {
             } catch {
                 debugPrint(
                 debugPrint(
                     "\(DebuggingIdentifiers.failed) \(#file) \(#function) Error while Insulin Deletion Task: \(error.localizedDescription)"
                     "\(DebuggingIdentifiers.failed) \(#file) \(#function) Error while Insulin Deletion Task: \(error.localizedDescription)"
@@ -291,30 +299,36 @@ extension DataTable {
             newDate: Date
             newDate: Date
         ) {
         ) {
             Task {
             Task {
-                // Get original date from entry to re-create the entry later with the updated values and the same date
-                guard let originalEntry = await getOriginalEntryValues(treatmentObjectID) else { return }
-
-                // Deletion logic for carb and FPU entries
-                await deleteOldEntries(
-                    treatmentObjectID,
-                    originalEntry: originalEntry,
-                    newCarbs: newCarbs,
-                    newFat: newFat,
-                    newProtein: newProtein,
-                    newNote: newNote
-                )
+                do {
+                    // Get original date from entry to re-create the entry later with the updated values and the same date
+                    guard let originalEntry = await getOriginalEntryValues(treatmentObjectID) else { return }
+
+                    // Deletion logic for carb and FPU entries
+                    try await deleteOldEntries(
+                        treatmentObjectID,
+                        originalEntry: originalEntry,
+                        newCarbs: newCarbs,
+                        newFat: newFat,
+                        newProtein: newProtein,
+                        newNote: newNote
+                    )
 
 
-                await createNewEntries(
-                    originalDate: newDate,
-                    newCarbs: newCarbs,
-                    newFat: newFat,
-                    newProtein: newProtein,
-                    newNote: newNote
-                )
+                    try await createNewEntries(
+                        originalDate: newDate,
+                        newCarbs: newCarbs,
+                        newFat: newFat,
+                        newProtein: newProtein,
+                        newNote: newNote
+                    )
+
+                    await syncWithServices()
 
 
-                await syncWithServices()
-                // Perform a determine basal sync to update cob
-                await apsManager.determineBasalSync()
+                    // Perform a determine basal sync to update cob
+                    try await apsManager.determineBasalSync()
+
+                } catch {
+                    debug(.default, "\(DebuggingIdentifiers.failed) failed to update entry: \(error.localizedDescription)")
+                }
             }
             }
         }
         }
 
 
@@ -324,7 +338,7 @@ extension DataTable {
             newFat: Decimal,
             newFat: Decimal,
             newProtein: Decimal,
             newProtein: Decimal,
             newNote: String
             newNote: String
-        ) async {
+        ) async throws {
             let newEntry = CarbsEntry(
             let newEntry = CarbsEntry(
                 id: UUID().uuidString,
                 id: UUID().uuidString,
                 createdAt: Date(),
                 createdAt: Date(),
@@ -339,7 +353,7 @@ extension DataTable {
             )
             )
 
 
             // Handles internally whether to create fake carbs or not based on whether fat > 0 or protein > 0
             // Handles internally whether to create fake carbs or not based on whether fat > 0 or protein > 0
-            await carbsStorage.storeCarbs([newEntry], areFetchedFromRemote: false)
+            try await carbsStorage.storeCarbs([newEntry], areFetchedFromRemote: false)
         }
         }
 
 
         /// Deletes the old carb/ FPU entries and creates new ones with updated values
         /// Deletes the old carb/ FPU entries and creates new ones with updated values
@@ -360,24 +374,24 @@ extension DataTable {
             newFat _: Decimal,
             newFat _: Decimal,
             newProtein _: Decimal,
             newProtein _: Decimal,
             newNote _: String
             newNote _: String
-        ) async {
+        ) async throws {
             if ((originalEntry.entryValues?.carbs ?? 0) == 0 && (originalEntry.entryValues?.fat ?? 0) > 0) ||
             if ((originalEntry.entryValues?.carbs ?? 0) == 0 && (originalEntry.entryValues?.fat ?? 0) > 0) ||
                 ((originalEntry.entryValues?.carbs ?? 0) == 0 && (originalEntry.entryValues?.protein ?? 0) > 0)
                 ((originalEntry.entryValues?.carbs ?? 0) == 0 && (originalEntry.entryValues?.protein ?? 0) > 0)
             {
             {
                 // Delete the zero-carb-entry and all its carb equivalents connected by the same fpuID from remote services and Core Data
                 // Delete the zero-carb-entry and all its carb equivalents connected by the same fpuID from remote services and Core Data
                 // Use fpuID
                 // Use fpuID
-                await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: true)
+                try await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: true)
             } else if ((originalEntry.entryValues?.carbs ?? 0) > 0 && (originalEntry.entryValues?.fat ?? 0) > 0) ||
             } else if ((originalEntry.entryValues?.carbs ?? 0) > 0 && (originalEntry.entryValues?.fat ?? 0) > 0) ||
                 ((originalEntry.entryValues?.carbs ?? 0) > 0 && (originalEntry.entryValues?.protein ?? 0) > 0)
                 ((originalEntry.entryValues?.carbs ?? 0) > 0 && (originalEntry.entryValues?.protein ?? 0) > 0)
             {
             {
                 // Delete carb entry and carb equivalents that are all connected by the same fpuID from remote services and Core Data
                 // Delete carb entry and carb equivalents that are all connected by the same fpuID from remote services and Core Data
                 // Use fpuID
                 // Use fpuID
-                await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: true)
+                try await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: true)
 
 
             } else {
             } else {
                 // Delete just the carb entry since there are no carb equivalents
                 // Delete just the carb entry since there are no carb equivalents
                 // Use NSManagedObjectID
                 // Use NSManagedObjectID
-                await deleteCarbs(treatmentObjectID)
+                try await deleteCarbs(treatmentObjectID)
             }
             }
         }
         }
 
 

+ 22 - 14
Trio/Sources/Modules/DataTable/View/DataTableRootView.swift

@@ -178,9 +178,10 @@ extension DataTable {
                         treatmentView(item)
                         treatmentView(item)
                     }
                     }
                 } else {
                 } else {
-                    HStack {
-                        Text("No data.")
-                    }
+                    ContentUnavailableView(
+                        "No data.",
+                        systemImage: "injection.needle"
+                    )
                 }
                 }
             }.listRowBackground(Color.chart)
             }.listRowBackground(Color.chart)
         }
         }
@@ -197,9 +198,10 @@ extension DataTable {
                         mealView(item)
                         mealView(item)
                     }
                     }
                 } else {
                 } else {
-                    HStack {
-                        Text("No data.")
-                    }
+                    ContentUnavailableView(
+                        "No data.",
+                        systemImage: "fork.knife"
+                    )
                 }
                 }
             }.listRowBackground(Color.chart)
             }.listRowBackground(Color.chart)
         }
         }
@@ -215,9 +217,10 @@ extension DataTable {
                         adjustmentView(for: item)
                         adjustmentView(for: item)
                     }
                     }
                 } else {
                 } else {
-                    HStack {
-                        Text("No data.")
-                    }
+                    ContentUnavailableView(
+                        "No data.",
+                        systemImage: "clock.arrow.2.circlepath"
+                    )
                 }
                 }
             }
             }
             .listRowBackground(Color.chart)
             .listRowBackground(Color.chart)
@@ -247,8 +250,12 @@ extension DataTable {
             }
             }
 
 
             let combined = overrides + tempTargets
             let combined = overrides + tempTargets
-            return combined.sorted(by: { $0.startDate > $1.startDate })
-        }
+            return combined.sorted {
+                if $0.startDate == $1.startDate {
+                    return $0.endDate > $1.endDate
+                }
+                return $0.startDate > $1.startDate
+            } }
 
 
         private struct AdjustmentItem: Identifiable {
         private struct AdjustmentItem: Identifiable {
             let id: NSManagedObjectID
             let id: NSManagedObjectID
@@ -384,9 +391,10 @@ extension DataTable {
                         }
                         }
                     }
                     }
                 } else {
                 } else {
-                    HStack {
-                        Text("No data.")
-                    }
+                    ContentUnavailableView(
+                        "No data.",
+                        systemImage: "drop.fill"
+                    )
                 }
                 }
             }.listRowBackground(Color.chart)
             }.listRowBackground(Color.chart)
                 .alert(isPresented: $showAlert) {
                 .alert(isPresented: $showAlert) {

+ 17 - 8
Trio/Sources/Modules/Home/HomeStateModel+Setup/BatterySetup.swift

@@ -4,14 +4,22 @@ import Foundation
 extension Home.StateModel {
 extension Home.StateModel {
     func setupBatteryArray() {
     func setupBatteryArray() {
         Task {
         Task {
-            let ids = await self.fetchBattery()
-            let batteryObjects: [OpenAPS_Battery] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
-            await updateBatteryArray(with: batteryObjects)
+            do {
+                let ids = try await self.fetchBattery()
+                let batteryObjects: [OpenAPS_Battery] = try await CoreDataStack.shared
+                    .getNSManagedObject(with: ids, context: viewContext)
+                await updateBatteryArray(with: batteryObjects)
+            } catch {
+                debug(
+                    .default,
+                    "\(DebuggingIdentifiers.failed) Error setting up battery array: \(error.localizedDescription)"
+                )
+            }
         }
         }
     }
     }
 
 
-    private func fetchBattery() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchBattery() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OpenAPS_Battery.self,
             ofType: OpenAPS_Battery.self,
             onContext: batteryFetchContext,
             onContext: batteryFetchContext,
             predicate: NSPredicate.predicateFor30MinAgo,
             predicate: NSPredicate.predicateFor30MinAgo,
@@ -19,9 +27,10 @@ extension Home.StateModel {
             ascending: false
             ascending: false
         )
         )
 
 
-        return await batteryFetchContext.perform {
-            guard let fetchedResults = results as? [OpenAPS_Battery] else { return [] }
-
+        return try await batteryFetchContext.perform {
+            guard let fetchedResults = results as? [OpenAPS_Battery] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
             return fetchedResults.map(\.objectID)
             return fetchedResults.map(\.objectID)
         }
         }
     }
     }

+ 28 - 14
Trio/Sources/Modules/Home/HomeStateModel+Setup/CarbSetup.swift

@@ -4,14 +4,19 @@ import Foundation
 extension Home.StateModel {
 extension Home.StateModel {
     func setupCarbsArray() {
     func setupCarbsArray() {
         Task {
         Task {
-            let ids = await self.fetchCarbs()
-            let carbObjects: [CarbEntryStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
-            await updateCarbsArray(with: carbObjects)
+            do {
+                let ids = try await self.fetchCarbs()
+                let carbObjects: [CarbEntryStored] = try await CoreDataStack.shared
+                    .getNSManagedObject(with: ids, context: viewContext)
+                await updateCarbsArray(with: carbObjects)
+            } catch {
+                debugPrint("\(DebuggingIdentifiers.failed) Error fetching carb objects: \(error) in \(#file):\(#line)")
+            }
         }
         }
     }
     }
 
 
-    private func fetchCarbs() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchCarbs() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
             ofType: CarbEntryStored.self,
             onContext: carbsFetchContext,
             onContext: carbsFetchContext,
             predicate: NSPredicate.carbsForChart,
             predicate: NSPredicate.carbsForChart,
@@ -20,8 +25,10 @@ extension Home.StateModel {
             batchSize: 5
             batchSize: 5
         )
         )
 
 
-        return await carbsFetchContext.perform {
-            guard let fetchedResults = results as? [CarbEntryStored] else { return [] }
+        return try await carbsFetchContext.perform {
+            guard let fetchedResults = results as? [CarbEntryStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             return fetchedResults.map(\.objectID)
             return fetchedResults.map(\.objectID)
         }
         }
@@ -33,14 +40,19 @@ extension Home.StateModel {
 
 
     func setupFPUsArray() {
     func setupFPUsArray() {
         Task {
         Task {
-            let ids = await self.fetchFPUs()
-            let fpuObjects: [CarbEntryStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
-            await updateFPUsArray(with: fpuObjects)
+            do {
+                let ids = try await self.fetchFPUs()
+                let fpuObjects: [CarbEntryStored] = try await CoreDataStack.shared
+                    .getNSManagedObject(with: ids, context: viewContext)
+                await updateFPUsArray(with: fpuObjects)
+            } catch {
+                debugPrint("\(DebuggingIdentifiers.failed) Error fetching FPU objects: \(error) in \(#file):\(#line)")
+            }
         }
         }
     }
     }
 
 
-    private func fetchFPUs() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchFPUs() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
             ofType: CarbEntryStored.self,
             onContext: fpuFetchContext,
             onContext: fpuFetchContext,
             predicate: NSPredicate.fpusForChart,
             predicate: NSPredicate.fpusForChart,
@@ -48,8 +60,10 @@ extension Home.StateModel {
             ascending: false
             ascending: false
         )
         )
 
 
-        return await fpuFetchContext.perform {
-            guard let fetchedResults = results as? [CarbEntryStored] else { return [] }
+        return try await fpuFetchContext.perform {
+            guard let fetchedResults = results as? [CarbEntryStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             return fetchedResults.map(\.objectID)
             return fetchedResults.map(\.objectID)
         }
         }

+ 22 - 16
Trio/Sources/Modules/Home/HomeStateModel+Setup/DeterminationSetup.swift

@@ -4,28 +4,34 @@ import Foundation
 extension Home.StateModel {
 extension Home.StateModel {
     func setupDeterminationsArray() {
     func setupDeterminationsArray() {
         Task {
         Task {
-            // Get the NSManagedObjectIDs
-            async let enactedObjectIds = determinationStorage
-                .fetchLastDeterminationObjectID(predicate: NSPredicate.enactedDetermination)
-            async let enactedAndNonEnactedObjectIds = fetchCobAndIob()
+            do {
+                // Get the NSManagedObjectIDs
+                async let enactedObjectIds = determinationStorage
+                    .fetchLastDeterminationObjectID(predicate: NSPredicate.enactedDetermination)
+                async let enactedAndNonEnactedObjectIds = fetchCobAndIob()
 
 
-            let enactedIDs = await enactedObjectIds
-            let enactedAndNonEnactedIds = await enactedAndNonEnactedObjectIds
+                let enactedIDs = try await enactedObjectIds
+                let enactedAndNonEnactedIds = try await enactedAndNonEnactedObjectIds
 
 
-            // Get the NSManagedObjects and return them on the Main Thread
-            await updateDeterminationsArray(with: enactedIDs, keyPath: \.determinationsFromPersistence)
-            await updateDeterminationsArray(with: enactedAndNonEnactedIds, keyPath: \.enactedAndNonEnactedDeterminations)
+                // Get the NSManagedObjects and return them on the Main Thread
+                try await updateDeterminationsArray(with: enactedIDs, keyPath: \.determinationsFromPersistence)
+                try await updateDeterminationsArray(with: enactedAndNonEnactedIds, keyPath: \.enactedAndNonEnactedDeterminations)
 
 
-            await updateForecastData()
+                await updateForecastData()
+            } catch let error as CoreDataError {
+                debug(.default, "Core Data error in setupDeterminationsArray: \(error.localizedDescription)")
+            } catch {
+                debug(.default, "Unexpected error in setupDeterminationsArray: \(error.localizedDescription)")
+            }
         }
         }
     }
     }
 
 
     @MainActor private func updateDeterminationsArray(
     @MainActor private func updateDeterminationsArray(
         with IDs: [NSManagedObjectID],
         with IDs: [NSManagedObjectID],
         keyPath: ReferenceWritableKeyPath<Home.StateModel, [OrefDetermination]>
         keyPath: ReferenceWritableKeyPath<Home.StateModel, [OrefDetermination]>
-    ) async {
+    ) async throws {
         // Fetch the objects off the main thread
         // Fetch the objects off the main thread
-        let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
+        let determinationObjects: [OrefDetermination] = try await CoreDataStack.shared
             .getNSManagedObject(with: IDs, context: viewContext)
             .getNSManagedObject(with: IDs, context: viewContext)
 
 
         // Update the array on the main thread
         // Update the array on the main thread
@@ -33,8 +39,8 @@ extension Home.StateModel {
     }
     }
 
 
     // Custom fetch to more efficiently filter only for cob and iob
     // Custom fetch to more efficiently filter only for cob and iob
-    private func fetchCobAndIob() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchCobAndIob() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OrefDetermination.self,
             ofType: OrefDetermination.self,
             onContext: determinationFetchContext,
             onContext: determinationFetchContext,
             predicate: NSPredicate.determinationsForCobIobCharts,
             predicate: NSPredicate.determinationsForCobIobCharts,
@@ -44,9 +50,9 @@ extension Home.StateModel {
             propertiesToFetch: ["cob", "iob", "deliverAt", "objectID"]
             propertiesToFetch: ["cob", "iob", "deliverAt", "objectID"]
         )
         )
 
 
-        return await determinationFetchContext.perform {
+        return try await determinationFetchContext.perform {
             guard let fetchedResults = results as? [[String: Any]] else {
             guard let fetchedResults = results as? [[String: Any]] else {
-                return []
+                throw CoreDataError.fetchError(function: #function, file: #file)
             }
             }
 
 
             // Update Chart Scales
             // Update Chart Scales

+ 39 - 61
Trio/Sources/Modules/Home/HomeStateModel+Setup/ForecastSetup.swift

@@ -3,108 +3,86 @@ import Foundation
 
 
 extension Home.StateModel {
 extension Home.StateModel {
     // Asynchronously preprocess Forecast data in a background thread
     // Asynchronously preprocess Forecast data in a background thread
-    func preprocessForecastData() async -> [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])] {
-        // Get the Determination ID on the main context
-        guard let id = await viewContext.perform({ self.enactedAndNonEnactedDeterminations.first?.objectID }) else {
-            return []
-        }
-
-        // Get the Forecast IDs for the Determination ID
-        // Here we can safely use a background context since we are using the NSManagedObjectID
-        let forecastIDs = await determinationStorage.getForecastIDs(for: id, in: taskContext)
-
-        var result: [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])] = []
-
-        // Use a task group to fetch Forecast VALUE IDs concurrently
-        await withTaskGroup(of: (UUID, NSManagedObjectID, [NSManagedObjectID]).self) { group in
-            for forecastID in forecastIDs {
-                group.addTask {
-                    // Fetch forecast value IDs asynchronously (but outside of perform)
-                    let forecastValueIDs = await self.determinationStorage.getForecastValueIDs(
-                        for: forecastID,
-                        in: self.taskContext
-                    )
-                    return (UUID(), forecastID, forecastValueIDs)
-                }
+    func preprocessForecastData() async -> [(
+        id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID]
+    )] {
+        do {
+            // Get the Determination ID on the main context
+            guard let determination = await viewContext.perform({
+                self.enactedAndNonEnactedDeterminations.first
+            }) else {
+                debug(.default, "No determination found for forecast preprocessing")
+                return []
             }
             }
 
 
-            // Collect the results from the task group
-            for await (uuid, forecastID, forecastValueIDs) in group {
-                result.append((id: uuid, forecastID: forecastID, forecastValueIDs: forecastValueIDs))
-            }
+            // Fetch complete forecast hierarchy with prefetched values
+            return try await determinationStorage.fetchForecastHierarchy(
+                for: determination.objectID,
+                in: taskContext
+            )
+        } catch {
+            debug(
+                .default,
+                "\(DebuggingIdentifiers.failed) Failed to preprocess forecast data: \(error.localizedDescription)"
+            )
+            return []
         }
         }
-
-        return result
     }
     }
 
 
     // Update forecast data and UI on the main thread
     // Update forecast data and UI on the main thread
     @MainActor func updateForecastData() async {
     @MainActor func updateForecastData() async {
-        // Preprocess forecast data on a background thread
         let forecastDataIDs = await preprocessForecastData()
         let forecastDataIDs = await preprocessForecastData()
 
 
-        // Use an Array of Int instead of ForecastValues to be able to pass values thread safe
         var allForecastValues = [[Int]]()
         var allForecastValues = [[Int]]()
         var preprocessedData = [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)]()
         var preprocessedData = [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)]()
 
 
-        // Use a task group to fetch forecast values concurrently
-        await withTaskGroup(of: (UUID, Forecast?, [ForecastValue]).self) { group in
-            for data in forecastDataIDs {
-                group.addTask {
-                    await self.determinationStorage
-                        .fetchForecastObjects(
-                            for: data,
-                            in: self.viewContext
-                        ) // This directly returns NSManagedobjects on the Main Thread
+        // Process prefetched data directly
+        for data in forecastDataIDs {
+            if let forecast = try? viewContext.existingObject(with: data.forecastID) as? Forecast {
+                let values = data.forecastValueIDs.compactMap {
+                    try? viewContext.existingObject(with: $0) as? ForecastValue
                 }
                 }
-            }
-
-            // Collect the results from the task group
-            for await (id, forecast, forecastValues) in group {
-                guard let forecast = forecast, !forecastValues.isEmpty else { continue }
 
 
-                // Extract only the 'value' from ForecastValue on the main thread
-                let forecastValueInts = forecastValues
-                    .compactMap { Int($0.value) }
+                // Extract values for graph
+                let forecastValueInts = values.map { Int($0.value) }
                 allForecastValues.append(forecastValueInts)
                 allForecastValues.append(forecastValueInts)
-                preprocessedData.append(contentsOf: forecastValues.map { (id: id, forecast: forecast, forecastValue: $0) })
+
+                // Add data for further processing
+                preprocessedData.append(contentsOf: values.map {
+                    (id: data.id, forecast: forecast, forecastValue: $0)
+                })
             }
             }
         }
         }
 
 
-        // Update Array on the Main Thread
+        // Update UI-relevant data
         self.preprocessedData = preprocessedData
         self.preprocessedData = preprocessedData
 
 
-        // Ensure there are forecast values to process
         guard !allForecastValues.isEmpty else {
         guard !allForecastValues.isEmpty else {
             minForecast = []
             minForecast = []
             maxForecast = []
             maxForecast = []
             return
             return
         }
         }
 
 
-        // Update minCount on the Main Thread
         minCount = max(12, allForecastValues.map(\.count).min() ?? 0)
         minCount = max(12, allForecastValues.map(\.count).min() ?? 0)
-
-        // Safely read minCount for use inside the detached task
         let localMinCount = minCount
         let localMinCount = minCount
 
 
         guard localMinCount > 0 else { return }
         guard localMinCount > 0 else { return }
 
 
-        // Copy allForecastValues to a local constant for thread safety
-        let localAllForecastValues = allForecastValues
-
-        // Calculate min and max forecast values in a background task
+        // Calculate min/max values for graph
         let (minResult, maxResult) = await Task.detached {
         let (minResult, maxResult) = await Task.detached {
             let minForecast = (0 ..< localMinCount).map { index in
             let minForecast = (0 ..< localMinCount).map { index in
-                localAllForecastValues.compactMap { $0.indices.contains(index) ? $0[index] : nil }.min() ?? 0
+                allForecastValues.compactMap { $0.indices.contains(index) ? $0[index] : nil }
+                    .min() ?? 0
             }
             }
 
 
             let maxForecast = (0 ..< localMinCount).map { index in
             let maxForecast = (0 ..< localMinCount).map { index in
-                localAllForecastValues.compactMap { $0.indices.contains(index) ? $0[index] : nil }.max() ?? 0
+                allForecastValues.compactMap { $0.indices.contains(index) ? $0[index] : nil }
+                    .max() ?? 0
             }
             }
 
 
             return (minForecast, maxForecast)
             return (minForecast, maxForecast)
         }.value
         }.value
 
 
-        // Update the properties on the main thread
         minForecast = minResult
         minForecast = minResult
         maxForecast = maxResult
         maxForecast = maxResult
     }
     }

+ 17 - 7
Trio/Sources/Modules/Home/HomeStateModel+Setup/GlucoseSetup.swift

@@ -4,14 +4,22 @@ import Foundation
 extension Home.StateModel {
 extension Home.StateModel {
     func setupGlucoseArray() {
     func setupGlucoseArray() {
         Task {
         Task {
-            let ids = await self.fetchGlucose()
-            let glucoseObjects: [GlucoseStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
-            await updateGlucoseArray(with: glucoseObjects)
+            do {
+                let ids = try await self.fetchGlucose()
+                let glucoseObjects: [GlucoseStored] = try await CoreDataStack.shared
+                    .getNSManagedObject(with: ids, context: viewContext)
+                await updateGlucoseArray(with: glucoseObjects)
+            } catch {
+                debug(
+                    .default,
+                    "\(DebuggingIdentifiers.failed) Error setting up glucose array: \(error.localizedDescription)"
+                )
+            }
         }
         }
     }
     }
 
 
-    private func fetchGlucose() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchGlucose() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             ofType: GlucoseStored.self,
             onContext: glucoseFetchContext,
             onContext: glucoseFetchContext,
             predicate: NSPredicate.glucose,
             predicate: NSPredicate.glucose,
@@ -20,8 +28,10 @@ extension Home.StateModel {
             batchSize: 50
             batchSize: 50
         )
         )
 
 
-        return await glucoseFetchContext.perform {
-            guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+        return try await glucoseFetchContext.perform {
+            guard let fetchedResults = results as? [GlucoseStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             // Update Main Chart Y Axis Values
             // Update Main Chart Y Axis Values
             // Perform everything on "context" to be thread safe
             // Perform everything on "context" to be thread safe

+ 32 - 17
Trio/Sources/Modules/Home/HomeStateModel+Setup/OverrideSetup.swift

@@ -5,14 +5,21 @@ extension Home.StateModel {
     // Setup Overrides
     // Setup Overrides
     func setupOverrides() {
     func setupOverrides() {
         Task {
         Task {
-            let ids = await self.fetchOverrides()
-            let overrideObjects: [OverrideStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
-            await updateOverrideArray(with: overrideObjects)
+            do {
+                let ids = try await self.fetchOverrides()
+                let overrideObjects: [OverrideStored] = try await CoreDataStack.shared
+                    .getNSManagedObject(with: ids, context: viewContext)
+                await updateOverrideArray(with: overrideObjects)
+            } catch let error as CoreDataError {
+                debug(.default, "Core Data error in setupOverrides: \(error.localizedDescription)")
+            } catch {
+                debug(.default, "Unexpected error in setupOverrides: \(error.localizedDescription)")
+            }
         }
         }
     }
     }
 
 
-    private func fetchOverrides() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchOverrides() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
             ofType: OverrideStored.self,
             onContext: overrideFetchContext,
             onContext: overrideFetchContext,
             predicate: NSPredicate.lastActiveOverride, // this predicate filters for all Overrides within the last 24h
             predicate: NSPredicate.lastActiveOverride, // this predicate filters for all Overrides within the last 24h
@@ -20,9 +27,10 @@ extension Home.StateModel {
             ascending: false
             ascending: false
         )
         )
 
 
-        return await overrideFetchContext.perform {
-            guard let fetchedResults = results as? [OverrideStored] else { return [] }
-
+        return try await overrideFetchContext.perform {
+            guard let fetchedResults = results as? [OverrideStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
             return fetchedResults.map(\.objectID)
             return fetchedResults.map(\.objectID)
         }
         }
     }
     }
@@ -41,16 +49,22 @@ extension Home.StateModel {
     // Setup expired Overrides
     // Setup expired Overrides
     func setupOverrideRunStored() {
     func setupOverrideRunStored() {
         Task {
         Task {
-            let ids = await self.fetchOverrideRunStored()
-            let overrideRunObjects: [OverrideRunStored] = await CoreDataStack.shared
-                .getNSManagedObject(with: ids, context: viewContext)
-            await updateOverrideRunStoredArray(with: overrideRunObjects)
+            do {
+                let ids = try await self.fetchOverrideRunStored()
+                let overrideRunObjects: [OverrideRunStored] = try await CoreDataStack.shared
+                    .getNSManagedObject(with: ids, context: viewContext)
+                await updateOverrideRunStoredArray(with: overrideRunObjects)
+            } catch let error as CoreDataError {
+                debug(.default, "Core Data error in setupOverrideRunStored: \(error.localizedDescription)")
+            } catch {
+                debug(.default, "Unexpected error in setupOverrideRunStored: \(error.localizedDescription)")
+            }
         }
         }
     }
     }
 
 
-    private func fetchOverrideRunStored() async -> [NSManagedObjectID] {
+    private func fetchOverrideRunStored() async throws -> [NSManagedObjectID] {
         let predicate = NSPredicate(format: "startDate >= %@", Date.oneDayAgo as NSDate)
         let predicate = NSPredicate(format: "startDate >= %@", Date.oneDayAgo as NSDate)
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideRunStored.self,
             ofType: OverrideRunStored.self,
             onContext: overrideFetchContext,
             onContext: overrideFetchContext,
             predicate: predicate,
             predicate: predicate,
@@ -58,9 +72,10 @@ extension Home.StateModel {
             ascending: false
             ascending: false
         )
         )
 
 
-        return await overrideFetchContext.perform {
-            guard let fetchedResults = results as? [OverrideRunStored] else { return [] }
-
+        return try await overrideFetchContext.perform {
+            guard let fetchedResults = results as? [OverrideRunStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
             return fetchedResults.map(\.objectID)
             return fetchedResults.map(\.objectID)
         }
         }
     }
     }

+ 30 - 13
Trio/Sources/Modules/Home/HomeStateModel+Setup/PumpHistorySetup.swift

@@ -4,14 +4,22 @@ import Foundation
 extension Home.StateModel {
 extension Home.StateModel {
     func setupInsulinArray() {
     func setupInsulinArray() {
         Task {
         Task {
-            let ids = await self.fetchInsulin()
-            let insulinObjects: [PumpEventStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
-            await updateInsulinArray(with: insulinObjects)
+            do {
+                let ids = try await self.fetchInsulin()
+                let insulinObjects: [PumpEventStored] = try await CoreDataStack.shared
+                    .getNSManagedObject(with: ids, context: viewContext)
+                await updateInsulinArray(with: insulinObjects)
+            } catch {
+                debug(
+                    .default,
+                    "\(DebuggingIdentifiers.failed) Error setting up insulin array: \(error.localizedDescription)"
+                )
+            }
         }
         }
     }
     }
 
 
-    private func fetchInsulin() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchInsulin() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: PumpEventStored.self,
             ofType: PumpEventStored.self,
             onContext: pumpHistoryFetchContext,
             onContext: pumpHistoryFetchContext,
             predicate: NSPredicate.pumpHistoryLast24h,
             predicate: NSPredicate.pumpHistoryLast24h,
@@ -20,9 +28,9 @@ extension Home.StateModel {
             batchSize: 30
             batchSize: 30
         )
         )
 
 
-        return await pumpHistoryFetchContext.perform {
+        return try await pumpHistoryFetchContext.perform {
             guard let pumpEvents = results as? [PumpEventStored] else {
             guard let pumpEvents = results as? [PumpEventStored] else {
-                return []
+                throw CoreDataError.fetchError(function: #function, file: #file)
             }
             }
 
 
             return pumpEvents.map(\.objectID)
             return pumpEvents.map(\.objectID)
@@ -48,13 +56,20 @@ extension Home.StateModel {
     // The predicate filters out all external boluses to prevent the progress bar from displaying the amount of an external bolus when an external bolus is added after a pump bolus
     // The predicate filters out all external boluses to prevent the progress bar from displaying the amount of an external bolus when an external bolus is added after a pump bolus
     func setupLastBolus() {
     func setupLastBolus() {
         Task {
         Task {
-            guard let id = await self.fetchLastBolus() else { return }
-            await updateLastBolus(with: id)
+            do {
+                guard let id = try await self.fetchLastBolus() else { return }
+                await updateLastBolus(with: id)
+            } catch {
+                debug(
+                    .default,
+                    "\(DebuggingIdentifiers.failed) Error setting up last bolus: \(error.localizedDescription)"
+                )
+            }
         }
         }
     }
     }
 
 
-    func fetchLastBolus() async -> NSManagedObjectID? {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func fetchLastBolus() async throws -> NSManagedObjectID? {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: PumpEventStored.self,
             ofType: PumpEventStored.self,
             onContext: pumpHistoryFetchContext,
             onContext: pumpHistoryFetchContext,
             predicate: NSPredicate.lastPumpBolus,
             predicate: NSPredicate.lastPumpBolus,
@@ -63,8 +78,10 @@ extension Home.StateModel {
             fetchLimit: 1
             fetchLimit: 1
         )
         )
 
 
-        return await pumpHistoryFetchContext.perform {
-            guard let fetchedResults = results as? [PumpEventStored] else { return [].first }
+        return try await pumpHistoryFetchContext.perform {
+            guard let fetchedResults = results as? [PumpEventStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             return fetchedResults.map(\.objectID).first
             return fetchedResults.map(\.objectID).first
         }
         }

+ 34 - 16
Trio/Sources/Modules/Home/HomeStateModel+Setup/TempTargetSetup.swift

@@ -4,15 +4,22 @@ import Foundation
 extension Home.StateModel {
 extension Home.StateModel {
     func setupTempTargetsStored() {
     func setupTempTargetsStored() {
         Task {
         Task {
-            let ids = await self.fetchTempTargets()
-            let tempTargetObjects: [TempTargetStored] = await CoreDataStack.shared
-                .getNSManagedObject(with: ids, context: viewContext)
-            await updateTempTargetsArray(with: tempTargetObjects)
+            do {
+                let ids = try await self.fetchTempTargets()
+                let tempTargetObjects: [TempTargetStored] = try await CoreDataStack.shared
+                    .getNSManagedObject(with: ids, context: viewContext)
+                await updateTempTargetsArray(with: tempTargetObjects)
+            } catch {
+                debug(
+                    .default,
+                    "\(DebuggingIdentifiers.failed) Error setting up tempTargetStored: \(error.localizedDescription)"
+                )
+            }
         }
         }
     }
     }
 
 
-    private func fetchTempTargets() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchTempTargets() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetStored.self,
             ofType: TempTargetStored.self,
             onContext: tempTargetFetchContext,
             onContext: tempTargetFetchContext,
             predicate: NSPredicate.lastActiveTempTarget,
             predicate: NSPredicate.lastActiveTempTarget,
@@ -20,8 +27,10 @@ extension Home.StateModel {
             ascending: false
             ascending: false
         )
         )
 
 
-        return await tempTargetFetchContext.perform {
-            guard let fetchedResults = results as? [TempTargetStored] else { return [] }
+        return try await tempTargetFetchContext.perform {
+            guard let fetchedResults = results as? [TempTargetStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
             return fetchedResults.map(\.objectID)
             return fetchedResults.map(\.objectID)
         }
         }
     }
     }
@@ -33,16 +42,23 @@ extension Home.StateModel {
     // Setup expired TempTargets
     // Setup expired TempTargets
     func setupTempTargetsRunStored() {
     func setupTempTargetsRunStored() {
         Task {
         Task {
-            let ids = await self.fetchTempTargetRunStored()
-            let tempTargetRunObjects: [TempTargetRunStored] = await CoreDataStack.shared
-                .getNSManagedObject(with: ids, context: viewContext)
-            await updateTempTargetRunStoredArray(with: tempTargetRunObjects)
+            do {
+                let ids = try await self.fetchTempTargetRunStored()
+                let tempTargetRunObjects: [TempTargetRunStored] = try await CoreDataStack.shared
+                    .getNSManagedObject(with: ids, context: viewContext)
+                await updateTempTargetRunStoredArray(with: tempTargetRunObjects)
+            } catch {
+                debug(
+                    .default,
+                    "\(DebuggingIdentifiers.failed) Error setting up temp targetsRunStored: \(error.localizedDescription)"
+                )
+            }
         }
         }
     }
     }
 
 
-    private func fetchTempTargetRunStored() async -> [NSManagedObjectID] {
+    private func fetchTempTargetRunStored() async throws -> [NSManagedObjectID] {
         let predicate = NSPredicate(format: "startDate >= %@", Date.oneDayAgo as NSDate)
         let predicate = NSPredicate(format: "startDate >= %@", Date.oneDayAgo as NSDate)
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetRunStored.self,
             ofType: TempTargetRunStored.self,
             onContext: tempTargetFetchContext,
             onContext: tempTargetFetchContext,
             predicate: predicate,
             predicate: predicate,
@@ -50,8 +66,10 @@ extension Home.StateModel {
             ascending: false
             ascending: false
         )
         )
 
 
-        return await tempTargetFetchContext.perform {
-            guard let fetchedResults = results as? [TempTargetRunStored] else { return [] }
+        return try await tempTargetFetchContext.perform {
+            guard let fetchedResults = results as? [TempTargetRunStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
             return fetchedResults.map(\.objectID)
             return fetchedResults.map(\.objectID)
         }
         }
     }
     }

+ 12 - 12
Trio/Sources/Modules/Home/HomeStateModel.swift

@@ -215,7 +215,7 @@ extension Home {
         // These combine subscribers are only necessary due to the batch inserts of glucose/FPUs which do not trigger a ManagedObjectContext change notification
         // These combine subscribers are only necessary due to the batch inserts of glucose/FPUs which do not trigger a ManagedObjectContext change notification
         private func registerSubscribers() {
         private func registerSubscribers() {
             glucoseStorage.updatePublisher
             glucoseStorage.updatePublisher
-                .receive(on: DispatchQueue.global(qos: .background))
+                .receive(on: queue)
                 .sink { [weak self] _ in
                 .sink { [weak self] _ in
                     guard let self = self else { return }
                     guard let self = self else { return }
                     self.setupGlucoseArray()
                     self.setupGlucoseArray()
@@ -223,7 +223,7 @@ extension Home {
                 .store(in: &subscriptions)
                 .store(in: &subscriptions)
 
 
             carbsStorage.updatePublisher
             carbsStorage.updatePublisher
-                .receive(on: DispatchQueue.global(qos: .background))
+                .receive(on: queue)
                 .sink { [weak self] _ in
                 .sink { [weak self] _ in
                     guard let self = self else { return }
                     guard let self = self else { return }
                     self.setupFPUsArray()
                     self.setupFPUsArray()
@@ -232,22 +232,22 @@ extension Home {
         }
         }
 
 
         private func registerHandlers() {
         private func registerHandlers() {
-            coreDataPublisher?.filterByEntityName("OrefDetermination").sink { [weak self] _ in
+            coreDataPublisher?.filteredByEntityName("OrefDetermination").sink { [weak self] _ in
                 guard let self = self else { return }
                 guard let self = self else { return }
                 self.setupDeterminationsArray()
                 self.setupDeterminationsArray()
             }.store(in: &subscriptions)
             }.store(in: &subscriptions)
 
 
-            coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
+            coreDataPublisher?.filteredByEntityName("GlucoseStored").sink { [weak self] _ in
                 guard let self = self else { return }
                 guard let self = self else { return }
                 self.setupGlucoseArray()
                 self.setupGlucoseArray()
             }.store(in: &subscriptions)
             }.store(in: &subscriptions)
 
 
-            coreDataPublisher?.filterByEntityName("CarbEntryStored").sink { [weak self] _ in
+            coreDataPublisher?.filteredByEntityName("CarbEntryStored").sink { [weak self] _ in
                 guard let self = self else { return }
                 guard let self = self else { return }
                 self.setupCarbsArray()
                 self.setupCarbsArray()
             }.store(in: &subscriptions)
             }.store(in: &subscriptions)
 
 
-            coreDataPublisher?.filterByEntityName("PumpEventStored").sink { [weak self] _ in
+            coreDataPublisher?.filteredByEntityName("PumpEventStored").sink { [weak self] _ in
                 guard let self = self else { return }
                 guard let self = self else { return }
                 self.setupInsulinArray()
                 self.setupInsulinArray()
                 self.setupLastBolus()
                 self.setupLastBolus()
@@ -255,27 +255,27 @@ extension Home {
                 self.displayPumpStatusBadge()
                 self.displayPumpStatusBadge()
             }.store(in: &subscriptions)
             }.store(in: &subscriptions)
 
 
-            coreDataPublisher?.filterByEntityName("OpenAPS_Battery").sink { [weak self] _ in
+            coreDataPublisher?.filteredByEntityName("OpenAPS_Battery").sink { [weak self] _ in
                 guard let self = self else { return }
                 guard let self = self else { return }
                 self.setupBatteryArray()
                 self.setupBatteryArray()
             }.store(in: &subscriptions)
             }.store(in: &subscriptions)
 
 
-            coreDataPublisher?.filterByEntityName("OverrideStored").sink { [weak self] _ in
+            coreDataPublisher?.filteredByEntityName("OverrideStored").sink { [weak self] _ in
                 guard let self = self else { return }
                 guard let self = self else { return }
                 self.setupOverrides()
                 self.setupOverrides()
             }.store(in: &subscriptions)
             }.store(in: &subscriptions)
 
 
-            coreDataPublisher?.filterByEntityName("OverrideRunStored").sink { [weak self] _ in
+            coreDataPublisher?.filteredByEntityName("OverrideRunStored").sink { [weak self] _ in
                 guard let self = self else { return }
                 guard let self = self else { return }
                 self.setupOverrideRunStored()
                 self.setupOverrideRunStored()
             }.store(in: &subscriptions)
             }.store(in: &subscriptions)
 
 
-            coreDataPublisher?.filterByEntityName("TempTargetStored").sink { [weak self] _ in
+            coreDataPublisher?.filteredByEntityName("TempTargetStored").sink { [weak self] _ in
                 guard let self = self else { return }
                 guard let self = self else { return }
                 self.setupTempTargetsStored()
                 self.setupTempTargetsStored()
             }.store(in: &subscriptions)
             }.store(in: &subscriptions)
 
 
-            coreDataPublisher?.filterByEntityName("TempTargetRunStored").sink { [weak self] _ in
+            coreDataPublisher?.filteredByEntityName("TempTargetRunStored").sink { [weak self] _ in
                 guard let self = self else { return }
                 guard let self = self else { return }
                 self.setupTempTargetsRunStored()
                 self.setupTempTargetsRunStored()
             }.store(in: &subscriptions)
             }.store(in: &subscriptions)
@@ -512,7 +512,7 @@ extension Home {
                 await apsManager.cancelBolus(nil)
                 await apsManager.cancelBolus(nil)
 
 
                 // perform determine basal sync, otherwise you have could end up with too much iob when opening the calculator again
                 // perform determine basal sync, otherwise you have could end up with too much iob when opening the calculator again
-                await apsManager.determineBasalSync()
+                try await apsManager.determineBasalSync()
             }
             }
         }
         }
 
 

+ 22 - 0
Trio/Sources/Modules/Home/View/Chart/ChartElements/GlucoseChartView.swift

@@ -76,3 +76,25 @@ struct GlucoseChartView: ChartContent {
         }
         }
     }
     }
 }
 }
+
+#Preview {
+    let previewStack = CoreDataStack.preview
+    return NavigationView {
+        VStack {
+            Chart {
+                GlucoseChartView(
+                    glucoseData: GlucoseStored.makePreviewGlucose(count: 24, provider: previewStack),
+                    units: .mgdL,
+                    highGlucose: 180,
+                    lowGlucose: 70,
+                    currentGlucoseTarget: 100,
+                    isSmoothingEnabled: false,
+                    glucoseColorScheme: .dynamicColor
+                )
+            }
+            .frame(height: 200)
+            .padding()
+        }
+        .navigationTitle("Glucose Chart")
+    }
+}

+ 4 - 3
Trio/Sources/Modules/Home/View/Chart/ChartElements/SelectionPopoverView.swift

@@ -38,7 +38,7 @@ struct SelectionPopoverView: ChartContent {
             .annotation(
             .annotation(
                 position: .top,
                 position: .top,
                 alignment: .center,
                 alignment: .center,
-                overflowResolution: .init(x: .fit(to: .chart), y: .fit(to: .chart))
+                overflowResolution: .init(x: .fit(to: .chart), y: .disabled)
             ) {
             ) {
                 selectionPopover
                 selectionPopover
             }
             }
@@ -67,7 +67,7 @@ struct SelectionPopoverView: ChartContent {
                 Text(selectedGlucose.date?.formatted(.dateTime.hour().minute(.twoDigits)) ?? "")
                 Text(selectedGlucose.date?.formatted(.dateTime.hour().minute(.twoDigits)) ?? "")
                     .font(.body).bold()
                     .font(.body).bold()
             }
             }
-            .font(.body).padding(.bottom, 5)
+            .font(.body).padding(.bottom, 2)
 
 
             HStack {
             HStack {
                 Text(glucoseToDisplay.description).bold() + Text(" \(units.rawValue)")
                 Text(glucoseToDisplay.description).bold() + Text(" \(units.rawValue)")
@@ -95,7 +95,8 @@ struct SelectionPopoverView: ChartContent {
                 .foregroundStyle(Color.orange).font(.body)
                 .foregroundStyle(Color.orange).font(.body)
             }
             }
         }
         }
-        .padding()
+        .padding(.horizontal)
+        .padding(.vertical, 2)
         .background {
         .background {
             RoundedRectangle(cornerRadius: 4)
             RoundedRectangle(cornerRadius: 4)
                 .fill(Color.chart.opacity(0.85))
                 .fill(Color.chart.opacity(0.85))

+ 2 - 2
Trio/Sources/Modules/Home/View/HomeRootView.swift

@@ -758,7 +758,7 @@ extension Home {
                 let bolusFraction = progress * (bolusTotal as Decimal)
                 let bolusFraction = progress * (bolusTotal as Decimal)
                 let bolusString =
                 let bolusString =
                     (bolusProgressFormatter.string(from: bolusFraction as NSNumber) ?? "0")
                     (bolusProgressFormatter.string(from: bolusFraction as NSNumber) ?? "0")
-                        + " of " +
+                        + String(localized: " of ", comment: "Bolus string partial message: 'x U of y U' in home view") +
                         (Formatter.decimalFormatterWithTwoFractionDigits.string(from: bolusTotal as NSNumber) ?? "0")
                         (Formatter.decimalFormatterWithTwoFractionDigits.string(from: bolusTotal as NSNumber) ?? "0")
                         + String(localized: " U", comment: "Insulin unit")
                         + String(localized: " U", comment: "Insulin unit")
 
 
@@ -1099,7 +1099,7 @@ extension Home {
                 tabBar()
                 tabBar()
 
 
                 if state.waitForSuggestion {
                 if state.waitForSuggestion {
-                    CustomProgressView(text: "Updating IOB...")
+                    CustomProgressView(text: String(localized: "Updating IOB...", comment: "Progress text when updating IOB"))
                 }
                 }
             }
             }
         }
         }

+ 9 - 2
Trio/Sources/Modules/ISFEditor/ISFEditorStateModel.swift

@@ -85,8 +85,15 @@ extension ISFEditor {
             initialItems = items.map { Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
             initialItems = items.map { Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
 
 
             Task.detached(priority: .low) {
             Task.detached(priority: .low) {
-                debug(.nightscout, "Attempting to upload ISF to Nightscout")
-                await self.nightscout.uploadProfiles()
+                do {
+                    debug(.nightscout, "Attempting to upload ISF to Nightscout")
+                    try await self.nightscout.uploadProfiles()
+                } catch {
+                    debug(
+                        .default,
+                        "\(DebuggingIdentifiers.failed) Faile to upload ISF to Nightscout: \(error.localizedDescription)"
+                    )
+                }
             }
             }
         }
         }
 
 

+ 75 - 36
Trio/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift

@@ -75,7 +75,14 @@ extension NightscoutConfig {
                     if enabled {
                     if enabled {
                         debug(.nightscout, "Upload has been enabled by the user.")
                         debug(.nightscout, "Upload has been enabled by the user.")
                         Task {
                         Task {
-                            await self.nightscoutManager.uploadProfiles()
+                            do {
+                                try await self.nightscoutManager.uploadProfiles()
+                            } catch {
+                                debug(
+                                    .default,
+                                    "\(DebuggingIdentifiers.failed) failed to upload profiles: \(error.localizedDescription)"
+                                )
+                            }
                         }
                         }
                     } else {
                     } else {
                         debug(.nightscout, "Upload has been disabled by the user.")
                         debug(.nightscout, "Upload has been disabled by the user.")
@@ -156,7 +163,9 @@ extension NightscoutConfig {
 
 
             do {
             do {
                 guard let fetchedProfile = await nightscoutManager.importSettings() else {
                 guard let fetchedProfile = await nightscoutManager.importSettings() else {
-                    importStatus = .failed
+                    await MainActor.run {
+                        importStatus = .failed
+                    }
                     throw NSError(
                     throw NSError(
                         domain: "ImportError",
                         domain: "ImportError",
                         code: 1,
                         code: 1,
@@ -178,7 +187,9 @@ extension NightscoutConfig {
                 }
                 }
 
 
                 if carbratios.contains(where: { $0.ratio <= 0 }) {
                 if carbratios.contains(where: { $0.ratio <= 0 }) {
-                    importStatus = .failed
+                    await MainActor.run {
+                        importStatus = .failed
+                    }
                     throw NSError(
                     throw NSError(
                         domain: "ImportError",
                         domain: "ImportError",
                         code: 2,
                         code: 2,
@@ -199,7 +210,9 @@ extension NightscoutConfig {
                 }
                 }
 
 
                 if pumpName != "Omnipod DASH", basals.contains(where: { $0.rate <= 0 }) {
                 if pumpName != "Omnipod DASH", basals.contains(where: { $0.rate <= 0 }) {
-                    importStatus = .failed
+                    await MainActor.run {
+                        importStatus = .failed
+                    }
                     throw NSError(
                     throw NSError(
                         domain: "ImportError",
                         domain: "ImportError",
                         code: 3,
                         code: 3,
@@ -208,7 +221,9 @@ extension NightscoutConfig {
                 }
                 }
 
 
                 if pumpName == "Omnipod DASH", basals.reduce(0, { $0 + $1.rate }) <= 0 {
                 if pumpName == "Omnipod DASH", basals.reduce(0, { $0 + $1.rate }) <= 0 {
-                    importStatus = .failed
+                    await MainActor.run {
+                        importStatus = .failed
+                    }
                     throw NSError(
                     throw NSError(
                         domain: "ImportError",
                         domain: "ImportError",
                         code: 4,
                         code: 4,
@@ -229,7 +244,9 @@ extension NightscoutConfig {
                 }
                 }
 
 
                 if sensitivities.contains(where: { $0.sensitivity <= 0 }) {
                 if sensitivities.contains(where: { $0.sensitivity <= 0 }) {
-                    importStatus = .failed
+                    await MainActor.run {
+                        importStatus = .failed
+                    }
                     throw NSError(
                     throw NSError(
                         domain: "ImportError",
                         domain: "ImportError",
                         code: 5,
                         code: 5,
@@ -261,25 +278,35 @@ extension NightscoutConfig {
                         RepeatingScheduleValue(startTime: TimeInterval($0.minutes * 60), value: Double($0.rate))
                         RepeatingScheduleValue(startTime: TimeInterval($0.minutes * 60), value: Double($0.rate))
                     }
                     }
 
 
-                    pump.syncBasalRateSchedule(items: syncValues) { result in
-                        switch result {
-                        case .success:
-                            self.storage.save(basals, as: OpenAPS.Settings.basalProfile)
-                            self.finalizeImport(
-                                carbratiosProfile: carbratiosProfile,
-                                sensitivitiesProfile: sensitivitiesProfile,
-                                targetsProfile: targetsProfile,
-                                dia: fetchedProfile.dia
-                            )
-                        case .failure:
-                            self.importErrors.append(
-                                "Settings were imported but the basal rates could not be saved to pump (communication error)."
-                            )
-                            self.importStatus = .failed
+                    await withCheckedContinuation { continuation in
+                        pump.syncBasalRateSchedule(items: syncValues) { [weak self] result in
+                            guard let self else {
+                                continuation.resume()
+                                return
+                            }
+
+                            switch result {
+                            case .success:
+                                self.storage.save(basals, as: OpenAPS.Settings.basalProfile)
+                                self.finalizeImport(
+                                    carbratiosProfile: carbratiosProfile,
+                                    sensitivitiesProfile: sensitivitiesProfile,
+                                    targetsProfile: targetsProfile,
+                                    dia: fetchedProfile.dia
+                                )
+                            case .failure:
+                                Task { @MainActor in
+                                    self.importErrors.append(
+                                        "Settings were imported but the basal rates could not be saved to pump (communication error)."
+                                    )
+                                    self.importStatus = .failed
+                                }
+                            }
+                            continuation.resume()
                         }
                         }
                     }
                     }
 
 
-                    if importErrors.isNotEmpty, importStatus == .failed {
+                    if await MainActor.run(body: { importErrors.isNotEmpty && importStatus == .failed }) {
                         throw NSError(
                         throw NSError(
                             domain: "ImportError",
                             domain: "ImportError",
                             code: 6,
                             code: 6,
@@ -298,7 +325,7 @@ extension NightscoutConfig {
                     )
                     )
                 }
                 }
             } catch {
             } catch {
-                DispatchQueue.main.async {
+                await MainActor.run {
                     self.importErrors.append(error.localizedDescription)
                     self.importErrors.append(error.localizedDescription)
                     debug(.service, "Settings import failed with error: \(error.localizedDescription)")
                     debug(.service, "Settings import failed with error: \(error.localizedDescription)")
                 }
                 }
@@ -344,20 +371,25 @@ extension NightscoutConfig {
             let glucose = await nightscoutManager.fetchGlucose(since: Date().addingTimeInterval(-1.days.timeInterval))
             let glucose = await nightscoutManager.fetchGlucose(since: Date().addingTimeInterval(-1.days.timeInterval))
 
 
             if glucose.isNotEmpty {
             if glucose.isNotEmpty {
-                await MainActor.run {
-                    self.backfilling = false
-                }
-
-                glucoseStorage.storeGlucose(glucose)
+                do {
+                    try await glucoseStorage.storeGlucose(glucose)
 
 
-                Task.detached {
-                    await self.healthKitManager.uploadGlucose()
+                    Task.detached {
+                        await self.healthKitManager.uploadGlucose()
+                    }
+                } catch let error as CoreDataError {
+                    debug(.nightscout, "Core Data error while storing backfilled glucose: \(error.localizedDescription)")
+                    message = "Error: \(error.localizedDescription)"
+                } catch {
+                    debug(.nightscout, "Unexpected error while storing backfilled glucose: \(error.localizedDescription)")
+                    message = "Error: \(error.localizedDescription)"
                 }
                 }
             } else {
             } else {
-                await MainActor.run {
-                    self.backfilling = false
-                    debug(.nightscout, "No glucose values found or fetched to backfill.")
-                }
+                debug(.nightscout, "No glucose values found or fetched to backfill.")
+            }
+
+            await MainActor.run {
+                self.backfilling = false
             }
             }
         }
         }
 
 
@@ -383,8 +415,15 @@ extension NightscoutConfig {
                         self.importedInsulinActionCurve = settings.insulinActionCurve
                         self.importedInsulinActionCurve = settings.insulinActionCurve
 
 
                         Task.detached(priority: .low) {
                         Task.detached(priority: .low) {
-                            debug(.nightscout, "Attempting to upload DIA to Nightscout after import review")
-                            await self.nightscoutManager.uploadProfiles()
+                            do {
+                                debug(.nightscout, "Attempting to upload DIA to Nightscout after import review")
+                                try await self.nightscoutManager.uploadProfiles()
+                            } catch {
+                                debug(
+                                    .default,
+                                    "\(DebuggingIdentifiers.failed) failed to upload DIA to Nightscout: \(error.localizedDescription)"
+                                )
+                            }
                         }
                         }
                     } receiveValue: {}
                     } receiveValue: {}
                     .store(in: &lifetime)
                     .store(in: &lifetime)

+ 19 - 1
Trio/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift

@@ -16,6 +16,8 @@ extension NightscoutConfig {
         @State var hintLabel: String?
         @State var hintLabel: String?
         @State private var decimalPlaceholder: Decimal = 0.0
         @State private var decimalPlaceholder: Decimal = 0.0
         @State private var booleanPlaceholder: Bool = false
         @State private var booleanPlaceholder: Bool = false
+        @State var backfillAlert: Alert?
+        @State var isBackfillAlertPresented = false
 
 
         @Environment(\.colorScheme) var colorScheme
         @Environment(\.colorScheme) var colorScheme
         @Environment(AppState.self) var appState
         @Environment(AppState.self) var appState
@@ -132,6 +134,16 @@ extension NightscoutConfig {
                                 Button {
                                 Button {
                                     Task {
                                     Task {
                                         await state.backfillGlucose()
                                         await state.backfillGlucose()
+                                        if !state.message.isEmpty && state.message.hasPrefix("Error:") {
+                                            DispatchQueue.main.async {
+                                                backfillAlert = Alert(
+                                                    title: Text("Backfill Failed"),
+                                                    message: Text(state.message),
+                                                    dismissButton: .default(Text("OK"))
+                                                )
+                                                isBackfillAlertPresented = true
+                                            }
+                                        }
                                     }
                                     }
                                 } label: {
                                 } label: {
                                     Text("Backfill Glucose")
                                     Text("Backfill Glucose")
@@ -174,7 +186,10 @@ extension NightscoutConfig {
                 .blur(radius: state.importStatus == .running ? 5 : 0)
                 .blur(radius: state.importStatus == .running ? 5 : 0)
 
 
                 if state.importStatus == .running {
                 if state.importStatus == .running {
-                    CustomProgressView(text: "Importing Profile...")
+                    CustomProgressView(text: String(
+                        localized: "Importing Profile...",
+                        comment: "Progress text when importing profile via Nightscout"
+                    ))
                 }
                 }
             }
             }
             .fullScreenCover(isPresented: $state.isImportResultReviewPresented, content: {
             .fullScreenCover(isPresented: $state.isImportResultReviewPresented, content: {
@@ -194,6 +209,9 @@ extension NightscoutConfig {
             .alert(isPresented: $isImportAlertPresented) {
             .alert(isPresented: $isImportAlertPresented) {
                 importAlert ?? Alert(title: Text("Unknown Error"))
                 importAlert ?? Alert(title: Text("Unknown Error"))
             }
             }
+            .alert(isPresented: $isBackfillAlertPresented) {
+                backfillAlert ?? Alert(title: Text("Unknown Error"))
+            }
             .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
             .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
             .onAppear(perform: configureView)
             .onAppear(perform: configureView)
         }
         }

+ 57 - 1
Trio/Sources/Modules/Settings/View/SettingsRootView.swift

@@ -5,6 +5,12 @@ import SwiftUI
 import Swinject
 import Swinject
 
 
 extension Settings {
 extension Settings {
+    struct VersionInfo: Equatable {
+        var latestVersion: String?
+        var isUpdateAvailable: Bool
+        var isBlacklisted: Bool
+    }
+
     struct RootView: BaseView {
     struct RootView: BaseView {
         let resolver: Resolver
         let resolver: Resolver
         @StateObject var state = StateModel()
         @StateObject var state = StateModel()
@@ -18,6 +24,11 @@ extension Settings {
         @State var hintLabel: String?
         @State var hintLabel: String?
         @State private var decimalPlaceholder: Decimal = 0.0
         @State private var decimalPlaceholder: Decimal = 0.0
         @State private var booleanPlaceholder: Bool = false
         @State private var booleanPlaceholder: Bool = false
+        @State private var versionInfo = VersionInfo(
+            latestVersion: nil,
+            isUpdateAvailable: false,
+            isBlacklisted: false
+        )
 
 
         @Environment(\.colorScheme) var colorScheme
         @Environment(\.colorScheme) var colorScheme
         @EnvironmentObject var appIcons: Icons
         @EnvironmentObject var appIcons: Icons
@@ -27,6 +38,37 @@ extension Settings {
             SettingItems.filteredItems(searchText: searchText)
             SettingItems.filteredItems(searchText: searchText)
         }
         }
 
 
+        @ViewBuilder var versionInfoView: some View {
+            let latestVersion = versionInfo.latestVersion
+            if let version = latestVersion {
+                let updateColor: Color = versionInfo.isUpdateAvailable ? .orange : .green
+                let versionIconName = versionInfo.isUpdateAvailable ? "exclamationmark.triangle.fill" : "checkmark.circle.fill"
+
+                VStack(alignment: .leading, spacing: 4) {
+                    HStack {
+                        Text("Latest version: \(version)")
+                            .font(.footnote)
+                            .foregroundColor(updateColor)
+                        Image(systemName: versionIconName)
+                            .foregroundColor(updateColor)
+                    }
+                    if versionInfo.isBlacklisted {
+                        HStack {
+                            Text("Warning: Known issues. Update now.")
+                                .font(.footnote)
+                                .foregroundColor(.red)
+                            Image(systemName: "exclamationmark.octagon.fill")
+                                .foregroundColor(.red)
+                        }
+                    }
+                }
+            } else {
+                Text("Latest version: Fetching...")
+                    .font(.footnote)
+                    .foregroundColor(.secondary)
+            }
+        }
+
         var body: some View {
         var body: some View {
             List {
             List {
                 if searchText.isEmpty {
                 if searchText.isEmpty {
@@ -46,7 +88,7 @@ extension Settings {
                                         .frame(width: 50, height: 50)
                                         .frame(width: 50, height: 50)
                                         .cornerRadius(10)
                                         .cornerRadius(10)
                                         .padding(.trailing, 10)
                                         .padding(.trailing, 10)
-                                    VStack(alignment: .leading) {
+                                    VStack(alignment: .leading, spacing: 4) {
                                         Text("Trio v\(versionNumber) (\(buildNumber))")
                                         Text("Trio v\(versionNumber) (\(buildNumber))")
                                             .font(.headline)
                                             .font(.headline)
                                         if let expirationDate = buildDetails.calculateExpirationDate() {
                                         if let expirationDate = buildDetails.calculateExpirationDate() {
@@ -63,6 +105,8 @@ extension Settings {
                                                 .font(.footnote)
                                                 .font(.footnote)
                                                 .foregroundColor(.secondary)
                                                 .foregroundColor(.secondary)
                                         }
                                         }
+
+                                        versionInfoView
                                     }
                                     }
                                 }
                                 }
                             }
                             }
@@ -312,6 +356,18 @@ extension Settings {
             }
             }
             .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
             .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
             .screenNavigation(self)
             .screenNavigation(self)
+            .onAppear {
+                AppVersionChecker.shared.refreshVersionInfo { _, latestVersion, isNewer, isBlacklisted in
+                    let updateAvailable = isNewer
+                    DispatchQueue.main.async {
+                        versionInfo = VersionInfo(
+                            latestVersion: latestVersion,
+                            isUpdateAvailable: updateAvailable,
+                            isBlacklisted: isBlacklisted
+                        )
+                    }
+                }
+            }
         }
         }
     }
     }
 }
 }

+ 32 - 26
Trio/Sources/Modules/Stat/StatStateModel.swift

@@ -46,35 +46,41 @@ extension Stat {
         }
         }
 
 
         private func fetchGlucose(for duration: Duration) async -> [NSManagedObjectID] {
         private func fetchGlucose(for duration: Duration) async -> [NSManagedObjectID] {
-            let predicate: NSPredicate
-
-            switch duration {
-            case .Day:
-                predicate = NSPredicate.glucoseForStatsDay
-            case .Week:
-                predicate = NSPredicate.glucoseForStatsWeek
-            case .Today:
-                predicate = NSPredicate.glucoseForStatsToday
-            case .Month:
-                predicate = NSPredicate.glucoseForStatsMonth
-            case .Total:
-                predicate = NSPredicate.glucoseForStatsTotal
-            }
+            do {
+                let predicate: NSPredicate
 
 
-            let results = await CoreDataStack.shared.fetchEntitiesAsync(
-                ofType: GlucoseStored.self,
-                onContext: context,
-                predicate: predicate,
-                key: "date",
-                ascending: false,
-                batchSize: 100,
-                propertiesToFetch: ["glucose", "objectID"]
-            )
+                switch duration {
+                case .Day:
+                    predicate = NSPredicate.glucoseForStatsDay
+                case .Week:
+                    predicate = NSPredicate.glucoseForStatsWeek
+                case .Today:
+                    predicate = NSPredicate.glucoseForStatsToday
+                case .Month:
+                    predicate = NSPredicate.glucoseForStatsMonth
+                case .Total:
+                    predicate = NSPredicate.glucoseForStatsTotal
+                }
 
 
-            return await context.perform {
-                guard let fetchedResults = results as? [[String: Any]] else { return [] }
+                let results = try await CoreDataStack.shared.fetchEntitiesAsync(
+                    ofType: GlucoseStored.self,
+                    onContext: context,
+                    predicate: predicate,
+                    key: "date",
+                    ascending: false,
+                    batchSize: 100,
+                    propertiesToFetch: ["glucose", "objectID"]
+                )
 
 
-                return fetchedResults.compactMap { $0["objectID"] as? NSManagedObjectID }
+                return try await context.perform {
+                    guard let fetchedResults = results as? [[String: Any]] else {
+                        throw CoreDataError.fetchError(function: #function, file: #file)
+                    }
+                    return fetchedResults.compactMap { $0["objectID"] as? NSManagedObjectID }
+                }
+            } catch {
+                debug(.default, "\(DebuggingIdentifiers.failed) Error fetching glucose for stats: \(error.localizedDescription)")
+                return []
             }
             }
         }
         }
 
 

+ 9 - 2
Trio/Sources/Modules/TargetsEditor/TargetsEditorStateModel.swift

@@ -83,8 +83,15 @@ extension TargetsEditor {
             }
             }
 
 
             Task.detached(priority: .low) {
             Task.detached(priority: .low) {
-                debug(.nightscout, "Attempting to upload targets to Nightscout")
-                await self.nightscout.uploadProfiles()
+                do {
+                    debug(.nightscout, "Attempting to upload targets to Nightscout")
+                    try await self.nightscout.uploadProfiles()
+                } catch {
+                    debug(
+                        .default,
+                        "\(DebuggingIdentifiers.failed) failed to upload targets to Nightscout: \(error.localizedDescription)"
+                    )
+                }
             }
             }
         }
         }
 
 

+ 173 - 145
Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift

@@ -40,13 +40,11 @@ extension Treatments {
         var minDelta: Decimal = 0
         var minDelta: Decimal = 0
         var expectedDelta: Decimal = 0
         var expectedDelta: Decimal = 0
         var minPredBG: Decimal = 0
         var minPredBG: Decimal = 0
-        var waitForSuggestion: Bool = false
+        var isAwaitingDeterminationResult: Bool = false
         var carbRatio: Decimal = 0
         var carbRatio: Decimal = 0
 
 
         var addButtonPressed: Bool = false
         var addButtonPressed: Bool = false
 
 
-        var waitForSuggestionInitial: Bool = false
-
         var target: Decimal = 0
         var target: Decimal = 0
         var cob: Int16 = 0
         var cob: Int16 = 0
         var iob: Decimal = 0
         var iob: Decimal = 0
@@ -122,6 +120,9 @@ extension Treatments {
 
 
         var isActive: Bool = false
         var isActive: Bool = false
 
 
+        var showDeterminationFailureAlert = false
+        var determinationFailureMessage = ""
+
         // Queue for handling Core Data change notifications
         // Queue for handling Core Data change notifications
         private let queue = DispatchQueue(label: "TreatmentsStateModel.queue", qos: .userInitiated)
         private let queue = DispatchQueue(label: "TreatmentsStateModel.queue", qos: .userInitiated)
         private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
         private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
@@ -170,32 +171,26 @@ extension Treatments {
         private func setupBolusStateConcurrently() {
         private func setupBolusStateConcurrently() {
             debug(.bolusState, "setupBolusStateConcurrently fired")
             debug(.bolusState, "setupBolusStateConcurrently fired")
             Task {
             Task {
-                await withTaskGroup(of: Void.self) { group in
-                    group.addTask {
-                        self.setupGlucoseArray()
-                    }
-                    group.addTask {
-                        self.setupDeterminationsAndForecasts()
-                    }
-                    group.addTask {
-                        await self.setupSettings()
-                    }
-                    group.addTask {
-                        self.registerObservers()
-                    }
-
-                    if self.waitForSuggestionInitial {
+                do {
+                    try await withThrowingTaskGroup(of: Void.self) { group in
+                        group.addTask {
+                            self.setupGlucoseArray()
+                        }
+                        group.addTask {
+                            self.setupDeterminationsAndForecasts()
+                        }
+                        group.addTask {
+                            await self.setupSettings()
+                        }
                         group.addTask {
                         group.addTask {
-                            let isDetermineBasalSuccessful = await self.apsManager.determineBasal()
-                            if !isDetermineBasalSuccessful {
-                                await MainActor.run {
-                                    self.waitForSuggestion = false
-                                    self.insulinRequired = 0
-                                    self.insulinRecommended = 0
-                                }
-                            }
+                            self.registerObservers()
                         }
                         }
+
+                        // Wait for all tasks to complete
+                        try await group.waitForAll()
                     }
                     }
+                } catch let error as NSError {
+                    debug(.default, "Failed to setup bolus state concurrently: \(error.localizedDescription)")
                 }
                 }
             }
             }
         }
         }
@@ -206,7 +201,7 @@ extension Treatments {
         ///   - `apsManager.bolusProgress` is a `CurrentValueSubject<Decimal?, Never>`.
         ///   - `apsManager.bolusProgress` is a `CurrentValueSubject<Decimal?, Never>`.
         ///   - When a bolus starts, this subject emits `0` (or a fraction like `0.1, 0.5, etc.`).
         ///   - When a bolus starts, this subject emits `0` (or a fraction like `0.1, 0.5, etc.`).
         ///   - When the bolus finishes, the subject is typically set to `nil`.
         ///   - When the bolus finishes, the subject is typically set to `nil`.
-        ///   - This treats ANY non-nil value as “bolus in progress.”
+        ///   - This treats ANY non-nil value as "bolus in progress."
         ///
         ///
         private func subscribeToBolusProgress() {
         private func subscribeToBolusProgress() {
             bolusProgressCancellable = apsManager.bolusProgress
             bolusProgressCancellable = apsManager.bolusProgress
@@ -379,7 +374,7 @@ extension Treatments {
         // MARK: CALCULATIONS FOR THE BOLUS CALCULATOR
         // MARK: CALCULATIONS FOR THE BOLUS CALCULATOR
 
 
         /// Calculate insulin recommendation
         /// Calculate insulin recommendation
-        @MainActor func calculateInsulin() async -> Decimal {
+        func calculateInsulin() async -> Decimal {
             let result = await bolusCalculationManager.handleBolusCalculation(
             let result = await bolusCalculationManager.handleBolusCalculation(
                 carbs: carbs,
                 carbs: carbs,
                 useFattyMealCorrection: useFattyMealCorrectionFactor,
                 useFattyMealCorrection: useFattyMealCorrectionFactor,
@@ -387,14 +382,16 @@ extension Treatments {
             )
             )
 
 
             // Update state properties with calculation results on main thread
             // Update state properties with calculation results on main thread
-            targetDifference = result.targetDifference
-            targetDifferenceInsulin = result.targetDifferenceInsulin
-            wholeCob = result.wholeCob
-            wholeCobInsulin = result.wholeCobInsulin
-            iobInsulinReduction = result.iobInsulinReduction
-            superBolusInsulin = result.superBolusInsulin
-            wholeCalc = result.wholeCalc
-            fifteenMinInsulin = result.fifteenMinutesInsulin
+            await MainActor.run {
+                targetDifference = result.targetDifference
+                targetDifferenceInsulin = result.targetDifferenceInsulin
+                wholeCob = result.wholeCob
+                wholeCobInsulin = result.wholeCobInsulin
+                iobInsulinReduction = result.iobInsulinReduction
+                superBolusInsulin = result.superBolusInsulin
+                wholeCalc = result.wholeCalc
+                fifteenMinInsulin = result.fifteenMinutesInsulin
+            }
 
 
             return apsManager.roundBolus(amount: result.insulinCalculated)
             return apsManager.roundBolus(amount: result.insulinCalculated)
         }
         }
@@ -417,7 +414,7 @@ extension Treatments {
                 }
                 }
 
 
                 if isInsulinGiven {
                 if isInsulinGiven {
-                    try await handleInsulin(isExternal: externalInsulin)
+                    await handleInsulin(isExternal: externalInsulin)
                 } else {
                 } else {
                     hideModal()
                     hideModal()
                     return
                     return
@@ -431,7 +428,9 @@ extension Treatments {
 
 
                 guard glucoseStorage.isGlucoseDataFresh(date) else {
                 guard glucoseStorage.isGlucoseDataFresh(date) else {
                     await MainActor.run {
                     await MainActor.run {
-                        waitForSuggestion = false
+                        isAwaitingDeterminationResult = false
+                        showDeterminationFailureAlert = true
+                        determinationFailureMessage = "Glucose data is stale"
                     }
                     }
                     return hideModal()
                     return hideModal()
                 }
                 }
@@ -440,17 +439,14 @@ extension Treatments {
 
 
         // MARK: - Insulin
         // MARK: - Insulin
 
 
-        private func handleInsulin(isExternal: Bool) async throws {
+        private func handleInsulin(isExternal: Bool) async {
             debug(.bolusState, "handleInsulin fired")
             debug(.bolusState, "handleInsulin fired")
+
             if !isExternal {
             if !isExternal {
                 await addPumpInsulin()
                 await addPumpInsulin()
             } else {
             } else {
                 await addExternalInsulin()
                 await addExternalInsulin()
             }
             }
-
-            await MainActor.run {
-                self.waitForSuggestion = true
-            }
         }
         }
 
 
         func addPumpInsulin() async {
         func addPumpInsulin() async {
@@ -464,6 +460,10 @@ extension Treatments {
             do {
             do {
                 let authenticated = try await unlockmanager.unlock()
                 let authenticated = try await unlockmanager.unlock()
                 if authenticated {
                 if authenticated {
+                    // show loading animation
+                    await MainActor.run {
+                        self.isAwaitingDeterminationResult = true
+                    }
                     await apsManager.enactBolus(amount: maxAmount, isSMB: false, callback: nil)
                     await apsManager.enactBolus(amount: maxAmount, isSMB: false, callback: nil)
                 } else {
                 } else {
                     print("authentication failed")
                     print("authentication failed")
@@ -471,10 +471,9 @@ extension Treatments {
             } catch {
             } catch {
                 print("authentication error for pump bolus: \(error.localizedDescription)")
                 print("authentication error for pump bolus: \(error.localizedDescription)")
                 await MainActor.run {
                 await MainActor.run {
-                    self.waitForSuggestion = false
-                    if self.addButtonPressed {
-                        self.hideModal()
-                    }
+                    self.isAwaitingDeterminationResult = false
+                    self.showDeterminationFailureAlert = true
+                    self.determinationFailureMessage = error.localizedDescription
                 }
                 }
             }
             }
         }
         }
@@ -494,20 +493,23 @@ extension Treatments {
             do {
             do {
                 let authenticated = try await unlockmanager.unlock()
                 let authenticated = try await unlockmanager.unlock()
                 if authenticated {
                 if authenticated {
+                    // show loading animation
+                    await MainActor.run {
+                        self.isAwaitingDeterminationResult = true
+                    }
                     // store external dose to pump history
                     // store external dose to pump history
                     await pumpHistoryStorage.storeExternalInsulinEvent(amount: amount, timestamp: date)
                     await pumpHistoryStorage.storeExternalInsulinEvent(amount: amount, timestamp: date)
                     // perform determine basal sync
                     // perform determine basal sync
-                    await apsManager.determineBasalSync()
+                    try await apsManager.determineBasalSync()
                 } else {
                 } else {
                     print("authentication failed")
                     print("authentication failed")
                 }
                 }
             } catch {
             } catch {
                 print("authentication error for external insulin: \(error.localizedDescription)")
                 print("authentication error for external insulin: \(error.localizedDescription)")
                 await MainActor.run {
                 await MainActor.run {
-                    self.waitForSuggestion = false
-                    if self.addButtonPressed {
-                        self.hideModal()
-                    }
+                    self.isAwaitingDeterminationResult = false
+                    self.showDeterminationFailureAlert = true
+                    self.determinationFailureMessage = error.localizedDescription
                 }
                 }
             }
             }
         }
         }
@@ -515,35 +517,39 @@ extension Treatments {
         // MARK: - Carbs
         // MARK: - Carbs
 
 
         func saveMeal() async {
         func saveMeal() async {
-            guard carbs > 0 || fat > 0 || protein > 0 else { return }
-
-            await MainActor.run {
-                self.carbs = min(self.carbs, self.maxCarbs)
-                self.fat = min(self.fat, self.maxFat)
-                self.protein = min(self.protein, self.maxProtein)
-                self.id_ = UUID().uuidString
-            }
+            do {
+                guard carbs > 0 || fat > 0 || protein > 0 else { return }
 
 
-            let carbsToStore = [CarbsEntry(
-                id: id_,
-                createdAt: now,
-                actualDate: date,
-                carbs: carbs,
-                fat: fat,
-                protein: protein,
-                note: note,
-                enteredBy: CarbsEntry.local,
-                isFPU: false,
-                fpuID: fat > 0 || protein > 0 ? UUID().uuidString : nil
-            )]
-            await carbsStorage.storeCarbs(carbsToStore, areFetchedFromRemote: false)
-
-            // only perform determine basal sync if the user doesn't use the pump bolus, otherwise the enact bolus func in the APSManger does a sync
-            if amount <= 0 {
                 await MainActor.run {
                 await MainActor.run {
-                    self.waitForSuggestion = true
+                    self.carbs = min(self.carbs, self.maxCarbs)
+                    self.fat = min(self.fat, self.maxFat)
+                    self.protein = min(self.protein, self.maxProtein)
+                    self.id_ = UUID().uuidString
                 }
                 }
-                await apsManager.determineBasalSync()
+
+                let carbsToStore = [CarbsEntry(
+                    id: id_,
+                    createdAt: now,
+                    actualDate: date,
+                    carbs: carbs,
+                    fat: fat,
+                    protein: protein,
+                    note: note,
+                    enteredBy: CarbsEntry.local,
+                    isFPU: false,
+                    fpuID: fat > 0 || protein > 0 ? UUID().uuidString : nil
+                )]
+                try await carbsStorage.storeCarbs(carbsToStore, areFetchedFromRemote: false)
+
+                // only perform determine basal sync if the user doesn't use the pump bolus, otherwise the enact bolus func in the APSManger does a sync
+                if amount <= 0 {
+                    await MainActor.run {
+                        self.isAwaitingDeterminationResult = true
+                    }
+                    try await apsManager.determineBasalSync()
+                }
+            } catch {
+                debug(.default, "\(DebuggingIdentifiers.failed) Failed to save carbs: \(error.localizedDescription)")
             }
             }
         }
         }
 
 
@@ -598,7 +604,7 @@ extension Treatments.StateModel: DeterminationObserver, BolusFailureObserver {
 
 
         DispatchQueue.main.async {
         DispatchQueue.main.async {
             debug(.bolusState, "determinationDidUpdate fired")
             debug(.bolusState, "determinationDidUpdate fired")
-            self.waitForSuggestion = false
+            self.isAwaitingDeterminationResult = false
             if self.addButtonPressed {
             if self.addButtonPressed {
                 self.hideModal()
                 self.hideModal()
             }
             }
@@ -608,7 +614,7 @@ extension Treatments.StateModel: DeterminationObserver, BolusFailureObserver {
     func bolusDidFail() {
     func bolusDidFail() {
         DispatchQueue.main.async {
         DispatchQueue.main.async {
             debug(.bolusState, "bolusDidFail fired")
             debug(.bolusState, "bolusDidFail fired")
-            self.waitForSuggestion = false
+            self.isAwaitingDeterminationResult = false
             if self.addButtonPressed {
             if self.addButtonPressed {
                 self.hideModal()
                 self.hideModal()
             }
             }
@@ -618,7 +624,7 @@ extension Treatments.StateModel: DeterminationObserver, BolusFailureObserver {
 
 
 extension Treatments.StateModel {
 extension Treatments.StateModel {
     private func registerHandlers() {
     private func registerHandlers() {
-        coreDataPublisher?.filterByEntityName("OrefDetermination").sink { [weak self] _ in
+        coreDataPublisher?.filteredByEntityName("OrefDetermination").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task {
             Task {
                 await self.setupDeterminationsArray()
                 await self.setupDeterminationsArray()
@@ -628,7 +634,7 @@ extension Treatments.StateModel {
         }.store(in: &subscriptions)
         }.store(in: &subscriptions)
 
 
         // Due to the Batch insert this only is used for observing Deletion of Glucose entries
         // Due to the Batch insert this only is used for observing Deletion of Glucose entries
-        coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
+        coreDataPublisher?.filteredByEntityName("GlucoseStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             self.setupGlucoseArray()
             self.setupGlucoseArray()
         }.store(in: &subscriptions)
         }.store(in: &subscriptions)
@@ -651,14 +657,22 @@ extension Treatments.StateModel {
     // Glucose
     // Glucose
     private func setupGlucoseArray() {
     private func setupGlucoseArray() {
         Task {
         Task {
-            let ids = await self.fetchGlucose()
-            let glucoseObjects: [GlucoseStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
-            await updateGlucoseArray(with: glucoseObjects)
+            do {
+                let ids = try await self.fetchGlucose()
+                let glucoseObjects: [GlucoseStored] = try await CoreDataStack.shared
+                    .getNSManagedObject(with: ids, context: viewContext)
+                await updateGlucoseArray(with: glucoseObjects)
+            } catch {
+                debug(
+                    .default,
+                    "\(DebuggingIdentifiers.failed) Error setting up glucose array: \(error.localizedDescription)"
+                )
+            }
         }
         }
     }
     }
 
 
-    private func fetchGlucose() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchGlucose() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             ofType: GlucoseStored.self,
             onContext: glucoseFetchContext,
             onContext: glucoseFetchContext,
             predicate: NSPredicate.glucose,
             predicate: NSPredicate.glucose,
@@ -666,8 +680,10 @@ extension Treatments.StateModel {
             ascending: false
             ascending: false
         )
         )
 
 
-        return await glucoseFetchContext.perform {
-            guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+        return try await glucoseFetchContext.perform {
+            guard let fetchedResults = results as? [GlucoseStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             return fetchedResults.map(\.objectID)
             return fetchedResults.map(\.objectID)
         }
         }
@@ -686,71 +702,83 @@ extension Treatments.StateModel {
 
 
     // Determinations
     // Determinations
     private func setupDeterminationsArray() async {
     private func setupDeterminationsArray() async {
-        // Fetch object IDs on a background thread
-        let fetchedObjectIDs = await determinationStorage.fetchLastDeterminationObjectID(
-            predicate: NSPredicate.predicateFor30MinAgoForDetermination
-        )
+        do {
+            let fetchedObjectIDs = try await determinationStorage.fetchLastDeterminationObjectID(
+                predicate: NSPredicate.predicateFor30MinAgoForDetermination
+            )
 
 
-        // Update determinationObjectIDs on the main thread
-        await MainActor.run {
-            determinationObjectIDs = fetchedObjectIDs
-        }
+            await MainActor.run {
+                determinationObjectIDs = fetchedObjectIDs
+            }
 
 
-        let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
-            .getNSManagedObject(with: determinationObjectIDs, context: viewContext)
+            let determinationObjects: [OrefDetermination] = try await CoreDataStack.shared
+                .getNSManagedObject(with: determinationObjectIDs, context: viewContext)
 
 
-        updateDeterminationsArray(with: determinationObjects)
+            updateDeterminationsArray(with: determinationObjects)
+        } catch let error as CoreDataError {
+            debug(.default, "Core Data error: \(error.localizedDescription)")
+        } catch {
+            debug(.default, "Unexpected error: \(error.localizedDescription)")
+        }
     }
     }
 
 
     private func mapForecastsForChart() async -> Determination? {
     private func mapForecastsForChart() async -> Determination? {
-        let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
-            .getNSManagedObject(with: determinationObjectIDs, context: determinationFetchContext)
+        do {
+            let determinationObjects: [OrefDetermination] = try await CoreDataStack.shared
+                .getNSManagedObject(with: determinationObjectIDs, context: determinationFetchContext)
 
 
-        return await determinationFetchContext.perform {
-            guard let determinationObject = determinationObjects.first else {
-                return nil
-            }
-
-            let eventualBG = determinationObject.eventualBG?.intValue
-
-            let forecastsSet = determinationObject.forecasts ?? []
-            let predictions = Predictions(
-                iob: forecastsSet.extractValues(for: "iob"),
-                zt: forecastsSet.extractValues(for: "zt"),
-                cob: forecastsSet.extractValues(for: "cob"),
-                uam: forecastsSet.extractValues(for: "uam")
-            )
+            return await determinationFetchContext.perform {
+                guard let determinationObject = determinationObjects.first else {
+                    return nil
+                }
 
 
-            return Determination(
-                id: UUID(),
-                reason: "",
-                units: 0,
-                insulinReq: 0,
-                eventualBG: eventualBG,
-                sensitivityRatio: 0,
-                rate: 0,
-                duration: 0,
-                iob: 0,
-                cob: 0,
-                predictions: predictions.isEmpty ? nil : predictions,
-                carbsReq: 0,
-                temp: nil,
-                bg: 0,
-                reservoir: 0,
-                isf: 0,
-                tdd: 0,
-                insulin: nil,
-                current_target: 0,
-                insulinForManualBolus: 0,
-                manualBolusErrorString: 0,
-                minDelta: 0,
-                expectedDelta: 0,
-                minGuardBG: 0,
-                minPredBG: 0,
-                threshold: 0,
-                carbRatio: 0,
-                received: false
+                let eventualBG = determinationObject.eventualBG?.intValue
+
+                let forecastsSet = determinationObject.forecasts ?? []
+                let predictions = Predictions(
+                    iob: forecastsSet.extractValues(for: "iob"),
+                    zt: forecastsSet.extractValues(for: "zt"),
+                    cob: forecastsSet.extractValues(for: "cob"),
+                    uam: forecastsSet.extractValues(for: "uam")
+                )
+
+                return Determination(
+                    id: UUID(),
+                    reason: "",
+                    units: 0,
+                    insulinReq: 0,
+                    eventualBG: eventualBG,
+                    sensitivityRatio: 0,
+                    rate: 0,
+                    duration: 0,
+                    iob: 0,
+                    cob: 0,
+                    predictions: predictions.isEmpty ? nil : predictions,
+                    carbsReq: 0,
+                    temp: nil,
+                    bg: 0,
+                    reservoir: 0,
+                    isf: 0,
+                    tdd: 0,
+                    insulin: nil,
+                    current_target: 0,
+                    insulinForManualBolus: 0,
+                    manualBolusErrorString: 0,
+                    minDelta: 0,
+                    expectedDelta: 0,
+                    minGuardBG: 0,
+                    minPredBG: 0,
+                    threshold: 0,
+                    carbRatio: 0,
+                    received: false
+                )
+            }
+        } catch {
+            debug(
+                .default,
+                "\(DebuggingIdentifiers.failed) Error mapping forecasts for chart: \(error.localizedDescription)"
             )
             )
+            return nil
         }
         }
     }
     }
 
 

+ 9 - 16
Trio/Sources/Modules/Treatments/View/TreatmentsRootView.swift

@@ -309,9 +309,9 @@ extension Treatments {
                     }
                     }
                     .listSectionSpacing(sectionSpacing)
                     .listSectionSpacing(sectionSpacing)
                 }
                 }
-                .blur(radius: state.waitForSuggestion ? 5 : 0)
+                .blur(radius: state.isAwaitingDeterminationResult ? 5 : 0)
 
 
-                if state.waitForSuggestion {
+                if state.isAwaitingDeterminationResult {
                     CustomProgressView(text: progressText.rawValue)
                     CustomProgressView(text: progressText.rawValue)
                 }
                 }
             }
             }
@@ -364,6 +364,13 @@ extension Treatments {
             }) {
             }) {
                 MealPresetView(state: state)
                 MealPresetView(state: state)
             }
             }
+            .alert("Determination Failed", isPresented: $state.showDeterminationFailureAlert) {
+                Button("OK", role: .cancel) {
+                    state.hideModal()
+                }
+            } message: {
+                Text("Failed to update COB/IOB: \(state.determinationFailureMessage)")
+            }
         }
         }
 
 
         var progressText: ProgressText {
         var progressText: ProgressText {
@@ -508,17 +515,3 @@ extension Treatments {
         }
         }
     }
     }
 }
 }
-
-// fix iOS 15 bug
-struct ActivityIndicator: UIViewRepresentable {
-    @Binding var isAnimating: Bool
-    let style: UIActivityIndicatorView.Style
-
-    func makeUIView(context _: UIViewRepresentableContext<ActivityIndicator>) -> UIActivityIndicatorView {
-        UIActivityIndicatorView(style: style)
-    }
-
-    func updateUIView(_ uiView: UIActivityIndicatorView, context _: UIViewRepresentableContext<ActivityIndicator>) {
-        isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
-    }
-}

+ 342 - 0
Trio/Sources/Services/AppVersionChecker/AppVersionChecker.swift

@@ -0,0 +1,342 @@
+import UIKit
+
+/// AppVersionChecker is a singleton responsible for checking the app's version status.
+/// It fetches version data from remote sources (GitHub), caches the results, and notifies the user
+/// if an update is available or if the current version is blacklisted.
+final class AppVersionChecker {
+    /// Shared singleton instance.
+    static let shared = AppVersionChecker()
+
+    /// Private initializer to enforce the singleton pattern.
+    private init() {}
+
+    // MARK: - Persisted Properties
+
+    /// Cached app version for which data was last fetched.
+    @Persisted(key: "cachedForVersion") private var cachedForVersion: String? = nil
+    /// The latest version fetched from GitHub.
+    @Persisted(key: "latestVersion") private var persistedLatestVersion: String? = nil
+    /// The date when the latest version was checked.
+    @Persisted(key: "latestVersionChecked") private var latestVersionChecked: Date? = .distantPast
+    /// Boolean flag indicating whether the current version is blacklisted.
+    @Persisted(key: "currentVersionBlackListed") private var currentVersionBlackListed: Bool = false
+    /// Timestamp for the last time a blacklist notification was shown.
+    @Persisted(key: "lastBlacklistNotificationShown") private var lastBlacklistNotificationShown: Date? = .distantPast
+    /// Timestamp for the last time a version update notification was shown.
+    @Persisted(key: "lastVersionUpdateNotificationShown") private var lastVersionUpdateNotificationShown: Date? = .distantPast
+    /// Timestamp for the last time an expiration notification was shown.
+    @Persisted(key: "lastExpirationNotificationShown") private var lastExpirationNotificationShown: Date? = .distantPast
+
+    // MARK: - Nested Types
+
+    /// GitHubDataType defines the type of data to fetch from GitHub for version checking.
+    private enum GitHubDataType {
+        /// The configuration file containing version information.
+        case versionConfig
+        /// The JSON file listing blacklisted versions.
+        case blacklistedVersions
+
+        /// Returns the URL string associated with the data type.
+        var url: String {
+            switch self {
+            case .versionConfig:
+                return "https://raw.githubusercontent.com/nightscout/Trio/refs/heads/main/Config.xcconfig"
+            case .blacklistedVersions:
+                return "https://raw.githubusercontent.com/nightscout/Trio/refs/heads/main/blacklisted-versions.json"
+            }
+        }
+    }
+
+    /// Model for decoding the blacklist JSON from GitHub.
+    private struct Blacklist: Decodable {
+        /// Array of blacklisted version entries.
+        let blacklistedVersions: [VersionEntry]
+    }
+
+    /// Model representing a single version entry in the blacklist.
+    private struct VersionEntry: Decodable {
+        /// The version string that is blacklisted.
+        let version: String
+    }
+
+    // MARK: - Public Methods
+
+    /**
+     Checks for a new or blacklisted version and presents an alert if necessary.
+
+     This method determines whether there is an update or if the current version is blacklisted.
+     Depending on the result, it displays an alert on the given view controller, ensuring that alerts
+     are not shown too frequently (24 hours for blacklist and 2 weeks for update notifications).
+
+     - Parameter viewController: The UIViewController on which to present any alerts.
+     */
+    func checkAndNotifyVersionStatus(in viewController: UIViewController) {
+        checkForNewVersion { [weak viewController] latestVersion, isNewer, isBlacklisted in
+            guard let vc = viewController else { return }
+            let now = Date()
+
+            // If the current version is blacklisted, show a critical update alert if not shown in the last 24 hours.
+            if isBlacklisted {
+                let lastShown = self.lastBlacklistNotificationShown ?? .distantPast
+                if now.timeIntervalSince(lastShown) > 86400 { // 24 hours
+                    self.showAlert(
+                        on: vc,
+                        title: String(localized: "Update Required", comment: "Title for critical update alert"),
+                        message: String(
+                            localized: "The current version has a critical issue and should be updated as soon as possible.",
+                            comment: "Message for critical update alert"
+                        )
+                    )
+                    self.lastBlacklistNotificationShown = now
+                    self.lastVersionUpdateNotificationShown = now
+                }
+            }
+            // Otherwise, if a newer version is available, show an update alert if not shown in the last 2 weeks.
+            else if isNewer {
+                let lastShown = self.lastVersionUpdateNotificationShown ?? .distantPast
+                if now.timeIntervalSince(lastShown) > 1_209_600 { // 2 weeks
+                    let versionText = latestVersion ?? String(localized: "Unknown", comment: "Fallback text for unknown version")
+                    self.showAlert(
+                        on: vc,
+                        title: String(localized: "Update Available", comment: "Title for update available alert"),
+                        message: String(
+                            localized: "A new version (\(versionText)) is available. It is recommended to update.",
+                            comment: "Message for update available alert"
+                        )
+                    )
+                    self.lastVersionUpdateNotificationShown = now
+                }
+            }
+        }
+    }
+
+    /**
+     Refreshes the version information and returns the current state.
+
+     This method triggers a version check (using cached values if valid or fetching fresh data)
+     and then returns the current app version along with the latest version info, a flag indicating
+     whether the latest version is newer, and a flag indicating if the current version is blacklisted.
+
+     - Parameter completion: A closure that receives the following parameters:
+     - currentVersion: The current app version.
+     - latestVersion: The latest version fetched from GitHub (if available).
+     - isNewer: `true` if the fetched version is newer than the current version.
+     - isBlacklisted: `true` if the current version is blacklisted.
+     */
+    func refreshVersionInfo(completion: @escaping (
+        String,
+        String?,
+        Bool,
+        Bool
+    ) -> Void) {
+        let currentVersion = version()
+        checkForNewVersion { latestVersion, isNewer, isBlacklisted in
+            completion(currentVersion, latestVersion, isNewer, isBlacklisted)
+        }
+    }
+
+    // MARK: - Core Version Checking Logic
+
+    /**
+     Checks whether there is a new or blacklisted version.
+
+     This method attempts to use cached version data if it is less than 24 hours old and
+     corresponds to the current app version. If the cache is invalid or outdated,
+     it fetches fresh data from GitHub.
+
+     - Parameter completion: A closure that receives:
+     - latestVersion: The latest version string (if available).
+     - isNewer: `true` if the fetched version is newer than the current version.
+     - isBlacklisted: `true` if the current version is blacklisted.
+     */
+    private func checkForNewVersion(completion: @escaping (String?, Bool, Bool) -> Void) {
+        let currentVersion = version()
+        let now = Date()
+
+        // Retrieve cached values.
+        let lastChecked = latestVersionChecked ?? .distantPast
+        let cachedVersion = cachedForVersion
+        let persistedLatest = persistedLatestVersion
+        let isBlacklistedCached = currentVersionBlackListed
+
+        // If the current app version has changed, reset notification timestamps.
+        if let cachedVersion = cachedVersion, cachedVersion != currentVersion {
+            lastBlacklistNotificationShown = .distantPast
+            lastVersionUpdateNotificationShown = .distantPast
+        }
+
+        // Use cached data if it is valid (less than 24 hours old) and matches the current version.
+        if let cachedVersion = cachedVersion,
+           cachedVersion == currentVersion,
+           now.timeIntervalSince(lastChecked) < 24 * 3600,
+           let persistedLatest = persistedLatest
+        {
+            let isNewer = isVersion(persistedLatest, newerThan: currentVersion)
+            completion(persistedLatest, isNewer, isBlacklistedCached)
+            return
+        }
+
+        // Otherwise, fetch fresh data from GitHub and update the cache.
+        fetchDataAndUpdateCache(currentVersion: currentVersion, completion: completion)
+    }
+
+    /**
+     Fetches version and blacklist data from GitHub, updates persisted values, and invokes the completion handler.
+
+     This method performs two sequential network requests: first for the version configuration and then for the
+     blacklisted versions. After parsing the fetched data and comparing version values, it updates the cache and calls
+     the completion handler with the results.
+
+     - Parameters:
+     - currentVersion: The current app version.
+     - completion: A closure that receives:
+     - latestVersion: The latest version string from GitHub (if available).
+     - isNewer: `true` if the fetched version is newer than the current version.
+     - isBlacklisted: `true` if the current version is blacklisted.
+     */
+    private func fetchDataAndUpdateCache(currentVersion: String, completion: @escaping (String?, Bool, Bool) -> Void) {
+        fetchData(for: .versionConfig) { versionData in
+            self.fetchData(for: .blacklistedVersions) { blacklistData in
+                DispatchQueue.main.async {
+                    // Parse the version from the fetched configuration data.
+                    let fetchedVersion = versionData
+                        .flatMap { String(data: $0, encoding: .utf8) }
+                        .flatMap { self.parseVersionFromConfig(contents: $0) }
+
+                    // Determine if the fetched version is newer than the current version.
+                    let isNewer = fetchedVersion.map {
+                        self.isVersion($0, newerThan: currentVersion)
+                    } ?? false
+
+                    // Determine if the current version is blacklisted.
+                    let isBlacklisted = (try? blacklistData.flatMap {
+                        try JSONDecoder().decode(Blacklist.self, from: $0)
+                    })?.blacklistedVersions
+                        .map(\.version)
+                        .contains(currentVersion) ?? false
+
+                    // Update persisted cache.
+                    self.persistedLatestVersion = fetchedVersion
+                    self.latestVersionChecked = Date()
+                    self.currentVersionBlackListed = isBlacklisted
+                    self.cachedForVersion = currentVersion
+
+                    completion(fetchedVersion, isNewer, isBlacklisted)
+                }
+            }
+        }
+    }
+
+    // MARK: - Data Fetching Helper
+
+    /**
+     Fetches data from GitHub for a specified data type.
+
+     This helper method builds a URL from the provided GitHubDataType and executes a network request.
+     If the request is successful and returns valid data (HTTP status 200), the data is passed to the completion handler.
+
+     - Parameters:
+     - dataType: The type of GitHub data to fetch (version configuration or blacklisted versions).
+     - completion: A closure that receives the fetched data as an optional `Data` object.
+     */
+    private func fetchData(for dataType: GitHubDataType, completion: @escaping (Data?) -> Void) {
+        guard let url = URL(string: dataType.url) else {
+            completion(nil)
+            return
+        }
+
+        URLSession.shared.dataTask(with: url) { data, response, error in
+            guard let data = data, error == nil,
+                  let httpResponse = response as? HTTPURLResponse,
+                  httpResponse.statusCode == 200
+            else {
+                completion(nil)
+                return
+            }
+            completion(data)
+        }.resume()
+    }
+
+    // MARK: - Helpers
+
+    /**
+     Parses the version string from the contents of a configuration file.
+
+     The method scans each line of the provided content for an occurrence of "APP_VERSION" and then
+     extracts the version number following the "=" delimiter.
+
+     - Parameter contents: A string containing the contents of the configuration file.
+     - Returns: The extracted version string if found; otherwise, `nil`.
+     */
+    private func parseVersionFromConfig(contents: String) -> String? {
+        let lines = contents.split(separator: "\n")
+        for line in lines {
+            if line.contains("APP_VERSION") {
+                let components = line.split(separator: "=").map {
+                    $0.trimmingCharacters(in: .whitespacesAndNewlines)
+                }
+                if components.count > 1 {
+                    return components[1]
+                }
+            }
+        }
+        return nil
+    }
+
+    /**
+     Compares two version strings to determine if the fetched version is newer than the current version.
+
+     The version strings are split into numeric components and compared sequentially.
+     If any component of the fetched version is greater than its counterpart in the current version,
+     the function returns `true`; if lower, it returns `false`.
+
+     - Parameters:
+     - fetchedVersion: The version string obtained from GitHub.
+     - currentVersion: The current app version.
+     - Returns: `true` if the fetched version is newer than the current version; otherwise, `false`.
+     */
+    private func isVersion(_ fetchedVersion: String, newerThan currentVersion: String) -> Bool {
+        let fetchedComponents = fetchedVersion.split(separator: ".").map { Int($0) ?? 0 }
+        let currentComponents = currentVersion.split(separator: ".").map { Int($0) ?? 0 }
+
+        let maxCount = max(fetchedComponents.count, currentComponents.count)
+        for i in 0 ..< maxCount {
+            let fetched = i < fetchedComponents.count ? fetchedComponents[i] : 0
+            let current = i < currentComponents.count ? currentComponents[i] : 0
+            if fetched > current {
+                return true
+            } else if fetched < current {
+                return false
+            }
+        }
+        return false
+    }
+
+    /**
+     Retrieves the current app version from the main bundle.
+
+     - Returns: The current app version as defined in the app's Info.plist under "CFBundleShortVersionString",
+     or `"Unknown"` if not available.
+     */
+    private func version() -> String {
+        Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown"
+    }
+
+    /**
+     Presents an alert on the specified view controller with a given title and message.
+
+     The alert is dispatched to the main thread to ensure UI updates occur correctly.
+
+     - Parameters:
+     - viewController: The UIViewController on which the alert should be presented.
+     - title: The title text for the alert.
+     - message: The body message of the alert.
+     */
+    private func showAlert(on viewController: UIViewController, title: String, message: String) {
+        DispatchQueue.main.async {
+            let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
+            alert.addAction(UIAlertAction(title: "OK", style: .default))
+            viewController.present(alert, animated: true)
+        }
+    }
+}

+ 102 - 67
Trio/Sources/Services/BolusCalculator/BolusCalculationManager.swift

@@ -191,8 +191,8 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
 
 
     /// Fetches recent glucose readings from CoreData
     /// Fetches recent glucose readings from CoreData
     /// - Returns: Array of NSManagedObjectIDs for glucose readings
     /// - Returns: Array of NSManagedObjectIDs for glucose readings
-    private func fetchGlucose() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchGlucose() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             ofType: GlucoseStored.self,
             onContext: glucoseFetchContext,
             onContext: glucoseFetchContext,
             predicate: NSPredicate.glucose,
             predicate: NSPredicate.glucose,
@@ -201,8 +201,10 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
             fetchLimit: 288
             fetchLimit: 288
         )
         )
 
 
-        return await glucoseFetchContext.perform {
-            guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+        return try await glucoseFetchContext.perform {
+            guard let fetchedResults = results as? [GlucoseStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
             return fetchedResults.map(\.objectID)
             return fetchedResults.map(\.objectID)
         }
         }
     }
     }
@@ -266,64 +268,73 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
         carbs: Decimal,
         carbs: Decimal,
         useFattyMealCorrection: Bool,
         useFattyMealCorrection: Bool,
         useSuperBolus: Bool
         useSuperBolus: Bool
-    ) async -> CalculationInput {
-        // Get settings
-        let settings = await getSettings()
-
-        // Get max bolus
-        let maxBolus = await getPumpSettings().maxBolus
-
-        // Get current profile values
-        let currentBasal = await getCurrentSettingValue(for: .basal)
-        let currentCarbRatio = await getCurrentSettingValue(for: .carbRatio)
-        let currentBGTarget = await getCurrentSettingValue(for: .bgTarget)
-        let currentISF = await getCurrentSettingValue(for: .isf)
-
-        // Fetch glucose data
-        let glucoseIds = await fetchGlucose()
-        let glucoseObjects: [GlucoseStored] = await CoreDataStack.shared.getNSManagedObject(
-            with: glucoseIds,
-            context: glucoseFetchContext
-        )
-        let glucoseVars = await glucoseFetchContext.perform {
-            self.updateGlucoseVariables(with: glucoseObjects)
-        }
+    ) async throws -> CalculationInput {
+        do {
+            // Get settings
+            let settings = await getSettings()
+
+            // Get max bolus
+            let maxBolus = await getPumpSettings().maxBolus
+
+            // Get current profile values
+            let currentBasal = await getCurrentSettingValue(for: .basal)
+            let currentCarbRatio = await getCurrentSettingValue(for: .carbRatio)
+            let currentBGTarget = await getCurrentSettingValue(for: .bgTarget)
+            let currentISF = await getCurrentSettingValue(for: .isf)
+
+            // Fetch glucose data
+            let glucoseIds = try await fetchGlucose()
+            let glucoseObjects: [GlucoseStored] = try await CoreDataStack.shared.getNSManagedObject(
+                with: glucoseIds,
+                context: glucoseFetchContext
+            )
+            let glucoseVars = await glucoseFetchContext.perform {
+                self.updateGlucoseVariables(with: glucoseObjects)
+            }
 
 
-        // Fetch determination data
-        let determinationIds = await determinationStorage.fetchLastDeterminationObjectID(
-            predicate: NSPredicate.predicateFor30MinAgoForDetermination
-        )
-        let determinationObjects: [OrefDetermination] = await CoreDataStack.shared.getNSManagedObject(
-            with: determinationIds,
-            context: determinationFetchContext
-        )
-        let bolusVars = await determinationFetchContext.perform {
-            self.updateBolusCalculatorVariables(
-                with: determinationObjects,
-                currentBGTarget: currentBGTarget,
-                currentISF: currentISF,
-                currentCarbRatio: currentCarbRatio,
-                currentBasal: currentBasal
+            // Fetch determination data
+            let determinationIds = try await determinationStorage.fetchLastDeterminationObjectID(
+                predicate: NSPredicate.predicateFor30MinAgoForDetermination
             )
             )
-        }
+            let determinationObjects: [OrefDetermination] = try await CoreDataStack.shared.getNSManagedObject(
+                with: determinationIds,
+                context: determinationFetchContext
+            )
+            let bolusVars = await determinationFetchContext.perform {
+                self.updateBolusCalculatorVariables(
+                    with: determinationObjects,
+                    currentBGTarget: currentBGTarget,
+                    currentISF: currentISF,
+                    currentCarbRatio: currentCarbRatio,
+                    currentBasal: currentBasal
+                )
+            }
 
 
-        return CalculationInput(
-            carbs: carbs,
-            currentBG: glucoseVars.currentBG,
-            deltaBG: glucoseVars.deltaBG,
-            target: bolusVars.target,
-            isf: bolusVars.isf,
-            carbRatio: bolusVars.carbRatio,
-            iob: bolusVars.iob,
-            cob: bolusVars.cob,
-            useFattyMealCorrectionFactor: useFattyMealCorrection,
-            fattyMealFactor: settings.fattyMealFactor,
-            useSuperBolus: useSuperBolus,
-            sweetMealFactor: settings.sweetMealFactor,
-            basal: bolusVars.basal,
-            fraction: settings.fraction,
-            maxBolus: maxBolus
-        )
+            return CalculationInput(
+                carbs: carbs,
+                currentBG: glucoseVars.currentBG,
+                deltaBG: glucoseVars.deltaBG,
+                target: bolusVars.target,
+                isf: bolusVars.isf,
+                carbRatio: bolusVars.carbRatio,
+                iob: bolusVars.iob,
+                cob: bolusVars.cob,
+                useFattyMealCorrectionFactor: useFattyMealCorrection,
+                fattyMealFactor: settings.fattyMealFactor,
+                useSuperBolus: useSuperBolus,
+                sweetMealFactor: settings.sweetMealFactor,
+                basal: bolusVars.basal,
+                fraction: settings.fraction,
+                maxBolus: maxBolus
+            )
+        } catch {
+            debug(
+                .default,
+                "\(DebuggingIdentifiers.failed) Error preparing calculation input: \(error.localizedDescription)"
+            )
+            // Return default values in case of error
+            throw error
+        }
     }
     }
 
 
     /// Calculates the recommended insulin dose based on various parameters
     /// Calculates the recommended insulin dose based on various parameters
@@ -401,14 +412,38 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
     ///   - useFattyMealCorrection: Whether to apply fatty meal correction
     ///   - useFattyMealCorrection: Whether to apply fatty meal correction
     ///   - useSuperBolus: Whether to use super bolus calculation
     ///   - useSuperBolus: Whether to use super bolus calculation
     /// - Returns: CalculationResult containing the calculated insulin dose and details
     /// - Returns: CalculationResult containing the calculated insulin dose and details
-    func handleBolusCalculation(carbs: Decimal, useFattyMealCorrection: Bool, useSuperBolus: Bool) async -> CalculationResult {
-        let input = await prepareCalculationInput(
-            carbs: carbs,
-            useFattyMealCorrection: useFattyMealCorrection,
-            useSuperBolus: useSuperBolus
-        )
-        let result = await calculateInsulin(input: input)
-        return result
+    func handleBolusCalculation(
+        carbs: Decimal,
+        useFattyMealCorrection: Bool,
+        useSuperBolus: Bool
+    ) async -> CalculationResult {
+        do {
+            let input = try await prepareCalculationInput(
+                carbs: carbs,
+                useFattyMealCorrection: useFattyMealCorrection,
+                useSuperBolus: useSuperBolus
+            )
+            let result = await calculateInsulin(input: input)
+            return result
+        } catch {
+            debug(
+                .default,
+                "\(DebuggingIdentifiers.failed) Error in bolus calculation: \(error.localizedDescription)"
+            )
+            // Return safe default values
+            return CalculationResult(
+                insulinCalculated: 0,
+                wholeCalc: 0,
+                correctionInsulin: 0,
+                iobInsulinReduction: 0,
+                superBolusInsulin: 0,
+                targetDifference: 0,
+                targetDifferenceInsulin: 0,
+                fifteenMinutesInsulin: 0,
+                wholeCob: 0,
+                wholeCobInsulin: 0
+            )
+        }
     }
     }
 }
 }
 
 

+ 22 - 18
Trio/Sources/Services/Calendar/CalendarManager.swift

@@ -89,7 +89,7 @@ final class BaseCalendarManager: CalendarManager, Injectable {
     }
     }
 
 
     private func registerHandlers() {
     private func registerHandlers() {
-        coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
+        coreDataPublisher?.filteredByEntityName("GlucoseStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task {
             Task {
                 await self.createEvent()
                 await self.createEvent()
@@ -175,8 +175,8 @@ final class BaseCalendarManager: CalendarManager, Injectable {
         EKEventStore().calendars(for: .event).map(\.title)
         EKEventStore().calendars(for: .event).map(\.title)
     }
     }
 
 
-    private func getLastDetermination() async -> NSManagedObjectID? {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func getLastDetermination() async throws -> NSManagedObjectID? {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OrefDetermination.self,
             ofType: OrefDetermination.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: NSPredicate.predicateFor30MinAgoForDetermination,
             predicate: NSPredicate.predicateFor30MinAgoForDetermination,
@@ -186,15 +186,17 @@ final class BaseCalendarManager: CalendarManager, Injectable {
             propertiesToFetch: ["timestamp", "cob", "iob", "objectID"]
             propertiesToFetch: ["timestamp", "cob", "iob", "objectID"]
         )
         )
 
 
-        return await backgroundContext.perform {
-            guard let fetchedResults = results as? [[String: Any]] else { return nil }
+        return try await backgroundContext.perform {
+            guard let fetchedResults = results as? [[String: Any]] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             return fetchedResults.first?["objectID"] as? NSManagedObjectID
             return fetchedResults.first?["objectID"] as? NSManagedObjectID
         }
         }
     }
     }
 
 
-    private func fetchGlucose() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchGlucose() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             ofType: GlucoseStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: NSPredicate.predicateFor30MinAgo,
             predicate: NSPredicate.predicateFor30MinAgo,
@@ -203,27 +205,29 @@ final class BaseCalendarManager: CalendarManager, Injectable {
             propertiesToFetch: ["objectID", "glucose"]
             propertiesToFetch: ["objectID", "glucose"]
         )
         )
 
 
-        return await backgroundContext.perform {
-            guard let fetchedResults = results as? [[String: Any]] else { return [] }
+        return try await backgroundContext.perform {
+            guard let fetchedResults = results as? [[String: Any]] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             return fetchedResults.compactMap { $0["objectID"] as? NSManagedObjectID }
             return fetchedResults.compactMap { $0["objectID"] as? NSManagedObjectID }
         }
         }
     }
     }
 
 
     @MainActor func createEvent() async {
     @MainActor func createEvent() async {
-        guard settingsManager.settings.useCalendar, let calendar = currentCalendar,
-              let determinationId = await getLastDetermination() else { return }
+        do {
+            guard settingsManager.settings.useCalendar, let calendar = currentCalendar,
+                  let determinationId = try await getLastDetermination() else { return }
 
 
-        // Ignore the update if the determinationId is the same as it was at last update
-        if determinationId == previousDeterminationId {
-            return
-        }
+            // Ignore the update if the determinationId is the same as it was at last update
+            if determinationId == previousDeterminationId {
+                return
+            }
 
 
-        let glucoseIds = await fetchGlucose()
+            let glucoseIds = try await fetchGlucose()
 
 
-        deleteAllEvents(in: calendar)
+            deleteAllEvents(in: calendar)
 
 
-        do {
             guard let determinationObject = try viewContext.existingObject(with: determinationId) as? OrefDetermination
             guard let determinationObject = try viewContext.existingObject(with: determinationId) as? OrefDetermination
             else { return }
             else { return }
 
 

+ 76 - 69
Trio/Sources/Services/ContactImage/ContactImageManager.swift

@@ -77,7 +77,7 @@ final class BaseContactImageManager: NSObject, ContactImageManager, Injectable {
     // MARK: - Core Data observation
     // MARK: - Core Data observation
 
 
     private func registerHandlers() {
     private func registerHandlers() {
-        coreDataPublisher?.filterByEntityName("OrefDetermination").sink { [weak self] _ in
+        coreDataPublisher?.filteredByEntityName("OrefDetermination").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task {
             Task {
                 await self.updateContactImageState()
                 await self.updateContactImageState()
@@ -88,8 +88,8 @@ final class BaseContactImageManager: NSObject, ContactImageManager, Injectable {
 
 
     // MARK: - Core Data Fetches
     // MARK: - Core Data Fetches
 
 
-    private func fetchlastDetermination() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchlastDetermination() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OrefDetermination.self,
             ofType: OrefDetermination.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: NSPredicate(format: "deliverAt >= %@", Date.halfHourAgo as NSDate), // fetches enacted and suggested
             predicate: NSPredicate(format: "deliverAt >= %@", Date.halfHourAgo as NSDate), // fetches enacted and suggested
@@ -98,15 +98,17 @@ final class BaseContactImageManager: NSObject, ContactImageManager, Injectable {
             fetchLimit: 1
             fetchLimit: 1
         )
         )
 
 
-        return await backgroundContext.perform {
-            guard let fetchedResults = results as? [OrefDetermination] else { return [] }
+        return try await backgroundContext.perform {
+            guard let fetchedResults = results as? [OrefDetermination] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             return fetchedResults.map(\.objectID)
             return fetchedResults.map(\.objectID)
         }
         }
     }
     }
 
 
-    private func fetchGlucose() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchGlucose() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             ofType: GlucoseStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: NSPredicate.predicateFor20MinAgo,
             predicate: NSPredicate.predicateFor20MinAgo,
@@ -115,9 +117,9 @@ final class BaseContactImageManager: NSObject, ContactImageManager, Injectable {
             fetchLimit: 3 /// We only need 1-3 values, depending on whether the user wants to show delta or not
             fetchLimit: 3 /// We only need 1-3 values, depending on whether the user wants to show delta or not
         )
         )
 
 
-        return await backgroundContext.perform {
+        return try await backgroundContext.perform {
             guard let glucoseResults = results as? [GlucoseStored] else {
             guard let glucoseResults = results as? [GlucoseStored] else {
-                return []
+                throw CoreDataError.fetchError(function: #function, file: #file)
             }
             }
 
 
             return glucoseResults.map(\.objectID)
             return glucoseResults.map(\.objectID)
@@ -180,72 +182,77 @@ final class BaseContactImageManager: NSObject, ContactImageManager, Injectable {
     /// and updates the `state` object, which represents the current contact trick state.
     /// and updates the `state` object, which represents the current contact trick state.
     /// - Important: This function must be called on the main actor to ensure thread safety. Otherwise, we would need to ensure thread safety by either using an actor or a perform closure
     /// - Important: This function must be called on the main actor to ensure thread safety. Otherwise, we would need to ensure thread safety by either using an actor or a perform closure
     @MainActor func updateContactImageState() async {
     @MainActor func updateContactImageState() async {
-        // Get NSManagedObjectIDs on backgroundContext
-        let glucoseValuesIds = await fetchGlucose()
-        let determinationIds = await fetchlastDetermination()
-
-        // Get NSManagedObjects on MainActor
-        let glucoseObjects: [GlucoseStored] = await CoreDataStack.shared
-            .getNSManagedObject(with: glucoseValuesIds, context: viewContext)
-        let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
-            .getNSManagedObject(with: determinationIds, context: viewContext)
-        let lastDetermination = determinationObjects.last
-
-        if let firstGlucoseValue = glucoseObjects.first {
-            let value = settingsManager.settings.units == .mgdL
-                ? Decimal(firstGlucoseValue.glucose)
-                : Decimal(firstGlucoseValue.glucose).asMmolL
-
-            state.glucose = Formatter.glucoseFormatter(for: units).string(from: value as NSNumber)
-            state.trend = firstGlucoseValue.directionEnum?.symbol
-
-            let delta = glucoseObjects.count >= 2
-                ? Decimal(firstGlucoseValue.glucose) - Decimal(glucoseObjects.dropFirst().first?.glucose ?? 0)
-                : 0
-            let deltaConverted = settingsManager.settings.units == .mgdL ? delta : delta.asMmolL
-            state.delta = deltaFormatter.string(from: deltaConverted as NSNumber)
-        }
+        do {
+            // Get NSManagedObjectIDs on backgroundContext
+            let glucoseValuesIds = try await fetchGlucose()
+            let determinationIds = try await fetchlastDetermination()
+
+            // Get NSManagedObjects on MainActor
+            let glucoseObjects: [GlucoseStored] = try await CoreDataStack.shared
+                .getNSManagedObject(with: glucoseValuesIds, context: viewContext)
+            let determinationObjects: [OrefDetermination] = try await CoreDataStack.shared
+                .getNSManagedObject(with: determinationIds, context: viewContext)
+            let lastDetermination = determinationObjects.last
+
+            if let firstGlucoseValue = glucoseObjects.first {
+                let value = settingsManager.settings.units == .mgdL
+                    ? Decimal(firstGlucoseValue.glucose)
+                    : Decimal(firstGlucoseValue.glucose).asMmolL
+
+                state.glucose = Formatter.glucoseFormatter(for: units).string(from: value as NSNumber)
+                state.trend = firstGlucoseValue.directionEnum?.symbol
+
+                let delta = glucoseObjects.count >= 2
+                    ? Decimal(firstGlucoseValue.glucose) - Decimal(glucoseObjects.dropFirst().first?.glucose ?? 0)
+                    : 0
+                let deltaConverted = settingsManager.settings.units == .mgdL ? delta : delta.asMmolL
+                state.delta = deltaFormatter.string(from: deltaConverted as NSNumber)
+            }
 
 
-        state.lastLoopDate = lastDetermination?.timestamp
+            state.lastLoopDate = lastDetermination?.timestamp
 
 
-        let iobValue = lastDetermination?.iob as? Decimal ?? 0.0
-        state.iob = iobValue
-        state.iobText = Formatter.decimalFormatterWithOneFractionDigit.string(from: iobValue as NSNumber)
+            let iobValue = lastDetermination?.iob as? Decimal ?? 0.0
+            state.iob = iobValue
+            state.iobText = Formatter.decimalFormatterWithOneFractionDigit.string(from: iobValue as NSNumber)
 
 
-        // we need to do it complex and unelegant, otherwise unwrapping and parsing of cob results in 0
-        if let cobValue = lastDetermination?.cob {
-            state.cob = Decimal(cobValue)
-            state.cobText = Formatter.integerFormatter.string(from: Int(cobValue) as NSNumber)
+            // we need to do it complex and unelegant, otherwise unwrapping and parsing of cob results in 0
+            if let cobValue = lastDetermination?.cob {
+                state.cob = Decimal(cobValue)
+                state.cobText = Formatter.integerFormatter.string(from: Int(cobValue) as NSNumber)
 
 
-        } else {
-            state.cob = 0
-            state.cobText = "0"
-        }
+            } else {
+                state.cob = 0
+                state.cobText = "0"
+            }
 
 
-        if let eventualBG = settingsManager.settings.units == .mgdL ? lastDetermination?
-            .eventualBG : lastDetermination?
-            .eventualBG?.decimalValue.asMmolL as NSDecimalNumber?
-        {
-            let eventualBGAsString = Formatter.decimalFormatterWithOneFractionDigit.string(from: eventualBG)
-            state.eventualBG = eventualBGAsString.map { "⇢ " + $0 }
-        }
+            if let eventualBG = settingsManager.settings.units == .mgdL ? lastDetermination?
+                .eventualBG : lastDetermination?
+                .eventualBG?.decimalValue.asMmolL as NSDecimalNumber?
+            {
+                let eventualBGAsString = Formatter.decimalFormatterWithOneFractionDigit.string(from: eventualBG)
+                state.eventualBG = eventualBGAsString.map { "⇢ " + $0 }
+            }
 
 
-        // TODO: workaround for now: set low value to 55, to have dynamic color shades between 55 and user-set low (approx. 70); same for high glucose
-        let hardCodedLow = Decimal(55)
-        let hardCodedHigh = Decimal(220)
-        let isDynamicColorScheme = settingsManager.settings.glucoseColorScheme == .dynamicColor
-        let highGlucoseColorValue = isDynamicColorScheme ? hardCodedHigh : settingsManager.settings.highGlucose
-        let lowGlucoseColorValue = isDynamicColorScheme ? hardCodedLow : settingsManager.settings.lowGlucose
-
-        state.highGlucoseColorValue = units == .mgdL ? highGlucoseColorValue : highGlucoseColorValue.asMmolL
-        state.lowGlucoseColorValue = units == .mgdL ? lowGlucoseColorValue : lowGlucoseColorValue.asMmolL
-        state
-            .targetGlucose = await getCurrentGlucoseTarget() ??
-            (settingsManager.settings.units == .mgdL ? Decimal(100) : 100.asMmolL)
-        state.glucoseColorScheme = settingsManager.settings.glucoseColorScheme
-
-        // Notify delegate about state update on main thread
-        await MainActor.run {
+            // TODO: workaround for now: set low value to 55, to have dynamic color shades between 55 and user-set low (approx. 70); same for high glucose
+            let hardCodedLow = Decimal(55)
+            let hardCodedHigh = Decimal(220)
+            let isDynamicColorScheme = settingsManager.settings.glucoseColorScheme == .dynamicColor
+            let highGlucoseColorValue = isDynamicColorScheme ? hardCodedHigh : settingsManager.settings.highGlucose
+            let lowGlucoseColorValue = isDynamicColorScheme ? hardCodedLow : settingsManager.settings.lowGlucose
+
+            state.highGlucoseColorValue = units == .mgdL ? highGlucoseColorValue : highGlucoseColorValue.asMmolL
+            state.lowGlucoseColorValue = units == .mgdL ? lowGlucoseColorValue : lowGlucoseColorValue.asMmolL
+            state
+                .targetGlucose = await getCurrentGlucoseTarget() ??
+                (settingsManager.settings.units == .mgdL ? Decimal(100) : 100.asMmolL)
+            state.glucoseColorScheme = settingsManager.settings.glucoseColorScheme
+
+            // Notify delegate about state update on main thread
+            await MainActor.run {
+                delegate?.contactImageManagerDidUpdateState(state)
+            }
+        } catch {
+            // Still notify delegate with current state, even if there was an error
             delegate?.contactImageManagerDidUpdateState(state)
             delegate?.contactImageManagerDidUpdateState(state)
         }
         }
     }
     }

+ 108 - 77
Trio/Sources/Services/HealthKit/HealthKitManager.swift

@@ -94,7 +94,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
     }
     }
 
 
     private func registerHandlers() {
     private func registerHandlers() {
-        coreDataPublisher?.filterByEntityName("PumpEventStored").sink { [weak self] _ in
+        coreDataPublisher?.filteredByEntityName("PumpEventStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task { [weak self] in
             Task { [weak self] in
                 guard let self = self else { return }
                 guard let self = self else { return }
@@ -102,7 +102,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
             }
             }
         }.store(in: &subscriptions)
         }.store(in: &subscriptions)
 
 
-        coreDataPublisher?.filterByEntityName("CarbEntryStored").sink { [weak self] _ in
+        coreDataPublisher?.filteredByEntityName("CarbEntryStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task { [weak self] in
             Task { [weak self] in
                 guard let self = self else { return }
                 guard let self = self else { return }
@@ -111,7 +111,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
         }.store(in: &subscriptions)
         }.store(in: &subscriptions)
 
 
         // This works only for manual Glucose
         // This works only for manual Glucose
-        coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
+        coreDataPublisher?.filteredByEntityName("GlucoseStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task { [weak self] in
             Task { [weak self] in
                 guard let self = self else { return }
                 guard let self = self else { return }
@@ -156,8 +156,18 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
     // Glucose Upload
     // Glucose Upload
 
 
     func uploadGlucose() async {
     func uploadGlucose() async {
-        await uploadGlucose(glucoseStorage.getGlucoseNotYetUploadedToHealth())
-        await uploadGlucose(glucoseStorage.getManualGlucoseNotYetUploadedToHealth())
+        do {
+            let glucose = try await glucoseStorage.getGlucoseNotYetUploadedToHealth()
+            await uploadGlucose(glucose)
+
+            let manualGlucose = try await glucoseStorage.getManualGlucoseNotYetUploadedToHealth()
+            await uploadGlucose(manualGlucose)
+        } catch {
+            debug(
+                .service,
+                "\(DebuggingIdentifiers.failed) Error fetching glucose for health upload: \(error.localizedDescription)"
+            )
+        }
     }
     }
 
 
     func uploadGlucose(_ glucose: [BloodGlucose]) async {
     func uploadGlucose(_ glucose: [BloodGlucose]) async {
@@ -228,7 +238,15 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
     // Carbs Upload
     // Carbs Upload
 
 
     func uploadCarbs() async {
     func uploadCarbs() async {
-        await uploadCarbs(carbsStorage.getCarbsNotYetUploadedToHealth())
+        do {
+            let carbs = try await carbsStorage.getCarbsNotYetUploadedToHealth()
+            await uploadCarbs(carbs)
+        } catch {
+            debug(
+                .service,
+                "\(DebuggingIdentifiers.failed) Error fetching carbs for health upload: \(error.localizedDescription)"
+            )
+        }
     }
     }
 
 
     func uploadCarbs(_ carbs: [CarbsEntry]) async {
     func uploadCarbs(_ carbs: [CarbsEntry]) async {
@@ -341,7 +359,15 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
     // Insulin Upload
     // Insulin Upload
 
 
     func uploadInsulin() async {
     func uploadInsulin() async {
-        await uploadInsulin(pumpHistoryStorage.getPumpHistoryNotYetUploadedToHealth())
+        do {
+            let events = try await pumpHistoryStorage.getPumpHistoryNotYetUploadedToHealth()
+            await uploadInsulin(events)
+        } catch {
+            debug(
+                .service,
+                "\(DebuggingIdentifiers.failed) Error fetching insulin events for health upload: \(error.localizedDescription)"
+            )
+        }
     }
     }
 
 
     func uploadInsulin(_ insulinEvents: [PumpHistoryEvent]) async {
     func uploadInsulin(_ insulinEvents: [PumpHistoryEvent]) async {
@@ -350,87 +376,92 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
               checkWriteToHealthPermissions(objectTypeToHealthStore: sampleType),
               checkWriteToHealthPermissions(objectTypeToHealthStore: sampleType),
               insulinEvents.isNotEmpty else { return }
               insulinEvents.isNotEmpty else { return }
 
 
-        // Fetch existing temp basal entries from Core Data for the last 24 hours
-        let fetchedInsulinEntries = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: PumpEventStored.self,
-            onContext: backgroundContext,
-            predicate: NSCompoundPredicate(andPredicateWithSubpredicates: [
-                NSPredicate.pumpHistoryLast24h,
-                NSPredicate(format: "tempBasal != nil")
-            ]),
-            key: "timestamp",
-            ascending: true,
-            batchSize: 50
-        )
+        do {
+            let fetchedInsulinEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
+                ofType: PumpEventStored.self,
+                onContext: backgroundContext,
+                predicate: NSCompoundPredicate(andPredicateWithSubpredicates: [
+                    NSPredicate.pumpHistoryLast24h,
+                    NSPredicate(format: "tempBasal != nil")
+                ]),
+                key: "timestamp",
+                ascending: true,
+                batchSize: 50
+            )
 
 
-        var insulinSamples: [HKQuantitySample] = []
+            var insulinSamples: [HKQuantitySample] = []
 
 
-        await backgroundContext.perform {
-            guard let existingTempBasalEntries = fetchedInsulinEntries as? [PumpEventStored] else { return }
-
-            for event in insulinEvents {
-                switch event.type {
-                case .bolus:
-                    // For bolus events, create a HealthKit sample directly
-                    if let sample = self.createSample(for: event, sampleType: sampleType) {
-                        debug(.service, "Created HealthKit sample for bolus entry: \(sample)")
-                        insulinSamples.append(sample)
-                    }
-                case .tempBasal:
-                    // For temp basal events, process them and adjust overlapping durations if necessary
-                    guard let duration = event.duration, let amount = event.amount else { continue }
-
-                    let value = (Decimal(duration) / 60.0) * amount
-                    let valueRounded = self.deviceDataManager?.pumpManager?
-                        .roundToSupportedBolusVolume(units: Double(value)) ?? Double(value)
-
-                    // Use binary search for efficient lookup of matching entry
-                    if let matchingIndex = self.binarySearch(entries: existingTempBasalEntries, timestamp: event.timestamp) {
-                        let predecessorIndex = matchingIndex - 1
-
-                        if predecessorIndex >= 0 {
-                            let predecessorEntry = existingTempBasalEntries[predecessorIndex]
-
-                            if let adjustedSample = self.processPredecessorEntry(
-                                predecessorEntry,
-                                nextEventTimestamp: event.timestamp,
-                                sampleType: sampleType
-                            ) {
-                                insulinSamples.append(adjustedSample)
-                            }
-                        }
-
-                        let newEvent = PumpHistoryEvent(
-                            id: event.id,
-                            type: .tempBasal,
-                            timestamp: event.timestamp,
-                            amount: Decimal(valueRounded),
-                            duration: event.duration
-                        )
+            try await backgroundContext.perform {
+                guard let existingTempBasalEntries = fetchedInsulinEntries as? [PumpEventStored] else {
+                    throw CoreDataError.fetchError(function: #function, file: #file)
+                }
 
 
-                        if let sample = self.createSample(for: newEvent, sampleType: sampleType) {
-                            debug(.service, "Created HealthKit sample for initial temp basal entry: \(sample)")
+                for event in insulinEvents {
+                    switch event.type {
+                    case .bolus:
+                        // For bolus events, create a HealthKit sample directly
+                        if let sample = self.createSample(for: event, sampleType: sampleType) {
+                            debug(.service, "Created HealthKit sample for bolus entry: \(sample)")
                             insulinSamples.append(sample)
                             insulinSamples.append(sample)
                         }
                         }
-                    }
+                    case .tempBasal:
+                        // For temp basal events, process them and adjust overlapping durations if necessary
+                        guard let duration = event.duration, let amount = event.amount else { continue }
+
+                        let value = (Decimal(duration) / 60.0) * amount
+                        let valueRounded = self.deviceDataManager?.pumpManager?
+                            .roundToSupportedBolusVolume(units: Double(value)) ?? Double(value)
+
+                        // Use binary search for efficient lookup of matching entry
+                        if let matchingIndex = self.binarySearch(entries: existingTempBasalEntries, timestamp: event.timestamp) {
+                            let predecessorIndex = matchingIndex - 1
+
+                            if predecessorIndex >= 0 {
+                                let predecessorEntry = existingTempBasalEntries[predecessorIndex]
+
+                                if let adjustedSample = self.processPredecessorEntry(
+                                    predecessorEntry,
+                                    nextEventTimestamp: event.timestamp,
+                                    sampleType: sampleType
+                                ) {
+                                    insulinSamples.append(adjustedSample)
+                                }
+                            }
+
+                            let newEvent = PumpHistoryEvent(
+                                id: event.id,
+                                type: .tempBasal,
+                                timestamp: event.timestamp,
+                                amount: Decimal(valueRounded),
+                                duration: event.duration
+                            )
+
+                            if let sample = self.createSample(for: newEvent, sampleType: sampleType) {
+                                debug(.service, "Created HealthKit sample for initial temp basal entry: \(sample)")
+                                insulinSamples.append(sample)
+                            }
+                        }
 
 
-                default:
-                    break
+                    default:
+                        break
+                    }
                 }
                 }
             }
             }
-        }
 
 
-        do {
-            guard insulinSamples.isNotEmpty else {
-                debug(.service, "No insulin samples available for upload.")
-                return
-            }
+            do {
+                guard insulinSamples.isNotEmpty else {
+                    debug(.service, "No insulin samples available for upload.")
+                    return
+                }
 
 
-            try await healthKitStore.save(insulinSamples)
-            debug(.service, "Successfully stored \(insulinSamples.count) insulin samples in HealthKit.")
-            await updateInsulinAsUploaded(insulinEvents)
+                try await healthKitStore.save(insulinSamples)
+                debug(.service, "Successfully stored \(insulinSamples.count) insulin samples in HealthKit.")
+                await updateInsulinAsUploaded(insulinEvents)
+            } catch {
+                debug(.service, "Failed to upload insulin samples to HealthKit: \(error.localizedDescription)")
+            }
         } catch {
         } catch {
-            debug(.service, "Failed to upload insulin samples to HealthKit: \(error.localizedDescription)")
+            debug(.service, "\(DebuggingIdentifiers.failed) Error fetching temp basal entries: \(error.localizedDescription)")
         }
         }
     }
     }
 
 

+ 12 - 12
Trio/Sources/Services/LiveActivity/Data/DataManager.swift

@@ -4,8 +4,8 @@ import Foundation
 
 
 @available(iOS 16.2, *)
 @available(iOS 16.2, *)
 extension LiveActivityBridge {
 extension LiveActivityBridge {
-    func fetchAndMapGlucose() async -> [GlucoseData] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func fetchAndMapGlucose() async throws -> [GlucoseData] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             ofType: GlucoseStored.self,
             onContext: context,
             onContext: context,
             predicate: NSPredicate.predicateForSixHoursAgo,
             predicate: NSPredicate.predicateForSixHoursAgo,
@@ -14,9 +14,9 @@ extension LiveActivityBridge {
             fetchLimit: 72
             fetchLimit: 72
         )
         )
 
 
-        return await context.perform {
+        return try await context.perform {
             guard let glucoseResults = results as? [GlucoseStored] else {
             guard let glucoseResults = results as? [GlucoseStored] else {
-                return []
+                throw CoreDataError.fetchError(function: #function, file: #file)
             }
             }
 
 
             return glucoseResults.map {
             return glucoseResults.map {
@@ -25,8 +25,8 @@ extension LiveActivityBridge {
         }
         }
     }
     }
 
 
-    func fetchAndMapDetermination() async -> DeterminationData? {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func fetchAndMapDetermination() async throws -> DeterminationData? {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OrefDetermination.self,
             ofType: OrefDetermination.self,
             onContext: context,
             onContext: context,
             predicate: NSPredicate.predicateFor30MinAgoForDetermination,
             predicate: NSPredicate.predicateFor30MinAgoForDetermination,
@@ -36,9 +36,9 @@ extension LiveActivityBridge {
             propertiesToFetch: ["iob", "cob", "totalDailyDose", "currentTarget", "deliverAt"]
             propertiesToFetch: ["iob", "cob", "totalDailyDose", "currentTarget", "deliverAt"]
         )
         )
 
 
-        return await context.perform {
+        return try await context.perform {
             guard let determinationResults = results as? [[String: Any]] else {
             guard let determinationResults = results as? [[String: Any]] else {
-                return nil
+                throw CoreDataError.fetchError(function: #function, file: #file)
             }
             }
 
 
             return determinationResults.first.map {
             return determinationResults.first.map {
@@ -53,8 +53,8 @@ extension LiveActivityBridge {
         }
         }
     }
     }
 
 
-    func fetchAndMapOverride() async -> OverrideData? {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func fetchAndMapOverride() async throws -> OverrideData? {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
             ofType: OverrideStored.self,
             onContext: context,
             onContext: context,
             predicate: NSPredicate.predicateForOneDayAgo,
             predicate: NSPredicate.predicateForOneDayAgo,
@@ -64,9 +64,9 @@ extension LiveActivityBridge {
             propertiesToFetch: ["enabled", "name", "target", "date", "duration"]
             propertiesToFetch: ["enabled", "name", "target", "date", "duration"]
         )
         )
 
 
-        return await context.perform {
+        return try await context.perform {
             guard let overrideResults = results as? [[String: Any]] else {
             guard let overrideResults = results as? [[String: Any]] else {
-                return nil
+                throw CoreDataError.fetchError(function: #function, file: #file)
             }
             }
 
 
             return overrideResults.first.map {
             return overrideResults.first.map {

+ 34 - 21
Trio/Sources/Services/LiveActivity/LiveActivityBridge.swift

@@ -51,7 +51,6 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
     private let queue = DispatchQueue(label: "LiveActivityBridge.queue", qos: .userInitiated)
     private let queue = DispatchQueue(label: "LiveActivityBridge.queue", qos: .userInitiated)
     private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
     private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
     private var subscriptions = Set<AnyCancellable>()
     private var subscriptions = Set<AnyCancellable>()
-    private let orefDeterminationSubject = PassthroughSubject<Void, Never>()
 
 
     init(resolver: Resolver) {
     init(resolver: Resolver) {
         coreDataPublisher =
         coreDataPublisher =
@@ -99,49 +98,59 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
     }
     }
 
 
     private func registerHandler() {
     private func registerHandler() {
-        coreDataPublisher?.filterByEntityName("OverrideStored").sink { [weak self] _ in
+        coreDataPublisher?.filteredByEntityName("OverrideStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             self.overridesDidUpdate()
             self.overridesDidUpdate()
         }.store(in: &subscriptions)
         }.store(in: &subscriptions)
 
 
-        coreDataPublisher?.filterByEntityName("OrefDetermination").sink { [weak self] _ in
+        coreDataPublisher?.filteredByEntityName("GlucoseStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
-            self.orefDeterminationSubject.send()
+            self.setupGlucoseArray()
         }.store(in: &subscriptions)
         }.store(in: &subscriptions)
+
+        coreDataPublisher?.filteredByEntityName("OrefDetermination")
+            .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .utility))
+            .sink { [weak self] _ in
+                guard let self = self else { return }
+                self.cobOrIobDidUpdate()
+            }.store(in: &subscriptions)
     }
     }
 
 
     private func registerSubscribers() {
     private func registerSubscribers() {
         glucoseStorage.updatePublisher
         glucoseStorage.updatePublisher
-            .receive(on: DispatchQueue.global(qos: .background))
+            .receive(on: queue)
             .sink { [weak self] _ in
             .sink { [weak self] _ in
                 guard let self = self else { return }
                 guard let self = self else { return }
                 self.setupGlucoseArray()
                 self.setupGlucoseArray()
             }
             }
             .store(in: &subscriptions)
             .store(in: &subscriptions)
-
-        orefDeterminationSubject
-            .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .background))
-            .sink { [weak self] in
-                guard let self = self else { return }
-                self.cobOrIobDidUpdate()
-            }
-            .store(in: &subscriptions)
     }
     }
 
 
     private func cobOrIobDidUpdate() {
     private func cobOrIobDidUpdate() {
         Task { @MainActor in
         Task { @MainActor in
-            self.determination = await fetchAndMapDetermination()
-            if let determination = determination {
-                await self.updateContentState(determination)
+            do {
+                self.determination = try await fetchAndMapDetermination()
+                if let determination = determination {
+                    await self.updateContentState(determination)
+                }
+            } catch {
+                debug(
+                    .default,
+                    "\(DebuggingIdentifiers.failed) failed to fetch and map determination: \(error.localizedDescription)"
+                )
             }
             }
         }
         }
     }
     }
 
 
     private func overridesDidUpdate() {
     private func overridesDidUpdate() {
         Task { @MainActor in
         Task { @MainActor in
-            self.override = await fetchAndMapOverride()
-            if let determination = determination {
-                await self.updateContentState(determination)
+            do {
+                self.override = try await fetchAndMapOverride()
+                if let determination = determination {
+                    await self.updateContentState(determination)
+                }
+            } catch {
+                debug(.default, "\(DebuggingIdentifiers.failed) failed to fetch and map override: \(error.localizedDescription)")
             }
             }
         }
         }
     }
     }
@@ -200,8 +209,12 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
 
 
     private func setupGlucoseArray() {
     private func setupGlucoseArray() {
         Task { @MainActor in
         Task { @MainActor in
-            self.glucoseFromPersistence = await fetchAndMapGlucose()
-            glucoseDidUpdate(glucoseFromPersistence ?? [])
+            do {
+                self.glucoseFromPersistence = try await fetchAndMapGlucose()
+                glucoseDidUpdate(glucoseFromPersistence ?? [])
+            } catch {
+                debug(.default, "\(DebuggingIdentifiers.failed) failed to fetch glucose with error: \(error)")
+            }
         }
         }
     }
     }
 
 

+ 147 - 113
Trio/Sources/Services/Network/Nightscout/NightscoutManager.swift

@@ -12,14 +12,14 @@ protocol NightscoutManager: GlucoseSource {
     func deleteCarbs(withID id: String) async
     func deleteCarbs(withID id: String) async
     func deleteInsulin(withID id: String) async
     func deleteInsulin(withID id: String) async
     func deleteManualGlucose(withID id: String) async
     func deleteManualGlucose(withID id: String) async
-    func uploadDeviceStatus() async
+    func uploadDeviceStatus() async throws
     func uploadGlucose() async
     func uploadGlucose() async
     func uploadCarbs() async
     func uploadCarbs() async
     func uploadPumpHistory() async
     func uploadPumpHistory() async
     func uploadOverrides() async
     func uploadOverrides() async
     func uploadTempTargets() async
     func uploadTempTargets() async
     func uploadManualGlucose() async
     func uploadManualGlucose() async
-    func uploadProfiles() async
+    func uploadProfiles() async throws
     func uploadNoteTreatment(note: String) async
     func uploadNoteTreatment(note: String) async
     func importSettings() async -> ScheduledNightscoutProfile?
     func importSettings() async -> ScheduledNightscoutProfile?
     var cgmURL: URL? { get }
     var cgmURL: URL? { get }
@@ -104,11 +104,18 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         /// This way, we ensure the latest enacted determination is always part of `devicestatus` and avoid having instances
         /// This way, we ensure the latest enacted determination is always part of `devicestatus` and avoid having instances
         /// where the first uploaded non-enacted determination (i.e., "suggested"), lacks the "enacted" data.
         /// where the first uploaded non-enacted determination (i.e., "suggested"), lacks the "enacted" data.
         Task {
         Task {
-            async let lastEnactedDeterminationID = determinationStorage
-                .fetchLastDeterminationObjectID(predicate: NSPredicate.enactedDetermination)
+            do {
+                let lastEnactedDeterminationID = try await determinationStorage
+                    .fetchLastDeterminationObjectID(predicate: NSPredicate.enactedDetermination)
 
 
-            self.lastEnactedDetermination = await determinationStorage
-                .getOrefDeterminationNotYetUploadedToNightscout(lastEnactedDeterminationID)
+                self.lastEnactedDetermination = await determinationStorage
+                    .getOrefDeterminationNotYetUploadedToNightscout(lastEnactedDeterminationID)
+            } catch {
+                debug(
+                    .default,
+                    "\(DebuggingIdentifiers.failed) failed to fetch last enacted determination: \(error.localizedDescription)"
+                )
+            }
         }
         }
     }
     }
 
 
@@ -119,8 +126,12 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     }
     }
 
 
     private func registerHandlers() {
     private func registerHandlers() {
+        /// We add debouncing behavior here for two main reasons
+        /// 1. To ensure that any upload flag updates have properly been performed, and in subsequent fetching processes only truly unuploaded data is fetched
+        /// 2. To not spam the user's NS site with a high number of uploads in a very short amount of time (less than 1sec)
         coreDataPublisher?
         coreDataPublisher?
-            .filterByEntityName("OrefDetermination")
+            .filteredByEntityName("OrefDetermination")
+            .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .background))
             .sink { [weak self] objectIDs in
             .sink { [weak self] objectIDs in
                 guard let self = self else { return }
                 guard let self = self else { return }
 
 
@@ -129,62 +140,83 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                     do {
                     do {
                         // Fetch only those determination objects
                         // Fetch only those determination objects
                         let request: NSFetchRequest<OrefDetermination> = OrefDetermination.fetchRequest()
                         let request: NSFetchRequest<OrefDetermination> = OrefDetermination.fetchRequest()
-                        request.predicate = NSPredicate(format: "SELF IN %@", objectIDs)
+                        request.predicate = NSPredicate(
+                            format: "SELF IN %@ AND isUploadedToNS == NO",
+                            objectIDs
+                        )
                         let results = try self.backgroundContext.fetch(request)
                         let results = try self.backgroundContext.fetch(request)
 
 
-                        // Safely filter out anything that's deleted or already uploaded
-                        let unuploaded = results.filter { !$0.isDeleted && !$0.isUploadedToNS }
-
                         // If valid, proceed to send to subject for further processing
                         // If valid, proceed to send to subject for further processing
-                        if !unuploaded.isEmpty {
-                            self.orefDeterminationSubject.send()
+                        if !results.isEmpty {
+                            Task {
+                                do {
+                                    try await self.uploadDeviceStatus()
+                                } catch {
+                                    debug(.nightscout, "\(DebuggingIdentifiers.failed) failed to upload device status")
+                                }
+                            }
                         }
                         }
                     } catch {
                     } catch {
-                        debugPrint("Failed to fetch OrefDetermination objects: \(error)")
+                        debug(.nightscout, "\(DebuggingIdentifiers.failed) Failed to fetch OrefDetermination objects: \(error)")
                     }
                     }
                 }
                 }
             }
             }
             .store(in: &subscriptions)
             .store(in: &subscriptions)
 
 
-        coreDataPublisher?.filterByEntityName("OverrideStored").sink { [weak self] _ in
-            self?.uploadOverridesSubject.send()
-        }.store(in: &subscriptions)
+        coreDataPublisher?.filteredByEntityName("OverrideStored")
+            .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .background))
+            .sink { [weak self] _ in
+                guard let self = self else { return }
+                Task.detached {
+                    await self.uploadOverrides()
+                }
+            }.store(in: &subscriptions)
 
 
-        coreDataPublisher?.filterByEntityName("OverrideRunStored").sink { [weak self] _ in
-            self?.uploadOverridesSubject.send()
-        }.store(in: &subscriptions)
+        coreDataPublisher?.filteredByEntityName("OverrideRunStored")
+            .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .background))
+            .sink { [weak self] _ in
+                guard let self = self else { return }
+                Task.detached {
+                    await self.uploadOverrides()
+                }
+            }.store(in: &subscriptions)
 
 
-        coreDataPublisher?.filterByEntityName("TempTargetStored").sink { [weak self] _ in
-            guard let self = self else { return }
-            Task.detached {
-                await self.uploadTempTargets()
-            }
-        }.store(in: &subscriptions)
+        coreDataPublisher?.filteredByEntityName("TempTargetStored")
+            .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .background))
+            .sink { [weak self] _ in
+                guard let self = self else { return }
+                Task.detached {
+                    await self.uploadTempTargets()
+                }
+            }.store(in: &subscriptions)
 
 
-        coreDataPublisher?.filterByEntityName("TempTargetRunStored").sink { [weak self] _ in
-            guard let self = self else { return }
-            Task.detached {
-                await self.uploadTempTargets()
-            }
-        }.store(in: &subscriptions)
+        coreDataPublisher?.filteredByEntityName("TempTargetRunStored")
+            .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .background))
+            .sink { [weak self] _ in
+                guard let self = self else { return }
+                Task.detached {
+                    await self.uploadTempTargets()
+                }
+            }.store(in: &subscriptions)
 
 
-        coreDataPublisher?.filterByEntityName("PumpEventStored")
+        coreDataPublisher?.filteredByEntityName("PumpEventStored")
+            .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .background))
             .sink { [weak self] objectIDs in
             .sink { [weak self] objectIDs in
                 guard let self = self else { return }
                 guard let self = self else { return }
 
 
-                // Now hop onto the background context’s queue
                 self.backgroundContext.perform {
                 self.backgroundContext.perform {
                     do {
                     do {
                         let request: NSFetchRequest<PumpEventStored> = PumpEventStored.fetchRequest()
                         let request: NSFetchRequest<PumpEventStored> = PumpEventStored.fetchRequest()
-                        request.predicate = NSPredicate(format: "SELF IN %@", objectIDs)
+                        request.predicate = NSPredicate(
+                            format: "SELF IN %@ AND isUploadedToNS == NO",
+                            objectIDs
+                        )
                         let results = try self.backgroundContext.fetch(request)
                         let results = try self.backgroundContext.fetch(request)
 
 
-                        // Safely filter out anything that’s deleted or already uploaded
-                        let unuploaded = results.filter { !$0.isDeleted && !$0.isUploadedToNS }
-
-                        // If valid, proceed to send to subject for further processing
-                        if !unuploaded.isEmpty {
-                            self.uploadPumpHistorySubject.send()
+                        if !results.isEmpty {
+                            Task.detached {
+                                await self.uploadPumpHistory()
+                            }
                         }
                         }
                     } catch {
                     } catch {
                         debugPrint("Failed to fetch PumpEventStored objects: \(error)")
                         debugPrint("Failed to fetch PumpEventStored objects: \(error)")
@@ -193,7 +225,8 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             }
             }
             .store(in: &subscriptions)
             .store(in: &subscriptions)
 
 
-        coreDataPublisher?.filterByEntityName("CarbEntryStored")
+        coreDataPublisher?.filteredByEntityName("CarbEntryStored")
+            .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .background))
             .sink { [weak self] objectIDs in
             .sink { [weak self] objectIDs in
                 guard let self = self else { return }
                 guard let self = self else { return }
 
 
@@ -201,15 +234,17 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                 self.backgroundContext.perform {
                 self.backgroundContext.perform {
                     do {
                     do {
                         let request: NSFetchRequest<CarbEntryStored> = CarbEntryStored.fetchRequest()
                         let request: NSFetchRequest<CarbEntryStored> = CarbEntryStored.fetchRequest()
-                        request.predicate = NSPredicate(format: "SELF IN %@", objectIDs)
+                        request.predicate = NSPredicate(
+                            format: "SELF IN %@ AND isUploadedToNS == NO",
+                            objectIDs
+                        )
                         let results = try self.backgroundContext.fetch(request)
                         let results = try self.backgroundContext.fetch(request)
 
 
-                        // Safely filter out anything that’s deleted or already uploaded
-                        let unuploaded = results.filter { !$0.isDeleted && !$0.isUploadedToNS }
-
                         // If valid, proceed to send to subject for further processing
                         // If valid, proceed to send to subject for further processing
-                        if !unuploaded.isEmpty {
-                            self.uploadCarbsSubject.send()
+                        if !results.isEmpty {
+                            Task.detached {
+                                await self.uploadCarbs()
+                            }
                         }
                         }
                     } catch {
                     } catch {
                         debugPrint("Failed to fetch CarbEntryStored objects: \(error)")
                         debugPrint("Failed to fetch CarbEntryStored objects: \(error)")
@@ -218,10 +253,11 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             }
             }
             .store(in: &subscriptions)
             .store(in: &subscriptions)
 
 
-        coreDataPublisher?.filterByEntityName("GlucoseStored")
+        coreDataPublisher?.filteredByEntityName("GlucoseStored")
             .sink { [weak self] _ in
             .sink { [weak self] _ in
                 guard let self = self else { return }
                 guard let self = self else { return }
                 Task.detached {
                 Task.detached {
+                    await self.uploadGlucose()
                     await self.uploadManualGlucose()
                     await self.uploadManualGlucose()
                 }
                 }
             }
             }
@@ -230,7 +266,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
 
 
     func registerSubscribers() {
     func registerSubscribers() {
         glucoseStorage.updatePublisher
         glucoseStorage.updatePublisher
-            .receive(on: DispatchQueue.global(qos: .background))
+            .receive(on: queue)
             .sink { [weak self] _ in
             .sink { [weak self] _ in
                 guard let self = self else { return }
                 guard let self = self else { return }
                 Task {
                 Task {
@@ -238,49 +274,6 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                 }
                 }
             }
             }
             .store(in: &subscriptions)
             .store(in: &subscriptions)
-
-        /// We add debouncing behavior here for two main reasons
-        /// 1. To ensure that any upload flag updates have properly been performed, and in subsequent fetching processes only truly unuploaded data is fetched
-        /// 2. To not spam the user's NS site with a high number of uploads in a very short amount of time (less than 1sec)
-        orefDeterminationSubject
-            .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .background))
-            .sink { [weak self] in
-                guard let self = self else { return }
-                Task {
-                    await self.uploadDeviceStatus()
-                }
-            }
-            .store(in: &subscriptions)
-
-        uploadOverridesSubject
-            .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .background))
-            .sink { [weak self] in
-                guard let self = self else { return }
-                Task {
-                    await self.uploadOverrides()
-                }
-            }
-            .store(in: &subscriptions)
-
-        uploadPumpHistorySubject
-            .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .background))
-            .sink { [weak self] in
-                guard let self = self else { return }
-                Task {
-                    await self.uploadPumpHistory()
-                }
-            }
-            .store(in: &subscriptions)
-
-        uploadCarbsSubject
-            .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .background))
-            .sink { [weak self] in
-                guard let self = self else { return }
-                Task {
-                    await self.uploadCarbs()
-                }
-            }
-            .store(in: &subscriptions)
     }
     }
 
 
     func setupNotification() {
     func setupNotification() {
@@ -505,7 +498,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     ///
     ///
     /// - Note: Ensure `nightscoutAPI` is initialized and `isUploadEnabled` is set to `true` before invoking this function.
     /// - Note: Ensure `nightscoutAPI` is initialized and `isUploadEnabled` is set to `true` before invoking this function.
     /// - Returns: Nothing.
     /// - Returns: Nothing.
-    func uploadDeviceStatus() async {
+    func uploadDeviceStatus() async throws {
         guard let nightscout = nightscoutAPI, isUploadEnabled else {
         guard let nightscout = nightscoutAPI, isUploadEnabled else {
             debug(.nightscout, "NS API not available or upload disabled. Aborting NS Status upload.")
             debug(.nightscout, "NS API not available or upload disabled. Aborting NS Status upload.")
             return
             return
@@ -523,7 +516,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         async let fetchedIOBEntry = storage.retrieveAsync(OpenAPS.Monitor.iob, as: [IOBEntry].self)
         async let fetchedIOBEntry = storage.retrieveAsync(OpenAPS.Monitor.iob, as: [IOBEntry].self)
         async let fetchedPumpStatus = storage.retrieveAsync(OpenAPS.Monitor.status, as: PumpStatus.self)
         async let fetchedPumpStatus = storage.retrieveAsync(OpenAPS.Monitor.status, as: PumpStatus.self)
 
 
-        var (fetchedEnactedDetermination, fetchedSuggestedDetermination) = await (
+        var (fetchedEnactedDetermination, fetchedSuggestedDetermination) = try await (
             determinationStorage.getOrefDeterminationNotYetUploadedToNightscout(enactedDeterminationID),
             determinationStorage.getOrefDeterminationNotYetUploadedToNightscout(enactedDeterminationID),
             determinationStorage.getOrefDeterminationNotYetUploadedToNightscout(suggestedDeterminationID)
             determinationStorage.getOrefDeterminationNotYetUploadedToNightscout(suggestedDeterminationID)
         )
         )
@@ -692,7 +685,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         }
         }
     }
     }
 
 
-    func uploadProfiles() async {
+    func uploadProfiles() async throws {
         if isUploadEnabled {
         if isUploadEnabled {
             do {
             do {
                 guard let sensitivities = await storage.retrieveAsync(
                 guard let sensitivities = await storage.retrieveAsync(
@@ -791,7 +784,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                 let bundleIdentifier = Bundle.main.bundleIdentifier ?? ""
                 let bundleIdentifier = Bundle.main.bundleIdentifier ?? ""
                 let deviceToken = UserDefaults.standard.string(forKey: "deviceToken") ?? ""
                 let deviceToken = UserDefaults.standard.string(forKey: "deviceToken") ?? ""
                 let isAPNSProduction = UserDefaults.standard.bool(forKey: "isAPNSProduction")
                 let isAPNSProduction = UserDefaults.standard.bool(forKey: "isAPNSProduction")
-                let presetOverrides = await overridesStorage.getPresetOverridesForNightscout()
+                let presetOverrides = try await overridesStorage.getPresetOverridesForNightscout()
                 let teamID = Bundle.main.object(forInfoDictionaryKey: "TeamID") as? String ?? ""
                 let teamID = Bundle.main.object(forInfoDictionaryKey: "TeamID") as? String ?? ""
 
 
                 let profileStore = NightscoutProfileStore(
                 let profileStore = NightscoutProfileStore(
@@ -816,12 +809,11 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                     return
                     return
                 }
                 }
 
 
-                do {
-                    try await nightscout.uploadProfile(profileStore)
-                    debug(.nightscout, "Profile uploaded")
-                } catch {
-                    debug(.nightscout, "NightscoutManager uploadProfile: \(error.localizedDescription)")
-                }
+                try await nightscout.uploadProfile(profileStore)
+                debug(.nightscout, "Profile uploaded")
+            } catch {
+                debug(.nightscout, "NightscoutManager uploadProfile: \(error.localizedDescription)")
+                throw error
             }
             }
         } else {
         } else {
             debug(.nightscout, "Upload to NS disabled; aborting profile uploaded")
             debug(.nightscout, "Upload to NS disabled; aborting profile uploaded")
@@ -843,31 +835,73 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     }
     }
 
 
     func uploadGlucose() async {
     func uploadGlucose() async {
-        await uploadGlucose(glucoseStorage.getGlucoseNotYetUploadedToNightscout())
-        await uploadNonCoreDataTreatments(glucoseStorage.getCGMStateNotYetUploadedToNightscout())
+        do {
+            try await uploadGlucose(glucoseStorage.getGlucoseNotYetUploadedToNightscout())
+            try await uploadNonCoreDataTreatments(glucoseStorage.getCGMStateNotYetUploadedToNightscout())
+        } catch {
+            debug(
+                .nightscout,
+                "\(DebuggingIdentifiers.failed) failed to upload glucose with error: \(error.localizedDescription)"
+            )
+        }
     }
     }
 
 
     func uploadManualGlucose() async {
     func uploadManualGlucose() async {
-        await uploadManualGlucose(glucoseStorage.getManualGlucoseNotYetUploadedToNightscout())
+        do {
+            try await uploadManualGlucose(glucoseStorage.getManualGlucoseNotYetUploadedToNightscout())
+        } catch {
+            debug(
+                .nightscout,
+                "\(DebuggingIdentifiers.failed) failed to upload manual glucose with error: \(error.localizedDescription)"
+            )
+        }
     }
     }
 
 
     func uploadPumpHistory() async {
     func uploadPumpHistory() async {
-        await uploadPumpHistory(pumpHistoryStorage.getPumpHistoryNotYetUploadedToNightscout())
+        do {
+            try await uploadPumpHistory(pumpHistoryStorage.getPumpHistoryNotYetUploadedToNightscout())
+        } catch {
+            debug(
+                .nightscout,
+                "\(DebuggingIdentifiers.failed) failed to upload pump history with error: \(error.localizedDescription)"
+            )
+        }
     }
     }
 
 
     func uploadCarbs() async {
     func uploadCarbs() async {
-        await uploadCarbs(carbsStorage.getCarbsNotYetUploadedToNightscout())
-        await uploadCarbs(carbsStorage.getFPUsNotYetUploadedToNightscout())
+        do {
+            try await uploadCarbs(carbsStorage.getCarbsNotYetUploadedToNightscout())
+            try await uploadCarbs(carbsStorage.getFPUsNotYetUploadedToNightscout())
+        } catch {
+            debug(
+                .nightscout,
+                "\(DebuggingIdentifiers.failed) failed to upload carbs with error: \(error.localizedDescription)"
+            )
+        }
     }
     }
 
 
     func uploadOverrides() async {
     func uploadOverrides() async {
-        await uploadOverrides(overridesStorage.getOverridesNotYetUploadedToNightscout())
-        await uploadOverrideRuns(overridesStorage.getOverrideRunsNotYetUploadedToNightscout())
+        do {
+            try await uploadOverrides(overridesStorage.getOverridesNotYetUploadedToNightscout())
+            try await uploadOverrideRuns(overridesStorage.getOverrideRunsNotYetUploadedToNightscout())
+        } catch {
+            debug(
+                .nightscout,
+                "\(DebuggingIdentifiers.failed) failed to upload overrides with error: \(error.localizedDescription)"
+            )
+        }
     }
     }
 
 
     func uploadTempTargets() async {
     func uploadTempTargets() async {
-        await uploadTempTargets(await tempTargetsStorage.getTempTargetsNotYetUploadedToNightscout())
-        await uploadTempTargetRuns(await tempTargetsStorage.getTempTargetRunsNotYetUploadedToNightscout())
+        do {
+            try await uploadTempTargets(await tempTargetsStorage.getTempTargetsNotYetUploadedToNightscout())
+            try await uploadTempTargetRuns(await tempTargetsStorage.getTempTargetRunsNotYetUploadedToNightscout())
+        } catch {
+            debug(
+                .nightscout,
+                "\(DebuggingIdentifiers.failed) failed to upload temp targets with error: \(error.localizedDescription)"
+            )
+        }
     }
     }
 
 
     private func uploadGlucose(_ glucose: [BloodGlucose]) async {
     private func uploadGlucose(_ glucose: [BloodGlucose]) async {

+ 120 - 98
Trio/Sources/Services/Network/TidepoolManager.swift

@@ -108,7 +108,7 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
 
 
     /// Registers handlers for Core Data changes
     /// Registers handlers for Core Data changes
     private func registerHandlers() {
     private func registerHandlers() {
-        coreDataPublisher?.filterByEntityName("PumpEventStored").sink { [weak self] _ in
+        coreDataPublisher?.filteredByEntityName("PumpEventStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task { [weak self] in
             Task { [weak self] in
                 guard let self = self else { return }
                 guard let self = self else { return }
@@ -116,7 +116,7 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
             }
             }
         }.store(in: &subscriptions)
         }.store(in: &subscriptions)
 
 
-        coreDataPublisher?.filterByEntityName("CarbEntryStored").sink { [weak self] _ in
+        coreDataPublisher?.filteredByEntityName("CarbEntryStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task { [weak self] in
             Task { [weak self] in
                 guard let self = self else { return }
                 guard let self = self else { return }
@@ -125,7 +125,7 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
         }.store(in: &subscriptions)
         }.store(in: &subscriptions)
 
 
         // This works only for manual Glucose
         // This works only for manual Glucose
-        coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
+        coreDataPublisher?.filteredByEntityName("GlucoseStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task { [weak self] in
             Task { [weak self] in
                 guard let self = self else { return }
                 guard let self = self else { return }
@@ -187,7 +187,11 @@ extension BaseTidepoolManager: ServiceDelegate {
 /// Carb Upload and Deletion Functionality
 /// Carb Upload and Deletion Functionality
 extension BaseTidepoolManager {
 extension BaseTidepoolManager {
     func uploadCarbs() async {
     func uploadCarbs() async {
-        uploadCarbs(await carbsStorage.getCarbsNotYetUploadedToTidepool())
+        do {
+            try uploadCarbs(await carbsStorage.getCarbsNotYetUploadedToTidepool())
+        } catch {
+            debug(.service, "\(DebuggingIdentifiers.failed) Failed to upload carbs with error: \(error.localizedDescription)")
+        }
     }
     }
 
 
     func uploadCarbs(_ carbs: [CarbsEntry]) {
     func uploadCarbs(_ carbs: [CarbsEntry]) {
@@ -274,115 +278,129 @@ extension BaseTidepoolManager {
 /// Insulin Upload and Deletion Functionality
 /// Insulin Upload and Deletion Functionality
 extension BaseTidepoolManager {
 extension BaseTidepoolManager {
     func uploadInsulin() async {
     func uploadInsulin() async {
-        await uploadDose(await pumpHistoryStorage.getPumpHistoryNotYetUploadedToTidepool())
+        do {
+            let events = try await pumpHistoryStorage.getPumpHistoryNotYetUploadedToTidepool()
+            await uploadDose(events)
+        } catch {
+            debug(.service, "Error fetching pump history: \(error.localizedDescription)")
+        }
     }
     }
 
 
     func uploadDose(_ events: [PumpHistoryEvent]) async {
     func uploadDose(_ events: [PumpHistoryEvent]) async {
         guard !events.isEmpty, let tidepoolService = self.tidepoolService else { return }
         guard !events.isEmpty, let tidepoolService = self.tidepoolService else { return }
 
 
-        // Fetch all temp basal entries from Core Data for the last 24 hours
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: PumpEventStored.self,
-            onContext: backgroundContext,
-            predicate: NSCompoundPredicate(andPredicateWithSubpredicates: [
-                NSPredicate.pumpHistoryLast24h,
-                NSPredicate(format: "tempBasal != nil")
-            ]),
-            key: "timestamp",
-            ascending: true,
-            batchSize: 50
-        )
-
-        // Ensure that the processing happens within the background context for thread safety
-        await backgroundContext.perform {
-            guard let existingTempBasalEntries = results as? [PumpEventStored] else { return }
-
-            let insulinDoseEvents: [DoseEntry] = events.reduce([]) { result, event in
-                var result = result
-                switch event.type {
-                case .tempBasal:
-                    result
-                        .append(contentsOf: self.processTempBasalEvent(event, existingTempBasalEntries: existingTempBasalEntries))
-                case .bolus:
-                    let bolusDoseEntry = DoseEntry(
-                        type: .bolus,
-                        startDate: event.timestamp,
-                        endDate: event.timestamp,
-                        value: Double(event.amount!),
-                        unit: .units,
-                        deliveredUnits: nil,
-                        syncIdentifier: event.id,
-                        scheduledBasalRate: nil,
-                        insulinType: self.apsManager.pumpManager?.status.insulinType ?? nil,
-                        automatic: event.isSMB ?? true,
-                        manuallyEntered: event.isExternal ?? false
-                    )
-                    result.append(bolusDoseEntry)
-                default:
-                    break
+        do {
+            // Fetch all temp basal entries from Core Data for the last 24 hours
+            let results = try await CoreDataStack.shared.fetchEntitiesAsync(
+                ofType: PumpEventStored.self,
+                onContext: backgroundContext,
+                predicate: NSCompoundPredicate(andPredicateWithSubpredicates: [
+                    NSPredicate.pumpHistoryLast24h,
+                    NSPredicate(format: "tempBasal != nil")
+                ]),
+                key: "timestamp",
+                ascending: true,
+                batchSize: 50
+            )
+
+            // Ensure that the processing happens within the background context for thread safety
+            try await backgroundContext.perform {
+                guard let existingTempBasalEntries = results as? [PumpEventStored] else {
+                    throw CoreDataError.fetchError(function: #function, file: #file)
                 }
                 }
-                return result
-            }
-
-            debug(.service, "TIDEPOOL DOSE ENTRIES: \(insulinDoseEvents)")
 
 
-            let pumpEvents: [PersistedPumpEvent] = events.compactMap { event -> PersistedPumpEvent? in
-                if let pumpEventType = event.type.mapEventTypeToPumpEventType() {
-                    let dose: DoseEntry? = switch pumpEventType {
-                    case .suspend:
-                        DoseEntry(suspendDate: event.timestamp, automatic: true)
-                    case .resume:
-                        DoseEntry(resumeDate: event.timestamp, automatic: true)
+                let insulinDoseEvents: [DoseEntry] = events.reduce([]) { result, event in
+                    var result = result
+                    switch event.type {
+                    case .tempBasal:
+                        result
+                            .append(
+                                contentsOf: self
+                                    .processTempBasalEvent(event, existingTempBasalEntries: existingTempBasalEntries)
+                            )
+                    case .bolus:
+                        let bolusDoseEntry = DoseEntry(
+                            type: .bolus,
+                            startDate: event.timestamp,
+                            endDate: event.timestamp,
+                            value: Double(event.amount!),
+                            unit: .units,
+                            deliveredUnits: nil,
+                            syncIdentifier: event.id,
+                            scheduledBasalRate: nil,
+                            insulinType: self.apsManager.pumpManager?.status.insulinType ?? nil,
+                            automatic: event.isSMB ?? true,
+                            manuallyEntered: event.isExternal ?? false
+                        )
+                        result.append(bolusDoseEntry)
                     default:
                     default:
-                        nil
+                        break
                     }
                     }
-
-                    return PersistedPumpEvent(
-                        date: event.timestamp,
-                        persistedDate: event.timestamp,
-                        dose: dose,
-                        isUploaded: true,
-                        objectIDURL: URL(string: "x-coredata:///PumpEvent/\(event.id)")!,
-                        raw: event.id.data(using: .utf8),
-                        title: event.note,
-                        type: pumpEventType
-                    )
-                } else {
-                    return nil
+                    return result
                 }
                 }
-            }
 
 
-            self.processQueue.async {
-                tidepoolService.uploadDoseData(created: insulinDoseEvents, deleted: []) { result in
-                    switch result {
-                    case let .failure(error):
-                        debug(.nightscout, "Error synchronizing dose data with Tidepool: \(String(describing: error))")
-                    case .success:
-                        debug(.nightscout, "Success synchronizing dose data. Upload to Tidepool complete.")
-                        Task {
-                            let insulinEvents = events.filter {
-                                $0.type == .tempBasal || $0.type == .tempBasalDuration || $0.type == .bolus
-                            }
-                            await self.updateInsulinAsUploaded(insulinEvents)
+                debug(.service, "TIDEPOOL DOSE ENTRIES: \(insulinDoseEvents)")
+
+                let pumpEvents: [PersistedPumpEvent] = events.compactMap { event -> PersistedPumpEvent? in
+                    if let pumpEventType = event.type.mapEventTypeToPumpEventType() {
+                        let dose: DoseEntry? = switch pumpEventType {
+                        case .suspend:
+                            DoseEntry(suspendDate: event.timestamp, automatic: true)
+                        case .resume:
+                            DoseEntry(resumeDate: event.timestamp, automatic: true)
+                        default:
+                            nil
                         }
                         }
+
+                        return PersistedPumpEvent(
+                            date: event.timestamp,
+                            persistedDate: event.timestamp,
+                            dose: dose,
+                            isUploaded: true,
+                            objectIDURL: URL(string: "x-coredata:///PumpEvent/\(event.id)")!,
+                            raw: event.id.data(using: .utf8),
+                            title: event.note,
+                            type: pumpEventType
+                        )
+                    } else {
+                        return nil
                     }
                     }
                 }
                 }
 
 
-                tidepoolService.uploadPumpEventData(pumpEvents) { result in
-                    switch result {
-                    case let .failure(error):
-                        debug(.nightscout, "Error synchronizing pump events data: \(String(describing: error))")
-                    case .success:
-                        debug(.nightscout, "Success synchronizing pump events data. Upload to Tidepool complete.")
-                        Task {
-                            let pumpEventType = events.map { $0.type.mapEventTypeToPumpEventType() }
-                            let pumpEvents = events.filter { _ in pumpEventType.contains(pumpEventType) }
+                self.processQueue.async {
+                    tidepoolService.uploadDoseData(created: insulinDoseEvents, deleted: []) { result in
+                        switch result {
+                        case let .failure(error):
+                            debug(.nightscout, "Error synchronizing dose data with Tidepool: \(String(describing: error))")
+                        case .success:
+                            debug(.nightscout, "Success synchronizing dose data. Upload to Tidepool complete.")
+                            Task {
+                                let insulinEvents = events.filter {
+                                    $0.type == .tempBasal || $0.type == .tempBasalDuration || $0.type == .bolus
+                                }
+                                await self.updateInsulinAsUploaded(insulinEvents)
+                            }
+                        }
+                    }
 
 
-                            await self.updateInsulinAsUploaded(pumpEvents)
+                    tidepoolService.uploadPumpEventData(pumpEvents) { result in
+                        switch result {
+                        case let .failure(error):
+                            debug(.nightscout, "Error synchronizing pump events data: \(String(describing: error))")
+                        case .success:
+                            debug(.nightscout, "Success synchronizing pump events data. Upload to Tidepool complete.")
+                            Task {
+                                let pumpEventType = events.map { $0.type.mapEventTypeToPumpEventType() }
+                                let pumpEvents = events.filter { _ in pumpEventType.contains(pumpEventType) }
+
+                                await self.updateInsulinAsUploaded(pumpEvents)
+                            }
                         }
                         }
                     }
                     }
                 }
                 }
             }
             }
+        } catch {
+            debug(.service, "Error fetching temp basal entries: \(error.localizedDescription)")
         }
         }
     }
     }
 
 
@@ -573,11 +591,15 @@ extension BaseTidepoolManager {
 /// Glucose Upload Functionality
 /// Glucose Upload Functionality
 extension BaseTidepoolManager {
 extension BaseTidepoolManager {
     func uploadGlucose() async {
     func uploadGlucose() async {
-        uploadGlucose(await glucoseStorage.getGlucoseNotYetUploadedToTidepool())
-        uploadGlucose(
-            await glucoseStorage
-                .getManualGlucoseNotYetUploadedToTidepool()
-        )
+        do {
+            let glucose = try await glucoseStorage.getGlucoseNotYetUploadedToTidepool()
+            uploadGlucose(glucose)
+
+            let manualGlucose = try await glucoseStorage.getManualGlucoseNotYetUploadedToTidepool()
+            uploadGlucose(manualGlucose)
+        } catch {
+            debug(.service, "Error fetching glucose data: \(error.localizedDescription)")
+        }
     }
     }
 
 
     func uploadGlucose(_ glucose: [StoredGlucoseSample]) {
     func uploadGlucose(_ glucose: [StoredGlucoseSample]) {

+ 2 - 2
Trio/Sources/Services/RemoteControl/TrioRemoteControl+APNS.swift

@@ -1,7 +1,7 @@
 import Foundation
 import Foundation
 
 
 extension TrioRemoteControl {
 extension TrioRemoteControl {
-    internal func handleAPNSChanges(deviceToken: String?) async {
+    internal func handleAPNSChanges(deviceToken: String?) async throws {
         let previousDeviceToken = UserDefaults.standard.string(forKey: "deviceToken")
         let previousDeviceToken = UserDefaults.standard.string(forKey: "deviceToken")
         let previousIsAPNSProduction = UserDefaults.standard.bool(forKey: "isAPNSProduction")
         let previousIsAPNSProduction = UserDefaults.standard.bool(forKey: "isAPNSProduction")
 
 
@@ -21,7 +21,7 @@ extension TrioRemoteControl {
         }
         }
 
 
         if shouldUploadProfiles {
         if shouldUploadProfiles {
-            await nightscoutManager.uploadProfiles()
+            try await nightscoutManager.uploadProfiles()
         } else {
         } else {
             debug(.remoteControl, "No changes detected in device token or APNS environment.")
             debug(.remoteControl, "No changes detected in device token or APNS environment.")
         }
         }

+ 10 - 9
Trio/Sources/Services/RemoteControl/TrioRemoteControl+Bolus.swift

@@ -1,7 +1,7 @@
 import Foundation
 import Foundation
 
 
 extension TrioRemoteControl {
 extension TrioRemoteControl {
-    internal func handleBolusCommand(_ pushMessage: PushMessage) async {
+    internal func handleBolusCommand(_ pushMessage: PushMessage) async throws {
         guard let bolusAmount = pushMessage.bolusAmount else {
         guard let bolusAmount = pushMessage.bolusAmount else {
             await logError("Command rejected: bolus amount is missing or invalid.", pushMessage: pushMessage)
             await logError("Command rejected: bolus amount is missing or invalid.", pushMessage: pushMessage)
             return
             return
@@ -18,7 +18,7 @@ extension TrioRemoteControl {
         }
         }
 
 
         let maxIOB = settings.preferences.maxIOB
         let maxIOB = settings.preferences.maxIOB
-        let currentIOB = await fetchCurrentIOB()
+        let currentIOB = try await fetchCurrentIOB()
         if (currentIOB + bolusAmount) > maxIOB {
         if (currentIOB + bolusAmount) > maxIOB {
             await logError(
             await logError(
                 "Command rejected: bolus amount (\(bolusAmount) units) would exceed max IOB (\(maxIOB) units). Current IOB: \(currentIOB) units.",
                 "Command rejected: bolus amount (\(bolusAmount) units) would exceed max IOB (\(maxIOB) units). Current IOB: \(currentIOB) units.",
@@ -27,7 +27,8 @@ extension TrioRemoteControl {
             return
             return
         }
         }
 
 
-        let totalRecentBolusAmount = await fetchTotalRecentBolusAmount(since: Date(timeIntervalSince1970: pushMessage.timestamp))
+        let totalRecentBolusAmount =
+            try await fetchTotalRecentBolusAmount(since: Date(timeIntervalSince1970: pushMessage.timestamp))
 
 
         if totalRecentBolusAmount >= bolusAmount * 0.2 {
         if totalRecentBolusAmount >= bolusAmount * 0.2 {
             await logError(
             await logError(
@@ -55,10 +56,10 @@ extension TrioRemoteControl {
         )
         )
     }
     }
 
 
-    private func fetchCurrentIOB() async -> Decimal {
+    private func fetchCurrentIOB() async throws -> Decimal {
         let predicate = NSPredicate.predicateFor30MinAgoForDetermination
         let predicate = NSPredicate.predicateFor30MinAgoForDetermination
 
 
-        let determinations = await CoreDataStack.shared.fetchEntitiesAsync(
+        let determinations = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OrefDetermination.self,
             ofType: OrefDetermination.self,
             onContext: pumpHistoryFetchContext,
             onContext: pumpHistoryFetchContext,
             predicate: predicate,
             predicate: predicate,
@@ -73,20 +74,20 @@ extension TrioRemoteControl {
               let iob = firstResult["iob"] as? Decimal
               let iob = firstResult["iob"] as? Decimal
         else {
         else {
             await logError("Failed to fetch current IOB.")
             await logError("Failed to fetch current IOB.")
-            return Decimal(0)
+            throw CoreDataError.fetchError(function: #function, file: #file)
         }
         }
 
 
         return iob
         return iob
     }
     }
 
 
-    private func fetchTotalRecentBolusAmount(since date: Date) async -> Decimal {
+    private func fetchTotalRecentBolusAmount(since date: Date) async throws -> Decimal {
         let predicate = NSPredicate(
         let predicate = NSPredicate(
             format: "type == %@ AND timestamp > %@",
             format: "type == %@ AND timestamp > %@",
             PumpEventStored.EventType.bolus.rawValue,
             PumpEventStored.EventType.bolus.rawValue,
             date as NSDate
             date as NSDate
         )
         )
 
 
-        let results: Any = await CoreDataStack.shared.fetchEntitiesAsync(
+        let results: Any = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: PumpEventStored.self,
             ofType: PumpEventStored.self,
             onContext: pumpHistoryFetchContext,
             onContext: pumpHistoryFetchContext,
             predicate: predicate,
             predicate: predicate,
@@ -98,7 +99,7 @@ extension TrioRemoteControl {
 
 
         guard let bolusDictionaries = results as? [[String: Any]] else {
         guard let bolusDictionaries = results as? [[String: Any]] else {
             await logError("Failed to cast fetched bolus events. Fetched entities type: \(type(of: results))")
             await logError("Failed to cast fetched bolus events. Fetched entities type: \(type(of: results))")
-            return 0
+            throw CoreDataError.fetchError(function: #function, file: #file)
         }
         }
 
 
         let totalAmount = bolusDictionaries.compactMap { ($0["bolus.amount"] as? NSNumber)?.decimalValue }.reduce(0, +)
         let totalAmount = bolusDictionaries.compactMap { ($0["bolus.amount"] as? NSNumber)?.decimalValue }.reduce(0, +)

+ 2 - 2
Trio/Sources/Services/RemoteControl/TrioRemoteControl+Meal.swift

@@ -1,7 +1,7 @@
 import Foundation
 import Foundation
 
 
 extension TrioRemoteControl {
 extension TrioRemoteControl {
-    func handleMealCommand(_ pushMessage: PushMessage) async {
+    func handleMealCommand(_ pushMessage: PushMessage) async throws {
         guard pushMessage.carbs != nil || pushMessage.fat != nil || pushMessage.protein != nil else {
         guard pushMessage.carbs != nil || pushMessage.fat != nil || pushMessage.protein != nil else {
             await logError("Command rejected: meal data is incomplete or invalid.", pushMessage: pushMessage)
             await logError("Command rejected: meal data is incomplete or invalid.", pushMessage: pushMessage)
             return
             return
@@ -72,7 +72,7 @@ extension TrioRemoteControl {
             fpuID: fatDecimal ?? 0 > 0 || proteinDecimal ?? 0 > 0 ? UUID().uuidString : nil
             fpuID: fatDecimal ?? 0 > 0 || proteinDecimal ?? 0 > 0 ? UUID().uuidString : nil
         )
         )
 
 
-        await carbsStorage.storeCarbs([mealEntry], areFetchedFromRemote: false)
+        try await carbsStorage.storeCarbs([mealEntry], areFetchedFromRemote: false)
 
 
         debug(
         debug(
             .remoteControl,
             .remoteControl,

+ 37 - 23
Trio/Sources/Services/RemoteControl/TrioRemoteControl+Override.swift

@@ -12,21 +12,35 @@ extension TrioRemoteControl {
     }
     }
 
 
     @MainActor internal func handleStartOverrideCommand(_ pushMessage: PushMessage) async {
     @MainActor internal func handleStartOverrideCommand(_ pushMessage: PushMessage) async {
-        guard let overrideName = pushMessage.overrideName, !overrideName.isEmpty else {
-            await logError("Command rejected: override name is missing.", pushMessage: pushMessage)
-            return
-        }
+        do {
+            guard let overrideName = pushMessage.overrideName, !overrideName.isEmpty else {
+                await logError("Command rejected: override name is missing.", pushMessage: pushMessage)
+                return
+            }
 
 
-        let presetIDs = await overrideStorage.fetchForOverridePresets()
+            let presetIDs = try await overrideStorage.fetchForOverridePresets()
 
 
-        let presets = presetIDs.compactMap { id in
-            try? viewContext.existingObject(with: id) as? OverrideStored
-        }
+            let presets = try presetIDs.compactMap { id in
+                try viewContext.existingObject(with: id) as? OverrideStored
+            }
 
 
-        if let preset = presets.first(where: { $0.name == overrideName }) {
-            await enactOverridePreset(preset: preset, pushMessage: pushMessage)
-        } else {
-            await logError("Command rejected: override preset '\(overrideName)' not found.", pushMessage: pushMessage)
+            if let preset = presets.first(where: { $0.name == overrideName }) {
+                await enactOverridePreset(preset: preset, pushMessage: pushMessage)
+            } else {
+                await logError(
+                    "Command rejected: override preset '\(overrideName)' not found.",
+                    pushMessage: pushMessage
+                )
+            }
+        } catch {
+            debug(
+                .remoteControl,
+                "\(DebuggingIdentifiers.failed) Failed to handle start override command: \(error.localizedDescription)"
+            )
+            await logError(
+                "Command failed: \(error.localizedDescription)",
+                pushMessage: pushMessage
+            )
         }
         }
     }
     }
 
 
@@ -52,10 +66,10 @@ extension TrioRemoteControl {
     }
     }
 
 
     @MainActor private func disableAllActiveOverrides(except overrideID: NSManagedObjectID? = nil) async {
     @MainActor private func disableAllActiveOverrides(except overrideID: NSManagedObjectID? = nil) async {
-        let ids = await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0) // 0 = no fetch limit
+        do {
+            let ids = try await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0) // 0 = no fetch limit
 
 
-        let didPostNotification = await viewContext.perform { () -> Bool in
-            do {
+            let didPostNotification = try await viewContext.perform { () -> Bool in
                 let results = try ids.compactMap { id in
                 let results = try ids.compactMap { id in
                     try self.viewContext.existingObject(with: id) as? OverrideStored
                     try self.viewContext.existingObject(with: id) as? OverrideStored
                 }
                 }
@@ -89,16 +103,16 @@ extension TrioRemoteControl {
                 } else {
                 } else {
                     return false
                     return false
                 }
                 }
-            } catch {
-                debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active Overrides with error: \(error.localizedDescription)"
-                )
-                return false
             }
             }
-        }
 
 
-        if didPostNotification {
-            await awaitNotification(.didUpdateOverrideConfiguration)
+            if didPostNotification {
+                await awaitNotification(.didUpdateOverrideConfiguration)
+            }
+        } catch {
+            debug(
+                .remoteControl,
+                "\(DebuggingIdentifiers.failed) Failed to disable active overrides: \(error.localizedDescription)"
+            )
         }
         }
     }
     }
 }
 }

+ 15 - 15
Trio/Sources/Services/RemoteControl/TrioRemoteControl+TempTarget.swift

@@ -2,7 +2,7 @@ import CoreData
 import Foundation
 import Foundation
 
 
 extension TrioRemoteControl {
 extension TrioRemoteControl {
-    @MainActor func handleTempTargetCommand(_ pushMessage: PushMessage) async {
+    @MainActor func handleTempTargetCommand(_ pushMessage: PushMessage) async throws {
         guard let targetValue = pushMessage.target,
         guard let targetValue = pushMessage.target,
               let durationValue = pushMessage.duration
               let durationValue = pushMessage.duration
         else {
         else {
@@ -26,7 +26,7 @@ extension TrioRemoteControl {
             halfBasalTarget: settings.preferences.halfBasalExerciseTarget
             halfBasalTarget: settings.preferences.halfBasalExerciseTarget
         )
         )
 
 
-        await tempTargetsStorage.storeTempTarget(tempTarget: tempTarget)
+        try await tempTargetsStorage.storeTempTarget(tempTarget: tempTarget)
         tempTargetsStorage.saveTempTargetsToStorage([tempTarget])
         tempTargetsStorage.saveTempTargetsToStorage([tempTarget])
 
 
         debug(
         debug(
@@ -47,10 +47,10 @@ extension TrioRemoteControl {
     }
     }
 
 
     @MainActor func disableAllActiveTempTargets() async {
     @MainActor func disableAllActiveTempTargets() async {
-        let ids = await tempTargetsStorage.loadLatestTempTargetConfigurations(fetchLimit: 0)
+        do {
+            let ids = try await tempTargetsStorage.loadLatestTempTargetConfigurations(fetchLimit: 0)
 
 
-        let didPostNotification = await viewContext.perform { () -> Bool in
-            do {
+            let didPostNotification = try await viewContext.perform { () -> Bool in
                 let results = try ids.compactMap { id in
                 let results = try ids.compactMap { id in
                     try self.viewContext.existingObject(with: id) as? TempTargetStored
                     try self.viewContext.existingObject(with: id) as? TempTargetStored
                 }
                 }
@@ -68,8 +68,7 @@ extension TrioRemoteControl {
                     newTempTargetRunStored.name = canceledTempTarget.name
                     newTempTargetRunStored.name = canceledTempTarget.name
                     newTempTargetRunStored.startDate = canceledTempTarget.date ?? .distantPast
                     newTempTargetRunStored.startDate = canceledTempTarget.date ?? .distantPast
                     newTempTargetRunStored.endDate = Date()
                     newTempTargetRunStored.endDate = Date()
-                    newTempTargetRunStored
-                        .target = canceledTempTarget.target ?? 0
+                    newTempTargetRunStored.target = canceledTempTarget.target ?? 0
                     newTempTargetRunStored.tempTarget = canceledTempTarget
                     newTempTargetRunStored.tempTarget = canceledTempTarget
                     newTempTargetRunStored.isUploadedToNS = false
                     newTempTargetRunStored.isUploadedToNS = false
 
 
@@ -87,16 +86,17 @@ extension TrioRemoteControl {
                 } else {
                 } else {
                     return false
                     return false
                 }
                 }
-            } catch {
-                debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active TempTargets with error: \(error.localizedDescription)"
-                )
-                return false
             }
             }
-        }
 
 
-        if didPostNotification {
-            await awaitNotification(.didUpdateTempTargetConfiguration)
+            if didPostNotification {
+                await awaitNotification(.didUpdateTempTargetConfiguration)
+            }
+        } catch {
+            debug(
+                .remoteControl,
+                "\(DebuggingIdentifiers.failed) Failed to disable active temp targets: \(error.localizedDescription)"
+            )
+            await logError("Failed to disable temp targets: \(error.localizedDescription)")
         }
         }
     }
     }
 }
 }

+ 5 - 5
Trio/Sources/Services/RemoteControl/TrioRemoteControl.swift

@@ -22,7 +22,7 @@ class TrioRemoteControl: Injectable {
         injectServices(TrioApp.resolver)
         injectServices(TrioApp.resolver)
     }
     }
 
 
-    func handleRemoteNotification(pushMessage: PushMessage) async {
+    func handleRemoteNotification(pushMessage: PushMessage) async throws {
         let isTrioRemoteControlEnabled = UserDefaults.standard.bool(forKey: "isTrioRemoteControlEnabled")
         let isTrioRemoteControlEnabled = UserDefaults.standard.bool(forKey: "isTrioRemoteControlEnabled")
         guard isTrioRemoteControlEnabled else {
         guard isTrioRemoteControlEnabled else {
             await logError("Remote command received, but remote control is disabled in settings. Ignoring the command.")
             await logError("Remote command received, but remote control is disabled in settings. Ignoring the command.")
@@ -67,16 +67,16 @@ class TrioRemoteControl: Injectable {
 
 
         switch pushMessage.commandType {
         switch pushMessage.commandType {
         case .bolus:
         case .bolus:
-            await handleBolusCommand(pushMessage)
+            try await handleBolusCommand(pushMessage)
         case .tempTarget:
         case .tempTarget:
-            await handleTempTargetCommand(pushMessage)
+            try await handleTempTargetCommand(pushMessage)
         case .cancelTempTarget:
         case .cancelTempTarget:
             await cancelTempTarget(pushMessage)
             await cancelTempTarget(pushMessage)
         case .meal:
         case .meal:
-            await handleMealCommand(pushMessage)
+            try await handleMealCommand(pushMessage)
 
 
             if pushMessage.bolusAmount != nil {
             if pushMessage.bolusAmount != nil {
-                await handleBolusCommand(pushMessage)
+                try await handleBolusCommand(pushMessage)
             }
             }
         case .startOverride:
         case .startOverride:
             await handleStartOverrideCommand(pushMessage)
             await handleStartOverrideCommand(pushMessage)

+ 74 - 21
Trio/Sources/Services/Storage/FileStorage.swift

@@ -23,10 +23,14 @@ final class BaseFileStorage: FileStorage {
 
 
     func save<Value: JSON>(_ value: Value, as name: String) {
     func save<Value: JSON>(_ value: Value, as name: String) {
         processQueue.safeSync {
         processQueue.safeSync {
-            if let value = value as? RawJSON, let data = value.data(using: .utf8) {
-                try? Disk.save(data, to: .documents, as: name)
-            } else {
-                try? Disk.save(value, to: .documents, as: name, encoder: JSONCoding.encoder)
+            do {
+                if let value = value as? RawJSON, let data = value.data(using: .utf8) {
+                    try Disk.save(data, to: .documents, as: name)
+                } else {
+                    try Disk.save(value, to: .documents, as: name, encoder: JSONCoding.encoder)
+                }
+            } catch {
+                debug(.storage, "Failed to save file '\(name)': \(error.localizedDescription)")
             }
             }
         }
         }
     }
     }
@@ -34,10 +38,14 @@ final class BaseFileStorage: FileStorage {
     func saveAsync<Value: JSON>(_ value: Value, as name: String) async {
     func saveAsync<Value: JSON>(_ value: Value, as name: String) async {
         await withCheckedContinuation { continuation in
         await withCheckedContinuation { continuation in
             processQueue.safeSync {
             processQueue.safeSync {
-                if let value = value as? RawJSON, let data = value.data(using: .utf8) {
-                    try? Disk.save(data, to: .documents, as: name)
-                } else {
-                    try? Disk.save(value, to: .documents, as: name, encoder: JSONCoding.encoder)
+                do {
+                    if let value = value as? RawJSON, let data = value.data(using: .utf8) {
+                        try Disk.save(data, to: .documents, as: name)
+                    } else {
+                        try Disk.save(value, to: .documents, as: name, encoder: JSONCoding.encoder)
+                    }
+                } catch {
+                    debug(.storage, "Failed to save file '\(name)': \(error.localizedDescription)")
                 }
                 }
                 continuation.resume()
                 continuation.resume()
             }
             }
@@ -46,49 +54,81 @@ final class BaseFileStorage: FileStorage {
 
 
     func retrieve<Value: JSON>(_ name: String, as type: Value.Type) -> Value? {
     func retrieve<Value: JSON>(_ name: String, as type: Value.Type) -> Value? {
         processQueue.safeSync {
         processQueue.safeSync {
-            try? Disk.retrieve(name, from: .documents, as: type, decoder: JSONCoding.decoder)
+            do {
+                return try Disk.retrieve(name, from: .documents, as: type, decoder: JSONCoding.decoder)
+            } catch {
+                debug(.storage, "Failed to retrieve file '\(name)': \(error.localizedDescription)")
+                return nil
+            }
         }
         }
     }
     }
 
 
     func retrieveAsync<Value: JSON>(_ name: String, as type: Value.Type) async -> Value? {
     func retrieveAsync<Value: JSON>(_ name: String, as type: Value.Type) async -> Value? {
         await withCheckedContinuation { continuation in
         await withCheckedContinuation { continuation in
             processQueue.safeSync {
             processQueue.safeSync {
-                let result = try? Disk.retrieve(name, from: .documents, as: type, decoder: JSONCoding.decoder)
-                continuation.resume(returning: result)
+                do {
+                    let result = try Disk.retrieve(name, from: .documents, as: type, decoder: JSONCoding.decoder)
+                    continuation.resume(returning: result)
+                } catch {
+                    debug(.storage, "Failed to retrieve file '\(name)': \(error.localizedDescription)")
+                    continuation.resume(returning: nil)
+                }
             }
             }
         }
         }
     }
     }
 
 
     func retrieveRaw(_ name: String) -> RawJSON? {
     func retrieveRaw(_ name: String) -> RawJSON? {
         processQueue.safeSync {
         processQueue.safeSync {
-            guard let data = try? Disk.retrieve(name, from: .documents, as: Data.self) else {
+            do {
+                let data = try Disk.retrieve(name, from: .documents, as: Data.self)
+                guard let string = String(data: data, encoding: .utf8) else {
+                    debug(.storage, "Failed to decode data as UTF-8 string for file '\(name)'")
+                    return nil
+                }
+                return string
+            } catch {
+                debug(.storage, "Failed to retrieve file '\(name)': \(error.localizedDescription)")
                 return nil
                 return nil
             }
             }
-            return String(data: data, encoding: .utf8)
         }
         }
     }
     }
 
 
     func retrieveRawAsync(_ name: String) async -> RawJSON? {
     func retrieveRawAsync(_ name: String) async -> RawJSON? {
         await withCheckedContinuation { continuation in
         await withCheckedContinuation { continuation in
             processQueue.safeSync {
             processQueue.safeSync {
-                guard let data = try? Disk.retrieve(name, from: .documents, as: Data.self) else {
+                do {
+                    let data = try Disk.retrieve(name, from: .documents, as: Data.self)
+                    guard let string = String(data: data, encoding: .utf8) else {
+                        debug(.storage, "Failed to decode data as UTF-8 string for file '\(name)'")
+                        continuation.resume(returning: nil)
+                        return
+                    }
+                    continuation.resume(returning: string)
+                } catch {
+                    debug(.storage, "Failed to retrieve file '\(name)': \(error.localizedDescription)")
                     continuation.resume(returning: nil)
                     continuation.resume(returning: nil)
-                    return
                 }
                 }
-                continuation.resume(returning: String(data: data, encoding: .utf8))
             }
             }
         }
         }
     }
     }
 
 
     func append<Value: JSON>(_ newValue: Value, to name: String) {
     func append<Value: JSON>(_ newValue: Value, to name: String) {
         processQueue.safeSync {
         processQueue.safeSync {
-            try? Disk.append(newValue, to: name, in: .documents, decoder: JSONCoding.decoder, encoder: JSONCoding.encoder)
+            do {
+                try Disk.append(newValue, to: name, in: .documents, decoder: JSONCoding.decoder, encoder: JSONCoding.encoder)
+            } catch {
+                debug(.storage, "Failed to append to file '\(name)': \(error.localizedDescription)")
+            }
         }
         }
     }
     }
 
 
     func append<Value: JSON>(_ newValues: [Value], to name: String) {
     func append<Value: JSON>(_ newValues: [Value], to name: String) {
         processQueue.safeSync {
         processQueue.safeSync {
-            try? Disk.append(newValues, to: name, in: .documents, decoder: JSONCoding.decoder, encoder: JSONCoding.encoder)
+            do {
+                try Disk.append(newValues, to: name, in: .documents, decoder: JSONCoding.decoder, encoder: JSONCoding.encoder)
+            } catch {
+                debug(.storage, "Failed to append to file '\(name)': \(error.localizedDescription)")
+            }
         }
         }
     }
     }
 
 
@@ -130,13 +170,21 @@ final class BaseFileStorage: FileStorage {
 
 
     func remove(_ name: String) {
     func remove(_ name: String) {
         processQueue.safeSync {
         processQueue.safeSync {
-            try? Disk.remove(name, from: .documents)
+            do {
+                try Disk.remove(name, from: .documents)
+            } catch {
+                debug(.storage, "Failed to remove file '\(name)': \(error.localizedDescription)")
+            }
         }
         }
     }
     }
 
 
     func rename(_ name: String, to newName: String) {
     func rename(_ name: String, to newName: String) {
         processQueue.safeSync {
         processQueue.safeSync {
-            try? Disk.rename(name, in: .documents, to: newName)
+            do {
+                try Disk.rename(name, in: .documents, to: newName)
+            } catch {
+                debug(.storage, "Failed to rename file '\(name)' to '\(newName)': \(error.localizedDescription)")
+            }
         }
         }
     }
     }
 
 
@@ -147,7 +195,12 @@ final class BaseFileStorage: FileStorage {
     }
     }
 
 
     func urlFor(file: String) -> URL? {
     func urlFor(file: String) -> URL? {
-        try? Disk.url(for: file, in: .documents)
+        do {
+            return try Disk.url(for: file, in: .documents)
+        } catch {
+            debug(.storage, "Failed to get URL for file '\(file)': \(error.localizedDescription)")
+            return nil
+        }
     }
     }
 }
 }
 
 

+ 8 - 6
Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift

@@ -107,7 +107,7 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
 
 
     private func registerHandlers() {
     private func registerHandlers() {
         // Due to the Batch insert this only is used for observing Deletion of Glucose entries
         // Due to the Batch insert this only is used for observing Deletion of Glucose entries
-        coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
+        coreDataPublisher?.filteredByEntityName("GlucoseStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task {
             Task {
                 await self.sendGlucoseNotification()
                 await self.sendGlucoseNotification()
@@ -243,8 +243,8 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
         )
         )
     }
     }
 
 
-    private func fetchGlucoseIDs() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchGlucoseIDs() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             ofType: GlucoseStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: NSPredicate.predicateFor20MinAgo,
             predicate: NSPredicate.predicateFor20MinAgo,
@@ -253,8 +253,10 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
             fetchLimit: 3
             fetchLimit: 3
         )
         )
 
 
-        return await backgroundContext.perform {
-            guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+        return try await backgroundContext.perform {
+            guard let fetchedResults = results as? [GlucoseStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             return fetchedResults.map(\.objectID)
             return fetchedResults.map(\.objectID)
         }
         }
@@ -263,7 +265,7 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
     @MainActor private func sendGlucoseNotification() async {
     @MainActor private func sendGlucoseNotification() async {
         do {
         do {
             addAppBadge(glucose: nil)
             addAppBadge(glucose: nil)
-            let glucoseIDs = await fetchGlucoseIDs()
+            let glucoseIDs = try await fetchGlucoseIDs()
             let glucoseObjects = try glucoseIDs.compactMap { id in
             let glucoseObjects = try glucoseIDs.compactMap { id in
                 try viewContext.existingObject(with: id) as? GlucoseStored
                 try viewContext.existingObject(with: id) as? GlucoseStored
             }
             }

+ 219 - 175
Trio/Sources/Services/WatchManager/AppleWatchManager.swift

@@ -81,7 +81,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
     }
     }
 
 
     private func registerHandlers() {
     private func registerHandlers() {
-        coreDataPublisher?.filterByEntityName("OrefDetermination").sink { [weak self] _ in
+        coreDataPublisher?.filteredByEntityName("OrefDetermination").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task {
             Task {
                 let state = await self.setupWatchState()
                 let state = await self.setupWatchState()
@@ -90,7 +90,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
         }.store(in: &subscriptions)
         }.store(in: &subscriptions)
 
 
         // Due to the Batch insert this only is used for observing Deletion of Glucose entries
         // Due to the Batch insert this only is used for observing Deletion of Glucose entries
-        coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
+        coreDataPublisher?.filteredByEntityName("GlucoseStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task {
             Task {
                 let state = await self.setupWatchState()
                 let state = await self.setupWatchState()
@@ -98,14 +98,14 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
             }
             }
         }.store(in: &subscriptions)
         }.store(in: &subscriptions)
 
 
-        coreDataPublisher?.filterByEntityName("PumpEventStored").sink { [weak self] _ in
+        coreDataPublisher?.filteredByEntityName("PumpEventStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task {
             Task {
                 await self.getActiveBolusAmount()
                 await self.getActiveBolusAmount()
             }
             }
         }.store(in: &subscriptions)
         }.store(in: &subscriptions)
 
 
-        coreDataPublisher?.filterByEntityName("OverrideStored").sink { [weak self] _ in
+        coreDataPublisher?.filteredByEntityName("OverrideStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task {
             Task {
                 let state = await self.setupWatchState()
                 let state = await self.setupWatchState()
@@ -113,7 +113,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
             }
             }
         }.store(in: &subscriptions)
         }.store(in: &subscriptions)
 
 
-        coreDataPublisher?.filterByEntityName("TempTargetStored").sink { [weak self] _ in
+        coreDataPublisher?.filteredByEntityName("TempTargetStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task {
             Task {
                 let state = await self.setupWatchState()
                 let state = await self.setupWatchState()
@@ -149,184 +149,196 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
     /// Prepares the current state data to be sent to the Watch
     /// Prepares the current state data to be sent to the Watch
     /// - Returns: WatchState containing current glucose readings and trends and determination infos for displaying cob and iob in the view
     /// - Returns: WatchState containing current glucose readings and trends and determination infos for displaying cob and iob in the view
     func setupWatchState() async -> WatchState {
     func setupWatchState() async -> WatchState {
-        // Get NSManagedObjectIDs
-        let glucoseIds = await fetchGlucose()
-        // TODO: - if we want that the watch immediately displays updated cob and iob values when entered via treatment view from phone, we would need to use a predicate here that also filters for NON-ENACTED Determinations
-        let determinationIds = await determinationStorage.fetchLastDeterminationObjectID(
-            predicate: NSPredicate.predicateFor30MinAgoForDetermination
-        )
-        let overridePresetIds = await overrideStorage.fetchForOverridePresets()
-        let tempTargetPresetIds = await tempTargetStorage.fetchForTempTargetPresets()
-
-        // Get NSManagedObjects
-        let glucoseObjects: [GlucoseStored] = await CoreDataStack.shared
-            .getNSManagedObject(with: glucoseIds, context: backgroundContext)
-        let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
-            .getNSManagedObject(with: determinationIds, context: backgroundContext)
-        let overridePresetObjects: [OverrideStored] = await CoreDataStack.shared
-            .getNSManagedObject(with: overridePresetIds, context: backgroundContext)
-        let tempTargetPresetObjects: [TempTargetStored] = await CoreDataStack.shared
-            .getNSManagedObject(with: tempTargetPresetIds, context: backgroundContext)
-
-        return await backgroundContext.perform {
-            var watchState = WatchState(date: Date())
-
-            // Set lastLoopDate
-            let lastLoopMinutes = Int((Date().timeIntervalSince(self.apsManager.lastLoopDate) - 30) / 60) + 1
-            if lastLoopMinutes > 1440 {
-                watchState.lastLoopTime = "--"
-            } else {
-                watchState.lastLoopTime = "\(lastLoopMinutes) min"
-            }
-
-            // Set IOB and COB from latest determination
-            if let latestDetermination = determinationObjects.first {
-                let iob = latestDetermination.iob ?? 0
-                watchState.iob = Formatter.decimalFormatterWithTwoFractionDigits.string(from: iob)
+        do {
+            // Get NSManagedObjectIDs
+            let glucoseIds = try await fetchGlucose()
+            let determinationIds = try await determinationStorage.fetchLastDeterminationObjectID(
+                predicate: NSPredicate.predicateFor30MinAgoForDetermination
+            )
+            let overridePresetIds = try await overrideStorage.fetchForOverridePresets()
+            let tempTargetPresetIds = try await tempTargetStorage.fetchForTempTargetPresets()
+
+            // Get NSManagedObjects
+            let glucoseObjects: [GlucoseStored] = try await CoreDataStack.shared
+                .getNSManagedObject(with: glucoseIds, context: backgroundContext)
+            let determinationObjects: [OrefDetermination] = try await CoreDataStack.shared
+                .getNSManagedObject(with: determinationIds, context: backgroundContext)
+            let overridePresetObjects: [OverrideStored] = try await CoreDataStack.shared
+                .getNSManagedObject(with: overridePresetIds, context: backgroundContext)
+            let tempTargetPresetObjects: [TempTargetStored] = try await CoreDataStack.shared
+                .getNSManagedObject(with: tempTargetPresetIds, context: backgroundContext)
+
+            return await backgroundContext.perform {
+                var watchState = WatchState(date: Date())
+
+                // Set lastLoopDate
+                let lastLoopMinutes = Int((Date().timeIntervalSince(self.apsManager.lastLoopDate) - 30) / 60) + 1
+                if lastLoopMinutes > 1440 {
+                    watchState.lastLoopTime = "--"
+                } else {
+                    watchState.lastLoopTime = "\(lastLoopMinutes) min"
+                }
 
 
-                let cob = NSNumber(value: latestDetermination.cob)
-                watchState.cob = Formatter.integerFormatter.string(from: cob)
-            }
+                // Set IOB and COB from latest determination
+                if let latestDetermination = determinationObjects.first {
+                    let iob = latestDetermination.iob ?? 0
+                    watchState.iob = Formatter.decimalFormatterWithTwoFractionDigits.string(from: iob)
 
 
-            // Set override presets with their enabled status
-            watchState.overridePresets = overridePresetObjects.map { override in
-                OverridePresetWatch(
-                    name: override.name ?? "",
-                    isEnabled: override.enabled
-                )
-            }
+                    let cob = NSNumber(value: latestDetermination.cob)
+                    watchState.cob = Formatter.integerFormatter.string(from: cob)
+                }
 
 
-            guard let latestGlucose = glucoseObjects.first else {
-                return watchState
-            }
+                // Set override presets with their enabled status
+                watchState.overridePresets = overridePresetObjects.map { override in
+                    OverridePresetWatch(
+                        name: override.name ?? "",
+                        isEnabled: override.enabled
+                    )
+                }
 
 
-            // Assign currentGlucose and its color
-            /// Set current glucose with proper formatting
-            if self.units == .mgdL {
-                watchState.currentGlucose = "\(latestGlucose.glucose)"
-            } else {
-                let mgdlValue = Decimal(latestGlucose.glucose)
-                let latestGlucoseValue = mgdlValue.formattedAsMmolL
-                watchState.currentGlucose = "\(latestGlucoseValue)"
-            }
+                guard let latestGlucose = glucoseObjects.first else {
+                    return watchState
+                }
 
 
-            /// Calculate latest color
-            let hardCodedLow = Decimal(55)
-            let hardCodedHigh = Decimal(220)
-            let isDynamicColorScheme = self.glucoseColorScheme == .dynamicColor
-
-            let highGlucoseValue = isDynamicColorScheme ? hardCodedHigh : self.highGlucose
-            let lowGlucoseValue = isDynamicColorScheme ? hardCodedLow : self.lowGlucose
-            let highGlucoseColorValue = highGlucoseValue
-            let lowGlucoseColorValue = lowGlucoseValue
-            let targetGlucose = self.currentGlucoseTarget
-
-            let currentGlucoseColor = Trio.getDynamicGlucoseColor(
-                glucoseValue: Decimal(latestGlucose.glucose),
-                highGlucoseColorValue: highGlucoseColorValue,
-                lowGlucoseColorValue: lowGlucoseColorValue,
-                targetGlucose: targetGlucose,
-                glucoseColorScheme: self.glucoseColorScheme
-            )
+                // Assign currentGlucose and its color
+                /// Set current glucose with proper formatting
+                if self.units == .mgdL {
+                    watchState.currentGlucose = "\(latestGlucose.glucose)"
+                } else {
+                    let mgdlValue = Decimal(latestGlucose.glucose)
+                    let latestGlucoseValue = mgdlValue.formattedAsMmolL
+                    watchState.currentGlucose = "\(latestGlucoseValue)"
+                }
 
 
-            if Decimal(latestGlucose.glucose) <= self.lowGlucose || Decimal(latestGlucose.glucose) >= self.highGlucose {
-                watchState.currentGlucoseColorString = currentGlucoseColor.toHexString()
-            } else {
-                watchState.currentGlucoseColorString = "#ffffff" // white when in range; colored when out of range
-            }
+                /// Calculate latest color
+                let hardCodedLow = Decimal(55)
+                let hardCodedHigh = Decimal(220)
+                let isDynamicColorScheme = self.glucoseColorScheme == .dynamicColor
 
 
-            // Map glucose values
-            watchState.glucoseValues = glucoseObjects.compactMap { glucose in
-                let glucoseValue = self.units == .mgdL
-                    ? Double(glucose.glucose)
-                    : Double(truncating: Decimal(glucose.glucose).asMmolL as NSNumber)
+                let highGlucoseValue = isDynamicColorScheme ? hardCodedHigh : self.highGlucose
+                let lowGlucoseValue = isDynamicColorScheme ? hardCodedLow : self.lowGlucose
+                let highGlucoseColorValue = highGlucoseValue
+                let lowGlucoseColorValue = lowGlucoseValue
+                let targetGlucose = self.currentGlucoseTarget
 
 
-                let glucoseColor = Trio.getDynamicGlucoseColor(
-                    glucoseValue: Decimal(glucose.glucose),
+                let currentGlucoseColor = Trio.getDynamicGlucoseColor(
+                    glucoseValue: Decimal(latestGlucose.glucose),
                     highGlucoseColorValue: highGlucoseColorValue,
                     highGlucoseColorValue: highGlucoseColorValue,
                     lowGlucoseColorValue: lowGlucoseColorValue,
                     lowGlucoseColorValue: lowGlucoseColorValue,
                     targetGlucose: targetGlucose,
                     targetGlucose: targetGlucose,
                     glucoseColorScheme: self.glucoseColorScheme
                     glucoseColorScheme: self.glucoseColorScheme
                 )
                 )
 
 
-                return WatchGlucoseObject(date: glucose.date ?? Date(), glucose: glucoseValue, color: glucoseColor.toHexString())
-            }
-            .sorted { $0.date < $1.date }
-
-            // Set axis domain: min and max Y-axis values
-            // Apply unit parsing conditionally, if user uses mmol/L
-            let maxGlucoseValue = Decimal(glucoseObjects.map { Int($0.glucose) }.max() ?? 200)
-            var maxYValue = Decimal(200)
-
-            if maxGlucoseValue > maxYValue, maxGlucoseValue <= 225 {
-                maxYValue = Decimal(250)
-            } else if maxGlucoseValue > 225, maxGlucoseValue <= 275 {
-                maxYValue = Decimal(300)
-            } else if maxGlucoseValue > 275, maxGlucoseValue <= 325 {
-                maxYValue = Decimal(350)
-            } else if maxGlucoseValue > 325 {
-                maxYValue = Decimal(400)
-            }
-
-            if self.units == .mmolL {
-                maxYValue = Double(truncating: maxYValue as NSNumber).asMmolL
-            }
-            watchState.maxYAxisValue = maxYValue
+                if Decimal(latestGlucose.glucose) <= self.lowGlucose || Decimal(latestGlucose.glucose) >= self.highGlucose {
+                    watchState.currentGlucoseColorString = currentGlucoseColor.toHexString()
+                } else {
+                    watchState.currentGlucoseColorString = "#ffffff" // white when in range; colored when out of range
+                }
 
 
-            if self.units == .mmolL {
-                let minYValue = Double(truncating: watchState.minYAxisValue as NSNumber).asMmolL
-                watchState.minYAxisValue = minYValue
-            }
+                // Map glucose values
+                watchState.glucoseValues = glucoseObjects.compactMap { glucose in
+                    let glucoseValue = self.units == .mgdL
+                        ? Double(glucose.glucose)
+                        : Double(truncating: Decimal(glucose.glucose).asMmolL as NSNumber)
+
+                    let glucoseColor = Trio.getDynamicGlucoseColor(
+                        glucoseValue: Decimal(glucose.glucose),
+                        highGlucoseColorValue: highGlucoseColorValue,
+                        lowGlucoseColorValue: lowGlucoseColorValue,
+                        targetGlucose: targetGlucose,
+                        glucoseColorScheme: self.glucoseColorScheme
+                    )
 
 
-            // Convert direction to trend string
-            watchState.trend = latestGlucose.direction
+                    return WatchGlucoseObject(
+                        date: glucose.date ?? Date(),
+                        glucose: glucoseValue,
+                        color: glucoseColor.toHexString()
+                    )
+                }
+                .sorted { $0.date < $1.date }
+
+                // Set axis domain: min and max Y-axis values
+                // Apply unit parsing conditionally, if user uses mmol/L
+                let maxGlucoseValue = Decimal(glucoseObjects.map { Int($0.glucose) }.max() ?? 200)
+                var maxYValue = Decimal(200)
+
+                if maxGlucoseValue > maxYValue, maxGlucoseValue <= 225 {
+                    maxYValue = Decimal(250)
+                } else if maxGlucoseValue > 225, maxGlucoseValue <= 275 {
+                    maxYValue = Decimal(300)
+                } else if maxGlucoseValue > 275, maxGlucoseValue <= 325 {
+                    maxYValue = Decimal(350)
+                } else if maxGlucoseValue > 325 {
+                    maxYValue = Decimal(400)
+                }
 
 
-            // Calculate delta if we have at least 2 readings
-            if glucoseObjects.count >= 2 {
-                var deltaValue = Decimal(glucoseObjects[0].glucose - glucoseObjects[1].glucose)
+                if self.units == .mmolL {
+                    maxYValue = Double(truncating: maxYValue as NSNumber).asMmolL
+                }
+                watchState.maxYAxisValue = maxYValue
 
 
                 if self.units == .mmolL {
                 if self.units == .mmolL {
-                    deltaValue = Double(truncating: deltaValue as NSNumber).asMmolL
+                    let minYValue = Double(truncating: watchState.minYAxisValue as NSNumber).asMmolL
+                    watchState.minYAxisValue = minYValue
                 }
                 }
 
 
-                let formattedDelta = Formatter.glucoseFormatter(for: self.units)
-                    .string(from: deltaValue as NSNumber) ?? "0"
-                watchState.delta = deltaValue < 0 ? "\(formattedDelta)" : "+\(formattedDelta)"
-            }
+                // Convert direction to trend string
+                watchState.trend = latestGlucose.direction
 
 
-            // Set temp target presets with their enabled status
-            watchState.tempTargetPresets = tempTargetPresetObjects.map { tempTarget in
-                TempTargetPresetWatch(
-                    name: tempTarget.name ?? "",
-                    isEnabled: tempTarget.enabled
-                )
-            }
+                // Calculate delta if we have at least 2 readings
+                if glucoseObjects.count >= 2 {
+                    var deltaValue = Decimal(glucoseObjects[0].glucose - glucoseObjects[1].glucose)
+
+                    if self.units == .mmolL {
+                        deltaValue = Double(truncating: deltaValue as NSNumber).asMmolL
+                    }
+
+                    let formattedDelta = Formatter.glucoseFormatter(for: self.units)
+                        .string(from: deltaValue as NSNumber) ?? "0"
+                    watchState.delta = deltaValue < 0 ? "\(formattedDelta)" : "+\(formattedDelta)"
+                }
 
 
-            // Set units
-            watchState.units = self.units
+                // Set temp target presets with their enabled status
+                watchState.tempTargetPresets = tempTargetPresetObjects.map { tempTarget in
+                    TempTargetPresetWatch(
+                        name: tempTarget.name ?? "",
+                        isEnabled: tempTarget.enabled
+                    )
+                }
+
+                // Set units
+                watchState.units = self.units
+
+                // Add limits and pump specific dosing increment settings values
+                watchState.maxBolus = self.settingsManager.pumpSettings.maxBolus
+                watchState.maxCarbs = self.settingsManager.settings.maxCarbs
+                watchState.maxFat = self.settingsManager.settings.maxFat
+                watchState.maxProtein = self.settingsManager.settings.maxProtein
+                watchState.bolusIncrement = self.settingsManager.preferences.bolusIncrement
+                watchState.confirmBolusFaster = self.settingsManager.settings.confirmBolusFaster
 
 
-            // Add limits and pump specific dosing increment settings values
-            watchState.maxBolus = self.settingsManager.pumpSettings.maxBolus
-            watchState.maxCarbs = self.settingsManager.settings.maxCarbs
-            watchState.maxFat = self.settingsManager.settings.maxFat
-            watchState.maxProtein = self.settingsManager.settings.maxProtein
-            watchState.bolusIncrement = self.settingsManager.preferences.bolusIncrement
-            watchState.confirmBolusFaster = self.settingsManager.settings.confirmBolusFaster
+                debug(
+                    .watchManager,
+
+                    "📱 Setup WatchState - currentGlucose: \(watchState.currentGlucose ?? "nil"), trend: \(watchState.trend ?? "nil"), delta: \(watchState.delta ?? "nil"), values: \(watchState.glucoseValues.count)"
+                )
 
 
+                return watchState
+            }
+        } catch {
             debug(
             debug(
                 .watchManager,
                 .watchManager,
-
-                "📱 Setup WatchState - currentGlucose: \(watchState.currentGlucose ?? "nil"), trend: \(watchState.trend ?? "nil"), delta: \(watchState.delta ?? "nil"), values: \(watchState.glucoseValues.count)"
+                "\(DebuggingIdentifiers.failed) Error setting up watch state: \(error.localizedDescription)"
             )
             )
-
-            return watchState
+            // Return empty state in case of error
+            return WatchState(date: Date())
         }
         }
     }
     }
 
 
     /// Fetches recent glucose readings from CoreData
     /// Fetches recent glucose readings from CoreData
     /// - Returns: Array of NSManagedObjectIDs for glucose readings
     /// - Returns: Array of NSManagedObjectIDs for glucose readings
-    private func fetchGlucose() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchGlucose() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             ofType: GlucoseStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: NSPredicate.glucose,
             predicate: NSPredicate.glucose,
@@ -335,8 +347,10 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
             fetchLimit: 288
             fetchLimit: 288
         )
         )
 
 
-        return await backgroundContext.perform {
-            guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+        return try await backgroundContext.perform {
+            guard let fetchedResults = results as? [GlucoseStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             return fetchedResults.map(\.objectID)
             return fetchedResults.map(\.objectID)
         }
         }
@@ -344,8 +358,8 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
 
 
     /// Fetches last pump event that is a non-external bolus from CoreData
     /// Fetches last pump event that is a non-external bolus from CoreData
     /// - Returns: NSManagedObjectIDs for last bolus
     /// - Returns: NSManagedObjectIDs for last bolus
-    func fetchLastBolus() async -> NSManagedObjectID? {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    func fetchLastBolus() async throws -> NSManagedObjectID? {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: PumpEventStored.self,
             ofType: PumpEventStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: NSPredicate.lastPumpBolus,
             predicate: NSPredicate.lastPumpBolus,
@@ -354,8 +368,10 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
             fetchLimit: 1
             fetchLimit: 1
         )
         )
 
 
-        return await backgroundContext.perform {
-            guard let fetchedResults = results as? [PumpEventStored] else { return [].first }
+        return try await backgroundContext.perform {
+            guard let fetchedResults = results as? [PumpEventStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
 
             return fetchedResults.map(\.objectID).first
             return fetchedResults.map(\.objectID).first
         }
         }
@@ -363,11 +379,18 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
 
 
     /// Gets the active bolus amount by fetching last (active) bolus.
     /// Gets the active bolus amount by fetching last (active) bolus.
     @MainActor func getActiveBolusAmount() async {
     @MainActor func getActiveBolusAmount() async {
-        if let lastBolusObjectId = await fetchLastBolus() {
-            let lastBolusObject: [PumpEventStored] = await CoreDataStack.shared
-                .getNSManagedObject(with: [lastBolusObjectId], context: viewContext)
+        do {
+            if let lastBolusObjectId = try await fetchLastBolus() {
+                let lastBolusObject: [PumpEventStored] = try await CoreDataStack.shared
+                    .getNSManagedObject(with: [lastBolusObjectId], context: viewContext)
 
 
-            activeBolusAmount = lastBolusObject.first?.bolus?.amount?.doubleValue ?? 0.0
+                activeBolusAmount = lastBolusObject.first?.bolus?.amount?.doubleValue ?? 0.0
+            }
+        } catch {
+            debug(
+                .default,
+                "\(DebuggingIdentifiers.failed) Error getting active bolus amount: \(error.localizedDescription)"
+            )
         }
         }
     }
     }
 
 
@@ -551,7 +574,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                     debug(.watchManager, "📱 Bolus cancelled from watch")
                     debug(.watchManager, "📱 Bolus cancelled from watch")
 
 
                     // perform determine basal sync, otherwise you could end up with too much IOB when opening the calculator again
                     // perform determine basal sync, otherwise you could end up with too much IOB when opening the calculator again
-                    await self?.apsManager.determineBasalSync()
+                    try await self?.apsManager.determineBasalSync()
                 }
                 }
             }
             }
 
 
@@ -632,7 +655,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                 carbEntry.id = UUID()
                 carbEntry.id = UUID()
                 carbEntry.carbs = Double(truncating: amount as NSNumber)
                 carbEntry.carbs = Double(truncating: amount as NSNumber)
                 carbEntry.date = date
                 carbEntry.date = date
-                carbEntry.note = "Via Watch"
+                carbEntry.note = String(localized: "Via Watch", comment: "Note added to carb entry when entered via watch")
                 carbEntry.isFPU = false // set this to false to ensure watch-entered carbs are displayed in main chart
                 carbEntry.isFPU = false // set this to false to ensure watch-entered carbs are displayed in main chart
                 carbEntry.isUploadedToNS = false
                 carbEntry.isUploadedToNS = false
 
 
@@ -642,7 +665,13 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                     debug(.watchManager, "📱 Saved carbs from watch: \(amount)g at \(date)")
                     debug(.watchManager, "📱 Saved carbs from watch: \(amount)g at \(date)")
 
 
                     // Acknowledge success
                     // Acknowledge success
-                    self.sendAcknowledgment(toWatch: true, message: "Carbs logged successfully.")
+                    self.sendAcknowledgment(
+                        toWatch: true,
+                        message: String(
+                            localized: "Carbs logged successfully.",
+                            comment: "Success message sent to watch when carbs are logged successfully"
+                        )
+                    )
                 } catch {
                 } catch {
                     debug(.watchManager, "❌ Error saving carbs: \(error.localizedDescription)")
                     debug(.watchManager, "❌ Error saving carbs: \(error.localizedDescription)")
 
 
@@ -664,7 +693,10 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
 
 
             do {
             do {
                 // Notify Watch: "Saving carbs..."
                 // Notify Watch: "Saving carbs..."
-                self.sendAcknowledgment(toWatch: true, message: "Saving Carbs...")
+                self.sendAcknowledgment(
+                    toWatch: true,
+                    message: String(localized: "Saving Carbs...", comment: "Successful message sent to watch when saving carbs")
+                )
 
 
                 // Save carbs entry in Core Data
                 // Save carbs entry in Core Data
                 try await context.perform {
                 try await context.perform {
@@ -672,7 +704,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                     carbEntry.id = UUID()
                     carbEntry.id = UUID()
                     carbEntry.carbs = NSDecimalNumber(decimal: carbsAmount).doubleValue
                     carbEntry.carbs = NSDecimalNumber(decimal: carbsAmount).doubleValue
                     carbEntry.date = date
                     carbEntry.date = date
-                    carbEntry.note = "Via Watch"
+                    carbEntry.note = String(localized: "Via Watch", comment: "Note added to carb entry when entered via watch")
                     carbEntry.isFPU = false // set this to false to ensure watch-entered carbs are displayed in main chart
                     carbEntry.isFPU = false // set this to false to ensure watch-entered carbs are displayed in main chart
                     carbEntry.isUploadedToNS = false
                     carbEntry.isUploadedToNS = false
 
 
@@ -682,7 +714,13 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                 }
                 }
 
 
                 // Notify Watch: "Enacting bolus..."
                 // Notify Watch: "Enacting bolus..."
-                sendAcknowledgment(toWatch: true, message: "Enacting bolus...")
+                sendAcknowledgment(
+                    toWatch: true,
+                    message: String(
+                        localized: "Enacting bolus...",
+                        comment: "Successful message sent to watch when enacting bolus"
+                    )
+                )
 
 
                 // Enact bolus via APS Manager
                 // Enact bolus via APS Manager
                 let bolusDouble = NSDecimalNumber(decimal: bolusAmount).doubleValue
                 let bolusDouble = NSDecimalNumber(decimal: bolusAmount).doubleValue
@@ -692,7 +730,13 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                 }
                 }
                 debug(.watchManager, "📱 Enacted bolus from watch via APS Manager: \(bolusDouble) U")
                 debug(.watchManager, "📱 Enacted bolus from watch via APS Manager: \(bolusDouble) U")
                 // Notify Watch: "Carbs and bolus logged successfully"
                 // Notify Watch: "Carbs and bolus logged successfully"
-                sendAcknowledgment(toWatch: true, message: "Carbs and Bolus logged successfully.")
+                sendAcknowledgment(
+                    toWatch: true,
+                    message: String(
+                        localized: "Carbs and Bolus logged successfully.",
+                        comment: "Successful message sent to watch when logging carbs and bolus"
+                    )
+                )
 
 
             } catch {
             } catch {
                 debug(.watchManager, "❌ Error processing combined request: \(error.localizedDescription)")
                 debug(.watchManager, "❌ Error processing combined request: \(error.localizedDescription)")
@@ -705,7 +749,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
         Task {
         Task {
             let context = CoreDataStack.shared.newTaskContext()
             let context = CoreDataStack.shared.newTaskContext()
 
 
-            if let overrideId = await overrideStorage.fetchLatestActiveOverride() {
+            if let overrideId = try await overrideStorage.fetchLatestActiveOverride() {
                 let override = await context.perform {
                 let override = await context.perform {
                     context.object(with: overrideId) as? OverrideStored
                     context.object(with: overrideId) as? OverrideStored
                 }
                 }
@@ -743,12 +787,12 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
             let context = CoreDataStack.shared.newTaskContext()
             let context = CoreDataStack.shared.newTaskContext()
 
 
             // Fetch all presets to find the one to activate
             // Fetch all presets to find the one to activate
-            let presetIds = await overrideStorage.fetchForOverridePresets()
-            let presets: [OverrideStored] = await CoreDataStack.shared
+            let presetIds = try await overrideStorage.fetchForOverridePresets()
+            let presets: [OverrideStored] = try await CoreDataStack.shared
                 .getNSManagedObject(with: presetIds, context: context)
                 .getNSManagedObject(with: presetIds, context: context)
 
 
             // Check for active override
             // Check for active override
-            if let activeOverrideId = await overrideStorage.fetchLatestActiveOverride() {
+            if let activeOverrideId = try await overrideStorage.fetchLatestActiveOverride() {
                 let activeOverride = await context.perform {
                 let activeOverride = await context.perform {
                     context.object(with: activeOverrideId) as? OverrideStored
                     context.object(with: activeOverrideId) as? OverrideStored
                 }
                 }
@@ -795,12 +839,12 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
             let context = CoreDataStack.shared.newTaskContext()
             let context = CoreDataStack.shared.newTaskContext()
 
 
             // Fetch all presets to find the one to activate
             // Fetch all presets to find the one to activate
-            let presetIds = await tempTargetStorage.fetchForTempTargetPresets()
-            let presets: [TempTargetStored] = await CoreDataStack.shared
+            let presetIds = try await tempTargetStorage.fetchForTempTargetPresets()
+            let presets: [TempTargetStored] = try await CoreDataStack.shared
                 .getNSManagedObject(with: presetIds, context: context)
                 .getNSManagedObject(with: presetIds, context: context)
 
 
             // Check for active temp target
             // Check for active temp target
-            if let activeTempTargetId = await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 1).first {
+            if let activeTempTargetId = try await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 1).first {
                 let activeTempTarget = await context.perform {
                 let activeTempTarget = await context.perform {
                     context.object(with: activeTempTargetId) as? TempTargetStored
                     context.object(with: activeTempTargetId) as? TempTargetStored
                 }
                 }
@@ -867,7 +911,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
         Task {
         Task {
             let context = CoreDataStack.shared.newTaskContext()
             let context = CoreDataStack.shared.newTaskContext()
 
 
-            if let tempTargetId = await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 1).first {
+            if let tempTargetId = try await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 1).first {
                 let tempTarget = await context.perform {
                 let tempTarget = await context.perform {
                     context.object(with: tempTargetId) as? TempTargetStored
                     context.object(with: tempTargetId) as? TempTargetStored
                 }
                 }

+ 138 - 100
Trio/Sources/Services/WatchManager/GarminManager.swift

@@ -53,7 +53,7 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
     /// Stores, retrieves, and updates insulin dose determinations in CoreData.
     /// Stores, retrieves, and updates insulin dose determinations in CoreData.
     @Injected() private var determinationStorage: DeterminationStorage!
     @Injected() private var determinationStorage: DeterminationStorage!
 
 
-    /// Persists the users device list between app launches.
+    /// Persists the user's device list between app launches.
     @Persisted(key: "BaseGarminManager.persistedDevices") private var persistedDevices: [GarminDevice] = []
     @Persisted(key: "BaseGarminManager.persistedDevices") private var persistedDevices: [GarminDevice] = []
 
 
     /// Router for presenting alerts or navigation flows (injected via Swinject).
     /// Router for presenting alerts or navigation flows (injected via Swinject).
@@ -134,9 +134,16 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
             .sink { [weak self] _ in
             .sink { [weak self] _ in
                 guard let self = self else { return }
                 guard let self = self else { return }
                 Task {
                 Task {
-                    let watchState = await self.setupGarminWatchState()
-                    let watchStateData = try JSONEncoder().encode(watchState)
-                    self.sendWatchStateData(watchStateData)
+                    do {
+                        let watchState = try await self.setupGarminWatchState()
+                        let watchStateData = try JSONEncoder().encode(watchState)
+                        self.sendWatchStateData(watchStateData)
+                    } catch {
+                        debug(
+                            .watchManager,
+                            "\(DebuggingIdentifiers.failed) Error updating watch state: \(error.localizedDescription)"
+                        )
+                    }
                 }
                 }
             }
             }
             .store(in: &subscriptions)
             .store(in: &subscriptions)
@@ -150,26 +157,40 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
     /// When these change, we re-compute the Garmin watch state and send updates to the watch.
     /// When these change, we re-compute the Garmin watch state and send updates to the watch.
     private func registerHandlers() {
     private func registerHandlers() {
         coreDataPublisher?
         coreDataPublisher?
-            .filterByEntityName("OrefDetermination")
+            .filteredByEntityName("OrefDetermination")
             .sink { [weak self] _ in
             .sink { [weak self] _ in
                 guard let self = self else { return }
                 guard let self = self else { return }
                 Task {
                 Task {
-                    let watchState = await self.setupGarminWatchState()
-                    let watchStateData = try JSONEncoder().encode(watchState)
-                    self.sendWatchStateData(watchStateData)
+                    do {
+                        let watchState = try await self.setupGarminWatchState()
+                        let watchStateData = try JSONEncoder().encode(watchState)
+                        self.sendWatchStateData(watchStateData)
+                    } catch {
+                        debug(
+                            .watchManager,
+                            "\(DebuggingIdentifiers.failed) failed to update watch state: \(error.localizedDescription)"
+                        )
+                    }
                 }
                 }
             }
             }
             .store(in: &subscriptions)
             .store(in: &subscriptions)
 
 
         // Due to the batch insert, this only observes deletion of Glucose entries
         // Due to the batch insert, this only observes deletion of Glucose entries
         coreDataPublisher?
         coreDataPublisher?
-            .filterByEntityName("GlucoseStored")
+            .filteredByEntityName("GlucoseStored")
             .sink { [weak self] _ in
             .sink { [weak self] _ in
                 guard let self = self else { return }
                 guard let self = self else { return }
                 Task {
                 Task {
-                    let watchState = await self.setupGarminWatchState()
-                    let watchStateData = try JSONEncoder().encode(watchState)
-                    self.sendWatchStateData(watchStateData)
+                    do {
+                        let watchState = try await self.setupGarminWatchState()
+                        let watchStateData = try JSONEncoder().encode(watchState)
+                        self.sendWatchStateData(watchStateData)
+                    } catch {
+                        debug(
+                            .watchManager,
+                            "\(DebuggingIdentifiers.failed) failed to update watch state: \(error.localizedDescription)"
+                        )
+                    }
                 }
                 }
             }
             }
             .store(in: &subscriptions)
             .store(in: &subscriptions)
@@ -177,8 +198,8 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
 
 
     /// Fetches recent glucose readings from CoreData, up to 288 results.
     /// Fetches recent glucose readings from CoreData, up to 288 results.
     /// - Returns: An array of `NSManagedObjectID`s for glucose readings.
     /// - Returns: An array of `NSManagedObjectID`s for glucose readings.
-    private func fetchGlucose() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+    private func fetchGlucose() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             ofType: GlucoseStored.self,
             onContext: backgroundContext,
             onContext: backgroundContext,
             predicate: NSPredicate.glucose,
             predicate: NSPredicate.glucose,
@@ -187,109 +208,119 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
             fetchLimit: 288
             fetchLimit: 288
         )
         )
 
 
-        return await backgroundContext.perform {
-            guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+        return try await backgroundContext.perform {
+            guard let fetchedResults = results as? [GlucoseStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
             return fetchedResults.map(\.objectID)
             return fetchedResults.map(\.objectID)
         }
         }
     }
     }
 
 
     /// Builds a `GarminWatchState` reflecting the latest glucose, trend, delta, eventual BG, ISF, IOB, and COB.
     /// Builds a `GarminWatchState` reflecting the latest glucose, trend, delta, eventual BG, ISF, IOB, and COB.
     /// - Returns: A `GarminWatchState` containing the most recent device- and therapy-related info.
     /// - Returns: A `GarminWatchState` containing the most recent device- and therapy-related info.
-    func setupGarminWatchState() async -> GarminWatchState {
-        // Get Glucose IDs
-        let glucoseIds = await fetchGlucose()
-
-        // Fetch the latest OrefDetermination object if available
-        let determinationIds = await determinationStorage.fetchLastDeterminationObjectID(
-            predicate: NSPredicate.predicateFor30MinAgoForDetermination
-        )
+    func setupGarminWatchState() async throws -> GarminWatchState {
+        do {
+            // Get Glucose IDs
+            let glucoseIds = try await fetchGlucose()
+
+            // Fetch the latest OrefDetermination object if available
+            let determinationIds = try await determinationStorage.fetchLastDeterminationObjectID(
+                predicate: NSPredicate.predicateFor30MinAgoForDetermination
+            )
 
 
-        // Turn those IDs into live NSManagedObjects
-        let glucoseObjects: [GlucoseStored] = await CoreDataStack.shared
-            .getNSManagedObject(with: glucoseIds, context: backgroundContext)
-        let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
-            .getNSManagedObject(with: determinationIds, context: backgroundContext)
-
-        // Perform logic on the background context
-        return await backgroundContext.perform {
-            var watchState = GarminWatchState()
-
-            /// Pull `glucose`, `trendRaw`, `delta`, `lastLoopDateInterval`, `iob`, `cob`,  `isf`, and `eventualBGRaw` from the latest determination.
-            if let latestDetermination = determinationObjects.first {
-                watchState.lastLoopDateInterval = latestDetermination.timestamp.map {
-                    guard $0.timeIntervalSince1970 > 0 else { return 0 }
-                    return UInt64($0.timeIntervalSince1970)
+            // Turn those IDs into live NSManagedObjects
+            let glucoseObjects: [GlucoseStored] = try await CoreDataStack.shared
+                .getNSManagedObject(with: glucoseIds, context: backgroundContext)
+            let determinationObjects: [OrefDetermination] = try await CoreDataStack.shared
+                .getNSManagedObject(with: determinationIds, context: backgroundContext)
+
+            // Perform logic on the background context
+            return await backgroundContext.perform {
+                var watchState = GarminWatchState()
+
+                /// Pull `glucose`, `trendRaw`, `delta`, `lastLoopDateInterval`, `iob`, `cob`,  `isf`, and `eventualBGRaw` from the latest determination.
+                if let latestDetermination = determinationObjects.first {
+                    watchState.lastLoopDateInterval = latestDetermination.timestamp.map {
+                        guard $0.timeIntervalSince1970 > 0 else { return 0 }
+                        return UInt64($0.timeIntervalSince1970)
+                    }
+
+                    let iobValue = latestDetermination.iob ?? 0
+                    watchState.iob = Formatter.decimalFormatterWithTwoFractionDigits.string(from: iobValue)
+
+                    let cobNumber = NSNumber(value: latestDetermination.cob)
+                    watchState.cob = Formatter.integerFormatter.string(from: cobNumber)
+
+                    let insulinSensitivity = latestDetermination.insulinSensitivity ?? 0
+                    let eventualBG = latestDetermination.eventualBG ?? 0
+
+                    if self.units == .mgdL {
+                        watchState.isf = insulinSensitivity.description
+                        watchState.eventualBGRaw = Formatter.glucoseFormatter(for: self.units)
+                            .string(from: eventualBG) ?? "0"
+                    } else {
+                        let parsedIsf = Double(truncating: insulinSensitivity).asMmolL
+                        let parsedEventualBG = Double(truncating: eventualBG).asMmolL
+
+                        watchState.isf = parsedIsf.description
+                        watchState.eventualBGRaw = Formatter.glucoseFormatter(for: self.units)
+                            .string(from: parsedEventualBG as NSNumber) ?? "0"
+                    }
                 }
                 }
 
 
-                let iobValue = latestDetermination.iob ?? 0
-                watchState.iob = Formatter.decimalFormatterWithTwoFractionDigits.string(from: iobValue)
-
-                let cobNumber = NSNumber(value: latestDetermination.cob)
-                watchState.cob = Formatter.integerFormatter.string(from: cobNumber)
-
-                let insulinSensitivity = latestDetermination.insulinSensitivity ?? 0
-                let eventualBG = latestDetermination.eventualBG ?? 0
+                // If no glucose data is present, just return partial watch state
+                guard let latestGlucose = glucoseObjects.first else {
+                    return watchState
+                }
 
 
+                // Format the current glucose reading
                 if self.units == .mgdL {
                 if self.units == .mgdL {
-                    watchState.isf = insulinSensitivity.description
-                    watchState.eventualBGRaw = Formatter.glucoseFormatter(for: self.units)
-                        .string(from: eventualBG) ?? "0"
+                    watchState.glucose = "\(latestGlucose.glucose)"
                 } else {
                 } else {
-                    let parsedIsf = Double(truncating: insulinSensitivity).asMmolL
-                    let parsedEventualBG = Double(truncating: eventualBG).asMmolL
-
-                    watchState.isf = parsedIsf.description
-                    watchState.eventualBGRaw = Formatter.glucoseFormatter(for: self.units)
-                        .string(from: parsedEventualBG as NSNumber) ?? "0"
+                    let mgdlValue = Decimal(latestGlucose.glucose)
+                    let latestGlucoseValue = Double(truncating: mgdlValue.asMmolL as NSNumber)
+                    watchState.glucose = "\(latestGlucoseValue)"
                 }
                 }
-            }
-
-            // If no glucose data is present, just return partial watch state
-            guard let latestGlucose = glucoseObjects.first else {
-                return watchState
-            }
 
 
-            // Format the current glucose reading
-            if self.units == .mgdL {
-                watchState.glucose = "\(latestGlucose.glucose)"
-            } else {
-                let mgdlValue = Decimal(latestGlucose.glucose)
-                let latestGlucoseValue = Double(truncating: mgdlValue.asMmolL as NSNumber)
-                watchState.glucose = "\(latestGlucoseValue)"
-            }
+                // Convert direction to a textual trend
+                watchState.trendRaw = latestGlucose.direction ?? "--"
 
 
-            // Convert direction to a textual trend
-            watchState.trendRaw = latestGlucose.direction ?? "--"
+                // Calculate a glucose delta if we have at least two readings
+                if glucoseObjects.count >= 2 {
+                    var deltaValue = Decimal(glucoseObjects[0].glucose - glucoseObjects[1].glucose)
 
 
-            // Calculate a glucose delta if we have at least two readings
-            if glucoseObjects.count >= 2 {
-                var deltaValue = Decimal(glucoseObjects[0].glucose - glucoseObjects[1].glucose)
+                    if self.units == .mmolL {
+                        deltaValue = Double(truncating: deltaValue as NSNumber).asMmolL
+                    }
 
 
-                if self.units == .mmolL {
-                    deltaValue = Double(truncating: deltaValue as NSNumber).asMmolL
+                    let formattedDelta = Formatter.glucoseFormatter(for: self.units)
+                        .string(from: deltaValue as NSNumber) ?? "0"
+                    watchState.delta = deltaValue < 0 ? "\(formattedDelta)" : "+\(formattedDelta)"
                 }
                 }
 
 
-                let formattedDelta = Formatter.glucoseFormatter(for: self.units)
-                    .string(from: deltaValue as NSNumber) ?? "0"
-                watchState.delta = deltaValue < 0 ? "\(formattedDelta)" : "+\(formattedDelta)"
-            }
+                debug(
+                    .watchManager,
+                    """
+                    📱 Setup GarminWatchState - \
+                    glucose: \(watchState.glucose ?? "nil"), \
+                    trendRaw: \(watchState.trendRaw ?? "nil"), \
+                    delta: \(watchState.delta ?? "nil"), \
+                    eventualBGRaw: \(watchState.eventualBGRaw ?? "nil"), \
+                    isf: \(watchState.isf ?? "nil"), \
+                    cob: \(watchState.cob ?? "nil"), \
+                    iob: \(watchState.iob ?? "nil"), \
+                    lastLoopDateInterval: \(watchState.lastLoopDateInterval?.description ?? "nil")
+                    """
+                )
 
 
+                return watchState
+            }
+        } catch {
             debug(
             debug(
                 .watchManager,
                 .watchManager,
-                """
-                📱 Setup GarminWatchState - \
-                glucose: \(watchState.glucose ?? "nil"), \
-                trendRaw: \(watchState.trendRaw ?? "nil"), \
-                delta: \(watchState.delta ?? "nil"), \
-                eventualBGRaw: \(watchState.eventualBGRaw ?? "nil"), \
-                isf: \(watchState.isf ?? "nil"), \
-                cob: \(watchState.cob ?? "nil"), \
-                iob: \(watchState.iob ?? "nil"), \
-                lastLoopDateInterval: \(watchState.lastLoopDateInterval?.description ?? "nil")
-                """
+                "\(DebuggingIdentifiers.failed) Error setting up Garmin watch state: \(error.localizedDescription)"
             )
             )
-
-            return watchState
+            throw error
         }
         }
     }
     }
 
 
@@ -357,7 +388,7 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
     }
     }
 
 
     /// Subscribes to any watch-state dictionaries published via `watchStateSubject`, and throttles them
     /// Subscribes to any watch-state dictionaries published via `watchStateSubject`, and throttles them
-    /// so updates arent sent too frequently. Each update triggers a broadcast to all watch apps.
+    /// so updates aren't sent too frequently. Each update triggers a broadcast to all watch apps.
     private func subscribeToWatchState() {
     private func subscribeToWatchState() {
         watchStateSubject
         watchStateSubject
             .throttle(for: .seconds(10), scheduler: DispatchQueue.main, latest: true)
             .throttle(for: .seconds(10), scheduler: DispatchQueue.main, latest: true)
@@ -419,7 +450,7 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
         .eraseToAnyPublisher()
         .eraseToAnyPublisher()
     }
     }
 
 
-    /// Updates the managers list of devices, typically after user selection or manual changes.
+    /// Updates the manager's list of devices, typically after user selection or manual changes.
     /// - Parameter devices: The new array of `IQDevice` objects to track.
     /// - Parameter devices: The new array of `IQDevice` objects to track.
     func updateDeviceList(_ devices: [IQDevice]) {
     func updateDeviceList(_ devices: [IQDevice]) {
         self.devices = devices
         self.devices = devices
@@ -527,7 +558,7 @@ extension BaseGarminManager: IQUIOverrideDelegate, IQDeviceEventDelegate, IQAppM
 
 
             do {
             do {
                 // Fetch the latest watch state (async) and encode it to JSON data
                 // Fetch the latest watch state (async) and encode it to JSON data
-                let watchState = await self.setupGarminWatchState()
+                let watchState = try await self.setupGarminWatchState()
                 let watchStateData = try JSONEncoder().encode(watchState)
                 let watchStateData = try JSONEncoder().encode(watchState)
 
 
                 // Now send that JSON data to the watch
                 // Now send that JSON data to the watch
@@ -560,9 +591,16 @@ extension BaseGarminManager: SettingsObserver {
         units = settingsManager.settings.units
         units = settingsManager.settings.units
 
 
         Task {
         Task {
-            let watchState = await setupGarminWatchState()
-            let watchStateData = try JSONEncoder().encode(watchState)
-            sendWatchStateData(watchStateData)
+            do {
+                let watchState = try await setupGarminWatchState()
+                let watchStateData = try JSONEncoder().encode(watchState)
+                sendWatchStateData(watchStateData)
+            } catch {
+                debug(
+                    .watchManager,
+                    "\(DebuggingIdentifiers.failed) failed to send watch state data: \(error.localizedDescription)"
+                )
+            }
         }
         }
     }
     }
 }
 }

+ 1 - 1
Trio/Sources/Shortcuts/Carbs/CarbPresetIntentRequest.swift

@@ -15,7 +15,7 @@ import Foundation
 
 
         let carbs = min(Decimal(quantityCarbs), settingsManager.settings.maxCarbs)
         let carbs = min(Decimal(quantityCarbs), settingsManager.settings.maxCarbs)
 
 
-        await carbsStorage.storeCarbs(
+        try await carbsStorage.storeCarbs(
             [CarbsEntry(
             [CarbsEntry(
                 id: UUID().uuidString,
                 id: UUID().uuidString,
                 createdAt: dateAdded,
                 createdAt: dateAdded,

+ 2 - 2
Trio/Sources/Shortcuts/Override/OverridePresetEntity.swift

@@ -18,10 +18,10 @@ struct OverridePreset: AppEntity, Identifiable {
 
 
 struct OverridePresetsQuery: EntityQuery {
 struct OverridePresetsQuery: EntityQuery {
     func entities(for identifiers: [OverridePreset.ID]) async throws -> [OverridePreset] {
     func entities(for identifiers: [OverridePreset.ID]) async throws -> [OverridePreset] {
-        await OverridePresetsIntentRequest().fetchIDs(identifiers)
+        try await OverridePresetsIntentRequest().fetchIDs(identifiers)
     }
     }
 
 
     func suggestedEntities() async throws -> [OverridePreset] {
     func suggestedEntities() async throws -> [OverridePreset] {
-        await OverridePresetsIntentRequest().fetchAndProcessOverrides()
+        try await OverridePresetsIntentRequest().fetchAndProcessOverrides()
     }
     }
 }
 }

+ 45 - 48
Trio/Sources/Shortcuts/Override/OverridePresetsIntentRequest.swift

@@ -9,13 +9,13 @@ import UIKit
         case noActiveOverride
         case noActiveOverride
     }
     }
 
 
-    func fetchAndProcessOverrides() async -> [OverridePreset] {
-        // Fetch all Override Presets via OverrideStorage
-        let allOverridePresetsIDs = await overrideStorage.fetchForOverridePresets()
+    func fetchAndProcessOverrides() async throws -> [OverridePreset] {
+        do {
+            // Fetch all Override Presets via OverrideStorage
+            let allOverridePresetsIDs = try await overrideStorage.fetchForOverridePresets()
 
 
-        // Since we are fetching on a different background Thread we need to unpack the NSManagedObjectID on the correct Thread first
-        return await coredataContext.perform {
-            do {
+            // Since we are fetching on a different background Thread we need to unpack the NSManagedObjectID on the correct Thread first
+            return try await coredataContext.perform {
                 let overrideObjects = try allOverridePresetsIDs.compactMap { id in
                 let overrideObjects = try allOverridePresetsIDs.compactMap { id in
                     try self.coredataContext.existingObject(with: id) as? OverrideStored
                     try self.coredataContext.existingObject(with: id) as? OverrideStored
                 }
                 }
@@ -25,18 +25,18 @@ import UIKit
                           let name = object.name else { return OverridePreset(id: UUID().uuidString, name: "") }
                           let name = object.name else { return OverridePreset(id: UUID().uuidString, name: "") }
                     return OverridePreset(id: id, name: name)
                     return OverridePreset(id: id, name: name)
                 }
                 }
-
-            } catch {
-                debugPrint(
-                    "\(#file) \(#function) \(DebuggingIdentifiers.failed) error while fetching/ processing the overrides Array: \(error.localizedDescription)"
-                )
-                return [OverridePreset(id: UUID().uuidString, name: "")]
             }
             }
+        } catch {
+            debug(
+                .default,
+                "\(DebuggingIdentifiers.failed) Error fetching/processing overrides: \(error.localizedDescription)"
+            )
+            throw error
         }
         }
     }
     }
 
 
-    func fetchIDs(_ uuid: [OverridePreset.ID]) async -> [OverridePreset] {
-        await coredataContext.perform {
+    func fetchIDs(_ uuid: [OverridePreset.ID]) async throws -> [OverridePreset] {
+        try await coredataContext.perform {
             let fetchRequest: NSFetchRequest<OverrideStored> = OverrideStored.fetchRequest()
             let fetchRequest: NSFetchRequest<OverrideStored> = OverrideStored.fetchRequest()
             fetchRequest.predicate = NSPredicate(format: "id IN %@", uuid)
             fetchRequest.predicate = NSPredicate(format: "id IN %@", uuid)
 
 
@@ -44,36 +44,40 @@ import UIKit
                 let result = try self.coredataContext.fetch(fetchRequest)
                 let result = try self.coredataContext.fetch(fetchRequest)
 
 
                 if result.isEmpty {
                 if result.isEmpty {
-                    debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) No OverrideStored found for ids: \(uuid)")
-                    return [OverridePreset(id: UUID().uuidString, name: "")]
+                    debug(
+                        .default,
+                        "\(DebuggingIdentifiers.failed) No OverrideStored found for ids: \(uuid)"
+                    )
+                    throw overridePresetsError.noTempOverrideFound
                 }
                 }
 
 
                 return result.map { overrideStored in
                 return result.map { overrideStored in
                     OverridePreset(id: overrideStored.id ?? UUID().uuidString, name: overrideStored.name ?? "")
                     OverridePreset(id: overrideStored.id ?? UUID().uuidString, name: overrideStored.name ?? "")
                 }
                 }
-            } catch let error as NSError {
-                debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch Override: \(error.localizedDescription)"
+            } catch {
+                debug(
+                    .default,
+                    "\(DebuggingIdentifiers.failed) Failed to fetch Override: \(error.localizedDescription)"
                 )
                 )
-                return [OverridePreset(id: UUID().uuidString, name: "")]
+                throw error
             }
             }
         }
         }
     }
     }
 
 
-    private func fetchOverrideID(_ preset: OverridePreset) async -> NSManagedObjectID? {
+    private func fetchOverrideID(_ preset: OverridePreset) async throws -> NSManagedObjectID {
         let fetchRequest: NSFetchRequest<OverrideStored> = OverrideStored.fetchRequest()
         let fetchRequest: NSFetchRequest<OverrideStored> = OverrideStored.fetchRequest()
         fetchRequest.predicate = NSPredicate(format: "id == %@", preset.id)
         fetchRequest.predicate = NSPredicate(format: "id == %@", preset.id)
         fetchRequest.fetchLimit = 1
         fetchRequest.fetchLimit = 1
 
 
-        return await coredataContext.perform {
-            do {
-                return try self.coredataContext.fetch(fetchRequest).first?.objectID
-            } catch {
-                debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch Override: \(error.localizedDescription)"
+        return try await coredataContext.perform {
+            guard let objectID = try self.coredataContext.fetch(fetchRequest).first?.objectID else {
+                debug(
+                    .default,
+                    "\(DebuggingIdentifiers.failed) No override found for preset: \(preset.name)"
                 )
                 )
-                return nil
+                throw overridePresetsError.noTempOverrideFound
             }
             }
+            return objectID
         }
         }
     }
     }
 
 
@@ -83,13 +87,11 @@ import UIKit
         backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "Override Upload") {
         backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "Override Upload") {
             guard backgroundTaskID != .invalid else { return }
             guard backgroundTaskID != .invalid else { return }
             Task {
             Task {
-                // End background task when the time is about to expire
                 UIApplication.shared.endBackgroundTask(backgroundTaskID)
                 UIApplication.shared.endBackgroundTask(backgroundTaskID)
             }
             }
             backgroundTaskID = .invalid
             backgroundTaskID = .invalid
         }
         }
 
 
-        // Defer block to end background task when function exits
         defer {
         defer {
             if backgroundTaskID != .invalid {
             if backgroundTaskID != .invalid {
                 Task {
                 Task {
@@ -101,37 +103,33 @@ import UIKit
 
 
         do {
         do {
             // Get NSManagedObjectID of Preset
             // Get NSManagedObjectID of Preset
-            guard let overrideID = await fetchOverrideID(preset),
-                  let overrideObject = try viewContext.existingObject(with: overrideID) as? OverrideStored
-            else { return false }
+            let overrideID = try await fetchOverrideID(preset)
+            guard let overrideObject = try viewContext.existingObject(with: overrideID) as? OverrideStored else {
+                throw overridePresetsError.noTempOverrideFound
+            }
 
 
             // Enable Override
             // Enable Override
             overrideObject.enabled = true
             overrideObject.enabled = true
             overrideObject.date = Date()
             overrideObject.date = Date()
             overrideObject.isUploadedToNS = false
             overrideObject.isUploadedToNS = false
 
 
-            // Disable previous overrides if necessary, without starting a background task
+            // Disable previous overrides if necessary
             await disableAllActiveOverrides(except: overrideID, createOverrideRunEntry: true, shouldStartBackgroundTask: false)
             await disableAllActiveOverrides(except: overrideID, createOverrideRunEntry: true, shouldStartBackgroundTask: false)
 
 
             if viewContext.hasChanges {
             if viewContext.hasChanges {
                 try viewContext.save()
                 try viewContext.save()
-
-                // Update State variables in OverrideView
                 Foundation.NotificationCenter.default.post(name: .willUpdateOverrideConfiguration, object: nil)
                 Foundation.NotificationCenter.default.post(name: .willUpdateOverrideConfiguration, object: nil)
-
-                // Await the notification
-                print("Waiting for notification...")
                 await awaitNotification(.didUpdateOverrideConfiguration)
                 await awaitNotification(.didUpdateOverrideConfiguration)
-                print("Notification received, continuing...")
-
                 return true
                 return true
             }
             }
+            return false
         } catch {
         } catch {
-            // Handle error and ensure background task is ended
-            debugPrint("Failed to enact Override: \(error.localizedDescription)")
+            debug(
+                .default,
+                "\(DebuggingIdentifiers.failed) Failed to enact override: \(error.localizedDescription)"
+            )
+            return false
         }
         }
-
-        return false
     }
     }
 
 
     func cancelOverride() async {
     func cancelOverride() async {
@@ -167,10 +165,9 @@ import UIKit
             }
             }
         }
         }
 
 
-        // Get NSManagedObjectID of all active overrides
-        let ids = await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0) // 0 = no fetch limit
-
         do {
         do {
+            // Get NSManagedObjectID of all active overrides
+            let ids = try await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0) // 0 = no fetch limit
             // Fetch existing OverrideStored objects
             // Fetch existing OverrideStored objects
             let results = try ids.compactMap { id in
             let results = try ids.compactMap { id in
                 try self.viewContext.existingObject(with: id) as? OverrideStored
                 try self.viewContext.existingObject(with: id) as? OverrideStored

+ 2 - 2
Trio/Sources/Shortcuts/State/StateIntentRequest.swift

@@ -61,7 +61,7 @@ final class StateIntentRequest: BaseIntentsRequest {
         -> (dateGlucose: Date, glucose: String, trend: String, delta: String)
         -> (dateGlucose: Date, glucose: String, trend: String, delta: String)
     {
     {
         do {
         do {
-            let results = CoreDataStack.shared.fetchEntities(
+            let results = try CoreDataStack.shared.fetchEntities(
                 ofType: GlucoseStored.self,
                 ofType: GlucoseStored.self,
                 onContext: onContext,
                 onContext: onContext,
                 predicate: NSPredicate.predicateFor30MinAgo,
                 predicate: NSPredicate.predicateFor30MinAgo,
@@ -102,7 +102,7 @@ final class StateIntentRequest: BaseIntentsRequest {
     }
     }
 
 
     func getIobAndCob(onContext: NSManagedObjectContext) throws -> (iob: Double, cob: Double) {
     func getIobAndCob(onContext: NSManagedObjectContext) throws -> (iob: Double, cob: Double) {
-        let results = CoreDataStack.shared.fetchEntities(
+        let results = try CoreDataStack.shared.fetchEntities(
             ofType: OrefDetermination.self,
             ofType: OrefDetermination.self,
             onContext: onContext,
             onContext: onContext,
             predicate: NSPredicate.enactedDetermination,
             predicate: NSPredicate.enactedDetermination,

+ 1 - 1
Trio/Sources/Shortcuts/TempPresets/TempPresetIntent.swift

@@ -25,6 +25,6 @@ struct TempPresetsQuery: EntityQuery {
     }
     }
 
 
     func suggestedEntities() async throws -> [TempPreset] {
     func suggestedEntities() async throws -> [TempPreset] {
-        await TempPresetsIntentRequest().fetchAndProcessTempTargets()
+        try await TempPresetsIntentRequest().fetchAndProcessTempTargets()
     }
     }
 }
 }

+ 7 - 8
Trio/Sources/Shortcuts/TempPresets/TempPresetsIntentRequest.swift

@@ -8,12 +8,12 @@ final class TempPresetsIntentRequest: BaseIntentsRequest {
         case noDurationDefined
         case noDurationDefined
     }
     }
 
 
-    func fetchAndProcessTempTargets() async -> [TempPreset] {
+    func fetchAndProcessTempTargets() async throws -> [TempPreset] {
         // Fetch all Temp Target Presets via TempTargetStorage
         // Fetch all Temp Target Presets via TempTargetStorage
-        let allTempTargetPresetsIDs = await tempTargetsStorage.fetchForTempTargetPresets()
+        let allTempTargetPresetsIDs = try await tempTargetsStorage.fetchForTempTargetPresets()
 
 
         // Perform the fetch and process on the Core Data context's thread
         // Perform the fetch and process on the Core Data context's thread
-        return await coredataContext.perform {
+        return try await coredataContext.perform {
             // Fetch existing TempTargetStored objects based on their NSManagedObjectIDs
             // Fetch existing TempTargetStored objects based on their NSManagedObjectIDs
             let tempTargetObjects: [TempTargetStored] = allTempTargetPresetsIDs.compactMap { id in
             let tempTargetObjects: [TempTargetStored] = allTempTargetPresetsIDs.compactMap { id in
                 guard let object = try? self.coredataContext.existingObject(with: id) as? TempTargetStored else {
                 guard let object = try? self.coredataContext.existingObject(with: id) as? TempTargetStored else {
@@ -24,14 +24,14 @@ final class TempPresetsIntentRequest: BaseIntentsRequest {
             }
             }
 
 
             // Map fetched TempTargetStored objects to TempPreset
             // Map fetched TempTargetStored objects to TempPreset
-            return tempTargetObjects.compactMap { object in
+            return try tempTargetObjects.compactMap { object in
                 guard let id = object.id,
                 guard let id = object.id,
                       let name = object.name,
                       let name = object.name,
                       let target = object.target?.decimalValue,
                       let target = object.target?.decimalValue,
                       let duration = object.duration?.decimalValue
                       let duration = object.duration?.decimalValue
                 else {
                 else {
                     debugPrint("\(#file) \(#function) Missing data for TempTargetStored object.")
                     debugPrint("\(#file) \(#function) Missing data for TempTargetStored object.")
-                    return TempPreset(id: UUID(), name: "", duration: 0)
+                    throw TempPresetsError.noTempTargetFound
                 }
                 }
                 return TempPreset(id: id, name: name, targetTop: target, duration: duration)
                 return TempPreset(id: id, name: name, targetTop: target, duration: duration)
             }
             }
@@ -199,10 +199,9 @@ final class TempPresetsIntentRequest: BaseIntentsRequest {
             }
             }
         }
         }
 
 
-        // Get NSManagedObjectID of all active temp Targets
-        let ids = await tempTargetsStorage.loadLatestTempTargetConfigurations(fetchLimit: 0)
-
         do {
         do {
+            // Get NSManagedObjectID of all active temp Targets
+            let ids = try await tempTargetsStorage.loadLatestTempTargetConfigurations(fetchLimit: 0)
             // Fetch existing OverrideStored objects
             // Fetch existing OverrideStored objects
             let results = try ids.compactMap { id in
             let results = try ids.compactMap { id in
                 try self.viewContext.existingObject(with: id) as? TempTargetStored
                 try self.viewContext.existingObject(with: id) as? TempTargetStored

+ 10 - 0
blacklisted-versions.json

@@ -0,0 +1,10 @@
+{
+    "blacklistedVersions": [
+        {
+            "version": "0.0.1"
+        },
+        {
+            "version": "0.0.2"
+        }
+    ]
+}