Parcourir la source

Merge branch 'dev' into 'stats-wip'

polscm32 il y a 1 an
Parent
commit
5c45147333
99 fichiers modifiés avec 4669 ajouts et 3266 suppressions
  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. 34 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. 15 9
      Trio/Sources/APS/DeviceDataManager.swift
  22. 58 85
      Trio/Sources/APS/FetchGlucoseManager.swift
  23. 39 35
      Trio/Sources/APS/FetchTreatmentsManager.swift
  24. 35 34
      Trio/Sources/APS/OpenAPS/OpenAPS.swift
  25. 31 29
      Trio/Sources/APS/Storage/CarbsStorage.swift
  26. 34 27
      Trio/Sources/APS/Storage/ContactImageStorage.swift
  27. 49 15
      Trio/Sources/APS/Storage/DeterminationStorage.swift
  28. 225 157
      Trio/Sources/APS/Storage/GlucoseStorage.swift
  29. 55 47
      Trio/Sources/APS/Storage/OverrideStorage.swift
  30. 187 206
      Trio/Sources/APS/Storage/PumpHistoryStorage.swift
  31. 49 38
      Trio/Sources/APS/Storage/TempTargetsStorage.swift
  32. 17 3
      Trio/Sources/Application/AppDelegate.swift
  33. 15 10
      Trio/Sources/Application/TrioApp.swift
  34. 1 1
      Trio/Sources/Helpers/MainChartHelper.swift
  35. 503 494
      Trio/Sources/Localizations/Main/Localizable.xcstrings
  36. 4 0
      Trio/Sources/Logger/Logger.swift
  37. 133 91
      Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+Overrides.swift
  38. 90 70
      Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+TempTargets.swift
  39. 11 7
      Trio/Sources/Modules/Adjustments/AdjustmentsStateModel.swift
  40. 23 21
      Trio/Sources/Modules/Adjustments/View/Overrides/EditOverrideForm.swift
  41. 16 8
      Trio/Sources/Modules/Adjustments/View/TempTargets/AddTempTargetForm.swift
  42. 2 2
      Trio/Sources/Modules/Adjustments/View/TempTargets/EditTempTargetForm.swift
  43. 9 2
      Trio/Sources/Modules/AlgorithmAdvancedSettings/AlgorithmAdvancedSettingsStateModel.swift
  44. 11 4
      Trio/Sources/Modules/AutosensSettings/AutosensSettingsStateModel.swift
  45. 6 2
      Trio/Sources/Modules/BasalProfileEditor/BasalProfileEditorStateModel.swift
  46. 26 22
      Trio/Sources/Modules/Calibrations/CalibrationsStateModel.swift
  47. 6 2
      Trio/Sources/Modules/CarbRatioEditor/CarbRatioEditorStateModel.swift
  48. 54 40
      Trio/Sources/Modules/DataTable/DataTableStateModel.swift
  49. 22 14
      Trio/Sources/Modules/DataTable/View/DataTableRootView.swift
  50. 17 8
      Trio/Sources/Modules/Home/HomeStateModel+Setup/BatterySetup.swift
  51. 28 14
      Trio/Sources/Modules/Home/HomeStateModel+Setup/CarbSetup.swift
  52. 22 16
      Trio/Sources/Modules/Home/HomeStateModel+Setup/DeterminationSetup.swift
  53. 39 61
      Trio/Sources/Modules/Home/HomeStateModel+Setup/ForecastSetup.swift
  54. 17 7
      Trio/Sources/Modules/Home/HomeStateModel+Setup/GlucoseSetup.swift
  55. 32 17
      Trio/Sources/Modules/Home/HomeStateModel+Setup/OverrideSetup.swift
  56. 30 13
      Trio/Sources/Modules/Home/HomeStateModel+Setup/PumpHistorySetup.swift
  57. 34 16
      Trio/Sources/Modules/Home/HomeStateModel+Setup/TempTargetSetup.swift
  58. 12 12
      Trio/Sources/Modules/Home/HomeStateModel.swift
  59. 22 0
      Trio/Sources/Modules/Home/View/Chart/ChartElements/GlucoseChartView.swift
  60. 4 3
      Trio/Sources/Modules/Home/View/Chart/ChartElements/SelectionPopoverView.swift
  61. 2 2
      Trio/Sources/Modules/Home/View/HomeRootView.swift
  62. 9 2
      Trio/Sources/Modules/ISFEditor/ISFEditorStateModel.swift
  63. 75 36
      Trio/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift
  64. 19 1
      Trio/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift
  65. 57 1
      Trio/Sources/Modules/Settings/View/SettingsRootView.swift
  66. 13 9
      Trio/Sources/Modules/Stat/StatStateModel+Setup/BolusStatsSetup.swift
  67. 23 19
      Trio/Sources/Modules/Stat/StatStateModel+Setup/LoopChartSetup.swift
  68. 13 9
      Trio/Sources/Modules/Stat/StatStateModel+Setup/MealStatsSetup.swift
  69. 14 10
      Trio/Sources/Modules/Stat/StatStateModel+Setup/TDDSetup.swift
  70. 33 27
      Trio/Sources/Modules/Stat/StatStateModel.swift
  71. 9 2
      Trio/Sources/Modules/TargetsEditor/TargetsEditorStateModel.swift
  72. 173 145
      Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift
  73. 9 16
      Trio/Sources/Modules/Treatments/View/TreatmentsRootView.swift
  74. 342 0
      Trio/Sources/Services/AppVersionChecker/AppVersionChecker.swift
  75. 102 67
      Trio/Sources/Services/BolusCalculator/BolusCalculationManager.swift
  76. 22 18
      Trio/Sources/Services/Calendar/CalendarManager.swift
  77. 76 69
      Trio/Sources/Services/ContactImage/ContactImageManager.swift
  78. 108 77
      Trio/Sources/Services/HealthKit/HealthKitManager.swift
  79. 12 12
      Trio/Sources/Services/LiveActivity/Data/DataManager.swift
  80. 34 21
      Trio/Sources/Services/LiveActivity/LiveActivityBridge.swift
  81. 147 113
      Trio/Sources/Services/Network/Nightscout/NightscoutManager.swift
  82. 120 98
      Trio/Sources/Services/Network/TidepoolManager.swift
  83. 2 2
      Trio/Sources/Services/RemoteControl/TrioRemoteControl+APNS.swift
  84. 10 9
      Trio/Sources/Services/RemoteControl/TrioRemoteControl+Bolus.swift
  85. 2 2
      Trio/Sources/Services/RemoteControl/TrioRemoteControl+Meal.swift
  86. 37 23
      Trio/Sources/Services/RemoteControl/TrioRemoteControl+Override.swift
  87. 15 15
      Trio/Sources/Services/RemoteControl/TrioRemoteControl+TempTarget.swift
  88. 5 5
      Trio/Sources/Services/RemoteControl/TrioRemoteControl.swift
  89. 74 21
      Trio/Sources/Services/Storage/FileStorage.swift
  90. 8 6
      Trio/Sources/Services/UserNotifications/UserNotificationsManager.swift
  91. 219 175
      Trio/Sources/Services/WatchManager/AppleWatchManager.swift
  92. 138 100
      Trio/Sources/Services/WatchManager/GarminManager.swift
  93. 1 1
      Trio/Sources/Shortcuts/Carbs/CarbPresetIntentRequest.swift
  94. 2 2
      Trio/Sources/Shortcuts/Override/OverridePresetEntity.swift
  95. 45 48
      Trio/Sources/Shortcuts/Override/OverridePresetsIntentRequest.swift
  96. 2 2
      Trio/Sources/Shortcuts/State/StateIntentRequest.swift
  97. 1 1
      Trio/Sources/Shortcuts/TempPresets/TempPresetIntent.swift
  98. 7 8
      Trio/Sources/Shortcuts/TempPresets/TempPresetsIntentRequest.swift
  99. 10 0
      blacklisted-versions.json

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

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

+ 90 - 16
Model/CoreDataObserver.swift

@@ -2,34 +2,108 @@ import Combine
 import CoreData
 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
-        .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>()
 
-            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> {
-    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 let inMemory: Bool
 
+    let persistentContainer: NSPersistentContainer
+
     private init(inMemory: Bool = false) {
         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
         notificationToken = Foundation.NotificationCenter.default.addObserver(
             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
@@ -88,7 +119,7 @@ class CoreDataStack: ObservableObject {
         let taskContext = persistentContainer.newBackgroundContext()
 
         /// ensure that the background contexts stay in sync with the main context
-        taskContext.automaticallyMergesChangesFromParent = true
+        taskContext.automaticallyMergesChangesFromParent = false
         taskContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
         taskContext.undoManager = nil
         return taskContext
@@ -98,14 +129,14 @@ class CoreDataStack: ObservableObject {
         do {
             try await fetchPersistentHistoryTransactionsAndChanges()
         } catch {
-            debugPrint("\(error.localizedDescription)")
+            debug(.coreData, "\(error.localizedDescription)")
         }
     }
 
     private func fetchPersistentHistoryTransactionsAndChanges() async throws {
         let taskContext = newTaskContext()
         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 {
             // Execute the persistent history change since the last transaction
@@ -120,7 +151,7 @@ class CoreDataStack: ObservableObject {
     }
 
     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
         /// - Tag: mergeChanges
         let viewContext = persistentContainer.viewContext
@@ -142,10 +173,11 @@ class CoreDataStack: ObservableObject {
             let deleteHistoryTokensRequest = NSPersistentHistoryChangeRequest.deleteHistory(before: date)
             do {
                 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 {
-                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
         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
     func deleteObject(identifiedBy objectID: NSManagedObjectID) async {
         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 {
             do {
@@ -178,9 +210,9 @@ extension CoreDataStack {
 
                 guard viewContext.hasChanges else { return }
                 try viewContext.save()
-                debugPrint("Successfully deleted data. \(DebuggingIdentifiers.succeeded)")
+                debug(.coreData, "Successfully deleted data. \(DebuggingIdentifiers.succeeded)")
             } 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,
         dateKey: String,
         days: Int,
-        isPresetKey: String? = nil
+        isPresetKey: String? = nil,
+        callingFunction: String = #function,
+        callingClass: String = #fileID
     ) async throws {
         let taskContext = newTaskContext()
         taskContext.name = "deleteContext"
@@ -219,7 +253,7 @@ extension CoreDataStack {
 
             // Guard check if there are NSManagedObjects older than the specified days
             guard !objectIDs.isEmpty else {
-//                debugPrint("No objects found older than \(days) days.")
+//                debug(.coreData,"No objects found older than \(days) days.")
                 return
             }
 
@@ -230,15 +264,15 @@ extension CoreDataStack {
                       let batchDeleteResult = fetchResult as? NSBatchDeleteResult,
                       let success = batchDeleteResult.result as? Bool, success
                 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 {
-            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,
         dateKey: String,
         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 {
         let taskContext = newTaskContext()
         taskContext.name = "deleteContext"
@@ -267,7 +303,7 @@ extension CoreDataStack {
             }
 
             guard !parentObjectIDs.isEmpty else {
-//                debugPrint("No \(parentType) objects found older than \(days) days.")
+//                debug(.coreData,"No \(parentType) objects found older than \(days) days.")
                 return
             }
 
@@ -281,7 +317,7 @@ extension CoreDataStack {
             }
 
             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
             }
 
@@ -292,17 +328,18 @@ extension CoreDataStack {
                       let batchDeleteResult = fetchResult as? NSBatchDeleteResult,
                       let success = batchDeleteResult.result as? Bool, success
                 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)"
             )
         } 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,
         callingFunction: String = #function,
         callingClass: String = #fileID
-    ) -> [Any] {
+    ) throws -> [Any] {
         let request = NSFetchRequest<NSFetchRequestResult>(entityName: String(describing: type))
         request.sortDescriptors = [NSSortDescriptor(key: key, ascending: ascending)]
         request.predicate = predicate
@@ -344,7 +381,7 @@ extension CoreDataStack {
         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
-        return context.performAndWait {
+        return try context.performAndWait {
             do {
                 if propertiesToFetch != nil {
                     return try context.fetch(request) as? [[String: Any]] ?? []
@@ -352,11 +389,10 @@ extension CoreDataStack {
                     return try context.fetch(request) as? [T] ?? []
                 }
             } 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,
         batchSize: Int? = nil,
         propertiesToFetch: [String]? = nil,
+        relationshipKeyPathsForPrefetching: [String]? = nil,
         callingFunction: String = #function,
         callingClass: String = #fileID
-    ) async -> Any {
+    ) async throws -> Any {
         let request: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: String(describing: type))
         request.sortDescriptors = [NSSortDescriptor(key: key, ascending: ascending)]
         request.predicate = predicate
+
         if let limit = fetchLimit {
             request.fetchLimit = limit
         }
@@ -389,11 +427,14 @@ extension CoreDataStack {
         } else {
             request.resultType = .managedObjectResultType
         }
+        if let prefetchKeyPaths = relationshipKeyPathsForPrefetching {
+            request.relationshipKeyPathsForPrefetching = prefetchKeyPaths
+        }
 
         context.name = "fetchContext"
         context.transactionAuthor = "fetchEntities"
 
-        return await context.perform {
+        return try await context.perform {
             do {
                 if propertiesToFetch != nil {
                     return try context.fetch(request) as? [[String: Any]] ?? []
@@ -401,10 +442,11 @@ extension CoreDataStack {
                     return try context.fetch(request) as? [T] ?? []
                 }
             } 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
     func getNSManagedObject<T: NSManagedObject>(
         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]()
             do {
                 for id in ids {
@@ -422,10 +466,13 @@ extension CoreDataStack {
                         objects.append(object)
                     }
                 }
+                return objects
             } catch {
-                debugPrint("Failed to fetch objects: \(error.localizedDescription)")
+                throw CoreDataError.fetchError(
+                    function: callingFunction,
+                    file: callingClass
+                )
             }
-            return objects
         }
     }
 }
@@ -442,7 +489,7 @@ extension CoreDataStack {
         do {
             try context.save()
         } catch {
-            debugPrint("Error saving context \(DebuggingIdentifiers.failed): \(error)")
+            debug(.coreData, "Error saving context \(DebuggingIdentifiers.failed): \(error)")
         }
     }
 }
@@ -458,11 +505,13 @@ extension NSManagedObjectContext {
         do {
             guard onContext.hasChanges else { return }
             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 {
-            debugPrint(
+            debug(
+                .coreData,
                 "Saving to Core Data failed in \(callingFunction) in \(callingClass): \(DebuggingIdentifiers.failed) with error \(error), \(error.userInfo)"
             )
             throw error

+ 24 - 21
Model/Helper/CoreDataError.swift

@@ -1,32 +1,35 @@
 import Foundation
 
 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 {
     var errorDescription: String? {
         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 }
     }
+
+    // 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 {

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

@@ -12,6 +12,32 @@ extension PumpEventStored {
         }
         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 {
@@ -70,7 +96,11 @@ extension NSPredicate {
 
     static var recentPumpHistory: NSPredicate {
         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 {

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

@@ -1,5 +1,5 @@
 <?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">
         <attribute name="amount" optional="YES" attributeType="Decimal" defaultValueString="0"/>
         <attribute name="isExternal" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
@@ -51,6 +51,9 @@
         <fetchIndex name="byDate">
             <fetchIndexElement property="date" type="Binary" order="descending"/>
         </fetchIndex>
+        <fetchIndex name="byValue">
+            <fetchIndexElement property="forecastValues" type="Binary" order="ascending"/>
+        </fetchIndex>
     </entity>
     <entity name="ForecastValue" representedClassName="ForecastValue" syncable="YES">
         <attribute name="index" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
@@ -140,6 +143,9 @@
         <fetchIndex name="byDate">
             <fetchIndexElement property="deliverAt" type="Binary" order="descending"/>
         </fetchIndex>
+        <fetchIndex name="byTimestamp">
+            <fetchIndexElement property="timestamp" type="Binary" order="ascending"/>
+        </fetchIndex>
     </entity>
     <entity name="OverrideRunStored" representedClassName="OverrideRunStored" syncable="YES">
         <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 {
                     Text("Bolus")
                     Spacer()
-                    Text(String(format: "%.2f U", adjustedBolusAmount))
+                    Text(String(format: "%.2f \(String(localized: "U", comment: "Insulin unit"))", adjustedBolusAmount))
                         .bold()
                         .foregroundStyle(Color.insulin)
                 }.padding(.horizontal)

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

@@ -25,7 +25,10 @@ struct BolusInputView: View {
     var body: some View {
         VStack {
             if state.showBolusCalculationProgress {
-                ProgressView("Calculating Bolus...")
+                ProgressView(String(
+                    localized: "Calculating Bolus...",
+                    comment: "Progress view text on watch when calculating bolus"
+                ))
                 Spacer()
             } else {
                 if effectiveBolusLimit <= 0 {
@@ -63,7 +66,7 @@ struct BolusInputView: View {
                         let bolusIncrement = Double(truncating: state.bolusIncrement as NSNumber)
                         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)
                             .font(.system(.title2, design: .rounded))
                             .foregroundColor(bolusAmount > 0.0 && bolusAmount >= effectiveBolusLimit ? .loopRed : .primary)
@@ -117,7 +120,10 @@ struct BolusInputView: View {
                     .tint(Color.insulin)
                     .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)
                         .foregroundStyle(.secondary)
                 }

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

@@ -47,7 +47,10 @@ struct BolusProgressOverlay: View {
                     .tint(progressGradient)
 
                 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.activeBolusAmount
                 ))

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

@@ -22,7 +22,8 @@ struct CarbsInputView: 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
         VStack {
@@ -45,7 +46,7 @@ struct CarbsInputView: View {
                 Spacer()
 
                 // 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)
                     .font(.system(.title2, design: .rounded))
                     .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()
 
-            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()
 

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

@@ -103,9 +103,9 @@ enum TreatmentOption: String, CaseIterable, Identifiable {
 
     var displayName: String {
         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'")
         }
     }
 }

+ 34 - 13
Trio.xcodeproj/project.pbxproj

@@ -487,6 +487,9 @@
 		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 */; };
 		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 */; };
 		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 */; };
@@ -516,6 +519,7 @@
 		DDA6E3202D258E0500C2988C /* OverrideHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E31F2D258E0500C2988C /* OverrideHelpView.swift */; };
 		DDA6E3222D25901100C2988C /* TempTargetHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E3212D25901100C2988C /* TempTargetHelpView.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 */; };
 		DDAA29852D2D1D9E006546A1 /* AdjustmentsRootView+TempTargets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAA29842D2D1D98006546A1 /* AdjustmentsRootView+TempTargets.swift */; };
 		DDB37CC52D05048F00D99BF4 /* ContactImageStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB37CC42D05048F00D99BF4 /* ContactImageStorage.swift */; };
@@ -1222,6 +1226,8 @@
 		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>"; };
 		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>"; };
 		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>"; };
@@ -1751,6 +1757,7 @@
 		3811DE9125C9D88200A708ED /* Services */ = {
 			isa = PBXGroup;
 			children = (
+				DDA9AC072D67291600E6F1A9 /* AppVersionChecker */,
 				BD7DB88C2D2C49FF003D3155 /* BolusCalculator */,
 				3811DE9225C9D88200A708ED /* Appearance */,
 				CEB434E128B8F9BC00B70274 /* Bluetooth */,
@@ -1996,21 +2003,22 @@
 		388E594F25AD948C0019842D = {
 			isa = PBXGroup;
 			children = (
+				DDA9AC0A2D678DAD00E6F1A9 /* blacklisted-versions.json */,
 				CE1F6DE62BAF1A180064EB8D /* BuildDetails.plist */,
 				38F3783A2613555C009DB701 /* Config.xcconfig */,
 				BD1CF8B72C1A4A8400CB930A /* ConfigOverride.xcconfig */,
-				3818AA48274C267000843DB3 /* Frameworks */,
-				6B1A8D1C2B14D91600E76752 /* LiveActivity */,
-				587A54C82BCDCE0F009D38E2 /* Model */,
-				3818AA44274C229000843DB3 /* Packages */,
-				388E595925AD948C0019842D /* Products */,
-				192F0FF5276AC36D0085BE4D /* Recovered References */,
 				388E595A25AD948C0019842D /* Trio */,
+				587A54C82BCDCE0F009D38E2 /* Model */,
 				38FCF3EE25E9028E0078B0D1 /* TrioTests */,
+				6B1A8D1C2B14D91600E76752 /* LiveActivity */,
 				BDFF7AA12D25FAC70016C40C /* Trio Watch App */,
 				BDFF7A9C2D25FA730016C40C /* Trio Watch App Extension */,
 				BDFF7AA02D25FAA80016C40C /* Trio Watch App Tests */,
 				DD09D6492D2B6253000D82C9 /* Trio Watch Complication */,
+				3818AA48274C267000843DB3 /* Frameworks */,
+				3818AA44274C229000843DB3 /* Packages */,
+				388E595925AD948C0019842D /* Products */,
+				192F0FF5276AC36D0085BE4D /* Recovered References */,
 			);
 			sourceTree = "<group>";
 		};
@@ -2387,11 +2395,11 @@
 		587A54C82BCDCE0F009D38E2 /* Model */ = {
 			isa = PBXGroup;
 			children = (
-				DDE179112C9100FA003CDDB7 /* Classes+Properties */,
 				BDF34F8F2C10CF8C00D51995 /* CoreDataStack.swift */,
-				5825D1622BD405AE00F36E9B /* Helper */,
-				DDD1631D2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodeld */,
 				BD4064D02C4ED26900582F43 /* CoreDataObserver.swift */,
+				DDD1631D2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodeld */,
+				DDE179112C9100FA003CDDB7 /* Classes+Properties */,
+				5825D1622BD405AE00F36E9B /* Helper */,
 			);
 			path = Model;
 			sourceTree = "<group>";
@@ -2992,6 +3000,14 @@
 			path = View;
 			sourceTree = "<group>";
 		};
+		DDA9AC072D67291600E6F1A9 /* AppVersionChecker */ = {
+			isa = PBXGroup;
+			children = (
+				DDA9AC082D672CEB00E6F1A9 /* AppVersionChecker.swift */,
+			);
+			path = AppVersionChecker;
+			sourceTree = "<group>";
+		};
 		DDC9B9962CFD2332003E7721 /* Nightscout */ = {
 			isa = PBXGroup;
 			children = (
@@ -3350,8 +3366,9 @@
 		388E595025AD948C0019842D /* Project object */ = {
 			isa = PBXProject;
 			attributes = {
+				BuildIndependentTargetsInParallel = YES;
 				LastSwiftUpdateCheck = 1620;
-				LastUpgradeCheck = 1240;
+				LastUpgradeCheck = 1620;
 				TargetAttributes = {
 					388E595725AD948C0019842D = {
 						CreatedOnToolsVersion = 12.3;
@@ -3453,6 +3470,7 @@
 			isa = PBXResourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				DD498F2B2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */,
 				6B1A8D242B14D91700E76752 /* Assets.xcassets in Resources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -3461,6 +3479,7 @@
 			isa = PBXResourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				DD498F2D2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */,
 				DD09D6442D2B553A000D82C9 /* Assets.xcassets in Resources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -3470,6 +3489,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				BDFF7A872D25F97D0016C40C /* Assets.xcassets in Resources */,
+				DD498F2C2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -3485,6 +3505,7 @@
 /* Begin PBXShellScriptBuildPhase section */
 		3811DEF525CA169200A708ED /* Swiftformat */ = {
 			isa = PBXShellScriptBuildPhase;
+			alwaysOutOfDate = 1;
 			buildActionMask = 12;
 			files = (
 			);
@@ -3522,6 +3543,7 @@
 		};
 		DD88C8DF2C4D583900F2D558 /* Run Script: Capture Build Details */ = {
 			isa = PBXShellScriptBuildPhase;
+			alwaysOutOfDate = 1;
 			buildActionMask = 2147483647;
 			files = (
 			);
@@ -3540,6 +3562,7 @@
 		};
 		DD88C8E02C4D716400F2D558 /* Run Script: get branch name and commit ID */ = {
 			isa = PBXShellScriptBuildPhase;
+			alwaysOutOfDate = 1;
 			buildActionMask = 2147483647;
 			files = (
 			);
@@ -3705,6 +3728,7 @@
 				DDA6E3572D25988500C2988C /* ContactImageHelpView.swift in Sources */,
 				38FE826D25CC8461001FF17A /* NightscoutAPI.swift in Sources */,
 				388358C825EEF6D200E024B2 /* BasalProfileEntry.swift in Sources */,
+				DDA9AC092D672CF100E6F1A9 /* AppVersionChecker.swift in Sources */,
 				3811DE0B25C9D32F00A708ED /* BaseView.swift in Sources */,
 				3811DE3225C9D49500A708ED /* HomeDataFlow.swift in Sources */,
 				DD32CFA22CC824E2003686D6 /* TrioRemoteControl+Helpers.swift in Sources */,
@@ -4017,7 +4041,6 @@
 				195D80B92AF697F700D25097 /* DynamicSettingsProvider.swift in Sources */,
 				DD09D47D2C5986DA003FEA5D /* CalendarEventSettingsProvider.swift in Sources */,
 				DD09D47B2C5986D1003FEA5D /* CalendarEventSettingsDataFlow.swift in Sources */,
-				6BCF84DD2B16843A003AD46E /* LiveActitiyAttributes.swift in Sources */,
 				DD1745202C55523E00211FAC /* SMBSettingsDataFlow.swift in Sources */,
 				D6D02515BBFBE64FEBE89856 /* DataTableRootView.swift in Sources */,
 				DD1745292C55642100211FAC /* SettingInputSection.swift in Sources */,
@@ -4035,7 +4058,6 @@
 				B7C465E9472624D8A2BE2A6A /* (null) in Sources */,
 				71D44AAB2CA5F5EA0036EE9E /* AlertPermissionsChecker.swift in Sources */,
 				320D030F724170A637F06D50 /* (null) in Sources */,
-				CE94598729E9E4110047C9C6 /* WatchConfigRootView.swift in Sources */,
 				19E1F7E829D082D0005C8D20 /* IconConfigDataFlow.swift in Sources */,
 				5A2325522BFCBF55003518CA /* NightscoutUploadView.swift in Sources */,
 				E3A08AAE59538BC8A8ABE477 /* GlucoseNotificationSettingsDataFlow.swift in Sources */,
@@ -4101,7 +4123,6 @@
 			buildActionMask = 2147483647;
 			files = (
 				6BCF84DE2B16843A003AD46E /* LiveActitiyAttributes.swift in Sources */,
-				6BCF84DE2B16843A003AD46E /* LiveActitiyAttributes.swift in Sources */,
 				DDCEBF5B2CC1B76400DF4C36 /* LiveActivity+Helper.swift in Sources */,
 				6B1A8D1E2B14D91600E76752 /* LiveActivityBundle.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"?>
 <Scheme
-   LastUpgradeVersion = "1600"
+   LastUpgradeVersion = "1620"
    version = "1.7">
    <BuildAction
       parallelizeBuildables = "YES"

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

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

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

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

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

@@ -20,8 +20,8 @@ protocol APSManager {
     var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> { get }
     var isManualTempBasal: Bool { get }
     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 roundBolus(amount: Decimal) -> Decimal
     var lastError: CurrentValueSubject<Error?, Never> { get }
@@ -130,7 +130,14 @@ final class BaseAPSManager: APSManager, Injectable {
             let wasParsed = storage.parseOnFileSettingsToMgdL()
             if wasParsed {
                 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
     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 {
         isLooping.send(false)
 
@@ -346,14 +381,16 @@ final class BaseAPSManager: APSManager, Injectable {
         return false
     }
 
-    func determineBasal() async -> Bool {
+    func determineBasal() async throws {
         debug(.apsManager, "Start determine basal")
 
         // 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
-        let isValidGlucoseData = await privateContext.perform {
+        let isValidGlucoseData = await privateContext.perform { [weak self] in
+            guard let self else { return false }
+
             guard glucose.count > 2 else {
                 debug(.apsManager, "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 {
             debug(.apsManager, "Glucose validation failed")
             processError(APSError.glucoseError(message: "Glucose validation failed"))
-            return false
+            return
         }
 
         do {
@@ -390,32 +427,30 @@ final class BaseAPSManager: APSManager, Injectable {
             async let autosenseResult = autosense()
 
             _ = try await autosenseResult
-            await openAPS.createProfiles()
+            try await openAPS.createProfiles()
             let determination = try await openAPS.determineBasal(currentTemp: await currentTemp, clock: now)
 
             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) {
                         $0.determinationDidUpdate(determination)
                     }
                 }
-                return true
-            } else {
-                return false
             }
         } 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? {
         do {
-            let temp = await fetchCurrentTempBasal(date: Date.now)
+            let temp = try await fetchCurrentTempBasal(date: Date.now)
             return try await openAPS.determineBasal(
                 currentTemp: temp,
                 clock: Date(),
@@ -447,12 +482,14 @@ final class BaseAPSManager: APSManager, Injectable {
 
         if let error = verifyStatus() {
             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()
                 }
             }
-            callback?(false, "Error! Failed to enact bolus.")
+            callback?(false, String(localized: "Error! Failed to enact bolus.", comment: "Error message for enacting a bolus"))
             return
         }
 
@@ -466,21 +503,26 @@ final class BaseAPSManager: APSManager, Injectable {
             try await pump.enactBolus(units: roundedAmount, automatic: isSMB)
             debug(.apsManager, "Bolus succeeded")
             if !isSMB {
-                await determineBasalSync()
+                try await determineBasalSync()
             }
             bolusProgress.send(0)
-            callback?(true, "Bolus enacted successfully.")
+            callback?(true, String(localized: "Bolus enacted successfully.", comment: "Success message for enacting a bolus"))
         } catch {
             warning(.apsManager, "Bolus failed with error: \(error.localizedDescription)")
             processError(APSError.pumpError(error))
             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()
                     }
                 }
             }
-            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 {
             _ = try await pump.cancelBolus()
             debug(.apsManager, "Bolus cancelled")
-            callback?(true, "Bolus cancelled successfully.")
+            callback?(true, String(localized: "Bolus cancelled successfully.", comment: "Success message for canceling a bolus"))
         } catch {
             debug(.apsManager, "Bolus cancellation failed with error: \(error.localizedDescription)")
             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 = 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,
             onContext: privateContext,
             predicate: NSPredicate.recentPumpHistory,
@@ -568,7 +613,7 @@ final class BaseAPSManager: APSManager, Injectable {
     }
 
     private func enactDetermination() async throws {
-        guard let determinationID = await determinationStorage
+        guard let determinationID = try await determinationStorage
             .fetchLastDeterminationObjectID(predicate: NSPredicate.predicateFor30MinAgoForDetermination).first
         else {
             throw APSError.apsError(message: "Determination not found")
@@ -623,35 +668,39 @@ final class BaseAPSManager: APSManager, Injectable {
     }
 
     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.enacted = wasEnacted
                 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)")
 
                 Task.detached(priority: .low) {
                     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
-    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,
             onContext: privateContext,
             predicate: predicate,
@@ -822,9 +871,9 @@ final class BaseAPSManager: APSManager, Injectable {
             batchSize: batchSize
         )
 
-        return await privateContext.perform {
+        return try await privateContext.perform {
             guard let glucoseResults = results as? [GlucoseStored] else {
-                return []
+                throw CoreDataError.fetchError(function: #function, file: #file)
             }
 
             return glucoseResults
@@ -849,7 +898,7 @@ final class BaseAPSManager: APSManager, Injectable {
             async let carbTotal = carbsForStats()
             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
             guard (-1 * (await lastLoopForStats ?? .distantPast).timeIntervalSinceNow.hours) > 22 else { return }
@@ -919,7 +968,7 @@ final class BaseAPSManager: APSManager, Injectable {
                 scheduled_basal: 0,
                 total_average: 0
             )
-            let processedGlucoseStats = await glucoseStats
+            guard let processedGlucoseStats = await glucoseStats else { return }
             let eA1cDisplayUnit = processedGlucoseStats.eA1cDisplayUnit
 
             let dailystat = await Statistics(
@@ -938,7 +987,7 @@ final class BaseAPSManager: APSManager, Injectable {
                 insulinType: insulin_type.rawValue,
                 peakActivityTime: iPa,
                 Carbs_24h: await carbTotal,
-                GlucoseStorage_Days: Decimal(roundDouble(processedGlucoseStats.numberofDays, 1)),
+                GlucoseStorage_Days: Decimal(roundDouble(Double(rawValue: processedGlucoseStats.numberofDays) ?? 0.0, 1)),
                 Statistics: Stats(
                     Distribution: processedGlucoseStats.TimeInRange,
                     Glucose: processedGlucoseStats.avg,
@@ -1098,175 +1147,153 @@ final class BaseAPSManager: APSManager, Injectable {
         return (currentTDD, tddTotalAverage)
     }
 
-    private func glucoseForStats() async
-        -> (
-            oneDayGlucose: (
-                ifcc: Double,
-                ngsp: Double,
-                average: Double,
-                median: Double,
-                sd: Double,
-                cv: Double,
-                readings: Double
-            ),
-            eA1cDisplayUnit: EstimatedA1cDisplayUnit,
-            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),
+        eA1cDisplayUnit: EstimatedA1cDisplayUnit,
+        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
-            ),
-            eA1cDisplayUnit: EstimatedA1cDisplayUnit,
-            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 eA1cDisplayUnit = self.settingsManager.settings.eA1cDisplayUnit
+
+                let hbs = Durations(
+                    day: eA1cDisplayUnit == .mmolMol ?
+                        self.roundDecimal(Decimal(oneDayGlucose.ifcc), 1) :
+                        self.roundDecimal(Decimal(oneDayGlucose.ngsp), 1),
+                    week: eA1cDisplayUnit == .mmolMol ?
+                        self.roundDecimal(Decimal(sevenDaysGlucose.ifcc), 1) :
+                        self.roundDecimal(Decimal(sevenDaysGlucose.ngsp), 1),
+                    month: eA1cDisplayUnit == .mmolMol ?
+                        self.roundDecimal(Decimal(thirtyDaysGlucose.ifcc), 1) :
+                        self.roundDecimal(Decimal(thirtyDaysGlucose.ngsp), 1),
+                    total: eA1cDisplayUnit == .mmolMol ?
+                        self.roundDecimal(Decimal(totalDaysGlucose.ifcc), 1) :
+                        self.roundDecimal(Decimal(totalDaysGlucose.ngsp), 1)
+                )
 
-            let eA1cDisplayUnit = self.settingsManager.settings.eA1cDisplayUnit
-
-            let hbs = Durations(
-                day: eA1cDisplayUnit == .mmolMol ?
-                    self.roundDecimal(Decimal(oneDayGlucose.ifcc), 1) :
-                    self.roundDecimal(Decimal(oneDayGlucose.ngsp), 1),
-                week: eA1cDisplayUnit == .mmolMol ?
-                    self.roundDecimal(Decimal(sevenDaysGlucose.ifcc), 1) :
-                    self.roundDecimal(Decimal(sevenDaysGlucose.ngsp), 1),
-                month: eA1cDisplayUnit == .mmolMol ?
-                    self.roundDecimal(Decimal(thirtyDaysGlucose.ifcc), 1) :
-                    self.roundDecimal(Decimal(thirtyDaysGlucose.ngsp), 1),
-                total: eA1cDisplayUnit == .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, eA1cDisplayUnit, 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, eA1cDisplayUnit, numberOfDays, TimeInRange, avg, hbs, variance)
+            return nil
         }
-
-        return result!
     }
 
     private func loopStats(loopStatRecord: LoopStats) {

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

@@ -50,7 +50,7 @@ private let staticPumpManagersByIdentifier: [String: PumpManagerUI.Type] = [
 private let accessLock = NSRecursiveLock(label: "BaseDeviceDataManager.accessLock")
 
 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() var alertHistoryStorage: AlertHistoryStorage!
     @Injected() private var storage: FileStorage!
@@ -512,15 +512,21 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
     ) {
         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(

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

@@ -8,8 +8,6 @@ import Swinject
 import UIKit
 
 protocol FetchGlucoseManager: SourceInfoProvider {
-    func updateGlucoseStore(newBloodGlucose: [BloodGlucose])
-    func refreshCGM()
     func updateGlucoseSource(cgmGlucoseSourceType: CGMType, cgmGlucosePluginId: String, newManager: CGMManagerUI?)
     func deleteGlucoseSource()
     func removeCalibrations()
@@ -77,6 +75,44 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         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!
 
     func removeCalibrations() {
@@ -171,32 +207,8 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         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,
             onContext: context,
             predicate: NSPredicate.predicateFor30MinAgo,
@@ -206,9 +218,13 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         ) 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
                 BloodGlucose(
                     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
         let newGlucose = overcalibrate(entries: glucose)
 
@@ -234,37 +250,33 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
 
         // start background time extension
         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 }
             UIApplication.shared.endBackgroundTask(bg)
             backGroundFetchBGTaskID = .invalid
         }
 
-        guard newGlucose.isNotEmpty else {
+        defer {
             if let backgroundTask = backGroundFetchBGTaskID {
-                UIApplication.shared.endBackgroundTask(backgroundTask)
+                Task {
+                    await UIApplication.shared.endBackgroundTask(backgroundTask)
+                }
                 backGroundFetchBGTaskID = .invalid
             }
-            return
         }
 
+        guard newGlucose.isNotEmpty else { return }
+
         filteredByDate = newGlucose.filter { $0.dateString > 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")
 
         // filter the data if it is the case
         if settingsManager.settings.smoothGlucose {
             // limited to 30 min of old glucose data
-            let oldGlucoseValues = processGlucose()
+            let oldGlucoseValues = try await processGlucose()
 
             var smoothedValues = oldGlucoseValues + filtered
             // smooth with 3 repeats
@@ -275,47 +287,8 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
             filtered = smoothedValues.filter { $0.dateString > syncDate }
         }
 
-        glucoseStorage.storeGlucose(filtered)
-
+        try await glucoseStorage.storeGlucose(filtered)
         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]? {

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

@@ -30,48 +30,52 @@ final class BaseFetchTreatmentsManager: FetchTreatmentsManager, Injectable {
                 debug(.nightscout, "Start fetching carbs and temptargets")
 
                 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
-    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,
             onContext: context,
             predicate: NSPredicate.predicateForOneDayAgoInMinutes,
@@ -111,9 +111,9 @@ final class OpenAPS {
             batchSize: 24
         )
 
-        return await context.perform {
+        return try await context.perform {
             guard let glucoseResults = results as? [GlucoseStored] else {
-                return ""
+                throw CoreDataError.fetchError(function: #function, file: #file)
             }
 
             // 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,
             onContext: context,
             predicate: NSPredicate.predicateForOneDayAgo,
@@ -130,9 +130,9 @@ final class OpenAPS {
             ascending: false
         )
 
-        let json = await context.perform {
+        let json = try await context.perform {
             guard let carbResults = results as? [CarbEntryStored] else {
-                return ""
+                throw CoreDataError.fetchError(function: #function, file: #file)
             }
 
             var jsonArray = self.jsonConverter.convertToJSON(carbResults)
@@ -170,8 +170,8 @@ final class OpenAPS {
         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,
             onContext: context,
             predicate: NSPredicate.pumpHistoryLast1440Minutes,
@@ -180,9 +180,9 @@ final class OpenAPS {
             batchSize: 50
         )
 
-        return await context.perform {
+        return try await context.perform {
             guard let pumpEventResults = results as? [PumpEventStored] else {
-                return nil
+                throw CoreDataError.fetchError(function: #function, file: #file)
             }
 
             return pumpEventResults.map(\.objectID)
@@ -302,10 +302,10 @@ final class OpenAPS {
             reservoir,
             preferences
         ) = await (
-            parsePumpHistory(await pumpHistoryObjectIDs, simulatedBolusAmount: simulatedBolusAmount),
-            carbs,
-            glucose,
-            oref2,
+            try parsePumpHistory(await pumpHistoryObjectIDs, simulatedBolusAmount: simulatedBolusAmount),
+            try carbs,
+            try glucose,
+            try oref2,
             profileAsync,
             basalAsync,
             autosenseAsync,
@@ -366,12 +366,12 @@ final class OpenAPS {
 
             return determination
         } 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
             let userPreferences = self.storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self)
             let weightPercentage = userPreferences?.weightPercentage ?? 1.0
@@ -381,10 +381,10 @@ final class OpenAPS {
             // Fetch historical events for Total Daily Dose (TDD) calculation
             let tenDaysAgo = Date().addingTimeInterval(-10.days.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
-            let activeOverrides = self.fetchActiveOverrides()
+            let activeOverrides = try self.fetchActiveOverrides()
             let isOverrideActive = activeOverrides.first?.enabled ?? false
             let overridePercentage = Decimal(activeOverrides.first?.percentage ?? 100)
             let isOverrideIndefinite = activeOverrides.first?.indefinite ?? true
@@ -448,9 +448,9 @@ final class OpenAPS {
 
         // Await the results of asynchronous tasks
         let (pumpHistoryJSON, carbsAsJSON, glucoseAsJSON, profile, basalProfile, tempTargets) = await (
-            parsePumpHistory(await pumpHistoryObjectIDs),
-            carbs,
-            glucose,
+            try parsePumpHistory(await pumpHistoryObjectIDs),
+            try carbs,
+            try glucose,
             getProfile,
             getBasalProfile,
             getTempTargets
@@ -477,7 +477,7 @@ final class OpenAPS {
         }
     }
 
-    func createProfiles() async {
+    func createProfiles() async throws {
         debug(.openAPS, "Start creating pump profile and user profile")
 
         // Load required settings and profiles asynchronously
@@ -507,9 +507,9 @@ final class OpenAPS {
         var adjustedPreferences = preferences
 
         // 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
-            if let activeTempTarget = self.fetchActiveTempTargets().first,
+            if let activeTempTarget = try self.fetchActiveTempTargets().first,
                activeTempTarget.enabled,
                let activeHBT = activeTempTarget.halfBasalTarget?.decimalValue,
                activeHBT != defaultHalfBasalTarget
@@ -555,11 +555,12 @@ final class OpenAPS {
                 .apsManager,
                 "\(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 {
-        await withCheckedContinuation { continuation in
+        try await withCheckedThrowingContinuation { continuation in
             jsWorker.inCommonContext { worker in
                 worker.evaluateBatch(scripts: [
                     Script(name: Prepare.log),
@@ -801,8 +802,8 @@ final class OpenAPS {
 
 // Non-Async fetch methods for oref2
 extension OpenAPS {
-    func fetchActiveTempTargets() -> [TempTargetStored] {
-        CoreDataStack.shared.fetchEntities(
+    func fetchActiveTempTargets() throws -> [TempTargetStored] {
+        try CoreDataStack.shared.fetchEntities(
             ofType: TempTargetStored.self,
             onContext: context,
             predicate: NSPredicate.lastActiveTempTarget,
@@ -812,8 +813,8 @@ extension OpenAPS {
         ) as? [TempTargetStored] ?? []
     }
 
-    func fetchActiveOverrides() -> [OverrideStored] {
-        CoreDataStack.shared.fetchEntities(
+    func fetchActiveOverrides() throws -> [OverrideStored] {
+        try CoreDataStack.shared.fetchEntities(
             ofType: OverrideStored.self,
             onContext: context,
             predicate: NSPredicate.lastActiveOverride,
@@ -823,8 +824,8 @@ extension OpenAPS {
         ) 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,
             onContext: context,
             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 {
     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 syncDate() -> Date
     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 {
@@ -38,11 +38,11 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         injectServices(resolver)
     }
 
-    func storeCarbs(_ entries: [CarbsEntry], areFetchedFromRemote: Bool) async {
+    func storeCarbs(_ entries: [CarbsEntry], areFetchedFromRemote: Bool) async throws {
         var entriesToStore = entries
 
         if areFetchedFromRemote {
-            entriesToStore = await filterRemoteEntries(entries: entriesToStore)
+            entriesToStore = try await filterRemoteEntries(entries: entriesToStore)
         }
 
         // Check for FPU-only entries (fat/protein without carbs)
@@ -71,9 +71,9 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         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
-        guard let existing24hCarbEntries = await CoreDataStack.shared.fetchEntitiesAsync(
+        guard let existing24hCarbEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
             onContext: coredataContext,
             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,
             onContext: coredataContext,
             predicate: NSPredicate.carbsNotYetUploadedToNightscout,
@@ -348,9 +348,9 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             ascending: false
         )
 
-        return await coredataContext.perform {
+        return try await coredataContext.perform {
             guard let carbEntries = results as? [CarbEntryStored] else {
-                return []
+                throw CoreDataError.fetchError(function: #function, file: #file)
             }
 
             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,
             onContext: coredataContext,
             predicate: NSPredicate.fpusNotYetUploadedToNightscout,
@@ -387,8 +387,10 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             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
                 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,
             onContext: coredataContext,
             predicate: NSPredicate.carbsNotYetUploadedToHealth,
@@ -424,11 +426,11 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             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
                 CarbsEntry(
                     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,
             onContext: coredataContext,
             predicate: NSPredicate.carbsNotYetUploadedToTidepool,
@@ -455,11 +457,11 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             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
                 CarbsEntry(
                     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.
     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
 
 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 getForecastValueIDs(for forecastID: NSManagedObjectID, in context: NSManagedObjectContext) async -> [NSManagedObjectID]
     func fetchForecastObjects(
@@ -12,29 +12,32 @@ protocol DeterminationStorage {
         in context: NSManagedObjectContext
     ) async -> (UUID, Forecast?, [ForecastValue])
     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 {
     private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
-    private let backgroundContext = CoreDataStack.shared.newTaskContext()
+    private let context = CoreDataStack.shared.newTaskContext()
 
     init(resolver: 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,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: predicate,
             key: "deliverAt",
             ascending: false,
             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)
         }
     }
@@ -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(
         for data: (id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID]),
         in context: NSManagedObjectContext
@@ -112,19 +115,19 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
 
     // Convert NSSet to array of Ints for Predictions
     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] = []
 
         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
                     if forecast.type == type {
                         let forecastValueIDs = forecast.forecastValues?.sorted(by: { $0.index < $1.index }).map(\.objectID) ?? []
 
                         for forecastValueID in forecastValueIDs {
-                            if let forecastValue = try? self.backgroundContext
+                            if let forecastValue = try? self.context
                                 .existingObject(with: forecastValueID) as? ForecastValue
                             {
                                 let forecastValueInt = Int(forecastValue.value)
@@ -153,9 +156,9 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
             uam: await parseForecastValues(ofType: "uam", from: determinationId)
         )
 
-        return await backgroundContext.perform {
+        return await context.perform {
             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
                 if let orefDetermination = orefDetermination {
@@ -201,4 +204,35 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
             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 {
     var updatePublisher: AnyPublisher<Void, Never> { get }
-    func storeGlucose(_ glucose: [BloodGlucose])
+    func storeGlucose(_ glucose: [BloodGlucose]) async throws
     func addManualGlucose(glucose: Int)
     func isGlucoseDataFresh(_ glucoseDate: Date?) -> Bool
     func syncDate() -> Date
     func filterTooFrequentGlucose(_ glucose: [BloodGlucose], at: Date) -> [BloodGlucose]
     func lastGlucoseDate() -> Date
     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 }
     func deleteGlucose(_ treatmentObjectID: NSManagedObjectID) async
 }
@@ -60,120 +60,163 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
         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) {
         coredataContext.perform {
             let newItem = GlucoseStored(context: self.coredataContext)
@@ -190,7 +233,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                 try self.coredataContext.save()
 
                 // 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 {
                 debugPrint(
                     "\(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 {
-        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.sortDescriptors = [NSSortDescriptor(keyPath: \GlucoseStored.date, ascending: false)]
+        fr.propertiesToFetch = ["date"]
         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 {
-                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 {
@@ -262,9 +313,9 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
         return filtered
     }
 
-    func fetchLatestGlucose() -> GlucoseStored? {
+    func fetchLatestGlucose() throws -> GlucoseStored? {
         let predicate = NSPredicate.predicateFor20MinAgo
-        return (CoreDataStack.shared.fetchEntities(
+        return (try CoreDataStack.shared.fetchEntities(
             ofType: GlucoseStored.self,
             onContext: coredataContext,
             predicate: predicate,
@@ -276,8 +327,8 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
     // Fetch glucose that is not uploaded to Nightscout yet
     /// - 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,
             onContext: coredataContext,
             predicate: NSPredicate.glucoseNotYetUploadedToNightscout,
@@ -285,8 +336,10 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             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
                 BloodGlucose(
@@ -307,8 +360,8 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
     // Fetch manual glucose that is not uploaded to Nightscout yet
     /// - 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,
             onContext: coredataContext,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToNightscout,
@@ -316,9 +369,11 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             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
                 NightscoutTreatment(
                     duration: nil,
@@ -361,8 +416,8 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
     // Fetch glucose that is not uploaded to Nightscout yet
     /// - 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,
             onContext: coredataContext,
             predicate: NSPredicate.glucoseNotYetUploadedToHealth,
@@ -370,9 +425,11 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             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
                 BloodGlucose(
                     _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
     /// - 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,
             onContext: coredataContext,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToHealth,
@@ -400,9 +457,11 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             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
                 BloodGlucose(
                     _id: result.id?.uuidString ?? UUID().uuidString,
@@ -421,8 +480,8 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
     // Fetch glucose that is not uploaded to Tidepool yet
     /// - 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,
             onContext: coredataContext,
             predicate: NSPredicate.glucoseNotYetUploadedToTidepool,
@@ -430,9 +489,11 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             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
                 BloodGlucose(
                     _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
     /// - 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,
             onContext: coredataContext,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToTidepool,
@@ -461,9 +522,11 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             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
                 BloodGlucose(
                     _id: result.id?.uuidString ?? UUID().uuidString,
@@ -510,19 +573,24 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     var alarm: GlucoseAlarm? {
         /// glucose can not be older than 20 minutes due to the predicate in the fetch request
         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
 
 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 storeOverride(override: Override) async
+    func storeOverride(override: Override) async throws
     func copyRunningOverride(_ override: OverrideStored) async -> NSManagedObjectID
     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 {
@@ -34,8 +34,8 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
         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,
             onContext: backgroundContext,
             predicate: NSPredicate(
@@ -47,15 +47,17 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
             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)
         }
     }
 
-    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,
             onContext: backgroundContext,
             predicate: NSPredicate.lastActiveOverride,
@@ -64,16 +66,18 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
             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)
         }
     }
 
     /// 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,
             onContext: backgroundContext,
             predicate: NSPredicate.allOverridePresets,
@@ -81,8 +85,10 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
             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)
         }
@@ -95,14 +101,14 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
         return overrideTarget.decimalValue
     }
 
-    func storeOverride(override: Override) async {
+    func storeOverride(override: Override) async throws {
         var presetCount = -1
         if override.isPreset {
-            let presets = await fetchForOverridePresets()
+            let presets = try await fetchForOverridePresets()
             presetCount = presets.count
         }
 
-        await backgroundContext.perform {
+        try await backgroundContext.perform {
             let newOverride = OverrideStored(context: self.backgroundContext)
 
             // override key meta data
@@ -151,14 +157,8 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
                 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)
     }
 
-    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,
             onContext: backgroundContext,
             predicate: NSPredicate.lastActiveAdjustmentNotYetUploadedToNightscout,
@@ -215,8 +215,10 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
             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
                 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,
             onContext: backgroundContext,
             predicate: NSPredicate(
@@ -245,8 +247,10 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
             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
                 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,
             onContext: backgroundContext,
             predicate: NSPredicate.allOverridePresets,
@@ -272,8 +276,10 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
             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
                 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,
             onContext: backgroundContext,
             predicate: NSPredicate.lastActiveOverride,
@@ -300,10 +306,12 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
             fetchLimit: 1
         )
 
-        return await backgroundContext.perform {
+        return try await backgroundContext.perform {
             guard let fetchedResults = results as? [OverrideStored],
                   let latestOverride = fetchedResults.first
-            else { return nil }
+            else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
 
             return latestOverride.objectID
         }

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

@@ -11,13 +11,11 @@ protocol PumpHistoryObserver {
 
 protocol PumpHistoryStorage {
     var updatePublisher: AnyPublisher<Void, Never> { get }
-    func storePumpEvents(_ events: [NewPumpEvent])
+    func storePumpEvents(_ events: [NewPumpEvent]) async throws
     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 {
@@ -27,6 +25,7 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
     @Injected() private var settings: SettingsManager!
 
     private let updateSubject = PassthroughSubject<Void, Never>()
+    private let context = CoreDataStack.shared.newTaskContext()
 
     var updatePublisher: AnyPublisher<Void, Never> {
         updateSubject.eraseToAnyPublisher()
@@ -39,190 +38,184 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
     typealias PumpEvent = PumpEventStored.EventType
     typealias TempType = PumpEventStored.TempType
 
-    private let context = CoreDataStack.shared.newTaskContext()
-
     private func roundDose(_ dose: Double, toIncrement increment: Double) -> Decimal {
         let roundedValue = (dose / increment).rounded() * increment
         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
                     }
+                    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 {
         if event.bolus!.isSMB {
             return .smb
@@ -286,8 +261,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
         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,
             onContext: context,
             predicate: NSPredicate.pumpEventsNotYetUploadedToNightscout,
@@ -295,8 +270,10 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
             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
                 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,
             onContext: context,
             predicate: NSPredicate.pumpEventsNotYetUploadedToHealth,
@@ -454,10 +431,12 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
             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 {
                 case PumpEvent.bolus.rawValue:
                     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,
             onContext: context,
             predicate: NSPredicate.pumpEventsNotYetUploadedToTidepool,
@@ -496,10 +475,12 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
             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 {
                 case PumpEvent.bolus.rawValue:
                     return PumpHistoryEvent(

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

@@ -8,18 +8,18 @@ protocol TempTargetsObserver {
 }
 
 protocol TempTargetsStorage {
-    func storeTempTarget(tempTarget: TempTarget) async
+    func storeTempTarget(tempTarget: TempTarget) async throws
     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 deleteTempTargetPreset(_ objectID: NSManagedObjectID) async
-    func loadLatestTempTargetConfigurations(fetchLimit: Int) async -> [NSManagedObjectID]
+    func loadLatestTempTargetConfigurations(fetchLimit: Int) async throws -> [NSManagedObjectID]
     func syncDate() -> Date
     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 current() -> TempTarget?
     func existsTempTarget(with date: Date) async -> Bool
@@ -38,8 +38,8 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
         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,
             onContext: backgroundContext,
             predicate: NSPredicate.lastActiveTempTarget,
@@ -48,16 +48,18 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
             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)
         }
     }
 
     /// 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,
             onContext: backgroundContext,
             predicate: NSPredicate.allTempTargetPresets,
@@ -65,17 +67,19 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
             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)
         }
     }
 
-    func fetchScheduledTempTargets() async -> [NSManagedObjectID] {
+    func fetchScheduledTempTargets() async throws -> [NSManagedObjectID] {
         let scheduledTempTargets = NSPredicate(format: "date > %@", Date() as NSDate)
 
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetStored.self,
             onContext: backgroundContext,
             predicate: scheduledTempTargets,
@@ -83,17 +87,19 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
             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)
         }
     }
 
-    func fetchScheduledTempTarget(for targetDate: Date) async -> [NSManagedObjectID] {
+    func fetchScheduledTempTarget(for targetDate: Date) async throws -> [NSManagedObjectID] {
         let predicate = NSPredicate(format: "date == %@", targetDate as NSDate)
 
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetStored.self,
             onContext: backgroundContext,
             predicate: predicate,
@@ -102,21 +108,23 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
             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
         if tempTarget.isPreset == true {
-            let presets = await fetchForTempTargetPresets()
+            let presets = try await fetchForTempTargetPresets()
             presetCount = presets.count
         }
 
-        await backgroundContext.perform {
+        try await backgroundContext.perform {
             let newTempTarget = TempTargetStored(context: self.backgroundContext)
             newTempTarget.date = tempTarget.createdAt
             newTempTarget.id = UUID()
@@ -144,9 +152,8 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
                 guard self.backgroundContext.hasChanges else { return }
                 try self.backgroundContext.save()
             } 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
     }
 
-    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,
             onContext: backgroundContext,
             predicate: NSPredicate.lastActiveAdjustmentNotYetUploadedToNightscout,
@@ -251,8 +258,10 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
             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
                 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,
             onContext: backgroundContext,
             predicate: NSPredicate(
@@ -290,8 +299,10 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
             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
                 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)
 
             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 {
             debug(.remoteControl, "Error decoding push message: \(error.localizedDescription)")
@@ -40,7 +47,14 @@ class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject, UNUserNoti
         let token = tokenParts.joined()
 
         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

@@ -71,11 +71,11 @@ import Swinject
         // Setup up the Core Data Stack
         coreDataStack = CoreDataStack.shared
 
+        // Explicitly initialize Core Data Stack
         do {
-            // Explicitly initialize Core Data Stacak
             try coreDataStack.initializeStack()
 
-            // Load services
+            // Only load services after successful Core Data initialization
             loadServices()
 
             // Fix bug in iOS 18 related to the translucent tab bar
@@ -84,12 +84,9 @@ import Swinject
             // Clear the persistentHistory and the NSManagedObjects that are older than 90 days every time the app starts
             cleanupOldData()
         } 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")
         }
     }
 
@@ -109,6 +106,14 @@ import Swinject
             if newScenePhase == .background {
                 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")) {
             await scheduleDatabaseCleaning()
@@ -142,9 +147,9 @@ import Swinject
         request.earliestBeginDate = .now.addingTimeInterval(7 * 24 * 60 * 60) // 7 days
         do {
             try BGTaskScheduler.shared.submit(request)
-            debugPrint("Task scheduled successfully")
+            debug(.coreData, "Task for cleaning database scheduled successfully")
         } 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 {
-        units == .mgdL ? 30 : 1.66
+        units == .mgdL ? 20 : (20 / 18)
     }
 
     static func calculateDuration(

Fichier diff supprimé car celui-ci est trop grand
+ 503 - 494
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 watchManager = Logger(category: .watchManager, reporter: baseReporter)
     static let coreData = Logger(category: .coreData, reporter: baseReporter)
+    static let storage = Logger(category: .storage, reporter: baseReporter)
 
     enum Category: String {
         case `default`
@@ -129,6 +130,7 @@ final class Logger {
         case bolusState
         case watchManager
         case coreData
+        case storage
 
         var name: String {
             rawValue.capitalizingFirstLetter()
@@ -147,6 +149,7 @@ final class Logger {
             case .bolusState: return .bolusState
             case .watchManager: return .watchManager
             case .coreData: return .coreData
+            case .storage: return .storage
             }
         }
 
@@ -163,6 +166,7 @@ final class Logger {
                  .openAPS,
                  .remoteControl,
                  .service,
+                 .storage,
                  .watchManager:
                 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.
     @MainActor func enactOverridePreset(withID id: NSManagedObjectID) async {
         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()
 
+            overrideToEnact.enabled = true
+            overrideToEnact.date = Date()
+            overrideToEnact.isUploadedToNS = false
+            isOverrideEnabled = true
+
             guard viewContext.hasChanges else { return }
             try viewContext.save()
 
@@ -29,19 +30,22 @@ extension Adjustments.StateModel {
     // MARK: - Disable Overrides
 
     /// 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
                 let results = try ids.compactMap { id in
                     try self.viewContext.existingObject(with: id) as? OverrideStored
                 }
                 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 {
                     // Use the first override to create a new OverrideRunStored entry
                     if let canceledOverride = results.first {
@@ -50,8 +54,9 @@ extension Adjustments.StateModel {
                         newOverrideRunStored.name = canceledOverride.name
                         newOverrideRunStored.startDate = canceledOverride.date ?? .distantPast
                         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.isUploadedToNS = false
                     }
@@ -67,11 +72,12 @@ extension Adjustments.StateModel {
                     try self.viewContext.save()
                     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.
     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.
     /// `enabled` has to be false
     /// `isPreset` has to be true
     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
@@ -154,8 +175,15 @@ extension Adjustments.StateModel {
     /// Sets up the array of Override Presets for UI display.
     func setupOverridePresetsArray() {
         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.
     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
@@ -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
     func updateLatestOverrideConfiguration() {
         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
     func updateLatestTempTargetConfiguration() {
         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.
     func setupTempTargets(
-        fetchFunction: @escaping () async -> [NSManagedObjectID],
+        fetchFunction: @escaping () async throws -> [NSManagedObjectID],
         updateFunction: @escaping @MainActor([TempTargetStored]) -> Void
     ) {
         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.
     func setupTempTargetPresetsArray() {
         setupTempTargets(
-            fetchFunction: tempTargetStorage.fetchForTempTargetPresets,
+            fetchFunction: { try await self.tempTargetStorage.fetchForTempTargetPresets() },
             updateFunction: { tempTargets in
                 self.tempTargetPresets = tempTargets
             }
@@ -93,7 +107,7 @@ extension Adjustments.StateModel {
     /// Sets up the scheduled Temp Targets array for the view.
     func setupScheduledTempTargetsArray() {
         setupTempTargets(
-            fetchFunction: tempTargetStorage.fetchScheduledTempTargets,
+            fetchFunction: { try await self.tempTargetStorage.fetchScheduledTempTargets() },
             updateFunction: { tempTargets in
                 self.scheduledTempTargets = tempTargets
             }
@@ -108,16 +122,16 @@ extension Adjustments.StateModel {
     }
 
     /// Saves a Temp Target based on whether it is scheduled or custom.
-    func invokeSaveOfCustomTempTargets() async {
+    func invokeSaveOfCustomTempTargets() async throws {
         if date > Date() {
-            await saveScheduledTempTarget()
+            try await saveScheduledTempTarget()
         } else {
-            await saveCustomTempTarget()
+            try await saveCustomTempTarget()
         }
     }
 
     /// Saves a scheduled Temp Target and activates it at the specified date.
-    func saveScheduledTempTarget() async {
+    func saveScheduledTempTarget() async throws {
         let date = self.date
         guard date > Date() else { return }
 
@@ -133,38 +147,45 @@ extension Adjustments.StateModel {
             enabled: false,
             halfBasalTarget: halfBasalTarget
         )
-        await tempTargetStorage.storeTempTarget(tempTarget: tempTarget)
+        try await tempTargetStorage.storeTempTarget(tempTarget: tempTarget)
         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.
     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.
@@ -177,7 +198,7 @@ extension Adjustments.StateModel {
     }
 
     /// Saves a custom Temp Target and disables existing ones.
-    func saveCustomTempTarget() async {
+    func saveCustomTempTarget() async throws {
         await disableAllActiveTempTargets(createTempTargetRunEntry: true)
         let tempTarget = TempTarget(
             name: tempTargetName,
@@ -192,7 +213,7 @@ extension Adjustments.StateModel {
             enabled: true,
             halfBasalTarget: halfBasalTarget
         )
-        await tempTargetStorage.storeTempTarget(tempTarget: tempTarget)
+        try await tempTargetStorage.storeTempTarget(tempTarget: tempTarget)
         tempTargetStorage.saveTempTargetsToStorage([tempTarget])
         await resetTempTargetState()
         isTempTargetEnabled = true
@@ -200,7 +221,7 @@ extension Adjustments.StateModel {
     }
 
     /// Creates a new Temp Target preset.
-    func saveTempTargetPreset() async {
+    func saveTempTargetPreset() async throws {
         let tempTarget = TempTarget(
             name: tempTargetName,
             createdAt: Date(),
@@ -213,7 +234,7 @@ extension Adjustments.StateModel {
             enabled: false,
             halfBasalTarget: halfBasalTarget
         )
-        await tempTargetStorage.storeTempTarget(tempTarget: tempTarget)
+        try await tempTargetStorage.storeTempTarget(tempTarget: tempTarget)
         await resetTempTargetState()
         setupTempTargetPresetsArray()
     }
@@ -221,19 +242,15 @@ extension Adjustments.StateModel {
     /// Enacts a Temp Target preset by enabling it.
     @MainActor func enactTempTargetPreset(withID id: NSManagedObjectID) async {
         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 {
                 try viewContext.save()
             }
@@ -241,11 +258,11 @@ extension Adjustments.StateModel {
             updateLatestTempTargetConfiguration()
 
             let tempTarget = TempTarget(
-                name: tempTargetToEnact?.name,
+                name: tempTargetToEnact.name,
                 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,
                 reason: TempTarget.custom,
                 isPreset: true,
@@ -259,12 +276,15 @@ extension Adjustments.StateModel {
     }
 
     /// 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
                 let results = try ids.compactMap { id in
                     try self.viewContext.existingObject(with: id) as? TempTargetStored
@@ -273,7 +293,7 @@ extension Adjustments.StateModel {
                 // If there are no results, return early
                 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 {
                     // Use the first temp target to create a new TempTargetRunStored entry
                     if let canceledTempTarget = results.first {
@@ -282,8 +302,7 @@ extension Adjustments.StateModel {
                         newTempTargetRunStored.name = canceledTempTarget.name
                         newTempTargetRunStored.startDate = canceledTempTarget.date ?? .distantPast
                         newTempTargetRunStored.endDate = Date()
-                        newTempTargetRunStored
-                            .target = canceledTempTarget.target ?? 0
+                        newTempTargetRunStored.target = canceledTempTarget.target ?? 0
                         newTempTargetRunStored.tempTarget = canceledTempTarget
                         newTempTargetRunStored.isUploadedToNS = false
                     }
@@ -303,11 +322,12 @@ extension Adjustments.StateModel {
                     // Update the storage
                     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() {
                 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: {
                     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: {
                     Text("Save Override")

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

@@ -249,10 +249,14 @@ struct AddTempTargetForm: View {
                 content: {
                     Button(action: {
                         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: {
                         Text("Start Temp Target")
@@ -266,10 +270,14 @@ struct AddTempTargetForm: View {
             Section {
                 Button(action: {
                     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: {
                     Text("Save as Preset")

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

@@ -312,9 +312,9 @@ struct EditTempTargetForm: View {
                         Task {
                             // TODO: - Creating a Run entry is probably needed for Overrides as well and the reason for "jumping" Overrides?
                             // Disable previous active Temp Targets
-                            await state.disableAllActiveOverrides(
+                            await state.disableAllActiveTempTargets(
                                 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

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

@@ -66,8 +66,15 @@ extension AlgorithmAdvancedSettings {
                         self.insulinActionCurve = settings.insulinActionCurve
 
                         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: {}
                     .store(in: &lifetime)

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

@@ -41,10 +41,17 @@ extension AutosensSettings {
 
         private func setupDeterminationsArray() {
             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) }
 
                         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:
                         // 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.
-        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,
                 onContext: backgroundContext,
                 predicate: NSPredicate.predicateFor20MinAgo,
@@ -46,9 +46,9 @@ extension Calibrations {
                 fetchLimit: 1 /// We only need the last value
             )
 
-            return await backgroundContext.perform {
+            return try await backgroundContext.perform {
                 guard let glucoseResults = results as? [GlucoseStored] else {
-                    return []
+                    throw CoreDataError.fetchError(function: #function, file: #file)
                 }
 
                 return glucoseResults.map(\.objectID)
@@ -56,28 +56,32 @@ extension Calibrations {
         }
 
         @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)
             initialItems = items.map { Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
             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
         func invokeCarbDeletionTask(_ treatmentObjectID: NSManagedObjectID, isFpuOrComplexMeal: Bool = false) {
             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
             await deleteFromServices(treatmentObjectID, isFPUDeletion: isFpuOrComplexMeal)
 
@@ -122,7 +126,7 @@ extension DataTable {
             await carbsStorage.deleteCarbsEntryStored(treatmentObjectID)
 
             // 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)
@@ -208,16 +212,20 @@ extension DataTable {
         /// - **Parameter**: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
         func invokeInsulinDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
             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 {
                 let authenticated = try await unlockmanager.unlock()
 
@@ -233,7 +241,7 @@ extension DataTable {
                 await CoreDataStack.shared.deleteObject(identifiedBy: treatmentObjectID)
 
                 // Perform a determine basal sync to update iob
-                await apsManager.determineBasalSync()
+                try await apsManager.determineBasalSync()
             } catch {
                 debugPrint(
                     "\(DebuggingIdentifiers.failed) \(#file) \(#function) Error while Insulin Deletion Task: \(error.localizedDescription)"
@@ -291,30 +299,36 @@ extension DataTable {
             newDate: Date
         ) {
             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,
             newProtein: Decimal,
             newNote: String
-        ) async {
+        ) async throws {
             let newEntry = CarbsEntry(
                 id: UUID().uuidString,
                 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
-            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
@@ -360,24 +374,24 @@ extension DataTable {
             newFat _: Decimal,
             newProtein _: Decimal,
             newNote _: String
-        ) async {
+        ) async throws {
             if ((originalEntry.entryValues?.carbs ?? 0) == 0 && (originalEntry.entryValues?.fat ?? 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
                 // 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) ||
                 ((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
                 // Use fpuID
-                await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: true)
+                try await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: true)
 
             } else {
                 // Delete just the carb entry since there are no carb equivalents
                 // 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)
                     }
                 } else {
-                    HStack {
-                        Text("No data.")
-                    }
+                    ContentUnavailableView(
+                        "No data.",
+                        systemImage: "injection.needle"
+                    )
                 }
             }.listRowBackground(Color.chart)
         }
@@ -197,9 +198,10 @@ extension DataTable {
                         mealView(item)
                     }
                 } else {
-                    HStack {
-                        Text("No data.")
-                    }
+                    ContentUnavailableView(
+                        "No data.",
+                        systemImage: "fork.knife"
+                    )
                 }
             }.listRowBackground(Color.chart)
         }
@@ -215,9 +217,10 @@ extension DataTable {
                         adjustmentView(for: item)
                     }
                 } else {
-                    HStack {
-                        Text("No data.")
-                    }
+                    ContentUnavailableView(
+                        "No data.",
+                        systemImage: "clock.arrow.2.circlepath"
+                    )
                 }
             }
             .listRowBackground(Color.chart)
@@ -247,8 +250,12 @@ extension DataTable {
             }
 
             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 {
             let id: NSManagedObjectID
@@ -384,9 +391,10 @@ extension DataTable {
                         }
                     }
                 } else {
-                    HStack {
-                        Text("No data.")
-                    }
+                    ContentUnavailableView(
+                        "No data.",
+                        systemImage: "drop.fill"
+                    )
                 }
             }.listRowBackground(Color.chart)
                 .alert(isPresented: $showAlert) {

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

@@ -4,14 +4,22 @@ import Foundation
 extension Home.StateModel {
     func setupBatteryArray() {
         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,
             onContext: batteryFetchContext,
             predicate: NSPredicate.predicateFor30MinAgo,
@@ -19,9 +27,10 @@ extension Home.StateModel {
             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)
         }
     }

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

@@ -4,14 +4,19 @@ import Foundation
 extension Home.StateModel {
     func setupCarbsArray() {
         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,
             onContext: carbsFetchContext,
             predicate: NSPredicate.carbsForChart,
@@ -20,8 +25,10 @@ extension Home.StateModel {
             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)
         }
@@ -33,14 +40,19 @@ extension Home.StateModel {
 
     func setupFPUsArray() {
         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,
             onContext: fpuFetchContext,
             predicate: NSPredicate.fpusForChart,
@@ -48,8 +60,10 @@ extension Home.StateModel {
             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)
         }

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

@@ -4,28 +4,34 @@ import Foundation
 extension Home.StateModel {
     func setupDeterminationsArray() {
         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(
         with IDs: [NSManagedObjectID],
         keyPath: ReferenceWritableKeyPath<Home.StateModel, [OrefDetermination]>
-    ) async {
+    ) async throws {
         // 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)
 
         // 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
-    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,
             onContext: determinationFetchContext,
             predicate: NSPredicate.determinationsForCobIobCharts,
@@ -44,9 +50,9 @@ extension Home.StateModel {
             propertiesToFetch: ["cob", "iob", "deliverAt", "objectID"]
         )
 
-        return await determinationFetchContext.perform {
+        return try await determinationFetchContext.perform {
             guard let fetchedResults = results as? [[String: Any]] else {
-                return []
+                throw CoreDataError.fetchError(function: #function, file: #file)
             }
 
             // Update Chart Scales

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

@@ -3,108 +3,86 @@ import Foundation
 
 extension Home.StateModel {
     // 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
     @MainActor func updateForecastData() async {
-        // Preprocess forecast data on a background thread
         let forecastDataIDs = await preprocessForecastData()
 
-        // Use an Array of Int instead of ForecastValues to be able to pass values thread safe
         var allForecastValues = [[Int]]()
         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)
-                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
 
-        // Ensure there are forecast values to process
         guard !allForecastValues.isEmpty else {
             minForecast = []
             maxForecast = []
             return
         }
 
-        // Update minCount on the Main Thread
         minCount = max(12, allForecastValues.map(\.count).min() ?? 0)
-
-        // Safely read minCount for use inside the detached task
         let localMinCount = minCount
 
         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 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
-                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)
         }.value
 
-        // Update the properties on the main thread
         minForecast = minResult
         maxForecast = maxResult
     }

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

@@ -4,14 +4,22 @@ import Foundation
 extension Home.StateModel {
     func setupGlucoseArray() {
         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,
             onContext: glucoseFetchContext,
             predicate: NSPredicate.glucose,
@@ -20,8 +28,10 @@ extension Home.StateModel {
             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
             // 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
     func setupOverrides() {
         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,
             onContext: overrideFetchContext,
             predicate: NSPredicate.lastActiveOverride, // this predicate filters for all Overrides within the last 24h
@@ -20,9 +27,10 @@ extension Home.StateModel {
             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)
         }
     }
@@ -41,16 +49,22 @@ extension Home.StateModel {
     // Setup expired Overrides
     func setupOverrideRunStored() {
         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 results = await CoreDataStack.shared.fetchEntitiesAsync(
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideRunStored.self,
             onContext: overrideFetchContext,
             predicate: predicate,
@@ -58,9 +72,10 @@ extension Home.StateModel {
             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)
         }
     }

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

@@ -4,14 +4,22 @@ import Foundation
 extension Home.StateModel {
     func setupInsulinArray() {
         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,
             onContext: pumpHistoryFetchContext,
             predicate: NSPredicate.pumpHistoryLast24h,
@@ -20,9 +28,9 @@ extension Home.StateModel {
             batchSize: 30
         )
 
-        return await pumpHistoryFetchContext.perform {
+        return try await pumpHistoryFetchContext.perform {
             guard let pumpEvents = results as? [PumpEventStored] else {
-                return []
+                throw CoreDataError.fetchError(function: #function, file: #file)
             }
 
             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
     func setupLastBolus() {
         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,
             onContext: pumpHistoryFetchContext,
             predicate: NSPredicate.lastPumpBolus,
@@ -63,8 +78,10 @@ extension Home.StateModel {
             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
         }

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

@@ -4,15 +4,22 @@ import Foundation
 extension Home.StateModel {
     func setupTempTargetsStored() {
         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,
             onContext: tempTargetFetchContext,
             predicate: NSPredicate.lastActiveTempTarget,
@@ -20,8 +27,10 @@ extension Home.StateModel {
             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)
         }
     }
@@ -33,16 +42,23 @@ extension Home.StateModel {
     // Setup expired TempTargets
     func setupTempTargetsRunStored() {
         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 results = await CoreDataStack.shared.fetchEntitiesAsync(
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetRunStored.self,
             onContext: tempTargetFetchContext,
             predicate: predicate,
@@ -50,8 +66,10 @@ extension Home.StateModel {
             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)
         }
     }

+ 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
         private func registerSubscribers() {
             glucoseStorage.updatePublisher
-                .receive(on: DispatchQueue.global(qos: .background))
+                .receive(on: queue)
                 .sink { [weak self] _ in
                     guard let self = self else { return }
                     self.setupGlucoseArray()
@@ -223,7 +223,7 @@ extension Home {
                 .store(in: &subscriptions)
 
             carbsStorage.updatePublisher
-                .receive(on: DispatchQueue.global(qos: .background))
+                .receive(on: queue)
                 .sink { [weak self] _ in
                     guard let self = self else { return }
                     self.setupFPUsArray()
@@ -232,22 +232,22 @@ extension Home {
         }
 
         private func registerHandlers() {
-            coreDataPublisher?.filterByEntityName("OrefDetermination").sink { [weak self] _ in
+            coreDataPublisher?.filteredByEntityName("OrefDetermination").sink { [weak self] _ in
                 guard let self = self else { return }
                 self.setupDeterminationsArray()
             }.store(in: &subscriptions)
 
-            coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
+            coreDataPublisher?.filteredByEntityName("GlucoseStored").sink { [weak self] _ in
                 guard let self = self else { return }
                 self.setupGlucoseArray()
             }.store(in: &subscriptions)
 
-            coreDataPublisher?.filterByEntityName("CarbEntryStored").sink { [weak self] _ in
+            coreDataPublisher?.filteredByEntityName("CarbEntryStored").sink { [weak self] _ in
                 guard let self = self else { return }
                 self.setupCarbsArray()
             }.store(in: &subscriptions)
 
-            coreDataPublisher?.filterByEntityName("PumpEventStored").sink { [weak self] _ in
+            coreDataPublisher?.filteredByEntityName("PumpEventStored").sink { [weak self] _ in
                 guard let self = self else { return }
                 self.setupInsulinArray()
                 self.setupLastBolus()
@@ -255,27 +255,27 @@ extension Home {
                 self.displayPumpStatusBadge()
             }.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 }
                 self.setupBatteryArray()
             }.store(in: &subscriptions)
 
-            coreDataPublisher?.filterByEntityName("OverrideStored").sink { [weak self] _ in
+            coreDataPublisher?.filteredByEntityName("OverrideStored").sink { [weak self] _ in
                 guard let self = self else { return }
                 self.setupOverrides()
             }.store(in: &subscriptions)
 
-            coreDataPublisher?.filterByEntityName("OverrideRunStored").sink { [weak self] _ in
+            coreDataPublisher?.filteredByEntityName("OverrideRunStored").sink { [weak self] _ in
                 guard let self = self else { return }
                 self.setupOverrideRunStored()
             }.store(in: &subscriptions)
 
-            coreDataPublisher?.filterByEntityName("TempTargetStored").sink { [weak self] _ in
+            coreDataPublisher?.filteredByEntityName("TempTargetStored").sink { [weak self] _ in
                 guard let self = self else { return }
                 self.setupTempTargetsStored()
             }.store(in: &subscriptions)
 
-            coreDataPublisher?.filterByEntityName("TempTargetRunStored").sink { [weak self] _ in
+            coreDataPublisher?.filteredByEntityName("TempTargetRunStored").sink { [weak self] _ in
                 guard let self = self else { return }
                 self.setupTempTargetsRunStored()
             }.store(in: &subscriptions)
@@ -512,7 +512,7 @@ extension Home {
                 await apsManager.cancelBolus(nil)
 
                 // 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(
                 position: .top,
                 alignment: .center,
-                overflowResolution: .init(x: .fit(to: .chart), y: .fit(to: .chart))
+                overflowResolution: .init(x: .fit(to: .chart), y: .disabled)
             ) {
                 selectionPopover
             }
@@ -67,7 +67,7 @@ struct SelectionPopoverView: ChartContent {
                 Text(selectedGlucose.date?.formatted(.dateTime.hour().minute(.twoDigits)) ?? "")
                     .font(.body).bold()
             }
-            .font(.body).padding(.bottom, 5)
+            .font(.body).padding(.bottom, 2)
 
             HStack {
                 Text(glucoseToDisplay.description).bold() + Text(" \(units.rawValue)")
@@ -95,7 +95,8 @@ struct SelectionPopoverView: ChartContent {
                 .foregroundStyle(Color.orange).font(.body)
             }
         }
-        .padding()
+        .padding(.horizontal)
+        .padding(.vertical, 2)
         .background {
             RoundedRectangle(cornerRadius: 4)
                 .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 bolusString =
                     (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")
                         + String(localized: " U", comment: "Insulin unit")
 
@@ -1099,7 +1099,7 @@ extension Home {
                 tabBar()
 
                 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) }
 
             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 {
                         debug(.nightscout, "Upload has been enabled by the user.")
                         Task {
-                            await self.nightscoutManager.uploadProfiles()
+                            do {
+                                try await self.nightscoutManager.uploadProfiles()
+                            } catch {
+                                debug(
+                                    .default,
+                                    "\(DebuggingIdentifiers.failed) failed to upload profiles: \(error.localizedDescription)"
+                                )
+                            }
                         }
                     } else {
                         debug(.nightscout, "Upload has been disabled by the user.")
@@ -156,7 +163,9 @@ extension NightscoutConfig {
 
             do {
                 guard let fetchedProfile = await nightscoutManager.importSettings() else {
-                    importStatus = .failed
+                    await MainActor.run {
+                        importStatus = .failed
+                    }
                     throw NSError(
                         domain: "ImportError",
                         code: 1,
@@ -178,7 +187,9 @@ extension NightscoutConfig {
                 }
 
                 if carbratios.contains(where: { $0.ratio <= 0 }) {
-                    importStatus = .failed
+                    await MainActor.run {
+                        importStatus = .failed
+                    }
                     throw NSError(
                         domain: "ImportError",
                         code: 2,
@@ -199,7 +210,9 @@ extension NightscoutConfig {
                 }
 
                 if pumpName != "Omnipod DASH", basals.contains(where: { $0.rate <= 0 }) {
-                    importStatus = .failed
+                    await MainActor.run {
+                        importStatus = .failed
+                    }
                     throw NSError(
                         domain: "ImportError",
                         code: 3,
@@ -208,7 +221,9 @@ extension NightscoutConfig {
                 }
 
                 if pumpName == "Omnipod DASH", basals.reduce(0, { $0 + $1.rate }) <= 0 {
-                    importStatus = .failed
+                    await MainActor.run {
+                        importStatus = .failed
+                    }
                     throw NSError(
                         domain: "ImportError",
                         code: 4,
@@ -229,7 +244,9 @@ extension NightscoutConfig {
                 }
 
                 if sensitivities.contains(where: { $0.sensitivity <= 0 }) {
-                    importStatus = .failed
+                    await MainActor.run {
+                        importStatus = .failed
+                    }
                     throw NSError(
                         domain: "ImportError",
                         code: 5,
@@ -261,25 +278,35 @@ extension NightscoutConfig {
                         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(
                             domain: "ImportError",
                             code: 6,
@@ -298,7 +325,7 @@ extension NightscoutConfig {
                     )
                 }
             } catch {
-                DispatchQueue.main.async {
+                await MainActor.run {
                     self.importErrors.append(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))
 
             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 {
-                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
 
                         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: {}
                     .store(in: &lifetime)

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

@@ -16,6 +16,8 @@ extension NightscoutConfig {
         @State var hintLabel: String?
         @State private var decimalPlaceholder: Decimal = 0.0
         @State private var booleanPlaceholder: Bool = false
+        @State var backfillAlert: Alert?
+        @State var isBackfillAlertPresented = false
 
         @Environment(\.colorScheme) var colorScheme
         @Environment(AppState.self) var appState
@@ -132,6 +134,16 @@ extension NightscoutConfig {
                                 Button {
                                     Task {
                                         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: {
                                     Text("Backfill Glucose")
@@ -174,7 +186,10 @@ extension NightscoutConfig {
                 .blur(radius: state.importStatus == .running ? 5 : 0)
 
                 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: {
@@ -194,6 +209,9 @@ extension NightscoutConfig {
             .alert(isPresented: $isImportAlertPresented) {
                 importAlert ?? Alert(title: Text("Unknown Error"))
             }
+            .alert(isPresented: $isBackfillAlertPresented) {
+                backfillAlert ?? Alert(title: Text("Unknown Error"))
+            }
             .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
             .onAppear(perform: configureView)
         }

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

@@ -5,6 +5,12 @@ import SwiftUI
 import Swinject
 
 extension Settings {
+    struct VersionInfo: Equatable {
+        var latestVersion: String?
+        var isUpdateAvailable: Bool
+        var isBlacklisted: Bool
+    }
+
     struct RootView: BaseView {
         let resolver: Resolver
         @StateObject var state = StateModel()
@@ -18,6 +24,11 @@ extension Settings {
         @State var hintLabel: String?
         @State private var decimalPlaceholder: Decimal = 0.0
         @State private var booleanPlaceholder: Bool = false
+        @State private var versionInfo = VersionInfo(
+            latestVersion: nil,
+            isUpdateAvailable: false,
+            isBlacklisted: false
+        )
 
         @Environment(\.colorScheme) var colorScheme
         @EnvironmentObject var appIcons: Icons
@@ -27,6 +38,37 @@ extension Settings {
             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 {
             List {
                 if searchText.isEmpty {
@@ -46,7 +88,7 @@ extension Settings {
                                         .frame(width: 50, height: 50)
                                         .cornerRadius(10)
                                         .padding(.trailing, 10)
-                                    VStack(alignment: .leading) {
+                                    VStack(alignment: .leading, spacing: 4) {
                                         Text("Trio v\(versionNumber) (\(buildNumber))")
                                             .font(.headline)
                                         if let expirationDate = buildDetails.calculateExpirationDate() {
@@ -63,6 +105,8 @@ extension Settings {
                                                 .font(.footnote)
                                                 .foregroundColor(.secondary)
                                         }
+
+                                        versionInfoView
                                     }
                                 }
                             }
@@ -312,6 +356,18 @@ extension Settings {
             }
             .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
             .screenNavigation(self)
+            .onAppear {
+                AppVersionChecker.shared.refreshVersionInfo { _, latestVersion, isNewer, isBlacklisted in
+                    let updateAvailable = isNewer
+                    DispatchQueue.main.async {
+                        versionInfo = VersionInfo(
+                            latestVersion: latestVersion,
+                            isUpdateAvailable: updateAvailable,
+                            isBlacklisted: isBlacklisted
+                        )
+                    }
+                }
+            }
         }
     }
 }

+ 13 - 9
Trio/Sources/Modules/Stat/StatStateModel+Setup/BolusStatsSetup.swift

@@ -23,15 +23,19 @@ extension Stat.StateModel {
     /// 3. Calculates and caches initial daily averages
     func setupBolusStats() {
         Task {
-            let (hourly, daily) = await fetchBolusStats()
+            do {
+                let (hourly, daily) = try await fetchBolusStats()
 
-            await MainActor.run {
-                self.hourlyBolusStats = hourly
-                self.dailyBolusStats = daily
-            }
+                await MainActor.run {
+                    self.hourlyBolusStats = hourly
+                    self.dailyBolusStats = daily
+                }
 
-            // Initially calculate and cache daily averages
-            await calculateAndCacheBolusAverages()
+                // Initially calculate and cache daily averages
+                await calculateAndCacheBolusAverages()
+            } catch {
+                debug(.default, "\(DebuggingIdentifiers.failed) failed to setup bolus stats: \(error.localizedDescription)")
+            }
         }
     }
 
@@ -43,9 +47,9 @@ extension Stat.StateModel {
     /// 2. Groups entries by hour and day
     /// 3. Calculates total insulin for each time period
     /// 4. Returns the processed statistics as (hourly: [BolusStats], daily: [BolusStats])
-    private func fetchBolusStats() async -> (hourly: [BolusStats], daily: [BolusStats]) {
+    private func fetchBolusStats() async throws -> (hourly: [BolusStats], daily: [BolusStats]) {
         // Fetch PumpEventStored entries from Core Data
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: BolusStored.self,
             onContext: bolusTaskContext,
             predicate: NSPredicate.pumpHistoryForStats,

+ 23 - 19
Trio/Sources/Modules/Stat/StatStateModel+Setup/LoopChartSetup.swift

@@ -31,20 +31,24 @@ extension Stat.StateModel {
     /// 3. Updating loop stat records on the main thread (!) for the Loop duration chart
     func setupLoopStatRecords() {
         Task {
-            let (recordIDs, failedRecordIDs) = await self.fetchLoopStatRecords(for: selectedDurationForLoopStats)
+            do {
+                let (recordIDs, failedRecordIDs) = try await self.fetchLoopStatRecords(for: selectedDurationForLoopStats)
 
-            // Update loop records for duration chart
-            await self.updateLoopStatRecords(allLoopIds: recordIDs)
+                // Update loop records for duration chart
+                await self.updateLoopStatRecords(allLoopIds: recordIDs)
 
-            // Calculate statistics and update on main thread
-            let stats = await self.getLoopStats(
-                allLoopIds: recordIDs,
-                failedLoopIds: failedRecordIDs,
-                duration: selectedDurationForLoopStats
-            )
+                // Calculate statistics and update on main thread
+                let stats = try await self.getLoopStats(
+                    allLoopIds: recordIDs,
+                    failedLoopIds: failedRecordIDs,
+                    duration: selectedDurationForLoopStats
+                )
 
-            await MainActor.run {
-                self.loopStats = stats
+                await MainActor.run {
+                    self.loopStats = stats
+                }
+            } catch {
+                debug(.default, "\(DebuggingIdentifiers.failed) failed to fetch loop stats: \(error.localizedDescription)")
             }
         }
     }
@@ -52,7 +56,7 @@ extension Stat.StateModel {
     /// Fetches loop statistics records for the specified duration
     /// - Parameter duration: The time period to fetch records for
     /// - Returns: A tuple containing arrays of NSManagedObjectIDs for (all loops, failed loops)
-    func fetchLoopStatRecords(for duration: Duration) async -> ([NSManagedObjectID], [NSManagedObjectID]) {
+    func fetchLoopStatRecords(for duration: Duration) async throws -> ([NSManagedObjectID], [NSManagedObjectID]) {
         // Calculate the date range based on selected duration
         let now = Date()
         let startDate: Date
@@ -91,7 +95,7 @@ extension Stat.StateModel {
         )
 
         // Wait for both results and convert to object IDs
-        let (allLoops, failedLoops) = await (allLoopsResult, failedLoopsResult)
+        let (allLoops, failedLoops) = try await (allLoopsResult, failedLoopsResult)
 
         return (
             (allLoops as? [LoopStatRecord] ?? []).map(\.objectID),
@@ -123,7 +127,7 @@ extension Stat.StateModel {
         allLoopIds: [NSManagedObjectID],
         failedLoopIds: [NSManagedObjectID],
         duration: Duration
-    ) async -> [(category: String, count: Int, percentage: Double)] {
+    ) async throws -> [(category: String, count: Int, percentage: Double)] {
         // Calculate the date range for glucose readings
         let now = Date()
         let startDate: Date
@@ -141,11 +145,11 @@ extension Stat.StateModel {
         }
 
         // Get glucose statistics
-        let totalGlucose = await calculateGlucoseStats(from: startDate, to: now)
+        let totalGlucose = try await calculateGlucoseStats(from: startDate, to: now)
 
         // Get NSManagedObject
-        let allLoops = await CoreDataStack.shared.getNSManagedObject(with: allLoopIds, context: loopTaskContext)
-        let failedLoops = await CoreDataStack.shared.getNSManagedObject(with: failedLoopIds, context: loopTaskContext)
+        let allLoops = try await CoreDataStack.shared.getNSManagedObject(with: allLoopIds, context: loopTaskContext)
+        let failedLoops = try await CoreDataStack.shared.getNSManagedObject(with: failedLoopIds, context: loopTaskContext)
 
         return await loopTaskContext.perform {
             let totalLoopsCount = allLoops.count
@@ -193,12 +197,12 @@ extension Stat.StateModel {
     private func calculateGlucoseStats(
         from startDate: Date,
         to _: Date
-    ) async -> Int {
+    ) async throws -> Int {
         // Create predicate for glucose readings
         let glucosePredicate = NSPredicate(format: "date >= %@", startDate as NSDate)
 
         // Fetch glucose readings asynchronously
-        let glucoseResult = await CoreDataStack.shared.fetchEntitiesAsync(
+        let glucoseResult = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: loopTaskContext,
             predicate: glucosePredicate,

+ 13 - 9
Trio/Sources/Modules/Stat/StatStateModel+Setup/MealStatsSetup.swift

@@ -23,15 +23,19 @@ extension Stat.StateModel {
     /// 3. Calculates and caches initial daily averages
     func setupMealStats() {
         Task {
-            let (hourly, daily) = await fetchMealStats()
+            do {
+                let (hourly, daily) = try await fetchMealStats()
 
-            await MainActor.run {
-                self.hourlyMealStats = hourly
-                self.dailyMealStats = daily
-            }
+                await MainActor.run {
+                    self.hourlyMealStats = hourly
+                    self.dailyMealStats = daily
+                }
 
-            // Initially calculate and cache daily averages
-            await calculateAndCacheDailyAverages()
+                // Initially calculate and cache daily averages
+                await calculateAndCacheDailyAverages()
+            } catch {
+                debug(.default, "\(DebuggingIdentifiers.failed) failed to fetch meal stats: \(error)")
+            }
         }
     }
 
@@ -43,9 +47,9 @@ extension Stat.StateModel {
     /// 2. Groups entries by hour and day
     /// 3. Calculates total macronutrients for each time period
     /// 4. Returns the processed statistics as (hourly: [MealStats], daily: [MealStats])
-    private func fetchMealStats() async -> (hourly: [MealStats], daily: [MealStats]) {
+    private func fetchMealStats() async throws -> (hourly: [MealStats], daily: [MealStats]) {
         // Fetch CarbEntryStored entries from Core Data
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
             onContext: mealTaskContext,
             predicate: NSPredicate.carbsForStats,

+ 14 - 10
Trio/Sources/Modules/Stat/StatStateModel+Setup/TDDSetup.swift

@@ -14,24 +14,28 @@ extension Stat.StateModel {
     /// Sets up TDD statistics by fetching and processing insulin data
     func setupTDDStats() {
         Task {
-            let (hourly, daily) = await fetchTDDStats()
-
-            await MainActor.run {
-                self.hourlyTDDStats = hourly
-                self.dailyTDDStats = daily
+            do {
+                let (hourly, daily) = try await fetchTDDStats()
+
+                await MainActor.run {
+                    self.hourlyTDDStats = hourly
+                    self.dailyTDDStats = daily
+                }
+
+                // Initially calculate and cache daily averages
+                await calculateAndCacheTDDAverages()
+            } catch {
+                debug(.default, "\(DebuggingIdentifiers.failed) failed fetching TDD stats: \(error.localizedDescription)")
             }
-
-            // Initially calculate and cache daily averages
-            await calculateAndCacheTDDAverages()
         }
     }
 
     /// Fetches and processes Total Daily Dose (TDD) statistics from CoreData
     /// - Returns: A tuple containing hourly and daily TDD statistics arrays
     /// - Note: Processes both hourly statistics for the last 10 days and complete daily statistics
-    private func fetchTDDStats() async -> (hourly: [TDDStats], daily: [TDDStats]) {
+    private func fetchTDDStats() async throws -> (hourly: [TDDStats], daily: [TDDStats]) {
         // Fetch temp basal records from CoreData
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempBasalStored.self,
             onContext: tddTaskContext,
             predicate: NSPredicate.pumpHistoryForStats,

+ 33 - 27
Trio/Sources/Modules/Stat/StatStateModel.swift

@@ -189,35 +189,41 @@ extension Stat {
         }
 
         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
-            }
-
-            let results = await CoreDataStack.shared.fetchEntitiesAsync(
-                ofType: GlucoseStored.self,
-                onContext: context,
-                predicate: predicate,
-                key: "date",
-                ascending: false,
-                batchSize: 100,
-                propertiesToFetch: ["glucose", "objectID"]
-            )
+            do {
+                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
+                }
 
-            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) {
-                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 expectedDelta: Decimal = 0
         var minPredBG: Decimal = 0
-        var waitForSuggestion: Bool = false
+        var isAwaitingDeterminationResult: Bool = false
         var carbRatio: Decimal = 0
 
         var addButtonPressed: Bool = false
 
-        var waitForSuggestionInitial: Bool = false
-
         var target: Decimal = 0
         var cob: Int16 = 0
         var iob: Decimal = 0
@@ -122,6 +120,9 @@ extension Treatments {
 
         var isActive: Bool = false
 
+        var showDeterminationFailureAlert = false
+        var determinationFailureMessage = ""
+
         // Queue for handling Core Data change notifications
         private let queue = DispatchQueue(label: "TreatmentsStateModel.queue", qos: .userInitiated)
         private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
@@ -170,32 +171,26 @@ extension Treatments {
         private func setupBolusStateConcurrently() {
             debug(.bolusState, "setupBolusStateConcurrently fired")
             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 {
-                            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>`.
         ///   - 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`.
-        ///   - This treats ANY non-nil value as “bolus in progress.”
+        ///   - This treats ANY non-nil value as "bolus in progress."
         ///
         private func subscribeToBolusProgress() {
             bolusProgressCancellable = apsManager.bolusProgress
@@ -379,7 +374,7 @@ extension Treatments {
         // MARK: CALCULATIONS FOR THE BOLUS CALCULATOR
 
         /// Calculate insulin recommendation
-        @MainActor func calculateInsulin() async -> Decimal {
+        func calculateInsulin() async -> Decimal {
             let result = await bolusCalculationManager.handleBolusCalculation(
                 carbs: carbs,
                 useFattyMealCorrection: useFattyMealCorrectionFactor,
@@ -387,14 +382,16 @@ extension Treatments {
             )
 
             // 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)
         }
@@ -417,7 +414,7 @@ extension Treatments {
                 }
 
                 if isInsulinGiven {
-                    try await handleInsulin(isExternal: externalInsulin)
+                    await handleInsulin(isExternal: externalInsulin)
                 } else {
                     hideModal()
                     return
@@ -431,7 +428,9 @@ extension Treatments {
 
                 guard glucoseStorage.isGlucoseDataFresh(date) else {
                     await MainActor.run {
-                        waitForSuggestion = false
+                        isAwaitingDeterminationResult = false
+                        showDeterminationFailureAlert = true
+                        determinationFailureMessage = "Glucose data is stale"
                     }
                     return hideModal()
                 }
@@ -440,17 +439,14 @@ extension Treatments {
 
         // MARK: - Insulin
 
-        private func handleInsulin(isExternal: Bool) async throws {
+        private func handleInsulin(isExternal: Bool) async {
             debug(.bolusState, "handleInsulin fired")
+
             if !isExternal {
                 await addPumpInsulin()
             } else {
                 await addExternalInsulin()
             }
-
-            await MainActor.run {
-                self.waitForSuggestion = true
-            }
         }
 
         func addPumpInsulin() async {
@@ -464,6 +460,10 @@ extension Treatments {
             do {
                 let authenticated = try await unlockmanager.unlock()
                 if authenticated {
+                    // show loading animation
+                    await MainActor.run {
+                        self.isAwaitingDeterminationResult = true
+                    }
                     await apsManager.enactBolus(amount: maxAmount, isSMB: false, callback: nil)
                 } else {
                     print("authentication failed")
@@ -471,10 +471,9 @@ extension Treatments {
             } catch {
                 print("authentication error for pump bolus: \(error.localizedDescription)")
                 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 {
                 let authenticated = try await unlockmanager.unlock()
                 if authenticated {
+                    // show loading animation
+                    await MainActor.run {
+                        self.isAwaitingDeterminationResult = true
+                    }
                     // store external dose to pump history
                     await pumpHistoryStorage.storeExternalInsulinEvent(amount: amount, timestamp: date)
                     // perform determine basal sync
-                    await apsManager.determineBasalSync()
+                    try await apsManager.determineBasalSync()
                 } else {
                     print("authentication failed")
                 }
             } catch {
                 print("authentication error for external insulin: \(error.localizedDescription)")
                 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
 
         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 {
-                    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 {
             debug(.bolusState, "determinationDidUpdate fired")
-            self.waitForSuggestion = false
+            self.isAwaitingDeterminationResult = false
             if self.addButtonPressed {
                 self.hideModal()
             }
@@ -608,7 +614,7 @@ extension Treatments.StateModel: DeterminationObserver, BolusFailureObserver {
     func bolusDidFail() {
         DispatchQueue.main.async {
             debug(.bolusState, "bolusDidFail fired")
-            self.waitForSuggestion = false
+            self.isAwaitingDeterminationResult = false
             if self.addButtonPressed {
                 self.hideModal()
             }
@@ -618,7 +624,7 @@ extension Treatments.StateModel: DeterminationObserver, BolusFailureObserver {
 
 extension Treatments.StateModel {
     private func registerHandlers() {
-        coreDataPublisher?.filterByEntityName("OrefDetermination").sink { [weak self] _ in
+        coreDataPublisher?.filteredByEntityName("OrefDetermination").sink { [weak self] _ in
             guard let self = self else { return }
             Task {
                 await self.setupDeterminationsArray()
@@ -628,7 +634,7 @@ extension Treatments.StateModel {
         }.store(in: &subscriptions)
 
         // 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 }
             self.setupGlucoseArray()
         }.store(in: &subscriptions)
@@ -651,14 +657,22 @@ extension Treatments.StateModel {
     // Glucose
     private func setupGlucoseArray() {
         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,
             onContext: glucoseFetchContext,
             predicate: NSPredicate.glucose,
@@ -666,8 +680,10 @@ extension Treatments.StateModel {
             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)
         }
@@ -686,71 +702,83 @@ extension Treatments.StateModel {
 
     // Determinations
     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? {
-        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)
                 }
-                .blur(radius: state.waitForSuggestion ? 5 : 0)
+                .blur(radius: state.isAwaitingDeterminationResult ? 5 : 0)
 
-                if state.waitForSuggestion {
+                if state.isAwaitingDeterminationResult {
                     CustomProgressView(text: progressText.rawValue)
                 }
             }
@@ -364,6 +364,13 @@ extension Treatments {
             }) {
                 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 {
@@ -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
     /// - 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,
             onContext: glucoseFetchContext,
             predicate: NSPredicate.glucose,
@@ -201,8 +201,10 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
             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)
         }
     }
@@ -266,64 +268,73 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
         carbs: Decimal,
         useFattyMealCorrection: 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
@@ -401,14 +412,38 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
     ///   - useFattyMealCorrection: Whether to apply fatty meal correction
     ///   - useSuperBolus: Whether to use super bolus calculation
     /// - 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() {
-        coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
+        coreDataPublisher?.filteredByEntityName("GlucoseStored").sink { [weak self] _ in
             guard let self = self else { return }
             Task {
                 await self.createEvent()
@@ -175,8 +175,8 @@ final class BaseCalendarManager: CalendarManager, Injectable {
         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,
             onContext: backgroundContext,
             predicate: NSPredicate.predicateFor30MinAgoForDetermination,
@@ -186,15 +186,17 @@ final class BaseCalendarManager: CalendarManager, Injectable {
             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
         }
     }
 
-    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,
             onContext: backgroundContext,
             predicate: NSPredicate.predicateFor30MinAgo,
@@ -203,27 +205,29 @@ final class BaseCalendarManager: CalendarManager, Injectable {
             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 }
         }
     }
 
     @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
             else { return }
 

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

@@ -77,7 +77,7 @@ final class BaseContactImageManager: NSObject, ContactImageManager, Injectable {
     // MARK: - Core Data observation
 
     private func registerHandlers() {
-        coreDataPublisher?.filterByEntityName("OrefDetermination").sink { [weak self] _ in
+        coreDataPublisher?.filteredByEntityName("OrefDetermination").sink { [weak self] _ in
             guard let self = self else { return }
             Task {
                 await self.updateContactImageState()
@@ -88,8 +88,8 @@ final class BaseContactImageManager: NSObject, ContactImageManager, Injectable {
 
     // 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,
             onContext: backgroundContext,
             predicate: NSPredicate(format: "deliverAt >= %@", Date.halfHourAgo as NSDate), // fetches enacted and suggested
@@ -98,15 +98,17 @@ final class BaseContactImageManager: NSObject, ContactImageManager, Injectable {
             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)
         }
     }
 
-    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,
             onContext: backgroundContext,
             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
         )
 
-        return await backgroundContext.perform {
+        return try await backgroundContext.perform {
             guard let glucoseResults = results as? [GlucoseStored] else {
-                return []
+                throw CoreDataError.fetchError(function: #function, file: #file)
             }
 
             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.
     /// - 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 {
-        // 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)
         }
     }

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

@@ -94,7 +94,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
     }
 
     private func registerHandlers() {
-        coreDataPublisher?.filterByEntityName("PumpEventStored").sink { [weak self] _ in
+        coreDataPublisher?.filteredByEntityName("PumpEventStored").sink { [weak self] _ in
             guard let self = self else { return }
             Task { [weak self] in
                 guard let self = self else { return }
@@ -102,7 +102,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
             }
         }.store(in: &subscriptions)
 
-        coreDataPublisher?.filterByEntityName("CarbEntryStored").sink { [weak self] _ in
+        coreDataPublisher?.filteredByEntityName("CarbEntryStored").sink { [weak self] _ in
             guard let self = self else { return }
             Task { [weak self] in
                 guard let self = self else { return }
@@ -111,7 +111,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
         }.store(in: &subscriptions)
 
         // 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 }
             Task { [weak self] in
                 guard let self = self else { return }
@@ -156,8 +156,18 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
     // Glucose Upload
 
     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 {
@@ -228,7 +238,15 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
     // Carbs Upload
 
     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 {
@@ -341,7 +359,15 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
     // Insulin Upload
 
     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 {
@@ -350,87 +376,92 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
               checkWriteToHealthPermissions(objectTypeToHealthStore: sampleType),
               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)
                         }
-                    }
+                    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 {
-            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, *)
 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,
             onContext: context,
             predicate: NSPredicate.predicateForSixHoursAgo,
@@ -14,9 +14,9 @@ extension LiveActivityBridge {
             fetchLimit: 72
         )
 
-        return await context.perform {
+        return try await context.perform {
             guard let glucoseResults = results as? [GlucoseStored] else {
-                return []
+                throw CoreDataError.fetchError(function: #function, file: #file)
             }
 
             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,
             onContext: context,
             predicate: NSPredicate.predicateFor30MinAgoForDetermination,
@@ -36,9 +36,9 @@ extension LiveActivityBridge {
             propertiesToFetch: ["iob", "cob", "totalDailyDose", "currentTarget", "deliverAt"]
         )
 
-        return await context.perform {
+        return try await context.perform {
             guard let determinationResults = results as? [[String: Any]] else {
-                return nil
+                throw CoreDataError.fetchError(function: #function, file: #file)
             }
 
             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,
             onContext: context,
             predicate: NSPredicate.predicateForOneDayAgo,
@@ -64,9 +64,9 @@ extension LiveActivityBridge {
             propertiesToFetch: ["enabled", "name", "target", "date", "duration"]
         )
 
-        return await context.perform {
+        return try await context.perform {
             guard let overrideResults = results as? [[String: Any]] else {
-                return nil
+                throw CoreDataError.fetchError(function: #function, file: #file)
             }
 
             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 var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
     private var subscriptions = Set<AnyCancellable>()
-    private let orefDeterminationSubject = PassthroughSubject<Void, Never>()
 
     init(resolver: Resolver) {
         coreDataPublisher =
@@ -99,49 +98,59 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
     }
 
     private func registerHandler() {
-        coreDataPublisher?.filterByEntityName("OverrideStored").sink { [weak self] _ in
+        coreDataPublisher?.filteredByEntityName("OverrideStored").sink { [weak self] _ in
             guard let self = self else { return }
             self.overridesDidUpdate()
         }.store(in: &subscriptions)
 
-        coreDataPublisher?.filterByEntityName("OrefDetermination").sink { [weak self] _ in
+        coreDataPublisher?.filteredByEntityName("GlucoseStored").sink { [weak self] _ in
             guard let self = self else { return }
-            self.orefDeterminationSubject.send()
+            self.setupGlucoseArray()
         }.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() {
         glucoseStorage.updatePublisher
-            .receive(on: DispatchQueue.global(qos: .background))
+            .receive(on: queue)
             .sink { [weak self] _ in
                 guard let self = self else { return }
                 self.setupGlucoseArray()
             }
             .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() {
         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() {
         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() {
         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 deleteInsulin(withID id: String) async
     func deleteManualGlucose(withID id: String) async
-    func uploadDeviceStatus() async
+    func uploadDeviceStatus() async throws
     func uploadGlucose() async
     func uploadCarbs() async
     func uploadPumpHistory() async
     func uploadOverrides() async
     func uploadTempTargets() async
     func uploadManualGlucose() async
-    func uploadProfiles() async
+    func uploadProfiles() async throws
     func uploadNoteTreatment(note: String) async
     func importSettings() async -> ScheduledNightscoutProfile?
     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
         /// where the first uploaded non-enacted determination (i.e., "suggested"), lacks the "enacted" data.
         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() {
+        /// 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?
-            .filterByEntityName("OrefDetermination")
+            .filteredByEntityName("OrefDetermination")
+            .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .background))
             .sink { [weak self] objectIDs in
                 guard let self = self else { return }
 
@@ -129,62 +140,83 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                     do {
                         // Fetch only those determination objects
                         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)
 
-                        // 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.orefDeterminationSubject.send()
+                        if !results.isEmpty {
+                            Task {
+                                do {
+                                    try await self.uploadDeviceStatus()
+                                } catch {
+                                    debug(.nightscout, "\(DebuggingIdentifiers.failed) failed to upload device status")
+                                }
+                            }
                         }
                     } catch {
-                        debugPrint("Failed to fetch OrefDetermination objects: \(error)")
+                        debug(.nightscout, "\(DebuggingIdentifiers.failed) Failed to fetch OrefDetermination objects: \(error)")
                     }
                 }
             }
             .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
                 guard let self = self else { return }
 
-                // Now hop onto the background context’s queue
                 self.backgroundContext.perform {
                     do {
                         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)
 
-                        // 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 {
                         debugPrint("Failed to fetch PumpEventStored objects: \(error)")
@@ -193,7 +225,8 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             }
             .store(in: &subscriptions)
 
-        coreDataPublisher?.filterByEntityName("CarbEntryStored")
+        coreDataPublisher?.filteredByEntityName("CarbEntryStored")
+            .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .background))
             .sink { [weak self] objectIDs in
                 guard let self = self else { return }
 
@@ -201,15 +234,17 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                 self.backgroundContext.perform {
                     do {
                         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)
 
-                        // 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.uploadCarbsSubject.send()
+                        if !results.isEmpty {
+                            Task.detached {
+                                await self.uploadCarbs()
+                            }
                         }
                     } catch {
                         debugPrint("Failed to fetch CarbEntryStored objects: \(error)")
@@ -218,10 +253,11 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             }
             .store(in: &subscriptions)
 
-        coreDataPublisher?.filterByEntityName("GlucoseStored")
+        coreDataPublisher?.filteredByEntityName("GlucoseStored")
             .sink { [weak self] _ in
                 guard let self = self else { return }
                 Task.detached {
+                    await self.uploadGlucose()
                     await self.uploadManualGlucose()
                 }
             }
@@ -230,7 +266,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
 
     func registerSubscribers() {
         glucoseStorage.updatePublisher
-            .receive(on: DispatchQueue.global(qos: .background))
+            .receive(on: queue)
             .sink { [weak self] _ in
                 guard let self = self else { return }
                 Task {
@@ -238,49 +274,6 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                 }
             }
             .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() {
@@ -505,7 +498,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     ///
     /// - Note: Ensure `nightscoutAPI` is initialized and `isUploadEnabled` is set to `true` before invoking this function.
     /// - Returns: Nothing.
-    func uploadDeviceStatus() async {
+    func uploadDeviceStatus() async throws {
         guard let nightscout = nightscoutAPI, isUploadEnabled else {
             debug(.nightscout, "NS API not available or upload disabled. Aborting NS Status upload.")
             return
@@ -523,7 +516,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         async let fetchedIOBEntry = storage.retrieveAsync(OpenAPS.Monitor.iob, as: [IOBEntry].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(suggestedDeterminationID)
         )
@@ -692,7 +685,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         }
     }
 
-    func uploadProfiles() async {
+    func uploadProfiles() async throws {
         if isUploadEnabled {
             do {
                 guard let sensitivities = await storage.retrieveAsync(
@@ -791,7 +784,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                 let bundleIdentifier = Bundle.main.bundleIdentifier ?? ""
                 let deviceToken = UserDefaults.standard.string(forKey: "deviceToken") ?? ""
                 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 profileStore = NightscoutProfileStore(
@@ -816,12 +809,11 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                     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 {
             debug(.nightscout, "Upload to NS disabled; aborting profile uploaded")
@@ -843,31 +835,73 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     }
 
     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 {
-        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 {
-        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 {
-        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 {
-        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 {
-        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 {

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

@@ -108,7 +108,7 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
 
     /// Registers handlers for Core Data changes
     private func registerHandlers() {
-        coreDataPublisher?.filterByEntityName("PumpEventStored").sink { [weak self] _ in
+        coreDataPublisher?.filteredByEntityName("PumpEventStored").sink { [weak self] _ in
             guard let self = self else { return }
             Task { [weak self] in
                 guard let self = self else { return }
@@ -116,7 +116,7 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
             }
         }.store(in: &subscriptions)
 
-        coreDataPublisher?.filterByEntityName("CarbEntryStored").sink { [weak self] _ in
+        coreDataPublisher?.filteredByEntityName("CarbEntryStored").sink { [weak self] _ in
             guard let self = self else { return }
             Task { [weak self] in
                 guard let self = self else { return }
@@ -125,7 +125,7 @@ final class BaseTidepoolManager: TidepoolManager, Injectable {
         }.store(in: &subscriptions)
 
         // 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 }
             Task { [weak self] in
                 guard let self = self else { return }
@@ -187,7 +187,11 @@ extension BaseTidepoolManager: ServiceDelegate {
 /// Carb Upload and Deletion Functionality
 extension BaseTidepoolManager {
     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]) {
@@ -274,115 +278,129 @@ extension BaseTidepoolManager {
 /// Insulin Upload and Deletion Functionality
 extension BaseTidepoolManager {
     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 {
         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:
-                        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
 extension BaseTidepoolManager {
     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]) {

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

@@ -1,7 +1,7 @@
 import Foundation
 
 extension TrioRemoteControl {
-    internal func handleAPNSChanges(deviceToken: String?) async {
+    internal func handleAPNSChanges(deviceToken: String?) async throws {
         let previousDeviceToken = UserDefaults.standard.string(forKey: "deviceToken")
         let previousIsAPNSProduction = UserDefaults.standard.bool(forKey: "isAPNSProduction")
 
@@ -21,7 +21,7 @@ extension TrioRemoteControl {
         }
 
         if shouldUploadProfiles {
-            await nightscoutManager.uploadProfiles()
+            try await nightscoutManager.uploadProfiles()
         } else {
             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
 
 extension TrioRemoteControl {
-    internal func handleBolusCommand(_ pushMessage: PushMessage) async {
+    internal func handleBolusCommand(_ pushMessage: PushMessage) async throws {
         guard let bolusAmount = pushMessage.bolusAmount else {
             await logError("Command rejected: bolus amount is missing or invalid.", pushMessage: pushMessage)
             return
@@ -18,7 +18,7 @@ extension TrioRemoteControl {
         }
 
         let maxIOB = settings.preferences.maxIOB
-        let currentIOB = await fetchCurrentIOB()
+        let currentIOB = try await fetchCurrentIOB()
         if (currentIOB + bolusAmount) > maxIOB {
             await logError(
                 "Command rejected: bolus amount (\(bolusAmount) units) would exceed max IOB (\(maxIOB) units). Current IOB: \(currentIOB) units.",
@@ -27,7 +27,8 @@ extension TrioRemoteControl {
             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 {
             await logError(
@@ -55,10 +56,10 @@ extension TrioRemoteControl {
         )
     }
 
-    private func fetchCurrentIOB() async -> Decimal {
+    private func fetchCurrentIOB() async throws -> Decimal {
         let predicate = NSPredicate.predicateFor30MinAgoForDetermination
 
-        let determinations = await CoreDataStack.shared.fetchEntitiesAsync(
+        let determinations = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OrefDetermination.self,
             onContext: pumpHistoryFetchContext,
             predicate: predicate,
@@ -73,20 +74,20 @@ extension TrioRemoteControl {
               let iob = firstResult["iob"] as? Decimal
         else {
             await logError("Failed to fetch current IOB.")
-            return Decimal(0)
+            throw CoreDataError.fetchError(function: #function, file: #file)
         }
 
         return iob
     }
 
-    private func fetchTotalRecentBolusAmount(since date: Date) async -> Decimal {
+    private func fetchTotalRecentBolusAmount(since date: Date) async throws -> Decimal {
         let predicate = NSPredicate(
             format: "type == %@ AND timestamp > %@",
             PumpEventStored.EventType.bolus.rawValue,
             date as NSDate
         )
 
-        let results: Any = await CoreDataStack.shared.fetchEntitiesAsync(
+        let results: Any = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: PumpEventStored.self,
             onContext: pumpHistoryFetchContext,
             predicate: predicate,
@@ -98,7 +99,7 @@ extension TrioRemoteControl {
 
         guard let bolusDictionaries = results as? [[String: Any]] else {
             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, +)

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

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

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

@@ -12,21 +12,35 @@ extension TrioRemoteControl {
     }
 
     @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 {
-        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
                     try self.viewContext.existingObject(with: id) as? OverrideStored
                 }
@@ -89,16 +103,16 @@ extension TrioRemoteControl {
                 } else {
                     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
 
 extension TrioRemoteControl {
-    @MainActor func handleTempTargetCommand(_ pushMessage: PushMessage) async {
+    @MainActor func handleTempTargetCommand(_ pushMessage: PushMessage) async throws {
         guard let targetValue = pushMessage.target,
               let durationValue = pushMessage.duration
         else {
@@ -26,7 +26,7 @@ extension TrioRemoteControl {
             halfBasalTarget: settings.preferences.halfBasalExerciseTarget
         )
 
-        await tempTargetsStorage.storeTempTarget(tempTarget: tempTarget)
+        try await tempTargetsStorage.storeTempTarget(tempTarget: tempTarget)
         tempTargetsStorage.saveTempTargetsToStorage([tempTarget])
 
         debug(
@@ -47,10 +47,10 @@ extension TrioRemoteControl {
     }
 
     @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
                     try self.viewContext.existingObject(with: id) as? TempTargetStored
                 }
@@ -68,8 +68,7 @@ extension TrioRemoteControl {
                     newTempTargetRunStored.name = canceledTempTarget.name
                     newTempTargetRunStored.startDate = canceledTempTarget.date ?? .distantPast
                     newTempTargetRunStored.endDate = Date()
-                    newTempTargetRunStored
-                        .target = canceledTempTarget.target ?? 0
+                    newTempTargetRunStored.target = canceledTempTarget.target ?? 0
                     newTempTargetRunStored.tempTarget = canceledTempTarget
                     newTempTargetRunStored.isUploadedToNS = false
 
@@ -87,16 +86,17 @@ extension TrioRemoteControl {
                 } else {
                     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)
     }
 
-    func handleRemoteNotification(pushMessage: PushMessage) async {
+    func handleRemoteNotification(pushMessage: PushMessage) async throws {
         let isTrioRemoteControlEnabled = UserDefaults.standard.bool(forKey: "isTrioRemoteControlEnabled")
         guard isTrioRemoteControlEnabled else {
             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 {
         case .bolus:
-            await handleBolusCommand(pushMessage)
+            try await handleBolusCommand(pushMessage)
         case .tempTarget:
-            await handleTempTargetCommand(pushMessage)
+            try await handleTempTargetCommand(pushMessage)
         case .cancelTempTarget:
             await cancelTempTarget(pushMessage)
         case .meal:
-            await handleMealCommand(pushMessage)
+            try await handleMealCommand(pushMessage)
 
             if pushMessage.bolusAmount != nil {
-                await handleBolusCommand(pushMessage)
+                try await handleBolusCommand(pushMessage)
             }
         case .startOverride:
             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) {
         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 {
         await withCheckedContinuation { continuation in
             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()
             }
@@ -46,49 +54,81 @@ final class BaseFileStorage: FileStorage {
 
     func retrieve<Value: JSON>(_ name: String, as type: Value.Type) -> Value? {
         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? {
         await withCheckedContinuation { continuation in
             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? {
         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 String(data: data, encoding: .utf8)
         }
     }
 
     func retrieveRawAsync(_ name: String) async -> RawJSON? {
         await withCheckedContinuation { continuation in
             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)
-                    return
                 }
-                continuation.resume(returning: String(data: data, encoding: .utf8))
             }
         }
     }
 
     func append<Value: JSON>(_ newValue: Value, to name: String) {
         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) {
         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) {
         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) {
         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? {
-        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() {
         // 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 }
             Task {
                 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,
             onContext: backgroundContext,
             predicate: NSPredicate.predicateFor20MinAgo,
@@ -253,8 +253,10 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
             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)
         }
@@ -263,7 +265,7 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
     @MainActor private func sendGlucoseNotification() async {
         do {
             addAppBadge(glucose: nil)
-            let glucoseIDs = await fetchGlucoseIDs()
+            let glucoseIDs = try await fetchGlucoseIDs()
             let glucoseObjects = try glucoseIDs.compactMap { id in
                 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() {
-        coreDataPublisher?.filterByEntityName("OrefDetermination").sink { [weak self] _ in
+        coreDataPublisher?.filteredByEntityName("OrefDetermination").sink { [weak self] _ in
             guard let self = self else { return }
             Task {
                 let state = await self.setupWatchState()
@@ -90,7 +90,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
         }.store(in: &subscriptions)
 
         // 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 }
             Task {
                 let state = await self.setupWatchState()
@@ -98,14 +98,14 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
             }
         }.store(in: &subscriptions)
 
-        coreDataPublisher?.filterByEntityName("PumpEventStored").sink { [weak self] _ in
+        coreDataPublisher?.filteredByEntityName("PumpEventStored").sink { [weak self] _ in
             guard let self = self else { return }
             Task {
                 await self.getActiveBolusAmount()
             }
         }.store(in: &subscriptions)
 
-        coreDataPublisher?.filterByEntityName("OverrideStored").sink { [weak self] _ in
+        coreDataPublisher?.filteredByEntityName("OverrideStored").sink { [weak self] _ in
             guard let self = self else { return }
             Task {
                 let state = await self.setupWatchState()
@@ -113,7 +113,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
             }
         }.store(in: &subscriptions)
 
-        coreDataPublisher?.filterByEntityName("TempTargetStored").sink { [weak self] _ in
+        coreDataPublisher?.filteredByEntityName("TempTargetStored").sink { [weak self] _ in
             guard let self = self else { return }
             Task {
                 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
     /// - Returns: WatchState containing current glucose readings and trends and determination infos for displaying cob and iob in the view
     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,
                     lowGlucoseColorValue: lowGlucoseColorValue,
                     targetGlucose: targetGlucose,
                     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 {
-                    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(
                 .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
     /// - 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,
             onContext: backgroundContext,
             predicate: NSPredicate.glucose,
@@ -335,8 +347,10 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
             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)
         }
@@ -344,8 +358,8 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
 
     /// Fetches last pump event that is a non-external bolus from CoreData
     /// - 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,
             onContext: backgroundContext,
             predicate: NSPredicate.lastPumpBolus,
@@ -354,8 +368,10 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
             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
         }
@@ -363,11 +379,18 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
 
     /// Gets the active bolus amount by fetching last (active) bolus.
     @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")
 
                     // 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.carbs = Double(truncating: amount as NSNumber)
                 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.isUploadedToNS = false
 
@@ -642,7 +665,13 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                     debug(.watchManager, "📱 Saved carbs from watch: \(amount)g at \(date)")
 
                     // 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 {
                     debug(.watchManager, "❌ Error saving carbs: \(error.localizedDescription)")
 
@@ -664,7 +693,10 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
 
             do {
                 // 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
                 try await context.perform {
@@ -672,7 +704,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                     carbEntry.id = UUID()
                     carbEntry.carbs = NSDecimalNumber(decimal: carbsAmount).doubleValue
                     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.isUploadedToNS = false
 
@@ -682,7 +714,13 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                 }
 
                 // 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
                 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")
                 // 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 {
                 debug(.watchManager, "❌ Error processing combined request: \(error.localizedDescription)")
@@ -705,7 +749,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
         Task {
             let context = CoreDataStack.shared.newTaskContext()
 
-            if let overrideId = await overrideStorage.fetchLatestActiveOverride() {
+            if let overrideId = try await overrideStorage.fetchLatestActiveOverride() {
                 let override = await context.perform {
                     context.object(with: overrideId) as? OverrideStored
                 }
@@ -743,12 +787,12 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
             let context = CoreDataStack.shared.newTaskContext()
 
             // 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)
 
             // Check for active override
-            if let activeOverrideId = await overrideStorage.fetchLatestActiveOverride() {
+            if let activeOverrideId = try await overrideStorage.fetchLatestActiveOverride() {
                 let activeOverride = await context.perform {
                     context.object(with: activeOverrideId) as? OverrideStored
                 }
@@ -795,12 +839,12 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
             let context = CoreDataStack.shared.newTaskContext()
 
             // 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)
 
             // 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 {
                     context.object(with: activeTempTargetId) as? TempTargetStored
                 }
@@ -867,7 +911,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
         Task {
             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 {
                     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.
     @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] = []
 
     /// Router for presenting alerts or navigation flows (injected via Swinject).
@@ -134,9 +134,16 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
             .sink { [weak self] _ in
                 guard let self = self else { return }
                 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)
@@ -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.
     private func registerHandlers() {
         coreDataPublisher?
-            .filterByEntityName("OrefDetermination")
+            .filteredByEntityName("OrefDetermination")
             .sink { [weak self] _ in
                 guard let self = self else { return }
                 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)
 
         // Due to the batch insert, this only observes deletion of Glucose entries
         coreDataPublisher?
-            .filterByEntityName("GlucoseStored")
+            .filteredByEntityName("GlucoseStored")
             .sink { [weak self] _ in
                 guard let self = self else { return }
                 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)
@@ -177,8 +198,8 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
 
     /// Fetches recent glucose readings from CoreData, up to 288 results.
     /// - 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,
             onContext: backgroundContext,
             predicate: NSPredicate.glucose,
@@ -187,109 +208,119 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
             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)
         }
     }
 
     /// 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.
-    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 {
-                    watchState.isf = insulinSensitivity.description
-                    watchState.eventualBGRaw = Formatter.glucoseFormatter(for: self.units)
-                        .string(from: eventualBG) ?? "0"
+                    watchState.glucose = "\(latestGlucose.glucose)"
                 } 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(
                 .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
-    /// 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() {
         watchStateSubject
             .throttle(for: .seconds(10), scheduler: DispatchQueue.main, latest: true)
@@ -419,7 +450,7 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
         .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.
     func updateDeviceList(_ devices: [IQDevice]) {
         self.devices = devices
@@ -527,7 +558,7 @@ extension BaseGarminManager: IQUIOverrideDelegate, IQDeviceEventDelegate, IQAppM
 
             do {
                 // 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)
 
                 // Now send that JSON data to the watch
@@ -560,9 +591,16 @@ extension BaseGarminManager: SettingsObserver {
         units = settingsManager.settings.units
 
         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)
 
-        await carbsStorage.storeCarbs(
+        try await carbsStorage.storeCarbs(
             [CarbsEntry(
                 id: UUID().uuidString,
                 createdAt: dateAdded,

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

@@ -18,10 +18,10 @@ struct OverridePreset: AppEntity, Identifiable {
 
 struct OverridePresetsQuery: EntityQuery {
     func entities(for identifiers: [OverridePreset.ID]) async throws -> [OverridePreset] {
-        await OverridePresetsIntentRequest().fetchIDs(identifiers)
+        try await OverridePresetsIntentRequest().fetchIDs(identifiers)
     }
 
     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
     }
 
-    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
                     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: "") }
                     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()
             fetchRequest.predicate = NSPredicate(format: "id IN %@", uuid)
 
@@ -44,36 +44,40 @@ import UIKit
                 let result = try self.coredataContext.fetch(fetchRequest)
 
                 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
                     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()
         fetchRequest.predicate = NSPredicate(format: "id == %@", preset.id)
         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") {
             guard backgroundTaskID != .invalid else { return }
             Task {
-                // End background task when the time is about to expire
                 UIApplication.shared.endBackgroundTask(backgroundTaskID)
             }
             backgroundTaskID = .invalid
         }
 
-        // Defer block to end background task when function exits
         defer {
             if backgroundTaskID != .invalid {
                 Task {
@@ -101,37 +103,33 @@ import UIKit
 
         do {
             // 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
             overrideObject.enabled = true
             overrideObject.date = Date()
             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)
 
             if viewContext.hasChanges {
                 try viewContext.save()
-
-                // Update State variables in OverrideView
                 Foundation.NotificationCenter.default.post(name: .willUpdateOverrideConfiguration, object: nil)
-
-                // Await the notification
-                print("Waiting for notification...")
                 await awaitNotification(.didUpdateOverrideConfiguration)
-                print("Notification received, continuing...")
-
                 return true
             }
+            return false
         } 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 {
@@ -167,10 +165,9 @@ import UIKit
             }
         }
 
-        // Get NSManagedObjectID of all active overrides
-        let ids = await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0) // 0 = no fetch limit
-
         do {
+            // Get NSManagedObjectID of all active overrides
+            let ids = try await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0) // 0 = no fetch limit
             // Fetch existing OverrideStored objects
             let results = try ids.compactMap { id in
                 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)
     {
         do {
-            let results = CoreDataStack.shared.fetchEntities(
+            let results = try CoreDataStack.shared.fetchEntities(
                 ofType: GlucoseStored.self,
                 onContext: onContext,
                 predicate: NSPredicate.predicateFor30MinAgo,
@@ -102,7 +102,7 @@ final class StateIntentRequest: BaseIntentsRequest {
     }
 
     func getIobAndCob(onContext: NSManagedObjectContext) throws -> (iob: Double, cob: Double) {
-        let results = CoreDataStack.shared.fetchEntities(
+        let results = try CoreDataStack.shared.fetchEntities(
             ofType: OrefDetermination.self,
             onContext: onContext,
             predicate: NSPredicate.enactedDetermination,

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

@@ -25,6 +25,6 @@ struct TempPresetsQuery: EntityQuery {
     }
 
     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
     }
 
-    func fetchAndProcessTempTargets() async -> [TempPreset] {
+    func fetchAndProcessTempTargets() async throws -> [TempPreset] {
         // 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
-        return await coredataContext.perform {
+        return try await coredataContext.perform {
             // Fetch existing TempTargetStored objects based on their NSManagedObjectIDs
             let tempTargetObjects: [TempTargetStored] = allTempTargetPresetsIDs.compactMap { id in
                 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
-            return tempTargetObjects.compactMap { object in
+            return try tempTargetObjects.compactMap { object in
                 guard let id = object.id,
                       let name = object.name,
                       let target = object.target?.decimalValue,
                       let duration = object.duration?.decimalValue
                 else {
                     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)
             }
@@ -199,10 +199,9 @@ final class TempPresetsIntentRequest: BaseIntentsRequest {
             }
         }
 
-        // Get NSManagedObjectID of all active temp Targets
-        let ids = await tempTargetsStorage.loadLatestTempTargetConfigurations(fetchLimit: 0)
-
         do {
+            // Get NSManagedObjectID of all active temp Targets
+            let ids = try await tempTargetsStorage.loadLatestTempTargetConfigurations(fetchLimit: 0)
             // Fetch existing OverrideStored objects
             let results = try ids.compactMap { id in
                 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"
+        }
+    ]
+}