Просмотр исходного кода

Merge pull request #382 from nightscout/dev

v0.3.0 (13)
Mike Plante 1 год назад
Родитель
Сommit
4988efe390
100 измененных файлов с 52862 добавлено и 32324 удалено
  1. 0 1
      Model/Classes+Properties/OrefDetermination+CoreDataProperties.swift
  2. 43 0
      Model/CoreDataInitializationCoordinator.swift
  3. 100 32
      Model/CoreDataStack.swift
  4. 5 0
      Model/Helper/CarbEntryStored+helper.swift
  5. 5 0
      Model/Helper/Determination+helper.swift
  6. 5 0
      Model/Helper/PumpEvent+helper.swift
  7. 0 1
      Model/TrioCoreDataPersistentContainer.xcdatamodeld/TrioCoreDataPersistentContainer.xcdatamodel/contents
  8. 1 0
      Trio Watch App Extension/WatchState.swift
  9. 144 20
      Trio.xcodeproj/project.pbxproj
  10. BIN
      Trio/Resources/Assets.xcassets/app_icon_images/trioCircledNoBackground.imageset/ComplicationIcon.png
  11. 12 0
      Trio/Resources/Assets.xcassets/app_icon_images/trioCircledNoBackground.imageset/Contents.json
  12. BIN
      Trio/Resources/Assets.xcassets/app_icons/trioCircledNoBackground.imageset/ComplicationIcon.png
  13. 12 0
      Trio/Resources/Assets.xcassets/app_icons/trioCircledNoBackground.imageset/Contents.json
  14. 6 1
      Trio/Resources/Info.plist
  15. 1 1
      Trio/Resources/javascript/bundle/determine-basal.js
  16. 1 3
      Trio/Resources/json/defaults/freeaps/freeaps_settings.json
  17. 20 54
      Trio/Sources/APS/APSManager.swift
  18. 253 44
      Trio/Sources/APS/CGM/PluginSource.swift
  19. 99 24
      Trio/Sources/APS/FetchGlucoseManager.swift
  20. 14 12
      Trio/Sources/APS/OpenAPS/OpenAPS.swift
  21. 0 2
      Trio/Sources/APS/Storage/DeterminationStorage.swift
  22. 61 0
      Trio/Sources/APS/Storage/OverrideStorage.swift
  23. 1 1
      Trio/Sources/APS/Storage/TDDStorage.swift
  24. 3 3
      Trio/Sources/Application/AppDelegate.swift
  25. 94 24
      Trio/Sources/Application/TrioApp.swift
  26. 1 1
      Trio/Sources/Config/Config.swift
  27. 29 0
      Trio/Sources/Helpers/BackgroundTask+Helper.swift
  28. 19 0
      Trio/Sources/Helpers/Calendar+GlucoseStatsChart.swift
  29. 1 1
      Trio/Sources/Helpers/CustomProgressView.swift
  30. 14 0
      Trio/Sources/Helpers/Formatters.swift
  31. 21 0
      Trio/Sources/Helpers/TherapySettingsUtil.swift
  32. 46942 31295
      Trio/Sources/Localizations/Main/Localizable.xcstrings
  33. 3 3
      Trio/Sources/Models/ColorSchemeOption.swift
  34. 0 20
      Trio/Sources/Models/Determination.swift
  35. 1 1
      Trio/Sources/Models/HbA1cDisplayUnit.swift
  36. 2 2
      Trio/Sources/Models/GlucoseColorScheme.swift
  37. 9 4
      Trio/Sources/Models/GlucoseNotificationsOption.swift
  38. 1 0
      Trio/Sources/Models/Icons.swift
  39. 4 0
      Trio/Sources/Models/Oref2_variables.swift
  40. 3 3
      Trio/Sources/Models/Statistics.swift
  41. 6 0
      Trio/Sources/Models/TDD.swift
  42. 0 22
      Trio/Sources/Models/TotalInsulinDisplayType.swift
  43. 3 13
      Trio/Sources/Models/TrioSettings.swift
  44. 6 5
      Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+Helpers.swift
  45. 32 7
      Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+Overrides.swift
  46. 6 5
      Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+TempTargets.swift
  47. 3 3
      Trio/Sources/Modules/Adjustments/AdjustmentsStateModel.swift
  48. 6 6
      Trio/Sources/Modules/Adjustments/View/Overrides/AddOverrideForm.swift
  49. 1 1
      Trio/Sources/Modules/Adjustments/View/Overrides/AdjustmentsRootView+Overrides.swift
  50. 6 6
      Trio/Sources/Modules/Adjustments/View/Overrides/EditOverrideForm.swift
  51. 6 5
      Trio/Sources/Modules/Adjustments/View/TempTargets/AddTempTargetForm.swift
  52. 1 1
      Trio/Sources/Modules/Adjustments/View/TempTargets/EditTempTargetForm.swift
  53. 2 4
      Trio/Sources/Modules/BasalProfileEditor/View/BasalProfileEditorRootView.swift
  54. 0 4
      Trio/Sources/Modules/Base/BaseStateModel.swift
  55. 20 1
      Trio/Sources/Modules/Calibrations/View/CalibrationsRootView.swift
  56. 4 5
      Trio/Sources/Modules/CarbRatioEditor/View/CarbRatioEditorRootView.swift
  57. 12 4
      Trio/Sources/Modules/DataTable/View/CarbEntryEditorView.swift
  58. 10 6
      Trio/Sources/Modules/DataTable/View/DataTableRootView.swift
  59. 0 2
      Trio/Sources/Modules/GlucoseNotificationSettings/GlucoseNotificationSettingsStateModel.swift
  60. 0 22
      Trio/Sources/Modules/GlucoseNotificationSettings/View/GlucoseNotificationSettingsRootView.swift
  61. 52 0
      Trio/Sources/Modules/Home/HomeStateModel+Setup/CurrentTDDSetup.swift
  62. 2 2
      Trio/Sources/Modules/Home/HomeStateModel+Setup/TempTargetSetup.swift
  63. 19 65
      Trio/Sources/Modules/Home/HomeStateModel.swift
  64. 41 16
      Trio/Sources/Modules/Home/View/Chart/ChartElements/GlucoseChartView.swift
  65. 9 2
      Trio/Sources/Modules/Home/View/Chart/ChartElements/OverrideView.swift
  66. 0 3
      Trio/Sources/Modules/Home/View/Chart/MainChartView.swift
  67. 18 22
      Trio/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift
  68. 15 13
      Trio/Sources/Modules/Home/View/Header/LoopStatusView.swift
  69. 8 3
      Trio/Sources/Modules/Home/View/Header/LoopView.swift
  70. 8 37
      Trio/Sources/Modules/Home/View/HomeRootView.swift
  71. 2 2
      Trio/Sources/Modules/ISFEditor/View/ISFEditorRootView.swift
  72. 1 1
      Trio/Sources/Modules/LiveActivitySettings/View/LiveActivityWidgetConfiguration.swift
  73. 35 2
      Trio/Sources/Modules/Main/MainStateModel.swift
  74. 91 0
      Trio/Sources/Modules/Main/View/MainLoadingView.swift
  75. 3 2
      Trio/Sources/Modules/ManualTempBasal/View/ManualTempBasalRootView.swift
  76. 1 5
      Trio/Sources/Modules/Settings/SettingItems.swift
  77. 0 1
      Trio/Sources/Modules/Snooze/View/SnoozeRootView.swift
  78. 144 0
      Trio/Sources/Modules/Stat/StatStateModel+Setup/AreaChartSetup.swift
  79. 274 0
      Trio/Sources/Modules/Stat/StatStateModel+Setup/BolusStatsSetup.swift
  80. 246 0
      Trio/Sources/Modules/Stat/StatStateModel+Setup/LoopChartSetup.swift
  81. 177 0
      Trio/Sources/Modules/Stat/StatStateModel+Setup/MealStatsSetup.swift
  82. 132 0
      Trio/Sources/Modules/Stat/StatStateModel+Setup/StackedChartSetup.swift
  83. 559 0
      Trio/Sources/Modules/Stat/StatStateModel+Setup/TDDSetup.swift
  84. 264 27
      Trio/Sources/Modules/Stat/StatStateModel.swift
  85. 192 0
      Trio/Sources/Modules/Stat/View/StatChartUtils.swift
  86. 346 113
      Trio/Sources/Modules/Stat/View/StatRootView.swift
  87. 0 287
      Trio/Sources/Modules/Stat/View/StatsView.swift
  88. 102 0
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseDistributionChart.swift
  89. 134 0
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseMetricsView.swift
  90. 242 0
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucosePercentileChart.swift
  91. 374 0
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseSectorChart.swift
  92. 411 0
      Trio/Sources/Modules/Stat/View/ViewElements/Insulin/BolusStatsView.swift
  93. 342 0
      Trio/Sources/Modules/Stat/View/ViewElements/Insulin/TotalDailyDoseChart.swift
  94. 83 0
      Trio/Sources/Modules/Stat/View/ViewElements/Looping/LoopBarChartView.swift
  95. 39 0
      Trio/Sources/Modules/Stat/View/ViewElements/Looping/LoopStatsView.swift
  96. 400 0
      Trio/Sources/Modules/Stat/View/ViewElements/Meal/MealStatsView.swift
  97. 2 1
      Trio/Sources/Modules/TargetBehavoir/TargetBehavoirStateModel.swift
  98. 27 5
      Trio/Sources/Modules/TargetBehavoir/View/TargetBehavoirRootView.swift
  99. 13 40
      Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift
  100. 0 0
      Trio/Sources/Modules/Treatments/View/MealPreset/AddMealPresetView.swift

+ 0 - 1
Model/Classes+Properties/OrefDetermination+CoreDataProperties.swift

@@ -37,7 +37,6 @@ public extension OrefDetermination {
     @NSManaged var threshold: NSDecimalNumber?
     @NSManaged var timestamp: Date?
     @NSManaged var timestampEnacted: Date?
-    @NSManaged var totalDailyDose: NSDecimalNumber?
     @NSManaged var forecasts: Set<Forecast>?
 }
 

+ 43 - 0
Model/CoreDataInitializationCoordinator.swift

@@ -0,0 +1,43 @@
+/// This actor provides us with logic to handle cases when a caller
+/// tries to initialize a coreDataStack that is already initialized.
+actor CoreDataInitializationCoordinator {
+    private var isInitialized = false
+    private var initializationTask: Task<Void, Error>?
+
+    /// Ensures that initialization only happens once and manages multiple concurrent initialization requests.
+    /// This actor provides synchronization for the CoreDataStack initialization process.
+    ///
+    /// - Parameters:
+    ///   - initialization: A closure that performs the actual initialization work.
+    /// - Throws: Any error that might occur during initialization.
+    /// - Returns: Void once initialization is complete.
+    func ensureInitialized(perform initialization: @escaping () async throws -> Void) async throws {
+        // If already initialized, return immediately
+        if isInitialized {
+            return
+        }
+
+        // If initialization is in progress, await the existing task
+        if let existingTask = initializationTask {
+            try await existingTask.value
+            return
+        }
+
+        // Start a new initialization task
+        let newTask = Task {
+            do {
+                try await initialization()
+                isInitialized = true
+            } catch {
+                // Clear task reference on failure
+                initializationTask = nil
+                throw error
+            }
+            // Clear task reference on success
+            initializationTask = nil
+        }
+
+        initializationTask = newTask
+        try await newTask.value
+    }
+}

+ 100 - 32
Model/CoreDataStack.swift

@@ -11,6 +11,9 @@ class CoreDataStack: ObservableObject {
 
     let persistentContainer: NSPersistentContainer
 
+    private let maxRetries = 3
+    private let initializationCoordinator = CoreDataInitializationCoordinator()
+
     private init(inMemory: Bool = false) {
         self.inMemory = inMemory
 
@@ -41,29 +44,12 @@ class CoreDataStack: ObservableObject {
         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,
-            object: nil,
-            queue: nil
-        ) { _ in
-            Task {
-                await self.fetchPersistentHistory()
-            }
-        }
     }
 
     deinit {
@@ -84,19 +70,18 @@ class CoreDataStack: ObservableObject {
     }
 
     // Factory method for tests
-    static func createForTests() -> CoreDataStack {
-        CoreDataStack(inMemory: true)
+    static func createForTests() async throws -> CoreDataStack {
+        let stack = CoreDataStack(inMemory: true)
+        try await stack.initializeStack()
+        return stack
     }
 
     // Used for Canvas Preview
-    static var preview: CoreDataStack = {
+    static func preview() async throws -> CoreDataStack {
         let stack = CoreDataStack(inMemory: true)
-        let context = stack.persistentContainer.viewContext
-
-        let pumpHistory = PumpEventStored.makePreviewEvents(count: 10, provider: stack)
-
+        try await stack.initializeStack()
         return stack
-    }()
+    }
 
     // Shared managed object model
     static var managedObjectModel: NSManagedObjectModel = {
@@ -119,7 +104,7 @@ class CoreDataStack: ObservableObject {
         let taskContext = persistentContainer.newBackgroundContext()
 
         /// ensure that the background contexts stay in sync with the main context
-        taskContext.automaticallyMergesChangesFromParent = false
+        taskContext.automaticallyMergesChangesFromParent = true
         taskContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
         taskContext.undoManager = nil
         return taskContext
@@ -183,13 +168,96 @@ class CoreDataStack: ObservableObject {
         }
     }
 
-    func initializeStack() throws {
-        // Force initialization of persistent container
-        let container = persistentContainer
+    private func setupPersistentStoreChangeNotifications() {
+        // Observe Core Data remote change notifications on the queue where the changes were made
+        notificationToken = Foundation.NotificationCenter.default.addObserver(
+            forName: .NSPersistentStoreRemoteChange,
+            object: nil,
+            queue: nil
+        ) { _ in
+            Task {
+                await self.fetchPersistentHistory()
+            }
+        }
+
+        debug(.coreData, "Set up persistent store change notifications")
+    }
+
+    /// Loads the persistent stores asynchronously.
+    ///
+    /// Converts the synchronous NSPersistentContainer loading process into an async/await compatible
+    /// function using a continuation.
+    ///
+    /// - Throws: Any errors encountered during the loading of persistent stores.
+    /// - Returns: Void once stores are loaded successfully
+    private func loadPersistentStores() async throws {
+        try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
+            persistentContainer.loadPersistentStores { storeDescription, error in
+                if let error = error {
+                    warning(.coreData, "Failed to load persistent stores: \(error.localizedDescription)")
+                    continuation.resume(throwing: error)
+                } else {
+                    debug(.coreData, "Successfully loaded persistent store: \(storeDescription.url?.absoluteString ?? "unknown")")
+                    continuation.resume(returning: ())
+                }
+            }
+        }
+    }
+
+    /// Public entry point for initializing the CoreData stack.
+    ///
+    /// Uses the initialization coordinator to ensure initialization happens only once,
+    /// even with concurrent calls. Subsequent calls will wait for the original initialization
+    /// to complete.
+    ///
+    /// - Throws: Any errors that occur during initialization.
+    /// - Returns: Void once initialization is complete.
+    func initializeStack() async throws {
+        try await initializationCoordinator.ensureInitialized {
+            try await self.initializeStack(retryCount: 0)
+        }
+    }
+
+    /// Private implementation of the initialization process with retry capability.
+    ///
+    /// Handles the actual initialization work including store loading, verification,
+    /// notification setup, and error handling with retry logic.
+    ///
+    /// - Parameter retryCount: The current retry attempt number, starting at 0.
+    /// - Throws: CoreDataError or any other error if initialization fails after all retries.
+    /// - Returns: Void when initialization completes successfully.
+    private func initializeStack(retryCount: Int) async throws {
+        do {
+            // Load stores asynchronously
+            try await loadPersistentStores()
+
+            // Verify the store is loaded
+            guard persistentContainer.persistentStoreCoordinator.persistentStores.isEmpty == false else {
+                let error = CoreDataError.storeNotInitializedError(function: #function, file: #file)
+                throw error
+            }
+
+            setupPersistentStoreChangeNotifications()
 
-        // Verify the store is loaded
-        guard container.persistentStoreCoordinator.persistentStores.isEmpty == false else {
-            throw CoreDataError.storeNotInitializedError(function: #function, file: #file)
+            debug(.coreData, "Core Data stack initialized successfully")
+
+        } catch {
+            debug(.coreData, "Failed to initialize Core Data stack: \(error.localizedDescription)")
+
+            // If we still have retries left, try again after a delay
+            if retryCount < maxRetries {
+                debug(.coreData, "Retrying initialization (\(retryCount + 1)/\(maxRetries))")
+
+                // Wait before retrying
+                try await Task.sleep(for: .seconds(1))
+
+                // Retry the initialization
+                try await initializeStack(retryCount: retryCount + 1)
+            } else {
+                // We've exhausted our retries
+                debug(.coreData, "Core Data initialization failed after \(maxRetries) attempts")
+                throw error
+            }
         }
     }
 }

+ 5 - 0
Model/Helper/CarbEntryStored+helper.swift

@@ -12,6 +12,11 @@ extension NSPredicate {
         return NSPredicate(format: "isFPU == false AND date >= %@ AND carbs > 0", date as NSDate)
     }
 
+    static var carbsForStats: NSPredicate {
+        let date = Date.threeMonthsAgo
+        return NSPredicate(format: "date >= %@", date as NSDate)
+    }
+
     static var carbsNotYetUploadedToNightscout: NSPredicate {
         let date = Date.oneDayAgo
         return NSPredicate(

+ 5 - 0
Model/Helper/Determination+helper.swift

@@ -50,4 +50,9 @@ extension NSPredicate {
             true as NSNumber
         )
     }
+
+    static var determinationsForStats: NSPredicate {
+        let date = Date.threeMonthsAgo
+        return NSPredicate(format: "deliverAt >= %@", date as NSDate)
+    }
 }

+ 5 - 0
Model/Helper/PumpEvent+helper.swift

@@ -89,6 +89,11 @@ extension NSPredicate {
         return NSPredicate(format: "timestamp >= %@", date as NSDate)
     }
 
+    static var pumpHistoryForStats: NSPredicate {
+        let date = Date.threeMonthsAgo
+        return NSPredicate(format: "pumpEvent.timestamp >= %@", date as NSDate)
+    }
+
     static var recentPumpHistory: NSPredicate {
         let date = Date.twentyMinutesAgo
         return NSPredicate(

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

@@ -138,7 +138,6 @@
         <attribute name="threshold" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
         <attribute name="timestampEnacted" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
-        <attribute name="totalDailyDose" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <relationship name="forecasts" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Forecast" inverseName="orefDetermination" inverseEntity="Forecast"/>
         <fetchIndex name="byDate">
             <fetchIndexElement property="deliverAt" type="Binary" order="descending"/>

+ 1 - 0
Trio Watch App Extension/WatchState.swift

@@ -350,6 +350,7 @@ import WatchConnectivity
                 // reset input amounts
                 self.bolusAmount = 0
                 self.carbsAmount = 0
+
                 // reset auth progress
                 self.confirmationProgress = 0
             }

+ 144 - 20
Trio.xcodeproj/project.pbxproj

@@ -37,9 +37,7 @@
 		1967DFBE29D052C200759F30 /* Icons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1967DFBD29D052C200759F30 /* Icons.swift */; };
 		1967DFC029D053AC00759F30 /* IconSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1967DFBF29D053AC00759F30 /* IconSelection.swift */; };
 		1967DFC229D053D300759F30 /* IconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1967DFC129D053D300759F30 /* IconImage.swift */; };
-		19A910302A24BF6300C8951B /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A9102F2A24BF6300C8951B /* StatsView.swift */; };
 		19A910362A24D6D700C8951B /* DateFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A910352A24D6D700C8951B /* DateFilter.swift */; };
-		19A910382A24EF3200C8951B /* ChartsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A910372A24EF3200C8951B /* ChartsView.swift */; };
 		19B0EF2128F6D66200069496 /* Statistics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19B0EF2028F6D66200069496 /* Statistics.swift */; };
 		19D466A329AA2B80004D5F33 /* MealSettingsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19D466A229AA2B80004D5F33 /* MealSettingsDataFlow.swift */; };
 		19D466A529AA2BD4004D5F33 /* MealSettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19D466A429AA2BD4004D5F33 /* MealSettingsProvider.swift */; };
@@ -204,14 +202,16 @@
 		38FEF3FC2737E53800574A46 /* MainStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF3FB2737E53800574A46 /* MainStateModel.swift */; };
 		38FEF3FE2738083E00574A46 /* CGMSettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF3FD2738083E00574A46 /* CGMSettingsProvider.swift */; };
 		38FEF413273B317A00574A46 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF412273B317A00574A46 /* HKUnit.swift */; };
+		3B2F77862D7E52ED005ED9FA /* TDD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B2F77852D7E52ED005ED9FA /* TDD.swift */; };
+		3B2F77882D7E5387005ED9FA /* CurrentTDDSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B2F77872D7E5387005ED9FA /* CurrentTDDSetup.swift */; };
+		3BAD36B22D7CDC1A00CC298D /* MainLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAD36B12D7CDC1400CC298D /* MainLoadingView.swift */; };
+		3BAD36CC2D7D420E00CC298D /* CoreDataInitializationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAD36CB2D7D420500CC298D /* CoreDataInitializationCoordinator.swift */; };
 		45252C95D220E796FDB3B022 /* ConfigEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F8A87AA037BD079BA3528BA /* ConfigEditorDataFlow.swift */; };
 		45717281F743594AA9D87191 /* ConfigEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 920DDB21E5D0EB813197500D /* ConfigEditorRootView.swift */; };
 		491D6FBD2D56741C00C49F67 /* TempTargetStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FBC2D56741C00C49F67 /* TempTargetStored+CoreDataProperties.swift */; };
 		491D6FBE2D56741C00C49F67 /* TempTargetRunStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FB92D56741C00C49F67 /* TempTargetRunStored+CoreDataClass.swift */; };
 		491D6FBF2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FBA2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift */; };
 		491D6FC02D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FBB2D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift */; };
-		49249B1C2D46E45E000F4866 /* CurrentTDDSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49249B1B2D46E45E000F4866 /* CurrentTDDSetup.swift */; };
-		49249B382D46E76A000F4866 /* TDD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49249B372D46E76A000F4866 /* TDD.swift */; };
 		49B9B57F2D5768D2009C6B59 /* AdjustmentStored+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49B9B57E2D5768D2009C6B59 /* AdjustmentStored+Helper.swift */; };
 		5075C1608E6249A51495C422 /* TargetsEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BDEA2DC60EDE0A3CA54DC73 /* TargetsEditorProvider.swift */; };
 		53F2382465BF74DB1A967C8B /* PumpConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8630D58BDAD6D9C650B9B39 /* PumpConfigProvider.swift */; };
@@ -292,6 +292,21 @@
 		BD04ECCE2D29952A008C5FEB /* BolusProgressOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD04ECCD2D299522008C5FEB /* BolusProgressOverlay.swift */; };
 		BD0B2EF32C5998E600B3298F /* MealPresetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD0B2EF22C5998E600B3298F /* MealPresetView.swift */; };
 		BD1661312B82ADAB00256551 /* CustomProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1661302B82ADAB00256551 /* CustomProgressView.swift */; };
+		BD249D862D42FBEC00412DEB /* GlucoseMetricsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D852D42FBE600412DEB /* GlucoseMetricsView.swift */; };
+		BD249D882D42FC0000412DEB /* BolusStatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D872D42FBFB00412DEB /* BolusStatsView.swift */; };
+		BD249D8A2D42FC1200412DEB /* GlucosePercentileChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D892D42FC0E00412DEB /* GlucosePercentileChart.swift */; };
+		BD249D8C2D42FC2C00412DEB /* GlucoseDistributionChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D8B2D42FC2500412DEB /* GlucoseDistributionChart.swift */; };
+		BD249D8E2D42FC3900412DEB /* LoopBarChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D8D2D42FC3600412DEB /* LoopBarChartView.swift */; };
+		BD249D902D42FC4500412DEB /* MealStatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D8F2D42FC4300412DEB /* MealStatsView.swift */; };
+		BD249D922D42FC5300412DEB /* GlucoseSectorChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D912D42FC5000412DEB /* GlucoseSectorChart.swift */; };
+		BD249D942D42FC5E00412DEB /* TotalDailyDoseChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D932D42FC5C00412DEB /* TotalDailyDoseChart.swift */; };
+		BD249D972D42FCBF00412DEB /* AreaChartSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D962D42FCBD00412DEB /* AreaChartSetup.swift */; };
+		BD249D992D42FCCD00412DEB /* BolusStatsSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D982D42FCCA00412DEB /* BolusStatsSetup.swift */; };
+		BD249D9B2D42FCDB00412DEB /* LoopChartSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D9A2D42FCD800412DEB /* LoopChartSetup.swift */; };
+		BD249D9D2D42FCF500412DEB /* MealStatsSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D9C2D42FCF300412DEB /* MealStatsSetup.swift */; };
+		BD249D9F2D42FD0600412DEB /* StackedChartSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D9E2D42FD0200412DEB /* StackedChartSetup.swift */; };
+		BD249DA12D42FD1200412DEB /* TDDSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249DA02D42FD1000412DEB /* TDDSetup.swift */; };
+		BD249DA72D42FE4600412DEB /* Calendar+GlucoseStatsChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249DA62D42FE3800412DEB /* Calendar+GlucoseStatsChart.swift */; };
 		BD2B464E0745FBE7B79913F4 /* NightscoutConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF768BD6264FF7D71D66767 /* NightscoutConfigProvider.swift */; };
 		BD2FF1A02AE29D43005D1C5D /* CheckboxToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */; };
 		BD3CC0722B0B89D50013189E /* MainChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD3CC0712B0B89D50013189E /* MainChartView.swift */; };
@@ -490,6 +505,7 @@
 		DD498F2D2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
 		DD4C57A82D73ADEA001BFF2C /* RestartLiveActivityIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4C57A72D73ADEA001BFF2C /* RestartLiveActivityIntent.swift */; };
 		DD4C57AA2D73B3E2001BFF2C /* RestartLiveActivityIntentRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4C57A92D73B3D9001BFF2C /* RestartLiveActivityIntentRequest.swift */; };
+		DD4C581F2D73C43D001BFF2C /* LoopStatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4C581E2D73C43D001BFF2C /* LoopStatsView.swift */; };
 		DD4FFF332D458EE600B6CFF9 /* GarminWatchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4FFF322D458EE600B6CFF9 /* GarminWatchState.swift */; };
 		DD5DC9F12CF3D97C00AB8703 /* AdjustmentsStateModel+Overrides.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5DC9F02CF3D96E00AB8703 /* AdjustmentsStateModel+Overrides.swift */; };
 		DD5DC9F32CF3D9DD00AB8703 /* AdjustmentsStateModel+TempTargets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5DC9F22CF3D9D600AB8703 /* AdjustmentsStateModel+TempTargets.swift */; };
@@ -499,15 +515,16 @@
 		DD68889D2C386E17006E3C44 /* NightscoutExercise.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD68889C2C386E17006E3C44 /* NightscoutExercise.swift */; };
 		DD6B7CB22C7B6F0800B75029 /* Rounding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB12C7B6F0800B75029 /* Rounding.swift */; };
 		DD6B7CB42C7B71F700B75029 /* ForecastDisplayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */; };
-		DD6B7CB62C7B748B00B75029 /* TotalInsulinDisplayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB52C7B748B00B75029 /* TotalInsulinDisplayType.swift */; };
 		DD6B7CB92C7BAC6900B75029 /* NightscoutImportResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB82C7BAC6900B75029 /* NightscoutImportResultView.swift */; };
 		DD6B7CBB2C7FBBFA00B75029 /* ReviewInsulinActionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CBA2C7FBBFA00B75029 /* ReviewInsulinActionView.swift */; };
 		DD6D67E42C9C253500660C9B /* ColorSchemeOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6D67E32C9C253500660C9B /* ColorSchemeOption.swift */; };
 		DD6F63CC2D27F615007D94CF /* TreatmentMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F63CB2D27F606007D94CF /* TreatmentMenuView.swift */; };
+		DD73FA0F2D74F58E00D19D1E /* BackgroundTask+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD73FA0E2D74F57300D19D1E /* BackgroundTask+Helper.swift */; };
 		DD8262CB2D289297009F6F62 /* BolusConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8262CA2D289297009F6F62 /* BolusConfirmationView.swift */; };
 		DD88C8E22C50420800F2D558 /* DefinitionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD88C8E12C50420800F2D558 /* DefinitionRow.swift */; };
 		DD940BAA2CA7585D000830A5 /* GlucoseColorScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */; };
 		DD940BAC2CA75889000830A5 /* DynamicGlucoseColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */; };
+		DD98ACC02D71013200C0778F /* StatChartUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD98ACBF2D71013200C0778F /* StatChartUtils.swift */; };
 		DD9ECB682CA99F4500AA7C45 /* TrioRemoteControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB672CA99F4500AA7C45 /* TrioRemoteControl.swift */; };
 		DD9ECB6A2CA99F6C00AA7C45 /* PushMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB692CA99F6C00AA7C45 /* PushMessage.swift */; };
 		DD9ECB702CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6D2CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift */; };
@@ -524,6 +541,7 @@
 		DDAA29852D2D1D9E006546A1 /* AdjustmentsRootView+TempTargets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAA29842D2D1D98006546A1 /* AdjustmentsRootView+TempTargets.swift */; };
 		DDB37CC52D05048F00D99BF4 /* ContactImageStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB37CC42D05048F00D99BF4 /* ContactImageStorage.swift */; };
 		DDB37CC72D05127500D99BF4 /* FontExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB37CC62D05127500D99BF4 /* FontExtensions.swift */; };
+		DDCAE8332D78D4A800B1BB51 /* TherapySettingsUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCAE8322D78D49C00B1BB51 /* TherapySettingsUtil.swift */; };
 		DDCE790F2D6F97FC000A4D7A /* SubmodulesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCE790E2D6F97F7000A4D7A /* SubmodulesView.swift */; };
 		DDCEBF5B2CC1B76400DF4C36 /* LiveActivity+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCEBF5A2CC1B76400DF4C36 /* LiveActivity+Helper.swift */; };
 		DDD163122C4C689900CD525A /* AdjustmentsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163112C4C689900CD525A /* AdjustmentsStateModel.swift */; };
@@ -533,7 +551,7 @@
 		DDD1631A2C4C695E00CD525A /* EditOverrideForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163192C4C695E00CD525A /* EditOverrideForm.swift */; };
 		DDD1631C2C4C697400CD525A /* AddOverrideForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD1631B2C4C697400CD525A /* AddOverrideForm.swift */; };
 		DDD1631F2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DDD1631D2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodeld */; };
-		DDD6D4D32CDE90720029439A /* HbA1cDisplayUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD6D4D22CDE90720029439A /* HbA1cDisplayUnit.swift */; };
+		DDD6D4D32CDE90720029439A /* EstimatedA1cDisplayUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD6D4D22CDE90720029439A /* EstimatedA1cDisplayUnit.swift */; };
 		DDE179522C910127003CDDB7 /* MealPresetStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179322C910127003CDDB7 /* MealPresetStored+CoreDataClass.swift */; };
 		DDE179532C910127003CDDB7 /* MealPresetStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179332C910127003CDDB7 /* MealPresetStored+CoreDataProperties.swift */; };
 		DDE179542C910127003CDDB7 /* LoopStatRecord+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179342C910127003CDDB7 /* LoopStatRecord+CoreDataClass.swift */; };
@@ -721,9 +739,7 @@
 		1967DFBF29D053AC00759F30 /* IconSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconSelection.swift; sourceTree = "<group>"; };
 		1967DFC129D053D300759F30 /* IconImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconImage.swift; sourceTree = "<group>"; };
 		199561C0275E61A50077B976 /* HealthKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HealthKit.framework; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS8.0.sdk/System/Library/Frameworks/HealthKit.framework; sourceTree = DEVELOPER_DIR; };
-		19A9102F2A24BF6300C8951B /* StatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsView.swift; sourceTree = "<group>"; };
 		19A910352A24D6D700C8951B /* DateFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFilter.swift; sourceTree = "<group>"; };
-		19A910372A24EF3200C8951B /* ChartsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartsView.swift; sourceTree = "<group>"; };
 		19B0EF2028F6D66200069496 /* Statistics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Statistics.swift; sourceTree = "<group>"; };
 		19D466A229AA2B80004D5F33 /* MealSettingsDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealSettingsDataFlow.swift; sourceTree = "<group>"; };
 		19D466A429AA2BD4004D5F33 /* MealSettingsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealSettingsProvider.swift; sourceTree = "<group>"; };
@@ -907,6 +923,10 @@
 		38FEF3FB2737E53800574A46 /* MainStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainStateModel.swift; sourceTree = "<group>"; };
 		38FEF3FD2738083E00574A46 /* CGMSettingsProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGMSettingsProvider.swift; sourceTree = "<group>"; };
 		38FEF412273B317A00574A46 /* HKUnit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HKUnit.swift; sourceTree = "<group>"; };
+		3B2F77852D7E52ED005ED9FA /* TDD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TDD.swift; sourceTree = "<group>"; };
+		3B2F77872D7E5387005ED9FA /* CurrentTDDSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentTDDSetup.swift; sourceTree = "<group>"; };
+		3BAD36B12D7CDC1400CC298D /* MainLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainLoadingView.swift; sourceTree = "<group>"; };
+		3BAD36CB2D7D420500CC298D /* CoreDataInitializationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataInitializationCoordinator.swift; sourceTree = "<group>"; };
 		3BDEA2DC60EDE0A3CA54DC73 /* TargetsEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorProvider.swift; sourceTree = "<group>"; };
 		3BF768BD6264FF7D71D66767 /* NightscoutConfigProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NightscoutConfigProvider.swift; sourceTree = "<group>"; };
 		3F60E97100041040446F44E7 /* PumpConfigStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpConfigStateModel.swift; sourceTree = "<group>"; };
@@ -917,8 +937,6 @@
 		491D6FBA2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetRunStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		491D6FBB2D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		491D6FBC2D56741C00C49F67 /* TempTargetStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
-		49249B1B2D46E45E000F4866 /* CurrentTDDSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentTDDSetup.swift; sourceTree = "<group>"; };
-		49249B372D46E76A000F4866 /* TDD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TDD.swift; sourceTree = "<group>"; };
 		49B9B57E2D5768D2009C6B59 /* AdjustmentStored+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentStored+Helper.swift"; sourceTree = "<group>"; };
 		4DD795BA46B193644D48138C /* TargetsEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorRootView.swift; sourceTree = "<group>"; };
 		505E09DC17A0C3D0AF4B66FE /* ISFEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorStateModel.swift; sourceTree = "<group>"; };
@@ -1000,6 +1018,21 @@
 		BD0B2EF22C5998E600B3298F /* MealPresetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealPresetView.swift; sourceTree = "<group>"; };
 		BD1661302B82ADAB00256551 /* CustomProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomProgressView.swift; sourceTree = "<group>"; };
 		BD1CF8B72C1A4A8400CB930A /* ConfigOverride.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigOverride.xcconfig; sourceTree = "<group>"; };
+		BD249D852D42FBE600412DEB /* GlucoseMetricsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseMetricsView.swift; sourceTree = "<group>"; };
+		BD249D872D42FBFB00412DEB /* BolusStatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusStatsView.swift; sourceTree = "<group>"; };
+		BD249D892D42FC0E00412DEB /* GlucosePercentileChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucosePercentileChart.swift; sourceTree = "<group>"; };
+		BD249D8B2D42FC2500412DEB /* GlucoseDistributionChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseDistributionChart.swift; sourceTree = "<group>"; };
+		BD249D8D2D42FC3600412DEB /* LoopBarChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopBarChartView.swift; sourceTree = "<group>"; };
+		BD249D8F2D42FC4300412DEB /* MealStatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealStatsView.swift; sourceTree = "<group>"; };
+		BD249D912D42FC5000412DEB /* GlucoseSectorChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseSectorChart.swift; sourceTree = "<group>"; };
+		BD249D932D42FC5C00412DEB /* TotalDailyDoseChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TotalDailyDoseChart.swift; sourceTree = "<group>"; };
+		BD249D962D42FCBD00412DEB /* AreaChartSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AreaChartSetup.swift; sourceTree = "<group>"; };
+		BD249D982D42FCCA00412DEB /* BolusStatsSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusStatsSetup.swift; sourceTree = "<group>"; };
+		BD249D9A2D42FCD800412DEB /* LoopChartSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopChartSetup.swift; sourceTree = "<group>"; };
+		BD249D9C2D42FCF300412DEB /* MealStatsSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealStatsSetup.swift; sourceTree = "<group>"; };
+		BD249D9E2D42FD0200412DEB /* StackedChartSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackedChartSetup.swift; sourceTree = "<group>"; };
+		BD249DA02D42FD1000412DEB /* TDDSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TDDSetup.swift; sourceTree = "<group>"; };
+		BD249DA62D42FE3800412DEB /* Calendar+GlucoseStatsChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Calendar+GlucoseStatsChart.swift"; sourceTree = "<group>"; };
 		BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxToggleStyle.swift; sourceTree = "<group>"; };
 		BD3CC0712B0B89D50013189E /* MainChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainChartView.swift; sourceTree = "<group>"; };
 		BD4064D02C4ED26900582F43 /* CoreDataObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataObserver.swift; sourceTree = "<group>"; };
@@ -1198,6 +1231,7 @@
 		DD3A3CE82D29C97800AE478E /* Helper+ButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Helper+ButtonStyles.swift"; sourceTree = "<group>"; };
 		DD4C57A72D73ADEA001BFF2C /* RestartLiveActivityIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestartLiveActivityIntent.swift; sourceTree = "<group>"; };
 		DD4C57A92D73B3D9001BFF2C /* RestartLiveActivityIntentRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestartLiveActivityIntentRequest.swift; sourceTree = "<group>"; };
+		DD4C581E2D73C43D001BFF2C /* LoopStatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopStatsView.swift; sourceTree = "<group>"; };
 		DD4FFF322D458EE600B6CFF9 /* GarminWatchState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarminWatchState.swift; sourceTree = "<group>"; };
 		DD5DC9F02CF3D96E00AB8703 /* AdjustmentsStateModel+Overrides.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentsStateModel+Overrides.swift"; sourceTree = "<group>"; };
 		DD5DC9F22CF3D9D600AB8703 /* AdjustmentsStateModel+TempTargets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentsStateModel+TempTargets.swift"; sourceTree = "<group>"; };
@@ -1207,15 +1241,16 @@
 		DD68889C2C386E17006E3C44 /* NightscoutExercise.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutExercise.swift; sourceTree = "<group>"; };
 		DD6B7CB12C7B6F0800B75029 /* Rounding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Rounding.swift; sourceTree = "<group>"; };
 		DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastDisplayType.swift; sourceTree = "<group>"; };
-		DD6B7CB52C7B748B00B75029 /* TotalInsulinDisplayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TotalInsulinDisplayType.swift; sourceTree = "<group>"; };
 		DD6B7CB82C7BAC6900B75029 /* NightscoutImportResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutImportResultView.swift; sourceTree = "<group>"; };
 		DD6B7CBA2C7FBBFA00B75029 /* ReviewInsulinActionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewInsulinActionView.swift; sourceTree = "<group>"; };
 		DD6D67E32C9C253500660C9B /* ColorSchemeOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorSchemeOption.swift; sourceTree = "<group>"; };
 		DD6F63CB2D27F606007D94CF /* TreatmentMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreatmentMenuView.swift; sourceTree = "<group>"; };
+		DD73FA0E2D74F57300D19D1E /* BackgroundTask+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BackgroundTask+Helper.swift"; sourceTree = "<group>"; };
 		DD8262CA2D289297009F6F62 /* BolusConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusConfirmationView.swift; sourceTree = "<group>"; };
 		DD88C8E12C50420800F2D558 /* DefinitionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefinitionRow.swift; sourceTree = "<group>"; };
 		DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseColorScheme.swift; sourceTree = "<group>"; };
 		DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicGlucoseColor.swift; sourceTree = "<group>"; };
+		DD98ACBF2D71013200C0778F /* StatChartUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatChartUtils.swift; sourceTree = "<group>"; };
 		DD9ECB672CA99F4500AA7C45 /* TrioRemoteControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioRemoteControl.swift; sourceTree = "<group>"; };
 		DD9ECB692CA99F6C00AA7C45 /* PushMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushMessage.swift; sourceTree = "<group>"; };
 		DD9ECB6D2CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigStateModel.swift; sourceTree = "<group>"; };
@@ -1235,6 +1270,7 @@
 		DDB37CC32D05044D00D99BF4 /* ContactTrickEntryStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContactTrickEntryStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDB37CC42D05048F00D99BF4 /* ContactImageStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactImageStorage.swift; sourceTree = "<group>"; };
 		DDB37CC62D05127500D99BF4 /* FontExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontExtensions.swift; sourceTree = "<group>"; };
+		DDCAE8322D78D49C00B1BB51 /* TherapySettingsUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TherapySettingsUtil.swift; sourceTree = "<group>"; };
 		DDCE790E2D6F97F7000A4D7A /* SubmodulesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmodulesView.swift; sourceTree = "<group>"; };
 		DDCEBF5A2CC1B76400DF4C36 /* LiveActivity+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LiveActivity+Helper.swift"; sourceTree = "<group>"; };
 		DDD163112C4C689900CD525A /* AdjustmentsStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdjustmentsStateModel.swift; sourceTree = "<group>"; };
@@ -1244,7 +1280,7 @@
 		DDD163192C4C695E00CD525A /* EditOverrideForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditOverrideForm.swift; sourceTree = "<group>"; };
 		DDD1631B2C4C697400CD525A /* AddOverrideForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddOverrideForm.swift; sourceTree = "<group>"; };
 		DDD1631E2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = TrioCoreDataPersistentContainer.xcdatamodel; sourceTree = "<group>"; };
-		DDD6D4D22CDE90720029439A /* HbA1cDisplayUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HbA1cDisplayUnit.swift; sourceTree = "<group>"; };
+		DDD6D4D22CDE90720029439A /* EstimatedA1cDisplayUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EstimatedA1cDisplayUnit.swift; sourceTree = "<group>"; };
 		DDE179322C910127003CDDB7 /* MealPresetStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MealPresetStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		DDE179332C910127003CDDB7 /* MealPresetStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MealPresetStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDE179342C910127003CDDB7 /* LoopStatRecord+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LoopStatRecord+CoreDataClass.swift"; sourceTree = "<group>"; };
@@ -1571,6 +1607,7 @@
 		19F95FF129F10F9C00314DDC /* Stat */ = {
 			isa = PBXGroup;
 			children = (
+				BD249D952D42FCA800412DEB /* StatStateModel+Setup */,
 				19F95FF229F10FBC00314DDC /* StatDataFlow.swift */,
 				19F95FF429F10FCF00314DDC /* StatProvider.swift */,
 				19F95FF629F10FEE00314DDC /* StatStateModel.swift */,
@@ -1582,9 +1619,9 @@
 		19F95FF829F10FF600314DDC /* View */ = {
 			isa = PBXGroup;
 			children = (
+				BD249D842D42FBD200412DEB /* ViewElements */,
 				19F95FF929F1102A00314DDC /* StatRootView.swift */,
-				19A9102F2A24BF6300C8951B /* StatsView.swift */,
-				19A910372A24EF3200C8951B /* ChartsView.swift */,
+				DD98ACBF2D71013200C0778F /* StatChartUtils.swift */,
 			);
 			path = View;
 			sourceTree = "<group>";
@@ -1706,6 +1743,7 @@
 		3811DE1F25C9D48300A708ED /* View */ = {
 			isa = PBXGroup;
 			children = (
+				3BAD36B12D7CDC1400CC298D /* MainLoadingView.swift */,
 				3811DE2025C9D48300A708ED /* MainRootView.swift */,
 			);
 			path = View;
@@ -2059,6 +2097,7 @@
 		388E5A5925B6F0250019842D /* Models */ = {
 			isa = PBXGroup;
 			children = (
+				3B2F77852D7E52ED005ED9FA /* TDD.swift */,
 				DD4FFF322D458EE600B6CFF9 /* GarminWatchState.swift */,
 				DD3078692D42F94000DE0490 /* GarminDevice.swift */,
 				DD3078672D42F5CE00DE0490 /* WatchGlucoseObject.swift */,
@@ -2111,9 +2150,8 @@
 				BDC2EA462C3045AD00E5BBD0 /* Override.swift */,
 				DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */,
 				DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */,
-				DD6B7CB52C7B748B00B75029 /* TotalInsulinDisplayType.swift */,
 				DD9ECB692CA99F6C00AA7C45 /* PushMessage.swift */,
-				DDD6D4D22CDE90720029439A /* HbA1cDisplayUnit.swift */,
+				DDD6D4D22CDE90720029439A /* EstimatedA1cDisplayUnit.swift */,
 			);
 			path = Models;
 			sourceTree = "<group>";
@@ -2121,6 +2159,9 @@
 		388E5A5A25B6F05F0019842D /* Helpers */ = {
 			isa = PBXGroup;
 			children = (
+				DDCAE8322D78D49C00B1BB51 /* TherapySettingsUtil.swift */,
+				BD249DA62D42FE3800412DEB /* Calendar+GlucoseStatsChart.swift */,
+				DD73FA0E2D74F57300D19D1E /* BackgroundTask+Helper.swift */,
 				CEF1ED6A2D58FB4600FAF41E /* CGMOptions.swift */,
 				C2A0A42E2CE0312C003B98E8 /* ConstantValues.swift */,
 				DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */,
@@ -2379,6 +2420,7 @@
 		58645B972CA2D16A008AFCE7 /* HomeStateModel+Setup */ = {
 			isa = PBXGroup;
 			children = (
+				3B2F77872D7E5387005ED9FA /* CurrentTDDSetup.swift */,
 				BD4E1A7B2D3686D400D21626 /* StartEndMarkerSetup.swift */,
 				BD4E1A792D3681AD00D21626 /* GlucoseTargetSetup.swift */,
 				BDA6CC872CAF219800F942F9 /* TempTargetSetup.swift */,
@@ -2397,6 +2439,7 @@
 		587A54C82BCDCE0F009D38E2 /* Model */ = {
 			isa = PBXGroup;
 			children = (
+				3BAD36CB2D7D420500CC298D /* CoreDataInitializationCoordinator.swift */,
 				BDF34F8F2C10CF8C00D51995 /* CoreDataStack.swift */,
 				BD4064D02C4ED26900582F43 /* CoreDataObserver.swift */,
 				DDD1631D2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodeld */,
@@ -2493,6 +2536,30 @@
 			path = View;
 			sourceTree = "<group>";
 		};
+		BD249D842D42FBD200412DEB /* ViewElements */ = {
+			isa = PBXGroup;
+			children = (
+				DDCAE97A2D79F99B00B1BB51 /* Glucose */,
+				DDCAE9792D79F99200B1BB51 /* Meal */,
+				DDCAE9782D79F98E00B1BB51 /* Insulin */,
+				DDCAE9772D79F98600B1BB51 /* Looping */,
+			);
+			path = ViewElements;
+			sourceTree = "<group>";
+		};
+		BD249D952D42FCA800412DEB /* StatStateModel+Setup */ = {
+			isa = PBXGroup;
+			children = (
+				BD249DA02D42FD1000412DEB /* TDDSetup.swift */,
+				BD249D9C2D42FCF300412DEB /* MealStatsSetup.swift */,
+				BD249D982D42FCCA00412DEB /* BolusStatsSetup.swift */,
+				BD249D962D42FCBD00412DEB /* AreaChartSetup.swift */,
+				BD249D9E2D42FD0200412DEB /* StackedChartSetup.swift */,
+				BD249D9A2D42FCD800412DEB /* LoopChartSetup.swift */,
+			);
+			path = "StatStateModel+Setup";
+			sourceTree = "<group>";
+		};
 		BD793CAD2CE7660C00D669AC /* Overrides */ = {
 			isa = PBXGroup;
 			children = (
@@ -3024,6 +3091,43 @@
 			path = Nightscout;
 			sourceTree = "<group>";
 		};
+		DDCAE9772D79F98600B1BB51 /* Looping */ = {
+			isa = PBXGroup;
+			children = (
+				DD4C581E2D73C43D001BFF2C /* LoopStatsView.swift */,
+				BD249D8D2D42FC3600412DEB /* LoopBarChartView.swift */,
+			);
+			path = Looping;
+			sourceTree = "<group>";
+		};
+		DDCAE9782D79F98E00B1BB51 /* Insulin */ = {
+			isa = PBXGroup;
+			children = (
+				BD249D932D42FC5C00412DEB /* TotalDailyDoseChart.swift */,
+				BD249D872D42FBFB00412DEB /* BolusStatsView.swift */,
+			);
+			path = Insulin;
+			sourceTree = "<group>";
+		};
+		DDCAE9792D79F99200B1BB51 /* Meal */ = {
+			isa = PBXGroup;
+			children = (
+				BD249D8F2D42FC4300412DEB /* MealStatsView.swift */,
+			);
+			path = Meal;
+			sourceTree = "<group>";
+		};
+		DDCAE97A2D79F99B00B1BB51 /* Glucose */ = {
+			isa = PBXGroup;
+			children = (
+				BD249D912D42FC5000412DEB /* GlucoseSectorChart.swift */,
+				BD249D8B2D42FC2500412DEB /* GlucoseDistributionChart.swift */,
+				BD249D892D42FC0E00412DEB /* GlucosePercentileChart.swift */,
+				BD249D852D42FBE600412DEB /* GlucoseMetricsView.swift */,
+			);
+			path = Glucose;
+			sourceTree = "<group>";
+		};
 		DDD163032C4C67B400CD525A /* Adjustments */ = {
 			isa = PBXGroup;
 			children = (
@@ -3595,6 +3699,7 @@
 				CE7CA3552A064973004BE681 /* ListStateIntent.swift in Sources */,
 				BDF530D82B40F8AC002CAF43 /* LockScreenView.swift in Sources */,
 				195D80B72AF697B800D25097 /* DynamicSettingsDataFlow.swift in Sources */,
+				DD98ACC02D71013200C0778F /* StatChartUtils.swift in Sources */,
 				3862CC2E2743F9F700BF832C /* CalendarManager.swift in Sources */,
 				CEA4F62329BE10F70011ADF7 /* SavitzkyGolayFilter.swift in Sources */,
 				38B4F3C325E2A20B00E76A18 /* PumpSetupView.swift in Sources */,
@@ -3646,6 +3751,7 @@
 				3811DEB125C9D88300A708ED /* Keychain.swift in Sources */,
 				DD17453E2C55BFB600211FAC /* AlgorithmAdvancedSettingsStateModel.swift in Sources */,
 				CE95BF572BA5F5FE00DC3DE3 /* PluginManager.swift in Sources */,
+				3BAD36B22D7CDC1A00CC298D /* MainLoadingView.swift in Sources */,
 				382C133725F13A1E00715CE1 /* InsulinSensitivities.swift in Sources */,
 				19D466A529AA2BD4004D5F33 /* MealSettingsProvider.swift in Sources */,
 				DD5DC9F72CF3DA9300AB8703 /* TargetPicker.swift in Sources */,
@@ -3654,7 +3760,7 @@
 				3811DE4125C9D4A100A708ED /* SettingsRootView.swift in Sources */,
 				38192E04261B82FA0094D973 /* ReachabilityManager.swift in Sources */,
 				38E44539274E411700EC9A94 /* Disk+UIImage.swift in Sources */,
-				DD6B7CB62C7B748B00B75029 /* TotalInsulinDisplayType.swift in Sources */,
+				3BAD36CC2D7D420E00CC298D /* CoreDataInitializationCoordinator.swift in Sources */,
 				388E595C25AD948C0019842D /* TrioApp.swift in Sources */,
 				38FEF3FC2737E53800574A46 /* MainStateModel.swift in Sources */,
 				DD1745352C55AE7E00211FAC /* TargetBehavoirRootView.swift in Sources */,
@@ -3672,13 +3778,15 @@
 				3811DE3125C9D49500A708ED /* HomeProvider.swift in Sources */,
 				FE41E4D629463EE20047FD55 /* NightscoutPreferences.swift in Sources */,
 				E013D872273AC6FE0014109C /* GlucoseSimulatorSource.swift in Sources */,
+				BD249D862D42FBEC00412DEB /* GlucoseMetricsView.swift in Sources */,
 				58645BA32CA2D325008AFCE7 /* BatterySetup.swift in Sources */,
 				388E5A5C25B6F0770019842D /* JSON.swift in Sources */,
 				3811DF0225CA9FEA00A708ED /* Credentials.swift in Sources */,
 				5837A5302BD2E3C700A5DC04 /* CarbEntryStored+helper.swift in Sources */,
 				389A572026079BAA00BC102F /* Interpolation.swift in Sources */,
 				DD9ECB702CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift in Sources */,
-				19A910382A24EF3200C8951B /* ChartsView.swift in Sources */,
+				BD249D942D42FC5E00412DEB /* TotalDailyDoseChart.swift in Sources */,
+				BD249D902D42FC4500412DEB /* MealStatsView.swift in Sources */,
 				DD32CF9A2CC8247B003686D6 /* TrioRemoteControl+Meal.swift in Sources */,
 				BDF34F832C10C5B600D51995 /* DataManager.swift in Sources */,
 				38B4F3C625E5017E00E76A18 /* NotificationCenter.swift in Sources */,
@@ -3691,6 +3799,7 @@
 				38A13D3225E28B4B00EAA382 /* PumpHistoryEvent.swift in Sources */,
 				E00EEC0627368630002FF094 /* UIAssembly.swift in Sources */,
 				3811DE1825C9D40400A708ED /* Router.swift in Sources */,
+				BD249DA12D42FD1200412DEB /* TDDSetup.swift in Sources */,
 				CE7950262998056D00FA576E /* CGMSetupView.swift in Sources */,
 				582FAE432C05102C00D1C13F /* CoreDataError.swift in Sources */,
 				38A0363B25ECF07E00FCBB52 /* GlucoseStorage.swift in Sources */,
@@ -3709,7 +3818,7 @@
 				DDA6E3202D258E0500C2988C /* OverrideHelpView.swift in Sources */,
 				DDA6E2502D22187500C2988C /* ChartLegendView.swift in Sources */,
 				3811DEAF25C9D88300A708ED /* KeyValueStorage.swift in Sources */,
-				DDD6D4D32CDE90720029439A /* HbA1cDisplayUnit.swift in Sources */,
+				DDD6D4D32CDE90720029439A /* EstimatedA1cDisplayUnit.swift in Sources */,
 				DDA6E3572D25988500C2988C /* ContactImageHelpView.swift in Sources */,
 				38FE826D25CC8461001FF17A /* NightscoutAPI.swift in Sources */,
 				388358C825EEF6D200E024B2 /* BasalProfileEntry.swift in Sources */,
@@ -3724,6 +3833,7 @@
 				CE1F6DE92BAF37C90064EB8D /* TidepoolConfigView.swift in Sources */,
 				3811DE5D25C9D4D500A708ED /* Publisher.swift in Sources */,
 				E00EEC0727368630002FF094 /* APSAssembly.swift in Sources */,
+				BD249D8A2D42FC1200412DEB /* GlucosePercentileChart.swift in Sources */,
 				38B4F3AF25E2979F00E76A18 /* IndexedCollection.swift in Sources */,
 				58D08B222C8DAA8E00AA37D3 /* OverrideView.swift in Sources */,
 				BD0B2EF32C5998E600B3298F /* MealPresetView.swift in Sources */,
@@ -3731,6 +3841,7 @@
 				DD6D67E42C9C253500660C9B /* ColorSchemeOption.swift in Sources */,
 				582DF9752C8CDB92001F516D /* GlucoseChartView.swift in Sources */,
 				BD432CA12D2F4E3600D1EB79 /* WatchMessageKeys.swift in Sources */,
+				DD4C581F2D73C43D001BFF2C /* LoopStatsView.swift in Sources */,
 				58A3D53A2C96D4DE003F90FC /* AddTempTargetForm.swift in Sources */,
 				DD1745302C55AE5300211FAC /* TargetBehaviorProvider.swift in Sources */,
 				58D08B382C8DFB6000AA37D3 /* BasalChart.swift in Sources */,
@@ -3761,16 +3872,17 @@
 				3811DE6125C9D4D500A708ED /* ViewModifiers.swift in Sources */,
 				3811DEAC25C9D88300A708ED /* NightscoutManager.swift in Sources */,
 				BD793CB22CE8033500D669AC /* TempTargetRunStored.swift in Sources */,
-				19A910302A24BF6300C8951B /* StatsView.swift in Sources */,
 				BD7DA9A92AE06E9200601B20 /* BolusCalculatorStateModel.swift in Sources */,
 				CEB434E528B8FF5D00B70274 /* UIColor.swift in Sources */,
 				190EBCCB29FF13CB00BA767D /* UserInterfaceSettingsRootView.swift in Sources */,
 				3811DEA925C9D88300A708ED /* AppearanceManager.swift in Sources */,
+				BD249D8E2D42FC3900412DEB /* LoopBarChartView.swift in Sources */,
 				CE7950242997D81700FA576E /* CGMSettingsView.swift in Sources */,
 				58237D9E2BCF0A6B00A47A79 /* PopupView.swift in Sources */,
 				BD793CB02CE7C61500D669AC /* OverrideRunStored+helper.swift in Sources */,
 				38D0B3D925EC07C400CB6E88 /* CarbsEntry.swift in Sources */,
 				DD32CF9C2CC82499003686D6 /* TrioRemoteControl+TempTarget.swift in Sources */,
+				BD249D882D42FC0000412DEB /* BolusStatsView.swift in Sources */,
 				38A9260525F012D8009E3739 /* CarbRatios.swift in Sources */,
 				38FCF3D625E8FDF40078B0D1 /* MD5.swift in Sources */,
 				DDD163142C4C68D300CD525A /* AdjustmentsProvider.swift in Sources */,
@@ -3778,6 +3890,7 @@
 				191F62682AD6B05A004D7911 /* NightscoutSettings.swift in Sources */,
 				3811DEAB25C9D88300A708ED /* HTTPResponseStatus.swift in Sources */,
 				3811DE5F25C9D4D500A708ED /* ProgressBar.swift in Sources */,
+				BD249D8C2D42FC2C00412DEB /* GlucoseDistributionChart.swift in Sources */,
 				38E87408274F9AD000975559 /* UserNotificationsManager.swift in Sources */,
 				CE82E02528E867BA00473A9C /* AlertStorage.swift in Sources */,
 				DD1745372C55B74200211FAC /* AlgorithmSettings.swift in Sources */,
@@ -3833,6 +3946,7 @@
 				1935364028496F7D001E0B16 /* Oref2_variables.swift in Sources */,
 				CE2FAD3A297D93F0001A872C /* BloodGlucoseExtensions.swift in Sources */,
 				38E4453A274E411700EC9A94 /* Disk+[UIImage].swift in Sources */,
+				DD73FA0F2D74F58E00D19D1E /* BackgroundTask+Helper.swift in Sources */,
 				72F1BD388F42FCA6C52E4500 /* ConfigEditorProvider.swift in Sources */,
 				E39E418C56A5A46B61D960EE /* ConfigEditorStateModel.swift in Sources */,
 				45717281F743594AA9D87191 /* ConfigEditorRootView.swift in Sources */,
@@ -3856,9 +3970,11 @@
 				491D6FBF2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift in Sources */,
 				491D6FC02D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift in Sources */,
 				DD1745442C55C60E00211FAC /* AutosensSettingsDataFlow.swift in Sources */,
+				BD249DA72D42FE4600412DEB /* Calendar+GlucoseStatsChart.swift in Sources */,
 				BDCAF2382C639F35002DC907 /* SettingItems.swift in Sources */,
 				58D08B342C8DF9A700AA37D3 /* CobIobChart.swift in Sources */,
 				642F76A05A4FF530463A9FD0 /* NightscoutConfigRootView.swift in Sources */,
+				BD249D9B2D42FCDB00412DEB /* LoopChartSetup.swift in Sources */,
 				BD7DA9AC2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift in Sources */,
 				AD3D2CD42CD01B9EB8F26522 /* PumpConfigDataFlow.swift in Sources */,
 				DD17452E2C55AE4800211FAC /* TargetBehavoirDataFlow.swift in Sources */,
@@ -3893,6 +4009,7 @@
 				DD9ECB712CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift in Sources */,
 				63E890B4D951EAA91C071D5C /* BasalProfileEditorStateModel.swift in Sources */,
 				38FEF3FA2737E42000574A46 /* BaseStateModel.swift in Sources */,
+				DDCAE8332D78D4A800B1BB51 /* TherapySettingsUtil.swift in Sources */,
 				BDA25EFD2D261C0000035F34 /* WatchState.swift in Sources */,
 				CC6C406E2ACDD69E009B8058 /* RawFetchedProfile.swift in Sources */,
 				385CEA8225F23DFD002D6D5B /* NightscoutStatus.swift in Sources */,
@@ -3911,6 +4028,7 @@
 				DDF847DF2C5C28780049BB3B /* LiveActivitySettingsProvider.swift in Sources */,
 				DDB37CC52D05048F00D99BF4 /* ContactImageStorage.swift in Sources */,
 				BD54A95B2D28087C00F9C1EE /* OverridePresetWatch.swift in Sources */,
+				3B2F77882D7E5387005ED9FA /* CurrentTDDSetup.swift in Sources */,
 				DBA5254DBB2586C98F61220C /* ISFEditorProvider.swift in Sources */,
 				BDF34EBE2C0A31D100D51995 /* CustomNotification.swift in Sources */,
 				BDC2EA472C3045AD00E5BBD0 /* Override.swift in Sources */,
@@ -3923,6 +4041,7 @@
 				BD7DA9A72AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift in Sources */,
 				38192E0D261BAF980094D973 /* ConvenienceExtensions.swift in Sources */,
 				88AB39B23C9552BD6E0C9461 /* ISFEditorRootView.swift in Sources */,
+				BD249D9D2D42FCF500412DEB /* MealStatsSetup.swift in Sources */,
 				BD6EB2D62C7D049B0086BBB6 /* LiveActivityWidgetConfiguration.swift in Sources */,
 				DD32CF982CC82463003686D6 /* TrioRemoteControl+Bolus.swift in Sources */,
 				F816825E28DB441200054060 /* HeartBeatManager.swift in Sources */,
@@ -3948,6 +4067,7 @@
 				DD17453A2C55BFA600211FAC /* AlgorithmAdvancedSettingsDataFlow.swift in Sources */,
 				5075C1608E6249A51495C422 /* TargetsEditorProvider.swift in Sources */,
 				E13B7DAB2A435F57066AF02E /* TargetsEditorStateModel.swift in Sources */,
+				BD249D992D42FCCD00412DEB /* BolusStatsSetup.swift in Sources */,
 				9702FF92A09C53942F20D7EA /* TargetsEditorRootView.swift in Sources */,
 				1967DFBE29D052C200759F30 /* Icons.swift in Sources */,
 				DDD163182C4C694000CD525A /* AdjustmentsRootView.swift in Sources */,
@@ -3966,6 +4086,7 @@
 				DDAA29852D2D1D9E006546A1 /* AdjustmentsRootView+TempTargets.swift in Sources */,
 				E592A3792CEEC038009A472C /* ContactImageRootView.swift in Sources */,
 				BDC531182D1062F200088832 /* ContactImageState.swift in Sources */,
+				BD249D9F2D42FD0600412DEB /* StackedChartSetup.swift in Sources */,
 				E592A37A2CEEC038009A472C /* ContactImageProvider.swift in Sources */,
 				CE82E02728E869DF00473A9C /* AlertEntry.swift in Sources */,
 				DD30786A2D42F94000DE0490 /* GarminDevice.swift in Sources */,
@@ -3977,6 +4098,7 @@
 				38AAF85525FFF846004AF583 /* CurrentGlucoseView.swift in Sources */,
 				041D1E995A6AE92E9289DC49 /* TreatmentsDataFlow.swift in Sources */,
 				DD32CF9E2CC824C5003686D6 /* TrioRemoteControl+Override.swift in Sources */,
+				BD249D922D42FC5300412DEB /* GlucoseSectorChart.swift in Sources */,
 				23888883D4EA091C88480FF2 /* TreatmentsProvider.swift in Sources */,
 				38E98A2D25F52DC400C0CED0 /* NSLocking+Extensions.swift in Sources */,
 				715120D22D3C2BB4005D9FB6 /* GlucoseNotificationsOption.swift in Sources */,
@@ -4008,6 +4130,7 @@
 				38E44534274E411700EC9A94 /* Disk+InternalHelpers.swift in Sources */,
 				38A00B2325FC2B55006BC0B0 /* LRUCache.swift in Sources */,
 				DDD163122C4C689900CD525A /* AdjustmentsStateModel.swift in Sources */,
+				3B2F77862D7E52ED005ED9FA /* TDD.swift in Sources */,
 				DD1745132C54169400211FAC /* DevicesView.swift in Sources */,
 				7F7B756BE8543965D9FDF1A2 /* DataTableDataFlow.swift in Sources */,
 				1D845DF2E3324130E1D95E67 /* DataTableProvider.swift in Sources */,
@@ -4029,6 +4152,7 @@
 				6EADD581738D64431902AC0A /* (null) in Sources */,
 				CE94598729E9E4110047C9C6 /* WatchConfigRootView.swift in Sources */,
 				DD5DC9FB2CF3E1B100AB8703 /* AdjustmentsStateModel+Helpers.swift in Sources */,
+				BD249D972D42FCBF00412DEB /* AreaChartSetup.swift in Sources */,
 				DDF847E42C5C288F0049BB3B /* LiveActivitySettingsRootView.swift in Sources */,
 				DD88C8E22C50420800F2D558 /* DefinitionRow.swift in Sources */,
 				B7C465E9472624D8A2BE2A6A /* (null) in Sources */,

BIN
Trio/Resources/Assets.xcassets/app_icon_images/trioCircledNoBackground.imageset/ComplicationIcon.png


+ 12 - 0
Trio/Resources/Assets.xcassets/app_icon_images/trioCircledNoBackground.imageset/Contents.json

@@ -0,0 +1,12 @@
+{
+  "images" : [
+    {
+      "filename" : "ComplicationIcon.png",
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
Trio/Resources/Assets.xcassets/app_icons/trioCircledNoBackground.imageset/ComplicationIcon.png


+ 12 - 0
Trio/Resources/Assets.xcassets/app_icons/trioCircledNoBackground.imageset/Contents.json

@@ -0,0 +1,12 @@
+{
+  "images" : [
+    {
+      "filename" : "ComplicationIcon.png",
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 6 - 1
Trio/Resources/Info.plist

@@ -107,7 +107,12 @@
 	<key>UIFileSharingEnabled</key>
 	<true/>
 	<key>UILaunchScreen</key>
-	<dict/>
+	<dict>
+		<key>UIColorName</key>
+		<string>Background_DarkBlue</string>
+		<key>UIImageName</key>
+		<string>trioCircledNoBackground</string>
+	</dict>
 	<key>UIRequiredDeviceCapabilities</key>
 	<array>
 		<string>armv7</string>

Разница между файлами не показана из-за своего большого размера
+ 1 - 1
Trio/Resources/javascript/bundle/determine-basal.js


+ 1 - 3
Trio/Resources/json/defaults/freeaps/freeaps_settings.json

@@ -14,21 +14,19 @@
   "displayCalendarIOBandCOB" : false,
   "glucoseBadge" : false,
   "glucoseNotificationsAlways" : false,
-  "useAlarmSound" : false,
   "addSourceInfoToGlucoseNotifications" : false,
   "lowGlucose" : 72,
   "highGlucose" : 270,
   "carbsRequiredThreshold" : 10,
   "showCarbsRequiredBadge" : true,
   "useFPUconversion" : true,
-  "totalInsulinDisplayType": "totalDailyDose",
   "individualAdjustmentFactor" : 0.5,
   "timeCap" : 8,
   "minuteInterval" : 30,
   "delay" : 60,
   "useAppleHealth" : false,
   "smoothGlucose" : false,
-  "hbA1cDisplayUnit" : "percent",
+  "eA1cDisplayUnit" : "percent",
   "high" : 180,
   "low" : 70,
   "hours" : 6,

+ 20 - 54
Trio/Sources/APS/APSManager.swift

@@ -81,7 +81,7 @@ final class BaseAPSManager: APSManager, Injectable {
 
     private var lifetime = Lifetime()
 
-    private var backGroundTaskID: UIBackgroundTaskIdentifier?
+    private var backgroundTaskID: UIBackgroundTaskIdentifier?
 
     var pumpManager: PumpManagerUI? {
         get { deviceDataManager.pumpManager }
@@ -224,7 +224,7 @@ final class BaseAPSManager: APSManager, Injectable {
             // Cleanup background task
             if let backgroundTask = backgroundTask {
                 await UIApplication.shared.endBackgroundTask(backgroundTask)
-                self.backGroundTaskID = .invalid
+                self.backgroundTaskID = .invalid
             }
         }
     }
@@ -250,13 +250,13 @@ final class BaseAPSManager: APSManager, Injectable {
     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 }
+            guard let self, let backgroundTask = self.backgroundTaskID else { return }
             Task {
                 UIApplication.shared.endBackgroundTask(backgroundTask)
             }
-            self.backGroundTaskID = .invalid
+            self.backgroundTaskID = .invalid
         }
-        backGroundTaskID = backgroundTask
+        backgroundTaskID = backgroundTask
 
         // Set loop start time
         lastLoopStartDate = Date()
@@ -325,9 +325,9 @@ final class BaseAPSManager: APSManager, Injectable {
 
         if let error = error {
             warning(.apsManager, "Loop failed with error: \(error.localizedDescription)")
-            if let backgroundTask = backGroundTaskID {
+            if let backgroundTask = backgroundTaskID {
                 await UIApplication.shared.endBackgroundTask(backgroundTask)
-                backGroundTaskID = .invalid
+                backgroundTaskID = .invalid
             }
             processError(error)
         } else {
@@ -343,9 +343,9 @@ final class BaseAPSManager: APSManager, Injectable {
         }
 
         // End of the BG tasks
-        if let backgroundTask = backGroundTaskID {
+        if let backgroundTask = backgroundTaskID {
             await UIApplication.shared.endBackgroundTask(backgroundTask)
-            backGroundTaskID = .invalid
+            backgroundTaskID = .invalid
         }
     }
 
@@ -973,7 +973,8 @@ final class BaseAPSManager: APSManager, Injectable {
                 total_average: 0
             )
             guard let processedGlucoseStats = await glucoseStats else { return }
-            let hbA1cDisplayUnit = processedGlucoseStats.hbA1cDisplayUnit
+
+            let eA1cDisplayUnit = processedGlucoseStats.eA1cDisplayUnit
 
             let dailystat = await Statistics(
                 created_at: Date(),
@@ -995,8 +996,8 @@ final class BaseAPSManager: APSManager, Injectable {
                 Statistics: Stats(
                     Distribution: processedGlucoseStats.TimeInRange,
                     Glucose: processedGlucoseStats.avg,
-                    HbA1c: processedGlucoseStats.hbs,
-                    Units: Units(Glucose: units.rawValue, HbA1c: hbA1cDisplayUnit.rawValue),
+                    EstimatedA1c: processedGlucoseStats.hbs,
+                    Units: Units(Glucose: units.rawValue, EstimatedA1c: eA1cDisplayUnit.rawValue),
                     LoopCycles: loopStats,
                     Insulin: insulin,
                     Variance: processedGlucoseStats.variance
@@ -1116,44 +1117,9 @@ final class BaseAPSManager: APSManager, Injectable {
         }
     }
 
-    private func tddForStats() async -> (currentTDD: Decimal, tddTotalAverage: Decimal) {
-        let requestTDD = OrefDetermination.fetchRequest() as NSFetchRequest<NSFetchRequestResult>
-        let sort = NSSortDescriptor(key: "timestamp", ascending: false)
-        let daysOf14Ago = Date().addingTimeInterval(-14.days.timeInterval)
-        requestTDD.predicate = NSPredicate(format: "timestamp > %@", daysOf14Ago as NSDate)
-        requestTDD.sortDescriptors = [sort]
-        requestTDD.propertiesToFetch = ["timestamp", "totalDailyDose"]
-        requestTDD.resultType = .dictionaryResultType
-
-        var currentTDD: Decimal = 0
-        var tddTotalAverage: Decimal = 0
-
-        let results = await privateContext.perform {
-            do {
-                let fetchedResults = try self.privateContext.fetch(requestTDD) as? [[String: Any]]
-                return fetchedResults ?? []
-            } catch {
-                debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to get TDD Data for Statistics Upload")
-                return []
-            }
-        }
-
-        if !results.isEmpty {
-            if let latestTDD = results.first?["totalDailyDose"] as? NSDecimalNumber {
-                currentTDD = latestTDD.decimalValue
-            }
-            let tddArray = results.compactMap { ($0["totalDailyDose"] as? NSDecimalNumber)?.decimalValue }
-            if !tddArray.isEmpty {
-                tddTotalAverage = tddArray.reduce(0, +) / Decimal(tddArray.count)
-            }
-        }
-
-        return (currentTDD, tddTotalAverage)
-    }
-
     private func glucoseForStats() async -> (
         oneDayGlucose: (ifcc: Double, ngsp: Double, average: Double, median: Double, sd: Double, cv: Double, readings: Double),
-        hbA1cDisplayUnit: HbA1cDisplayUnit,
+        eA1cDisplayUnit: EstimatedA1cDisplayUnit,
         numberofDays: Double,
         TimeInRange: TIRs,
         avg: Averages,
@@ -1202,19 +1168,19 @@ final class BaseAPSManager: APSManager, Injectable {
                     total: self.roundDecimal(Decimal(totalDaysGlucose.median), 1)
                 )
 
-                let hbA1cDisplayUnit = self.settingsManager.settings.hbA1cDisplayUnit
+                let eA1cDisplayUnit = self.settingsManager.settings.eA1cDisplayUnit
 
                 let hbs = Durations(
-                    day: hbA1cDisplayUnit == .mmolMol ?
+                    day: eA1cDisplayUnit == .mmolMol ?
                         self.roundDecimal(Decimal(oneDayGlucose.ifcc), 1) :
                         self.roundDecimal(Decimal(oneDayGlucose.ngsp), 1),
-                    week: hbA1cDisplayUnit == .mmolMol ?
+                    week: eA1cDisplayUnit == .mmolMol ?
                         self.roundDecimal(Decimal(sevenDaysGlucose.ifcc), 1) :
                         self.roundDecimal(Decimal(sevenDaysGlucose.ngsp), 1),
-                    month: hbA1cDisplayUnit == .mmolMol ?
+                    month: eA1cDisplayUnit == .mmolMol ?
                         self.roundDecimal(Decimal(thirtyDaysGlucose.ifcc), 1) :
                         self.roundDecimal(Decimal(thirtyDaysGlucose.ngsp), 1),
-                    total: hbA1cDisplayUnit == .mmolMol ?
+                    total: eA1cDisplayUnit == .mmolMol ?
                         self.roundDecimal(Decimal(totalDaysGlucose.ifcc), 1) :
                         self.roundDecimal(Decimal(totalDaysGlucose.ngsp), 1)
                 )
@@ -1289,7 +1255,7 @@ final class BaseAPSManager: APSManager, Injectable {
                 )
                 let variance = Variance(SD: standardDeviations, CV: cvs)
 
-                return (oneDayGlucose, hbA1cDisplayUnit, numberOfDays, TimeInRange, avg, hbs, variance)
+                return (oneDayGlucose, eA1cDisplayUnit, numberOfDays, TimeInRange, avg, hbs, variance)
             }
         } catch {
             debug(

+ 253 - 44
Trio/Sources/APS/CGM/PluginSource.swift

@@ -26,45 +26,222 @@ final class PluginSource: GlucoseSource {
         cgmManager?.cgmManagerDelegate = self
     }
 
-    /// Function that fetches blood glucose data
-    /// This function combines two data fetching mechanisms (`callBLEFetch` and `fetchIfNeeded`) into a single publisher.
-    /// It returns the first non-empty result from either of the sources within a 5-minute timeout period.
+    /// Fetches blood glucose data from available sources.
+    ///
+    /// This function combines two fetching mechanisms (`callBLEFetch` and `fetchIfNeeded`) into a single publisher.
+    /// It returns the first non-empty result from either of the sources within a 2-minute timeout period.
     /// If no valid data is fetched within the timeout, it returns an empty array.
     ///
     /// - Parameter timer: An optional `DispatchTimer` (not used in the function but can be used to trigger fetch logic).
     /// - Returns: An `AnyPublisher` that emits an array of `BloodGlucose` values or an empty array if an error occurs or the timeout is reached.
     func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
-        Publishers.Merge(
-            callBLEFetch(),
+        debug(.deviceManager, "PluginSource: fetch() called - combining BLE fetch and fetchIfNeeded")
+
+        // Check if CGM manager is available
+        if cgmManager == nil {
+            debug(.deviceManager, "PluginSource: fetch() - No CGM manager available, returning empty array immediately")
+            return Just([]).eraseToAnyPublisher()
+        }
+
+        // Create a publisher that will emit a timeout event after 2 minutes
+        let timeoutPublisher = Just(())
+            .delay(for: .seconds(120), scheduler: processQueue)
+            .map { _ -> [BloodGlucose] in
+                debug(.deviceManager, "PluginSource: fetch() - Global timeout reached, returning empty array")
+                return []
+            }
+            .eraseToAnyPublisher()
+
+        // Combine the BLE fetch, fetchIfNeeded, and timeout publishers
+        return Publishers.Merge3(
+            callBLEFetch()
+                .handleEvents(receiveOutput: { values in
+                    if !values.isEmpty {
+                        debug(.deviceManager, "PluginSource: fetch() - callBLEFetch returned \(values.count) values")
+                    }
+                }),
             fetchIfNeeded()
+                .handleEvents(receiveOutput: { values in
+                    if !values.isEmpty {
+                        debug(.deviceManager, "PluginSource: fetch() - fetchIfNeeded returned \(values.count) values")
+                    }
+                }),
+            timeoutPublisher
         )
-        .filter { !$0.isEmpty }
+        .filter { values in
+            let isEmpty = values.isEmpty
+            debug(.deviceManager, "PluginSource: filter - received \(values.count) values, isEmpty: \(isEmpty)")
+            return !isEmpty
+        }
         .first()
-        .timeout(60 * 5, scheduler: processQueue, options: nil, customError: nil)
+        .handleEvents(
+            receiveSubscription: { _ in debug(.deviceManager, "PluginSource: fetch publisher received subscription") },
+            receiveOutput: { values in
+                debug(.deviceManager, "PluginSource: fetch publisher emitting \(values.count) values") },
+            receiveCompletion: { completion in
+                if case .finished = completion {
+                    debug(.deviceManager, "PluginSource: fetch publisher completed normally")
+                } else {
+                    debug(.deviceManager, "PluginSource: fetch publisher completed with error or cancellation")
+                }
+            },
+            receiveCancel: { debug(.deviceManager, "PluginSource: fetch publisher was cancelled") }
+        )
         .replaceError(with: [])
+        .replaceEmpty(with: [])
         .eraseToAnyPublisher()
     }
 
+    /// Initiates a BLE-based blood glucose data fetch.
+    ///
+    /// This function returns a future-based publisher that will wait for a response from the BLE device.
+    /// If a response is not received within 60 seconds, the fetch operation times out and returns an empty array.
+    ///
+    /// - Returns: An `AnyPublisher` that emits an array of `BloodGlucose` values or an empty array if the fetch fails or times out.
     func callBLEFetch() -> AnyPublisher<[BloodGlucose], Never> {
-        Future<[BloodGlucose], Error> { [weak self] promise in
-            self?.promise = promise
+        debug(.deviceManager, "PluginSource: callBLEFetch() called")
+        return Future<[BloodGlucose], Error> { [weak self] promise in
+            guard let self = self else {
+                debug(.deviceManager, "PluginSource: callBLEFetch - self is nil, returning empty array")
+                promise(.success([]))
+                return
+            }
+
+            debug(.deviceManager, "PluginSource: callBLEFetch - storing promise for future resolution")
+
+            // If there's already a promise, resolve it with an empty array to avoid memory leaks
+            if self.promise != nil {
+                debug(.deviceManager, "PluginSource: callBLEFetch - found existing promise, resolving it with empty array")
+                self.promise?(.success([]))
+            }
+
+            // Store the new promise
+            self.promise = promise
+
+            // Create a timeout work item
+            let timeoutWorkItem = DispatchWorkItem { [weak self] in
+                guard let self = self else { return }
+
+                // Check if we still have a promise (it hasn't been fulfilled yet)
+                if self.promise != nil {
+                    debug(.deviceManager, "PluginSource: callBLEFetch - timeout reached, resolving promise with empty array")
+                    self.promise?(.success([]))
+                    self.promise = nil
+                }
+            }
+
+            // Schedule the timeout
+            self.processQueue.asyncAfter(deadline: .now() + 60, execute: timeoutWorkItem)
         }
+        .handleEvents(
+            receiveSubscription: { _ in debug(.deviceManager, "PluginSource: callBLEFetch received subscription") },
+            receiveOutput: { values in
+                debug(.deviceManager, "PluginSource: callBLEFetch received output with \(values.count) values") },
+            receiveCompletion: { completion in
+                if case let .failure(error) = completion {
+                    debug(.deviceManager, "PluginSource: callBLEFetch completed with error: \(error.localizedDescription)")
+                } else {
+                    debug(.deviceManager, "PluginSource: callBLEFetch completed successfully")
+                }
+            }
+        )
         .timeout(60 * 5, scheduler: processQueue, options: nil, customError: nil)
         .replaceError(with: [])
         .replaceEmpty(with: [])
         .eraseToAnyPublisher()
     }
 
+    /// Fetches new blood glucose data from the CGM if needed.
+    ///
+    /// This function communicates with the CGM manager to request new data, if available.
+    /// If the CGM manager is unavailable or no new data is provided within 30 seconds, an empty array is returned.
+    ///
+    /// - Returns: An `AnyPublisher` that emits an array of `BloodGlucose` values or an empty array if no new data is available or the operation times out.
     func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never> {
-        Future<[BloodGlucose], Error> { [weak self] promise in
-            guard let self = self else { return }
+        debug(.deviceManager, "PluginSource: fetchIfNeeded() called")
+        return Future<[BloodGlucose], Error> { [weak self] promise in
+            guard let self = self else {
+                debug(.deviceManager, "PluginSource: fetchIfNeeded - self is nil, returning empty array")
+                promise(.success([]))
+                return
+            }
+            debug(.deviceManager, "PluginSource: fetchIfNeeded - about to dispatch to processQueue")
             self.processQueue.async {
-                guard let cgmManager = self.cgmManager else { return }
+                guard let cgmManager = self.cgmManager else {
+                    debug(.deviceManager, "PluginSource: fetchIfNeeded - cgmManager is nil, returning empty array")
+                    promise(.success([]))
+                    return
+                }
+
+                // Log CGM manager details
+                debug(.deviceManager, "PluginSource: fetchIfNeeded - using CGM manager of type: \(type(of: cgmManager))")
+                debug(.deviceManager, "PluginSource: fetchIfNeeded - CGM manager identifier: \(cgmManager.pluginIdentifier)")
+                debug(
+                    .deviceManager,
+                    "PluginSource: fetchIfNeeded - CGM manager has valid sensor session: \(self.cgmHasValidSensorSession)"
+                )
+
+                // Set a timeout to ensure the promise is resolved
+                let timeoutWorkItem = DispatchWorkItem {
+                    debug(.deviceManager, "PluginSource: fetchIfNeeded - TIMEOUT reached, resolving promise with empty array")
+                    promise(.success([]))
+                }
+                self.processQueue.asyncAfter(deadline: .now() + 30, execute: timeoutWorkItem)
+
+                debug(.deviceManager, "PluginSource: fetchIfNeeded - calling fetchNewDataIfNeeded on cgmManager")
+
+                // Check if we have a valid sensor session
+                if !self.cgmHasValidSensorSession {
+                    debug(
+                        .deviceManager,
+                        "PluginSource: fetchIfNeeded - WARNING: CGM does not have a valid sensor session"
+                    )
+                }
+
+                debug(.deviceManager, "PluginSource: fetchIfNeeded - about to call fetchNewDataIfNeeded")
                 cgmManager.fetchNewDataIfNeeded { result in
-                    promise(self.readCGMResult(readingResult: result))
+                    // Cancel the timeout since we got a response
+                    timeoutWorkItem.cancel()
+
+                    debug(
+                        .deviceManager,
+                        "PluginSource: fetchIfNeeded - received callback from fetchNewDataIfNeeded with result: \(result)"
+                    )
+
+                    let processedResult = self.readCGMResult(readingResult: result)
+                    if case let .success(values) = processedResult {
+                        debug(.deviceManager, "PluginSource: fetchIfNeeded - processed result contains \(values.count) values")
+                        if !values.isEmpty {
+                            let firstValue = values.first!
+                            debug(
+                                .deviceManager,
+                                "PluginSource: fetchIfNeeded - first glucose value: \(firstValue.glucose ?? 0) mg/dL at \(firstValue.dateString)"
+                            )
+                        } else {
+                            debug(.deviceManager, "PluginSource: fetchIfNeeded - processed result contains no values")
+                        }
+                    } else if case let .failure(error) = processedResult {
+                        debug(
+                            .deviceManager,
+                            "PluginSource: fetchIfNeeded - processed result contains error: \(error.localizedDescription)"
+                        )
+                    }
+                    promise(processedResult)
                 }
             }
         }
+        .handleEvents(
+            receiveSubscription: { _ in debug(.deviceManager, "PluginSource: fetchIfNeeded received subscription") },
+            receiveOutput: { values in
+                debug(.deviceManager, "PluginSource: fetchIfNeeded received output with \(values.count) values") },
+            receiveCompletion: { completion in
+                if case let .failure(error) = completion {
+                    debug(.deviceManager, "PluginSource: fetchIfNeeded completed with error: \(error.localizedDescription)")
+                } else {
+                    debug(.deviceManager, "PluginSource: fetchIfNeeded completed successfully")
+                }
+            }
+        )
         .replaceError(with: [])
         .replaceEmpty(with: [])
         .eraseToAnyPublisher()
@@ -105,53 +282,81 @@ extension PluginSource: CGMManagerDelegate {
     func recordRetractedAlert(_: LoopKit.Alert, at _: Date) {}
 
     func cgmManagerWantsDeletion(_ manager: CGMManager) {
-        dispatchPrecondition(condition: .onQueue(processQueue))
-        debug(.deviceManager, " CGM Manager with identifier \(manager.pluginIdentifier) wants deletion")
-        glucoseManager?.deleteGlucoseSource()
+        processQueue.async { [weak self] in
+            guard let self = self else { return }
+
+            dispatchPrecondition(condition: .onQueue(self.processQueue))
+
+            debug(.deviceManager, " CGM Manager with identifier \(manager.pluginIdentifier) wants deletion")
+            self.glucoseManager?.deleteGlucoseSource()
+        }
     }
 
     func cgmManager(_: CGMManager, hasNew readingResult: CGMReadingResult) {
-        dispatchPrecondition(condition: .onQueue(processQueue))
-        promise?(readCGMResult(readingResult: readingResult))
-        debug(.deviceManager, "CGM PLUGIN - Direct return done")
+        processQueue.async { [weak self] in
+            guard let self = self else { return }
+
+            dispatchPrecondition(condition: .onQueue(self.processQueue))
+
+            self.promise?(self.readCGMResult(readingResult: readingResult))
+            debug(.deviceManager, "CGM PLUGIN - Direct return done")
+        }
     }
 
     func cgmManager(_: LoopKit.CGMManager, hasNew events: [LoopKit.PersistedCgmEvent]) {
-        dispatchPrecondition(condition: .onQueue(processQueue))
-        // TODO: Events in APS ?
-        // currently only display in log the date of the event
-        events.forEach { event in
-            debug(.deviceManager, "events from CGM at \(event.date)")
-
-            if event.type == .sensorStart {
-                self.glucoseManager?.removeCalibrations()
+        processQueue.async { [weak self] in
+            guard let self = self else { return }
+
+            dispatchPrecondition(condition: .onQueue(self.processQueue))
+
+            // TODO: Events in APS ?
+            // currently only display in log the date of the event
+            events.forEach { event in
+                debug(.deviceManager, "events from CGM at \(event.date)")
+
+                if event.type == .sensorStart {
+                    self.glucoseManager?.removeCalibrations()
+                }
             }
         }
     }
 
     func startDateToFilterNewData(for _: CGMManager) -> Date? {
-        dispatchPrecondition(condition: .onQueue(processQueue))
-        return glucoseStorage.lastGlucoseDate()
+        var date: Date?
+
+        processQueue.async { [weak self] in
+            guard let self = self else { return }
+
+            dispatchPrecondition(condition: .onQueue(self.processQueue))
+
+            date = glucoseStorage.lastGlucoseDate()
+        }
+
+        return date
     }
 
     func cgmManagerDidUpdateState(_ cgmManager: CGMManager) {
-        dispatchPrecondition(condition: .onQueue(processQueue))
+        processQueue.async { [weak self] in
+            guard let self = self else { return }
 
-        guard let fetchGlucoseManager = glucoseManager else {
-            debug(
-                .deviceManager,
-                "Could not gracefully unwrap FetchGlucoseManager upon observing LoopKit's cgmManagerDidUpdateState"
+            dispatchPrecondition(condition: .onQueue(self.processQueue))
+
+            guard let fetchGlucoseManager = self.glucoseManager else {
+                debug(
+                    .deviceManager,
+                    "Could not gracefully unwrap FetchGlucoseManager upon observing LoopKit's cgmManagerDidUpdateState"
+                )
+                return
+            }
+            // Adjust app-specific NS Upload setting value when CGM setting is changed
+            fetchGlucoseManager.settingsManager.settings.uploadGlucose = cgmManager.shouldSyncToRemoteService
+
+            fetchGlucoseManager.updateGlucoseSource(
+                cgmGlucoseSourceType: fetchGlucoseManager.settingsManager.settings.cgm,
+                cgmGlucosePluginId: fetchGlucoseManager.settingsManager.settings.cgmPluginIdentifier,
+                newManager: cgmManager as? CGMManagerUI
             )
-            return
         }
-        // Adjust app-specific NS Upload setting value when CGM setting is changed
-        fetchGlucoseManager.settingsManager.settings.uploadGlucose = cgmManager.shouldSyncToRemoteService
-
-        fetchGlucoseManager.updateGlucoseSource(
-            cgmGlucoseSourceType: fetchGlucoseManager.settingsManager.settings.cgm,
-            cgmGlucosePluginId: fetchGlucoseManager.settingsManager.settings.cgmPluginIdentifier,
-            newManager: cgmManager as? CGMManagerUI
-        )
     }
 
     func credentialStoragePrefix(for _: CGMManager) -> String {
@@ -162,7 +367,11 @@ extension PluginSource: CGMManagerDelegate {
     func cgmManager(_: CGMManager, didUpdate status: CGMManagerStatus) {
         debug(.deviceManager, "CGM Manager did update state to \(status)")
 
-        processQueue.async {
+        processQueue.async { [weak self] in
+            guard let self = self else { return }
+
+            dispatchPrecondition(condition: .onQueue(self.processQueue))
+
             if self.cgmHasValidSensorSession != status.hasValidSensorSession {
                 self.cgmHasValidSensorSession = status.hasValidSensorSession
             }

+ 99 - 24
Trio/Sources/APS/FetchGlucoseManager.swift

@@ -80,27 +80,67 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         timer.publisher
             .receive(on: processQueue)
             .flatMap { [self] _ -> AnyPublisher<[BloodGlucose], Never> in
-                debug(.nightscout, "FetchGlucoseManager timer heartbeat")
+                debug(.nightscout, "FetchGlucoseManager timer heartbeat triggered")
+
+                // Check if the glucose source is still available
                 if let glucoseSource = self.glucoseSource {
-                    return glucoseSource.fetch(self.timer).eraseToAnyPublisher()
+                    debug(
+                        .deviceManager,
+                        "FetchGlucoseManager: glucoseSource exists, calling fetch() on source type: \(type(of: glucoseSource))"
+                    )
+
+                    // Check if we have a CGM manager when using a plugin source
+                    if let pluginSource = glucoseSource as? PluginSource {
+                        if pluginSource.cgmManager == nil {
+                            debug(.deviceManager, "FetchGlucoseManager: WARNING - PluginSource has no CGM manager")
+                        } else {
+                            debug(
+                                .deviceManager,
+                                "FetchGlucoseManager: PluginSource has CGM manager of type: \(type(of: pluginSource.cgmManager!))"
+                            )
+                        }
+                    }
+
+                    return glucoseSource.fetch(self.timer)
+                        .handleEvents(
+                            receiveOutput: { values in
+                                debug(.deviceManager, "FetchGlucoseManager: fetch() returned \(values.count) glucose values")
+                                if !values.isEmpty {
+                                    let firstValue = values.first!
+                                    debug(
+                                        .deviceManager,
+                                        "FetchGlucoseManager: First glucose value: \(firstValue.glucose ?? 0) mg/dL at \(firstValue.dateString)"
+                                    )
+                                }
+                            }
+                        )
+                        .eraseToAnyPublisher()
                 } else {
+                    debug(.deviceManager, "FetchGlucoseManager: No glucoseSource available, returning empty publisher")
                     return Empty(completeImmediately: false).eraseToAnyPublisher()
                 }
             }
             .sink { glucose in
-                debug(.nightscout, "FetchGlucoseManager callback sensor")
+                debug(.nightscout, "FetchGlucoseManager callback received \(glucose.count) glucose values")
+                let date = self.glucoseStorage.syncDate()
+                debug(.deviceManager, "FetchGlucoseManager: sync date is \(date)")
                 Publishers.CombineLatest(
                     Just(glucose),
-                    Just(self.glucoseStorage.syncDate())
+                    Just(date)
                 )
                 .eraseToAnyPublisher()
                 .sink { newGlucose, syncDate in
+                    debug(
+                        .deviceManager,
+                        "FetchGlucoseManager: starting new task to invoke glucoseStoreAndHeartDecision with \(newGlucose.count) glucose values"
+                    )
                     Task {
                         do {
                             try await self.glucoseStoreAndHeartDecision(
                                 syncDate: syncDate,
                                 glucose: newGlucose
                             )
+                            debugPrint("\(#file) \(#function) glucoseStoreAndHeartDecision did complete")
                         } catch {
                             debug(.deviceManager, "Failed to store glucose: \(error.localizedDescription)")
                         }
@@ -109,6 +149,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
                 .store(in: &self.lifetime)
             }
             .store(in: &lifetime)
+//        debug(.deviceManager, "FetchGlucoseManager: timer.fire() and timer.resume() called")
         timer.fire()
         timer.resume()
     }
@@ -147,8 +188,17 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     }
 
     func updateGlucoseSource(cgmGlucoseSourceType: CGMType, cgmGlucosePluginId: String, newManager: CGMManagerUI?) {
+        debug(
+            .deviceManager,
+            "FetchGlucoseManager: updateGlucoseSource called with type: \(cgmGlucoseSourceType), pluginId: \(cgmGlucosePluginId), newManager: \(newManager != nil ? "provided" : "nil")"
+        )
+
         // if changed, remove all calibrations
         if self.cgmGlucoseSourceType != cgmGlucoseSourceType || self.cgmGlucosePluginId != cgmGlucosePluginId {
+            debug(
+                .deviceManager,
+                "FetchGlucoseManager: CGM type or plugin ID changed, removing calibrations and resetting managers"
+            )
             removeCalibrations()
             cgmManager = nil
             glucoseSource = nil
@@ -161,21 +211,42 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         // if plugin, if the same pluginID, no change required because the manager is available
         // if plugin, if not the same pluginID, need to reset the cgmManager
         // if plugin and newManager provides, update cgmManager
-        debug(.apsManager, "plugin : \(String(describing: cgmManager?.pluginIdentifier))")
+        debug(
+            .deviceManager,
+            "FetchGlucoseManager: Current plugin identifier: \(String(describing: cgmManager?.pluginIdentifier))"
+        )
 
         if let manager = newManager {
+            debug(.deviceManager, "FetchGlucoseManager: New manager provided of type: \(type(of: manager))")
             // If the pointer to manager is the *same* as our current `cgmManager`, skip re-init
             if manager !== cgmManager {
-                // or do a more thorough check to see if it is the same class & state
+                debug(.deviceManager, "FetchGlucoseManager: New manager is different from current manager, reinitializing")
                 removeCalibrations()
                 cgmManager = manager
                 glucoseSource = nil
+            } else {
+                debug(
+                    .deviceManager,
+                    "FetchGlucoseManager: New manager is the same instance as current manager, skipping reinitialization"
+                )
             }
         } else if self.cgmGlucoseSourceType == .plugin, cgmManager == nil, let rawCGMManager = rawCGMManager {
+            debug(
+                .deviceManager,
+                "FetchGlucoseManager: Plugin type with no manager but raw state available, initializing from raw state"
+            )
             cgmManager = cgmManagerFromRawValue(rawCGMManager)
+            if cgmManager != nil {
+                debug(
+                    .deviceManager,
+                    "FetchGlucoseManager: Successfully initialized CGM manager from raw state: \(type(of: cgmManager!))"
+                )
+            } else {
+                debug(.deviceManager, "FetchGlucoseManager: Failed to initialize CGM manager from raw state")
+            }
             updateManagerUnits(cgmManager)
-
         } else {
+            debug(.deviceManager, "FetchGlucoseManager: No new manager provided, saving current configuration")
             saveConfigManager()
         }
 
@@ -192,8 +263,17 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
             case .enlite:
                 glucoseSource = deviceDataManager
             case .plugin:
+                debug(.deviceManager, "FetchGlucoseManager: Creating PluginSource with current CGM manager")
                 glucoseSource = PluginSource(glucoseStorage: glucoseStorage, glucoseManager: self)
             }
+
+            if let source = glucoseSource {
+                debug(.deviceManager, "FetchGlucoseManager: Successfully created glucose source of type: \(type(of: source))")
+            } else {
+                debug(.deviceManager, "FetchGlucoseManager: No glucose source created, source is nil")
+            }
+        } else {
+            debug(.deviceManager, "FetchGlucoseManager: Keeping existing glucose source of type: \(type(of: glucoseSource!))")
         }
     }
 
@@ -248,29 +328,22 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         var filteredByDate: [BloodGlucose] = []
         var filtered: [BloodGlucose] = []
 
-        // start background time extension
-        var backGroundFetchBGTaskID: UIBackgroundTaskIdentifier?
-        backGroundFetchBGTaskID = await UIApplication.shared.beginBackgroundTask(withName: "save BG starting") {
-            guard let bg = backGroundFetchBGTaskID else { return }
-            UIApplication.shared.endBackgroundTask(bg)
-            backGroundFetchBGTaskID = .invalid
-        }
+        // Start background task
+        var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
+        backgroundTaskID = startBackgroundTask(withName: "Glucose Store and Heartbeat Decision")
 
-        defer {
-            if let backgroundTask = backGroundFetchBGTaskID {
-                Task {
-                    await UIApplication.shared.endBackgroundTask(backgroundTask)
-                }
-                backGroundFetchBGTaskID = .invalid
-            }
+        guard newGlucose.isNotEmpty else {
+            endBackgroundTaskSafely(&backgroundTaskID, taskName: "Glucose Store and Heartbeat Decision")
+            return
         }
 
-        guard newGlucose.isNotEmpty else { return }
-
         filteredByDate = newGlucose.filter { $0.dateString > syncDate }
         filtered = glucoseStorage.filterTooFrequentGlucose(filteredByDate, at: syncDate)
 
-        guard filtered.isNotEmpty else { return }
+        guard filtered.isNotEmpty else {
+            endBackgroundTaskSafely(&backgroundTaskID, taskName: "Glucose Store and Heartbeat Decision")
+            return
+        }
         debug(.deviceManager, "New glucose found")
 
         // filter the data if it is the case
@@ -289,6 +362,8 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
 
         try await glucoseStorage.storeGlucose(filtered)
         deviceDataManager.heartbeat(date: Date())
+
+        endBackgroundTaskSafely(&backgroundTaskID, taskName: "Glucose Store and Heartbeat Decision")
     }
 
     func sourceInfo() -> [String: Any]? {

+ 14 - 12
Trio/Sources/APS/OpenAPS/OpenAPS.swift

@@ -34,7 +34,6 @@ final class OpenAPS {
         await context.perform {
             let newOrefDetermination = OrefDetermination(context: self.context)
             newOrefDetermination.id = UUID()
-            newOrefDetermination.totalDailyDose = self.decimalToNSDecimalNumber(determination.tdd)
             newOrefDetermination.insulinSensitivity = self.decimalToNSDecimalNumber(determination.isf)
             newOrefDetermination.currentTarget = self.decimalToNSDecimalNumber(determination.current_target)
             newOrefDetermination.eventualBG = determination.eventualBG.map(NSDecimalNumber.init)
@@ -55,9 +54,6 @@ final class OpenAPS {
             newOrefDetermination.expectedDelta = self.decimalToNSDecimalNumber(determination.expectedDelta)
             newOrefDetermination.cob = Int16(Int(determination.cob ?? 0))
             newOrefDetermination.manualBolusErrorString = self.decimalToNSDecimalNumber(determination.manualBolusErrorString)
-            newOrefDetermination.tempBasal = determination.insulin?.temp_basal.map { NSDecimalNumber(decimal: $0) }
-            newOrefDetermination.scheduledBasal = determination.insulin?.scheduled_basal.map { NSDecimalNumber(decimal: $0) }
-            newOrefDetermination.bolus = determination.insulin?.bolus.map { NSDecimalNumber(decimal: $0) }
             newOrefDetermination.smbToDeliver = determination.units.map { NSDecimalNumber(decimal: $0) }
             newOrefDetermination.carbsRequired = Int16(Int(determination.carbsReq ?? 0))
             newOrefDetermination.isUploadedToNS = false
@@ -392,16 +388,16 @@ final class OpenAPS {
             let overrideTargetBG = activeOverrides.first?.target?.decimalValue ?? 0
 
             // Calculate averages for Total Daily Dose (TDD)
-            let totalTDD = historicalTDDData.compactMap { ($0["totalDailyDose"] as? NSDecimalNumber)?.decimalValue }.reduce(0, +)
+            let totalTDD = historicalTDDData.compactMap { ($0["total"] as? NSDecimalNumber)?.decimalValue }.reduce(0, +)
             let totalDaysCount = max(historicalTDDData.count, 1)
 
             // Fetch recent TDD data for the past two hours
-            let recentTDDData = historicalTDDData.filter { ($0["timestamp"] as? Date ?? Date()) >= twoHoursAgo }
+            let recentTDDData = historicalTDDData.filter { ($0["date"] as? Date ?? Date()) >= twoHoursAgo }
             let recentDataCount = max(recentTDDData.count, 1)
-            let recentTotalTDD = recentTDDData.compactMap { ($0["totalDailyDose"] as? NSDecimalNumber)?.decimalValue }
+            let recentTotalTDD = recentTDDData.compactMap { ($0["total"] as? NSDecimalNumber)?.decimalValue }
                 .reduce(0, +)
 
-            let currentTDD = historicalTDDData.last?["totalDailyDose"] as? Decimal ?? 0
+            let currentTDD = historicalTDDData.last?["total"] as? Decimal ?? 0
             let averageTDDLastTwoHours = recentTotalTDD / Decimal(recentDataCount)
             let averageTDDLastTenDays = totalTDD / Decimal(totalDaysCount)
             let weightedTDD = weightPercentage * averageTDDLastTwoHours + (1 - weightPercentage) * averageTDDLastTenDays
@@ -410,6 +406,7 @@ final class OpenAPS {
             let oref2Data = Oref2_variables(
                 average_total_data: currentTDD > 0 ? averageTDDLastTenDays : 0,
                 weightedAverage: currentTDD > 0 ? weightedTDD : 1,
+                currentTDD: currentTDD,
                 past2hoursAverage: currentTDD > 0 ? averageTDDLastTwoHours : 0,
                 date: Date(),
                 overridePercentage: overridePercentage,
@@ -518,6 +515,11 @@ final class OpenAPS {
                 adjustedPreferences.halfBasalExerciseTarget = activeHBT
                 debug(.openAPS, "Updated halfBasalExerciseTarget to active Temp Target value: \(activeHBT)")
             }
+            // Overwrite the lowTTlowersSens if autosensMax does not support it
+            if preferences.lowTemptargetLowersSensitivity, preferences.autosensMax <= 1 {
+                adjustedPreferences.lowTemptargetLowersSensitivity = false
+                debug(.openAPS, "Setting lowTTlowersSens to false due to insufficient autosensMax: \(preferences.autosensMax)")
+            }
         }
 
         do {
@@ -831,12 +833,12 @@ extension OpenAPS {
 
     func fetchHistoricalTDDData(from date: Date) throws -> [[String: Any]] {
         try CoreDataStack.shared.fetchEntities(
-            ofType: OrefDetermination.self,
+            ofType: TDDStored.self,
             onContext: context,
-            predicate: NSPredicate(format: "timestamp > %@ AND totalDailyDose > 0", date as NSDate),
-            key: "timestamp",
+            predicate: NSPredicate(format: "date > %@ AND total > 0", date as NSDate),
+            key: "date",
             ascending: true,
-            propertiesToFetch: ["timestamp", "totalDailyDose"]
+            propertiesToFetch: ["date", "total"]
         ) as? [[String: Any]] ?? []
     }
 }

+ 0 - 2
Trio/Sources/APS/Storage/DeterminationStorage.swift

@@ -182,8 +182,6 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
                         reservoir: self.decimal(from: orefDetermination.reservoir),
                         isf: self.decimal(from: orefDetermination.insulinSensitivity),
                         timestamp: orefDetermination.timestamp,
-                        tdd: self.decimal(from: orefDetermination.totalDailyDose),
-                        insulin: nil,
                         current_target: self.decimal(from: orefDetermination.currentTarget),
                         insulinForManualBolus: self.decimal(from: orefDetermination.insulinForManualBolus),
                         manualBolusErrorString: self.decimal(from: orefDetermination.manualBolusErrorString),

+ 61 - 0
Trio/Sources/APS/Storage/OverrideStorage.swift

@@ -12,6 +12,11 @@ protocol OverrideStorage {
     func deleteOverridePreset(_ objectID: NSManagedObjectID) async
     func getOverridesNotYetUploadedToNightscout() async throws -> [NightscoutExercise]
     func getOverrideRunsNotYetUploadedToNightscout() async throws -> [NightscoutExercise]
+    func checkIfShouldDeleteNightscoutOverrideEntry(
+        forCreatedAt createdAtString: String,
+        newDuration: Int?,
+        using nightscout: NightscoutAPI
+    ) async throws
     func getPresetOverridesForNightscout() async throws -> [NightscoutPresetOverride]
     func fetchLatestActiveOverride() async throws -> NSManagedObjectID?
 }
@@ -293,6 +298,62 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
         }
     }
 
+    /// This check is needed to force re-rendering of overrides in the Nightscout main chart
+    /// if the override duration has changed (cancelled, customized or replaced with other override),
+    /// since just updating durations in existing entries doesn't trigger re-rendering.
+    func checkIfShouldDeleteNightscoutOverrideEntry(
+        forCreatedAt createdAtString: String,
+        newDuration: Int?,
+        using nightscout: NightscoutAPI
+    ) async throws {
+        let formatter = ISO8601DateFormatter()
+        formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
+
+        guard let jsonDate = formatter.date(from: createdAtString) else {
+            debug(.nightscout, "Could not parse override created_at string: \(createdAtString)")
+            return
+        }
+
+        /// Define a tolerance window (in seconds)
+        /// This is neccessary to handle small rounding/conversion time differences
+        /// when comparing dates between core data and NightscoutExercise json
+        let tolerance: TimeInterval = 0.1
+        let lowerBound = jsonDate.addingTimeInterval(-tolerance)
+        let upperBound = jsonDate.addingTimeInterval(tolerance)
+
+        /// Build a predicate to fetch a stored override (from OverrideStored) whose date is within the tolerance window.
+        let predicate = NSPredicate(format: "date >= %@ AND date <= %@", lowerBound as NSDate, upperBound as NSDate)
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: OverrideStored.self,
+            onContext: context,
+            predicate: predicate,
+            key: "date",
+            ascending: false
+        )
+
+        let storedOverride: NightscoutExercise? = await context.perform {
+            guard let fetched = results as? [OverrideStored],
+                  let record = fetched.first,
+                  let recordDate = record.date else { return nil }
+            let duration = record.indefinite ? 43200 : record.duration ?? 0
+            return NightscoutExercise(
+                duration: Int(truncating: duration),
+                eventType: OverrideStored.EventType.nsExercise,
+                createdAt: recordDate,
+                enteredBy: NightscoutExercise.local,
+                notes: record.name ?? String(localized: "Custom Override"),
+                id: UUID(uuidString: record.id ?? UUID().uuidString)
+            )
+        }
+
+        if let existing = storedOverride {
+            // Only delete existing nightscout entries if the durations differ.
+            if let existingDuration = existing.duration, let newDuration = newDuration, existingDuration != newDuration {
+                try await nightscout.deleteNightscoutOverride(withCreatedAt: createdAtString)
+            }
+        }
+    }
+
     func getPresetOverridesForNightscout() async throws -> [NightscoutPresetOverride] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,

+ 1 - 1
Trio/Sources/APS/Storage/TDDStorage.swift

@@ -109,7 +109,7 @@ final class BaseTDDStorage: TDDStorage, Injectable {
         let bolusString = String(format: "%.2f", NSDecimalNumber(decimal: bolus.rounded(toPlaces: 2)).doubleValue)
         let tempBasalString = String(format: "%.2f", NSDecimalNumber(decimal: temp.rounded(toPlaces: 2)).doubleValue)
         let scheduledBasalString = String(format: "%.2f", NSDecimalNumber(decimal: scheduled.rounded(toPlaces: 2)).doubleValue)
-        let weightedAvgString = String(format: "%.2f", NSDecimalNumber(decimal: weighted?.rounded(toPlaces: 2) ?? 0))
+        let weightedAvgString = String(format: "%.2f", NSDecimalNumber(decimal: weighted?.rounded(toPlaces: 2) ?? 0).doubleValue)
         let hoursString = String(format: "%.5f", NSDecimalNumber(decimal: Decimal(hours).truncated(toPlaces: 5)).doubleValue)
 
         debug(.apsManager, """

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

@@ -4,11 +4,11 @@ import UserNotifications
 
 class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject, UNUserNotificationCenterDelegate {
     func application(
-        _ application: UIApplication,
+        _: UIApplication,
         didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?
     ) -> Bool {
-        application.registerForRemoteNotifications()
-        return true
+        // application.registerForRemoteNotifications()
+        true
     }
 
     func application(

+ 94 - 24
Trio/Sources/Application/TrioApp.swift

@@ -5,6 +5,11 @@ import Foundation
 import SwiftUI
 import Swinject
 
+extension Notification.Name {
+    static let initializationCompleted = Notification.Name("initializationCompleted")
+    static let initializationError = Notification.Name("initializationError")
+}
+
 @main struct TrioApp: App {
     @Environment(\.scenePhase) var scenePhase
 
@@ -13,9 +18,21 @@ import Swinject
     // Read the color scheme preference from UserDefaults; defaults to system default setting
     @AppStorage("colorSchemePreference") private var colorSchemePreference: ColorSchemeOption = .systemDefault
 
-    let coreDataStack: CoreDataStack
+    let coreDataStack = CoreDataStack.shared
+    class InitState {
+        var complete = false
+        var error = false
+    }
+
+    // We use both InitState and @State variables to track coreDataStack
+    // initialization. We need both to handle the cases when the coreDataStack
+    // finishes before the UI and when it finishes after. SwiftUI doesn't have
+    // clean mechanisms for handling background thread updates, thus this solution.
+    let initState = InitState()
 
     @State private var appState = AppState()
+    @State private var showLoadingView = true
+    @State private var showLoadingError = false
 
     // Dependencies Assembler
     // contain all dependencies Assemblies
@@ -72,36 +89,89 @@ import Swinject
             "Trio Started: v\(Bundle.main.releaseVersionNumber ?? "")(\(Bundle.main.buildVersionNumber ?? "")) [buildDate: \(String(describing: BuildDetails.default.buildDate()))] [buildExpires: \(String(describing: BuildDetails.default.calculateExpirationDate()))] [submodules: \(submodulesInfo)]"
         )
 
-        // Setup up the Core Data Stack
-        coreDataStack = CoreDataStack.shared
+        // Fix bug in iOS 18 related to the translucent tab bar
+        configureTabBarAppearance()
 
-        // Explicitly initialize Core Data Stack
-        do {
-            try coreDataStack.initializeStack()
+        deferredInitialization()
+    }
 
-            // Only load services after successful Core Data initialization
-            loadServices()
+    /// Handles the deferred initialization of core components.
+    ///
+    /// Performs CoreDataStack initialization asynchronously and notifies the UI
+    /// of completion or errors via notifications.
+    private func deferredInitialization() {
+        Task {
+            do {
+                try await coreDataStack.initializeStack()
 
-            // Fix bug in iOS 18 related to the translucent tab bar
-            configureTabBarAppearance()
+                await MainActor.run {
+                    // Only load services after successful Core Data initialization
+                    loadServices()
 
-            // Clear the persistentHistory and the NSManagedObjects that are older than 90 days every time the app starts
-            cleanupOldData()
-        } catch {
-            debug(.coreData, "\(DebuggingIdentifiers.failed) Failed to initialize Core Data Stack: \(error.localizedDescription)")
+                    // Clear the persistentHistory and the NSManagedObjects that are older than 90 days every time the app starts
+                    cleanupOldData()
 
-            fatalError("Core Data Stack initialization failed")
+                    self.initState.complete = true
+                    Foundation.NotificationCenter.default.post(name: .initializationCompleted, object: nil)
+                    UIApplication.shared.registerForRemoteNotifications()
+                }
+            } catch {
+                debug(
+                    .coreData,
+                    "\(DebuggingIdentifiers.failed) Failed to initialize Core Data Stack: \(error.localizedDescription)"
+                )
+
+                await MainActor.run {
+                    self.initState.error = true
+                    Foundation.NotificationCenter.default.post(name: .initializationError, object: nil)
+                }
+            }
         }
     }
 
+    /// Attempts to initialize the CoreDataStack again after a previous failure.
+    ///
+    /// Resets error states and triggers the initialization process from the beginning. Called in response
+    /// to a UI "retry" button press from the Main.LoadingView
+    private func retryCoreDataInitialization() {
+        showLoadingError = false
+        initState.error = false
+        deferredInitialization()
+    }
+
     var body: some Scene {
         WindowGroup {
-            Main.RootView(resolver: resolver)
-                .preferredColorScheme(colorScheme(for: colorSchemePreference) ?? nil)
-                .environment(\.managedObjectContext, coreDataStack.persistentContainer.viewContext)
-                .environment(appState)
-                .environmentObject(Icons())
-                .onOpenURL(perform: handleURL)
+            if self.showLoadingView {
+                Main.LoadingView(showError: $showLoadingError, retry: retryCoreDataInitialization)
+                    .onAppear {
+                        if self.initState.complete {
+                            Task { @MainActor in
+                                try? await Task.sleep(for: .seconds(1.8))
+                                self.showLoadingView = false
+                            }
+                        }
+                        if self.initState.error {
+                            self.showLoadingError = true
+                        }
+                    }
+                    .onReceive(Foundation.NotificationCenter.default.publisher(for: .initializationCompleted)) { _ in
+                        Task { @MainActor in
+                            try? await Task.sleep(for: .seconds(1.8))
+                            self.showLoadingView = false
+                        }
+                    }
+                    .onReceive(Foundation.NotificationCenter.default.publisher(for: .initializationError)) { _ in
+                        self.showLoadingError = true
+                    }
+
+            } else {
+                Main.RootView(resolver: resolver)
+                    .preferredColorScheme(colorScheme(for: colorSchemePreference) ?? nil)
+                    .environment(\.managedObjectContext, coreDataStack.persistentContainer.viewContext)
+                    .environment(appState)
+                    .environmentObject(Icons())
+                    .onOpenURL(perform: handleURL)
+            }
         }
         .onChange(of: scenePhase) { _, newScenePhase in
             debug(.default, "APPLICATION PHASE: \(newScenePhase)")
@@ -117,9 +187,9 @@ import Swinject
                 {
                     AppVersionChecker.shared.checkAndNotifyVersionStatus(in: rootVC)
                 }
-
-                // Check if we need to perform a database cleaning
-                performCleanupIfNecessary()
+                if initState.complete {
+                    performCleanupIfNecessary()
+                }
             }
         }
     }

+ 1 - 1
Trio/Sources/Config/Config.swift

@@ -4,6 +4,6 @@ import SwiftDate
 enum Config {
     static let treatWarningsAsErrors = true
     static let withSignPosts = false
-    static let loopInterval = 4.minutes.timeInterval
+    static let loopInterval = 3.minutes.timeInterval
     static let eхpirationInterval = 10.minutes.timeInterval
 }

+ 29 - 0
Trio/Sources/Helpers/BackgroundTask+Helper.swift

@@ -0,0 +1,29 @@
+import UIKit
+
+/// Ends a background task safely and ensures it is not called multiple times.
+///
+/// - Parameter taskID: The background task identifier to be ended.
+func endBackgroundTaskSafely(_ taskID: inout UIBackgroundTaskIdentifier, taskName: String = "Unnamed Task") {
+    if taskID != .invalid {
+        UIApplication.shared.endBackgroundTask(taskID)
+        debug(.default, "Background task '\(taskName)' ended successfully.")
+        taskID = .invalid
+    } else {
+        debug(.default, "Background task '\(taskName)' was already invalid or ended.")
+    }
+}
+
+/// Starts a background task and handles its expiration safely.
+///
+/// - Parameter name: The background task name.
+func startBackgroundTask(withName name: String) -> UIBackgroundTaskIdentifier {
+    var taskID = UIBackgroundTaskIdentifier.invalid
+
+    taskID = UIApplication.shared.beginBackgroundTask(withName: name) {
+        Task { @MainActor in
+            endBackgroundTaskSafely(&taskID, taskName: name)
+        }
+    }
+
+    return taskID
+}

+ 19 - 0
Trio/Sources/Helpers/Calendar+GlucoseStatsChart.swift

@@ -0,0 +1,19 @@
+import Foundation
+
+extension Calendar {
+    /// Converts an hour (0-23) to a Date object representing that hour on the current day.
+    /// This is used to properly position marks on the chart's time axis.
+    ///
+    /// - Parameter hour: Integer representing the hour of day (0-23)
+    /// - Returns: Date object set to the specified hour on the current day
+    ///
+    /// Example:
+    /// ```
+    /// calendar.dateForChartHour(14) // Returns today's date at 2:00 PM
+    /// calendar.dateForChartHour(0)  // Returns today's date at 12:00 AM
+    /// ```
+    func dateForChartHour(_ hour: Int) -> Date {
+        let today = startOfDay(for: Date())
+        return date(byAdding: .hour, value: hour, to: today) ?? today
+    }
+}

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

@@ -32,7 +32,7 @@ struct CustomProgressView: View {
                         .frame(width: 80, height: 3)
                         .offset(x: self.animate ? 180 : -180, y: 0)
                         .animation(
-                            Animation.linear(duration: 2)
+                            Animation.linear(duration: 1)
                                 .repeatForever(autoreverses: false), value: UUID()
                         )
                 )

+ 14 - 0
Trio/Sources/Helpers/Formatters.swift

@@ -42,6 +42,12 @@ extension Formatter {
         return dateFormatter
     }()
 
+    static let dayFormatter: DateFormatter = {
+        let formatter = DateFormatter()
+        formatter.dateFormat = "d"
+        return formatter
+    }()
+
     static let decimalFormatterWithOneFractionDigit: NumberFormatter = {
         let formatter = NumberFormatter()
         formatter.numberStyle = .decimal
@@ -80,6 +86,14 @@ extension Formatter {
         formatter.decimalSeparator = "."
         return formatter
     }()
+
+    static let timaAgoFormatter: NumberFormatter = {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 0
+        formatter.negativePrefix = ""
+        return formatter
+    }()
 }
 
 extension JSONDecoder.DateDecodingStrategy {

+ 21 - 0
Trio/Sources/Helpers/TherapySettingsUtil.swift

@@ -0,0 +1,21 @@
+import Foundation
+
+enum TherapySettingsUtil {
+    /// Parses a time string of therapy setting entry into a `Date` object using either "HH:mm:ss" or "HH:mm" formats.
+    /// This function ensures compatibility with time strings that may include or exclude seconds.
+    ///
+    /// - Parameter timeString: A string representing the time in "HH:mm:ss" or "HH:mm" format.
+    /// - Returns: A `Date` object set to today’s date with the extracted time, or `nil` if parsing fails.
+    static func parseTime(_ timeString: String) -> Date? {
+        let formats = ["HH:mm:ss", "HH:mm"]
+        for format in formats {
+            let formatter = DateFormatter()
+            formatter.dateFormat = format
+            formatter.timeZone = TimeZone.current
+            if let date = formatter.date(from: timeString) {
+                return date
+            }
+        }
+        return nil
+    }
+}

Разница между файлами не показана из-за своего большого размера
+ 46942 - 31295
Trio/Sources/Localizations/Main/Localizable.xcstrings


+ 3 - 3
Trio/Sources/Models/ColorSchemeOption.swift

@@ -7,9 +7,9 @@ enum ColorSchemeOption: String, JSON, CaseIterable, Identifiable {
 
     var displayName: String {
         switch self {
-        case .systemDefault: return "System Default"
-        case .light: return "Light"
-        case .dark: return "Dark"
+        case .systemDefault: return String(localized: "System Default")
+        case .light: return String(localized: "Light")
+        case .dark: return String(localized: "Dark")
         }
     }
 }

+ 0 - 20
Trio/Sources/Models/Determination.swift

@@ -19,8 +19,6 @@ struct Determination: JSON, Equatable {
     let reservoir: Decimal?
     var isf: Decimal?
     var timestamp: Date?
-    let tdd: Decimal?
-    let insulin: Insulin?
     var current_target: Decimal?
     let insulinForManualBolus: Decimal?
     let manualBolusErrorString: Decimal?
@@ -40,13 +38,6 @@ struct Predictions: JSON, Equatable {
     let uam: [Int]?
 }
 
-struct Insulin: JSON, Equatable {
-    let TDD: Decimal?
-    let bolus: Decimal?
-    let temp_basal: Decimal?
-    let scheduled_basal: Decimal?
-}
-
 extension Determination {
     private enum CodingKeys: String, CodingKey {
         case id
@@ -67,8 +58,6 @@ extension Determination {
         case reservoir
         case timestamp
         case isf = "ISF"
-        case tdd = "TDD"
-        case insulin
         case current_target
         case insulinForManualBolus
         case manualBolusErrorString
@@ -91,15 +80,6 @@ extension Predictions {
     }
 }
 
-extension Insulin {
-    private enum CodingKeys: String, CodingKey {
-        case TDD
-        case bolus
-        case temp_basal
-        case scheduled_basal
-    }
-}
-
 protocol DeterminationObserver {
     func determinationDidUpdate(_ determination: Determination)
 }

+ 1 - 1
Trio/Sources/Models/HbA1cDisplayUnit.swift

@@ -1,6 +1,6 @@
 import Foundation
 
-enum HbA1cDisplayUnit: String, JSON, CaseIterable, Identifiable, Codable, Hashable {
+enum EstimatedA1cDisplayUnit: String, JSON, CaseIterable, Identifiable, Codable, Hashable {
     var id: String { rawValue }
     case percent
     case mmolMol

+ 2 - 2
Trio/Sources/Models/GlucoseColorScheme.swift

@@ -10,9 +10,9 @@ public enum GlucoseColorScheme: String, JSON, CaseIterable, Identifiable, Codabl
     var displayName: String {
         switch self {
         case .staticColor:
-            return "Static"
+            return String(localized: "Static")
         case .dynamicColor:
-            return "Dynamic"
+            return String(localized: "Dynamic")
         }
     }
 }

+ 9 - 4
Trio/Sources/Models/GlucoseNotificationsOption.swift

@@ -5,18 +5,23 @@
 //  Created by Kimberlie Skandis on 1/18/25.
 //
 import Foundation
+import SwiftUI
 
 public enum GlucoseNotificationsOption: String, JSON, CaseIterable, Identifiable, Codable, Hashable {
-    public var id: String { rawValue }
     case disabled
     case alwaysEveryCGM
     case onlyAlarmLimits
 
+    public var id: String { rawValue }
+
     var displayName: String {
         switch self {
-        case .disabled: return "Disabled"
-        case .alwaysEveryCGM: return "Always"
-        case .onlyAlarmLimits: return "Only Alarm Limits"
+        case .disabled:
+            return String(localized: "Disabled", comment: "Option to disable glucose notifications")
+        case .alwaysEveryCGM:
+            return String(localized: "Always", comment: "Option to always notify on every CGM reading")
+        case .onlyAlarmLimits:
+            return String(localized: "Only Alarm Limits", comment: "Option to notify only when glucose reaches alarm limits")
         }
     }
 }

+ 1 - 0
Trio/Sources/Models/Icons.swift

@@ -7,6 +7,7 @@ enum Icon_: String, CaseIterable, Identifiable {
     case trioWhiteShadow
     case trioColorBG
     case trioWhite
+    case trioCircledNoBackground
     case trio3D
     case wilford = "diabeetus"
     case catWithPod

+ 4 - 0
Trio/Sources/Models/Oref2_variables.swift

@@ -2,6 +2,7 @@ import Foundation
 
 struct Oref2_variables: JSON, Equatable {
     var average_total_data: Decimal
+    var currentTDD: Decimal
     var weightedAverage: Decimal
     var past2hoursAverage: Decimal
     var date: Date
@@ -24,6 +25,7 @@ struct Oref2_variables: JSON, Equatable {
     init(
         average_total_data: Decimal,
         weightedAverage: Decimal,
+        currentTDD: Decimal,
         past2hoursAverage: Decimal,
         date: Date,
         overridePercentage: Decimal,
@@ -44,6 +46,7 @@ struct Oref2_variables: JSON, Equatable {
     ) {
         self.average_total_data = average_total_data
         self.weightedAverage = weightedAverage
+        self.currentTDD = currentTDD
         self.past2hoursAverage = past2hoursAverage
         self.date = date
         self.overridePercentage = overridePercentage
@@ -68,6 +71,7 @@ extension Oref2_variables {
     private enum CodingKeys: String, CodingKey {
         case average_total_data
         case weightedAverage
+        case currentTDD
         case past2hoursAverage
         case date
         case overridePercentage

+ 3 - 3
Trio/Sources/Models/Statistics.swift

@@ -117,7 +117,7 @@ struct Durations: JSON, Equatable {
 
 struct Units: JSON, Equatable {
     var Glucose: String
-    var HbA1c: String
+    var EstimatedA1c: String
 }
 
 struct Threshold: JSON, Equatable {
@@ -149,7 +149,7 @@ struct Variance: JSON, Equatable {
 struct Stats: JSON, Equatable {
     var Distribution: TIRs
     var Glucose: Averages
-    var HbA1c: Durations
+    var EstimatedA1c: Durations
     var Units: Units
     var LoopCycles: LoopCycles
     var Insulin: Ins
@@ -211,7 +211,7 @@ extension Stats {
     private enum CodingKeys: String, CodingKey {
         case Distribution
         case Glucose
-        case HbA1c
+        case EstimatedA1c
         case Units
         case LoopCycles
         case Insulin

+ 6 - 0
Trio/Sources/Models/TDD.swift

@@ -0,0 +1,6 @@
+import Foundation
+
+struct TDD: Codable, Equatable, Sendable {
+    let totalDailyDose: Decimal?
+    let timestamp: Date?
+}

+ 0 - 22
Trio/Sources/Models/TotalInsulinDisplayType.swift

@@ -1,22 +0,0 @@
-//
-//  TotalInsulinDisplayType.swift
-//  Trio
-//
-//  Created by Cengiz Deniz on 25.08.24.
-//
-import Foundation
-
-enum TotalInsulinDisplayType: String, JSON, CaseIterable, Identifiable, Codable, Hashable {
-    var id: String { rawValue }
-    case totalDailyDose
-    case totalInsulinInScope
-
-    var displayName: String {
-        switch self {
-        case .totalDailyDose:
-            return String(localized: "TDD", comment: "")
-        case .totalInsulinInScope:
-            return String(localized: "TINS", comment: "")
-        }
-    }
-}

+ 3 - 13
Trio/Sources/Models/TrioSettings.swift

@@ -35,21 +35,19 @@ struct TrioSettings: JSON, Equatable {
     var notificationsCarb: Bool = true
     var notificationsAlgorithm: Bool = true
     var glucoseNotificationsOption: GlucoseNotificationsOption = .onlyAlarmLimits
-    var useAlarmSound: Bool = false
     var addSourceInfoToGlucoseNotifications: Bool = false
     var lowGlucose: Decimal = 72
     var highGlucose: Decimal = 270
     var carbsRequiredThreshold: Decimal = 10
     var showCarbsRequiredBadge: Bool = true
     var useFPUconversion: Bool = true
-    var totalInsulinDisplayType: TotalInsulinDisplayType = .totalDailyDose
     var individualAdjustmentFactor: Decimal = 0.5
     var timeCap: Int = 8
     var minuteInterval: Int = 30
     var delay: Int = 60
     var useAppleHealth: Bool = false
     var smoothGlucose: Bool = false
-    var hbA1cDisplayUnit: HbA1cDisplayUnit = .percent
+    var eA1cDisplayUnit: EstimatedA1cDisplayUnit = .percent
     var high: Decimal = 180
     var low: Decimal = 70
     var hours: Int = 6
@@ -144,10 +142,6 @@ extension TrioSettings: Decodable {
             settings.useFPUconversion = useFPUconversion
         }
 
-        if let totalInsulinDisplayType = try? container.decode(TotalInsulinDisplayType.self, forKey: .totalInsulinDisplayType) {
-            settings.totalInsulinDisplayType = totalInsulinDisplayType
-        }
-
         if let individualAdjustmentFactor = try? container.decode(Decimal.self, forKey: .individualAdjustmentFactor) {
             settings.individualAdjustmentFactor = individualAdjustmentFactor
         }
@@ -207,10 +201,6 @@ extension TrioSettings: Decodable {
             settings.glucoseNotificationsOption = glucoseNotificationsOption
         }
 
-        if let useAlarmSound = try? container.decode(Bool.self, forKey: .useAlarmSound) {
-            settings.useAlarmSound = useAlarmSound
-        }
-
         if let addSourceInfoToGlucoseNotifications = try? container.decode(
             Bool.self,
             forKey: .addSourceInfoToGlucoseNotifications
@@ -274,8 +264,8 @@ extension TrioSettings: Decodable {
             settings.forecastDisplayType = forecastDisplayType
         }
 
-        if let hbA1cDisplayUnit = try? container.decode(HbA1cDisplayUnit.self, forKey: .hbA1cDisplayUnit) {
-            settings.hbA1cDisplayUnit = hbA1cDisplayUnit
+        if let eA1cDisplayUnit = try? container.decode(EstimatedA1cDisplayUnit.self, forKey: .eA1cDisplayUnit) {
+            settings.eA1cDisplayUnit = eA1cDisplayUnit
         }
 
         if let maxCarbs = try? container.decode(Decimal.self, forKey: .maxCarbs) {

+ 6 - 5
Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+Helpers.swift

@@ -47,18 +47,19 @@ extension Adjustments.StateModel {
         String(format: "%02d", hour)
     }
 
-    /// Converts a duration in minutes to a formatted string (e.g., "1 hr 30 min").
-    func formatHrMin(_ durationInMinutes: Int) -> String {
+    /// Converts a duration in minutes to a formatted string (e.g., "1 h 30 m").
+    func formatHoursAndMinutes(_ durationInMinutes: Int) -> String {
         let hours = durationInMinutes / 60
         let minutes = durationInMinutes % 60
 
         switch (hours, minutes) {
         case let (0, m):
-            return "\(m) min"
+            return "\(m)\u{00A0}" + String(localized: "m", comment: "Abbreviation for Minutes")
         case let (h, 0):
-            return "\(h) hr"
+            return "\(h)\u{00A0}" + String(localized: "h", comment: "h")
         default:
-            return "\(hours) hr \(minutes) min"
+            return hours.description + "\u{00A0}" + String(localized: "h", comment: "h") + "\u{00A0}" + minutes
+                .description + "\u{00A0}" + String(localized: "m", comment: "Abbreviation for Minutes")
         }
     }
 

+ 32 - 7
Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+Overrides.swift

@@ -1,6 +1,7 @@
 import Combine
 import CoreData
 import Foundation
+import SwiftUICore
 
 extension Adjustments.StateModel {
     // MARK: - Enact Overrides
@@ -370,14 +371,38 @@ extension Adjustments.StateModel {
 }
 
 enum IsfAndOrCrOptions: String, CaseIterable {
-    case isfAndCr = "ISF/CR"
-    case isf = "ISF"
-    case cr = "CR"
-    case nothing = "None"
+    case isfAndCr
+    case isf
+    case cr
+    case nothing
+
+    var displayName: String {
+        switch self {
+        case .isfAndCr:
+            return String(localized: "ISF/CR", comment: "Option for both ISF and CR")
+        case .isf:
+            return String(localized: "ISF", comment: "Option for Insulin Sensitivity Factor")
+        case .cr:
+            return String(localized: "CR", comment: "Option for Carb Ratio")
+        case .nothing:
+            return String(localized: "None", comment: "Option for no selection")
+        }
+    }
 }
 
 enum DisableSmbOptions: String, CaseIterable {
-    case dontDisable = "Don't Disable"
-    case disable = "Disable"
-    case disableOnSchedule = "Disable on Schedule"
+    case dontDisable
+    case disable
+    case disableOnSchedule
+
+    var displayName: String {
+        switch self {
+        case .dontDisable:
+            return String(localized: "Don't Disable", comment: "Option to keep SMB enabled")
+        case .disable:
+            return String(localized: "Disable", comment: "Option to disable SMB")
+        case .disableOnSchedule:
+            return String(localized: "Disable on Schedule", comment: "Option to disable SMB based on schedule")
+        }
+    }
 }

+ 6 - 5
Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+TempTargets.swift

@@ -400,7 +400,7 @@ extension Adjustments.StateModel {
     /// Determines if sensitivity adjustment is enabled based on target.
     func isAdjustSensEnabled(usingTarget initialTarget: Decimal? = nil) -> Bool {
         let target = initialTarget ?? tempTargetTarget
-        if target < normalTarget, lowTTlowersSens { return true }
+        if target < normalTarget, lowTTlowersSens && autosensMax > 1 { return true }
         if target > normalTarget, highTTraisesSens || isExerciseModeActive { return true }
         return false
     }
@@ -416,8 +416,9 @@ extension Adjustments.StateModel {
     /// Computes the high value for the slider based on the target.
     func computeSliderHigh(usingTarget initialTarget: Decimal? = nil) -> Double {
         let calcTarget = initialTarget ?? tempTargetTarget
-        guard calcTarget != 0 else { return Double(maxValue * 100) } // oref defined limit for increased insulin delivery
-        let maxSens = calcTarget > normalTarget ? 95 : Double(maxValue * 100)
+        guard calcTarget != 0
+        else { return Double(autosensMax * 100) } // oref defined limit for increased insulin delivery
+        let maxSens = calcTarget > normalTarget ? 95 : Double(autosensMax * 100)
         return maxSens
     }
 
@@ -431,10 +432,10 @@ extension Adjustments.StateModel {
         let deviationFromNormal = halfBasalTargetValue - normalTarget
 
         let adjustmentFactor = deviationFromNormal + (calcTarget - normalTarget)
-        let adjustmentRatio: Decimal = (deviationFromNormal * adjustmentFactor <= 0) ? maxValue : deviationFromNormal /
+        let adjustmentRatio: Decimal = (deviationFromNormal * adjustmentFactor <= 0) ? autosensMax : deviationFromNormal /
             adjustmentFactor
 
-        return Double(min(adjustmentRatio, maxValue) * 100).rounded()
+        return Double(min(adjustmentRatio, autosensMax) * 100).rounded()
     }
 }
 

+ 3 - 3
Trio/Sources/Modules/Adjustments/AdjustmentsStateModel.swift

@@ -59,7 +59,7 @@ extension Adjustments {
         var tempTargetPresets: [TempTargetStored] = []
         var scheduledTempTargets: [TempTargetStored] = []
         var percentage: Double = 100
-        var maxValue: Decimal = 1.2
+        var autosensMax: Decimal = 1.2
         var halfBasalTarget: Decimal = 160
         var settingHalfBasalTarget: Decimal = 160
         var highTTraisesSens: Bool = false
@@ -152,7 +152,7 @@ extension Adjustments {
             units = settingsManager.settings.units
             defaultSmbMinutes = settingsManager.preferences.maxSMBBasalMinutes
             defaultUamMinutes = settingsManager.preferences.maxUAMSMBBasalMinutes
-            maxValue = settingsManager.preferences.autosensMax
+            autosensMax = settingsManager.preferences.autosensMax
             settingHalfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
             halfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
             highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
@@ -262,7 +262,7 @@ extension Adjustments.StateModel: SettingsObserver, PreferencesObserver {
     func preferencesDidChange(_: Preferences) {
         defaultSmbMinutes = settingsManager.preferences.maxSMBBasalMinutes
         defaultUamMinutes = settingsManager.preferences.maxUAMSMBBasalMinutes
-        maxValue = settingsManager.preferences.autosensMax
+        autosensMax = settingsManager.preferences.autosensMax
         settingHalfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
         halfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
         highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity

+ 6 - 6
Trio/Sources/Modules/Adjustments/View/Overrides/AddOverrideForm.swift

@@ -124,7 +124,7 @@ struct AddOverrideForm: View {
                 // Picker for ISF/CR settings
                 Picker("Also Inversely Change", selection: $selectedIsfCrOption) {
                     ForEach(IsfAndOrCrOptions.allCases, id: \.self) { option in
-                        Text(option.rawValue).tag(option)
+                        Text(option.displayName).tag(option)
                     }
                 }
                 .pickerStyle(MenuPickerStyle())
@@ -188,7 +188,7 @@ struct AddOverrideForm: View {
                 // Picker for ISF/CR settings
                 Picker("Disable SMBs", selection: $selectedDisableSmbOption) {
                     ForEach(DisableSmbOptions.allCases, id: \.self) { option in
-                        Text(option.rawValue).tag(option)
+                        Text(option.displayName).tag(option)
                     }
                 }
                 .pickerStyle(MenuPickerStyle())
@@ -343,7 +343,7 @@ struct AddOverrideForm: View {
                     HStack {
                         Text("Duration")
                         Spacer()
-                        Text(state.formatHrMin(Int(state.overrideDuration)))
+                        Text(state.formatHoursAndMinutes(Int(state.overrideDuration)))
                             .foregroundColor(!displayPickerDuration ? .primary : .accentColor)
                             .onTapGesture {
                                 displayPickerDuration = toggleScrollWheel(displayPickerDuration)
@@ -448,15 +448,15 @@ struct AddOverrideForm: View {
             !state.advancedSettings && !state.smbIsOff && !state.smbIsScheduledOff
 
         if noDurationSpecified {
-            return (true, "Enable indefinitely or set a duration.")
+            return (true, String(localized: "Enable indefinitely or set a duration."))
         }
 
         if targetZeroWithOverride {
-            return (true, "Target glucose is out of range (\(state.units == .mgdL ? "72-270" : "4-14")).")
+            return (true, String(localized: "Target glucose is out of range (\(state.units == .mgdL ? "72-270" : "4-14"))."))
         }
 
         if allSettingsDefault {
-            return (true, "All settings are at default values.")
+            return (true, String(localized: "All settings are at default values."))
         }
 
         return (false, nil)

+ 1 - 1
Trio/Sources/Modules/Adjustments/View/Overrides/AdjustmentsRootView+Overrides.swift

@@ -163,7 +163,7 @@ extension Adjustments.RootView {
 
         let targetString = target.isEmpty ? "" : "\(target) \(state.units.rawValue)"
 
-        let durationString = indefinite ? "" : "\(state.formatHrMin(Int(duration)))"
+        let durationString = indefinite ? "" : "\(state.formatHoursAndMinutes(Int(duration)))"
 
         let scheduledSMBString: String = {
             guard preset.smbIsScheduledOff, preset.start != preset.end else { return "" }

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

@@ -187,7 +187,7 @@ struct EditOverrideForm: View {
                 // Picker for ISF/CR settings
                 Picker("Also Change", selection: $selectedIsfCrOption) {
                     ForEach(IsfAndOrCrOptions.allCases, id: \.self) { option in
-                        Text(option.rawValue).tag(option)
+                        Text(option.displayName).tag(option)
                     }
                 }
                 .pickerStyle(MenuPickerStyle())
@@ -257,7 +257,7 @@ struct EditOverrideForm: View {
                 // Picker for Disable SMB settings
                 Picker("Disable SMBs", selection: $selectedDisableSmbOption) {
                     ForEach(DisableSmbOptions.allCases, id: \.self) { option in
-                        Text(option.rawValue).tag(option)
+                        Text(option.displayName).tag(option)
                     }
                 }
                 .pickerStyle(MenuPickerStyle())
@@ -440,7 +440,7 @@ struct EditOverrideForm: View {
                     HStack {
                         Text("Duration")
                         Spacer()
-                        Text(state.formatHrMin(Int(truncating: duration as NSNumber)))
+                        Text(state.formatHoursAndMinutes(Int(truncating: duration as NSNumber)))
                             .foregroundColor(!displayPickerDuration ? .primary : .accentColor)
                             .onTapGesture {
                                 displayPickerDuration = toggleScrollWheel(displayPickerDuration)
@@ -557,15 +557,15 @@ struct EditOverrideForm: View {
             !smbIsOff && !smbIsScheduledOff
 
         if noDurationSpecified {
-            return (true, "Enable indefinitely or set a duration.")
+            return (true, String(localized: "Enable indefinitely or set a duration."))
         }
 
         if targetZeroWithOverride {
-            return (true, "Target glucose is out of range (\(state.units == .mgdL ? "72-270" : "4-14")).")
+            return (true, String(localized: "Target glucose is out of range (\(state.units == .mgdL ? "72-270" : "4-14"))."))
         }
 
         if allSettingsDefault {
-            return (true, "All settings are at default values.")
+            return (true, String(localized: "All settings are at default values."))
         }
 
         if !hasChanges {

+ 6 - 5
Trio/Sources/Modules/Adjustments/View/TempTargets/AddTempTargetForm.swift

@@ -84,7 +84,7 @@ struct AddTempTargetForm: View {
                 let settingsProvider = PickerSettingsProvider.shared
                 let glucoseSetting = PickerSetting(value: 0, step: targetStep, min: 80, max: 200, type: .glucose)
                 TargetPicker(
-                    label: "Target Glucose",
+                    label: String(localized: "Target Glucose"),
                     selection: Binding(
                         get: { state.tempTargetTarget },
                         set: { state.tempTargetTarget = $0 }
@@ -160,7 +160,7 @@ struct AddTempTargetForm: View {
                     HStack {
                         Text("Duration")
                         Spacer()
-                        Text(state.formatHrMin(Int(state.tempTargetDuration)))
+                        Text(state.formatHoursAndMinutes(Int(state.tempTargetDuration)))
                             .foregroundColor(
                                 !displayPickerDuration ?
                                     (state.tempTargetDuration > 0 ? .primary : .secondary) : .accentColor
@@ -205,13 +205,14 @@ struct AddTempTargetForm: View {
         let targetZero = state.tempTargetTarget < 80
 
         if noDurationSpecified {
-            return (true, "Set a duration!")
+            return (true, String(localized: "Set a duration!"))
         }
 
         if targetZero {
             return (
                 true,
-                "\(state.units == .mgdL ? "80 " : "4.4 ")" + state.units.rawValue + " needed as min. Glucose Target!"
+                "\(state.units == .mgdL ? "80 " : "4.4 ")" + state.units
+                    .rawValue + String(localized: " needed as min. Glucose Target)!")
             )
         }
 
@@ -227,7 +228,7 @@ struct AddTempTargetForm: View {
         }
 
         if isDateInFuture {
-            return (true, "Presets can't be saved with a future date!")
+            return (true, String(localized: "Presets cannot be saved with a future date!"))
         }
 
         return (false, nil)

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

@@ -232,7 +232,7 @@ struct EditTempTargetForm: View {
                     HStack {
                         Text("Duration")
                         Spacer()
-                        Text(state.formatHrMin(Int(duration)))
+                        Text(state.formatHoursAndMinutes(Int(duration)))
                             .foregroundColor(!displayPickerDuration ? (duration > 0 ? .primary : .secondary) : .accentColor)
                             .onTapGesture {
                                 displayPickerDuration = toggleScrollWheel(displayPickerDuration)

+ 2 - 4
Trio/Sources/Modules/BasalProfileEditor/View/BasalProfileEditorRootView.swift

@@ -195,10 +195,8 @@ extension BasalProfileEditor {
                     Picker(selection: $state.items[index].rateIndex, label: Text("Rate")) {
                         ForEach(0 ..< state.rateValues.count, id: \.self) { i in
                             Text(
-                                (
-                                    self.rateFormatter
-                                        .string(from: state.rateValues[i] as NSNumber) ?? ""
-                                ) + " U/hr"
+                                (self.rateFormatter.string(from: state.rateValues[i] as NSNumber) ?? "") + " " +
+                                    String(localized: "U/hr")
                             ).tag(i)
                         }
                     }

+ 0 - 4
Trio/Sources/Modules/Base/BaseStateModel.swift

@@ -11,10 +11,6 @@ protocol StateModel: ObservableObject {
     func view(for screen: Screen) -> AnyView
 }
 
-protocol CGMStateModel: StateModel {
-    var cgmCurrent: CGMType { get }
-}
-
 class BaseStateModel<Provider>: StateModel, Injectable where Provider: Trio.Provider {
     @Injected() var router: Router!
     @Injected() var settingsManager: SettingsManager!

+ 20 - 1
Trio/Sources/Modules/Calibrations/View/CalibrationsRootView.swift

@@ -16,6 +16,21 @@ extension Calibrations {
             return formatter
         }
 
+        private var manualGlucoseFormatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            if state.units == .mgdL {
+                formatter.maximumIntegerDigits = 3
+                formatter.maximumFractionDigits = 0
+            } else {
+                formatter.maximumIntegerDigits = 2
+                formatter.minimumFractionDigits = 0
+                formatter.maximumFractionDigits = 1
+            }
+            formatter.roundingMode = .halfUp
+            return formatter
+        }
+
         private var dateFormatter: DateFormatter {
             let formatter = DateFormatter()
             formatter.timeStyle = .short
@@ -30,7 +45,11 @@ extension Calibrations {
                         HStack {
                             Text("Meter glucose")
                             Spacer()
-                            TextFieldWithToolBar(text: $state.newCalibration, placeholder: "0", numberFormatter: formatter)
+                            TextFieldWithToolBar(
+                                text: $state.newCalibration,
+                                placeholder: "0",
+                                numberFormatter: manualGlucoseFormatter
+                            )
                             Text(state.units.rawValue).foregroundColor(.secondary)
                         }
                         Button {

+ 4 - 5
Trio/Sources/Modules/CarbRatioEditor/View/CarbRatioEditorRootView.swift

@@ -125,10 +125,8 @@ extension CarbRatioEditor {
                     Picker(selection: $state.items[index].rateIndex, label: Text("Ratio")) {
                         ForEach(0 ..< state.rateValues.count, id: \.self) { i in
                             Text(
-                                (
-                                    self.rateFormatter
-                                        .string(from: state.rateValues[i] as NSNumber) ?? ""
-                                ) + " g/U"
+                                (self.rateFormatter.string(from: state.rateValues[i] as NSNumber) ?? "") + " " +
+                                    String(localized: "g/U")
                             ).tag(i)
                         }
                     }
@@ -163,7 +161,8 @@ extension CarbRatioEditor {
                         HStack {
                             Text("Ratio").foregroundColor(.secondary)
                             Text(
-                                "\(rateFormatter.string(from: state.rateValues[item.rateIndex] as NSNumber) ?? "0") g/U"
+                                (rateFormatter.string(from: state.rateValues[item.rateIndex] as NSNumber) ?? "0") + " " +
+                                    String(localized: "g/U")
                             )
                             Spacer()
                             Text("starts at").foregroundColor(.secondary)

+ 12 - 4
Trio/Sources/Modules/DataTable/View/CarbEntryEditorView.swift

@@ -40,6 +40,14 @@ struct CarbEntryEditorView: View {
         _editedDate = State(initialValue: Date())
     }
 
+    private var mealFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumIntegerDigits = 3
+        formatter.maximumFractionDigits = 0
+        return formatter
+    }
+
     private var carbLimitExceeded: Bool {
         editedCarbs > state.settingsManager.settings.maxCarbs
     }
@@ -130,7 +138,7 @@ struct CarbEntryEditorView: View {
                             text: $editedCarbs,
                             placeholder: "0",
                             keyboardType: .numberPad,
-                            numberFormatter: Formatter.decimalFormatterWithOneFractionDigit
+                            numberFormatter: mealFormatter
                         )
                         Text("g").foregroundStyle(.secondary)
                     }
@@ -142,7 +150,7 @@ struct CarbEntryEditorView: View {
                                 text: $editedProtein,
                                 placeholder: "0",
                                 keyboardType: .numberPad,
-                                numberFormatter: Formatter.decimalFormatterWithOneFractionDigit
+                                numberFormatter: mealFormatter
                             )
                             Text("g").foregroundStyle(.secondary)
                         }
@@ -153,7 +161,7 @@ struct CarbEntryEditorView: View {
                                 text: $editedFat,
                                 placeholder: "0",
                                 keyboardType: .numberPad,
-                                numberFormatter: Formatter.decimalFormatterWithOneFractionDigit
+                                numberFormatter: mealFormatter
                             )
                             Text("g").foregroundStyle(.secondary)
                         }
@@ -161,7 +169,7 @@ struct CarbEntryEditorView: View {
 
                     HStack {
                         Image(systemName: "square.and.pencil")
-                        TextFieldWithToolBarString(text: $editedNote, placeholder: "Note...", maxLength: 25)
+                        TextFieldWithToolBarString(text: $editedNote, placeholder: String(localized: "Note..."), maxLength: 25)
                     }
                 }.listRowBackground(Color.chart)
 

+ 10 - 6
Trio/Sources/Modules/DataTable/View/DataTableRootView.swift

@@ -61,8 +61,11 @@ extension DataTable {
         private var manualGlucoseFormatter: NumberFormatter {
             let formatter = NumberFormatter()
             formatter.numberStyle = .decimal
-            formatter.maximumFractionDigits = 0
-            if state.units == .mmolL {
+            if state.units == .mgdL {
+                formatter.maximumIntegerDigits = 3
+                formatter.maximumFractionDigits = 0
+            } else {
+                formatter.maximumIntegerDigits = 2
                 formatter.minimumFractionDigits = 0
                 formatter.maximumFractionDigits = 1
             }
@@ -421,8 +424,8 @@ extension DataTable {
         }
 
         @ViewBuilder private func addGlucoseView() -> some View {
-            let limitLow: Decimal = state.units == .mmolL ? 0.8 : 14
-            let limitHigh: Decimal = state.units == .mmolL ? 40 : 720
+            let limitLow: Decimal = state.units == .mgdL ? Decimal(14) : 14.asMmolL
+            let limitHigh: Decimal = state.units == .mgdL ? Decimal(720) : 720.asMmolL
 
             NavigationView {
                 VStack {
@@ -433,8 +436,9 @@ extension DataTable {
                                 TextFieldWithToolBar(
                                     text: $state.manualGlucose,
                                     placeholder: " ... ",
-                                    shouldBecomeFirstResponder: true,
-                                    numberFormatter: manualGlucoseFormatter
+                                    keyboardType: state.units == .mgdL ? .numberPad : .decimalPad,
+                                    numberFormatter: manualGlucoseFormatter,
+                                    initialFocus: true
                                 )
                                 Text(state.units.rawValue).foregroundStyle(.secondary)
                             }

+ 0 - 2
Trio/Sources/Modules/GlucoseNotificationSettings/GlucoseNotificationSettingsStateModel.swift

@@ -4,7 +4,6 @@ extension GlucoseNotificationSettings {
     final class StateModel: BaseStateModel<Provider> {
         @Published var glucoseBadge = false
         @Published var glucoseNotificationsOption: GlucoseNotificationsOption = .onlyAlarmLimits
-        @Published var useAlarmSound = false
         @Published var addSourceInfoToGlucoseNotifications = false
         @Published var lowGlucose: Decimal = 0
         @Published var highGlucose: Decimal = 0
@@ -27,7 +26,6 @@ extension GlucoseNotificationSettings {
 
             subscribeSetting(\.glucoseBadge, on: $glucoseBadge) { glucoseBadge = $0 }
             subscribeSetting(\.glucoseNotificationsOption, on: $glucoseNotificationsOption) { glucoseNotificationsOption = $0 }
-            subscribeSetting(\.useAlarmSound, on: $useAlarmSound) { useAlarmSound = $0 }
             subscribeSetting(\.addSourceInfoToGlucoseNotifications, on: $addSourceInfoToGlucoseNotifications) {
                 addSourceInfoToGlucoseNotifications = $0 }
 

+ 0 - 22
Trio/Sources/Modules/GlucoseNotificationSettings/View/GlucoseNotificationSettingsRootView.swift

@@ -42,28 +42,6 @@ extension GlucoseNotificationSettings {
             List {
                 SettingInputSection(
                     decimalValue: $decimalPlaceholder,
-                    booleanValue: $state.useAlarmSound,
-                    shouldDisplayHint: $shouldDisplayHint,
-                    selectedVerboseHint: Binding(
-                        get: { selectedVerboseHint },
-                        set: {
-                            selectedVerboseHint = $0.map { AnyView($0) }
-                            hintLabel = String(localized: "Play Alarm Sound")
-                        }
-                    ),
-                    units: state.units,
-                    type: .boolean,
-                    label: String(localized: "Play Alarm Sound"),
-                    miniHint: String(localized: "Alarm with every Trio notification."),
-                    verboseHint: VStack(alignment: .leading, spacing: 10) {
-                        Text("Default: OFF").bold()
-                        Text(
-                            "This will cause a sound to be triggered by Trio notifications for Carbs Required, and Glucose Low/High Alarms."
-                        )
-                    }
-                )
-                SettingInputSection(
-                    decimalValue: $decimalPlaceholder,
                     booleanValue: $state.notificationsPump,
                     shouldDisplayHint: $shouldDisplayHint,
                     selectedVerboseHint: Binding(

+ 52 - 0
Trio/Sources/Modules/Home/HomeStateModel+Setup/CurrentTDDSetup.swift

@@ -0,0 +1,52 @@
+import CoreData
+import Foundation
+
+extension Home.StateModel {
+    func setupTDDArray() {
+        Task {
+            do {
+                // Get the NSManagedObjectIDs
+                let tddObjectIds = try await fetchTDDIDs()
+
+                // Get the NSManagedObjects and map them to TDD on the Main Thread
+                try await updateTDDArray(with: tddObjectIds, keyPath: \.fetchedTDDs)
+            } catch {
+                debug(.default, "\(DebuggingIdentifiers.failed) failed to fetch TDDs: \(error.localizedDescription)")
+            }
+        }
+    }
+
+    @MainActor private func updateTDDArray(
+        with IDs: [NSManagedObjectID],
+        keyPath: ReferenceWritableKeyPath<Home.StateModel, [TDD]>
+    ) async throws {
+        let tddObjects: [TDD] = try await CoreDataStack.shared
+            .getNSManagedObject(with: IDs, context: viewContext)
+            .compactMap { managedObject in
+                // Safely extract date and total as optional
+                let timestamp = managedObject.value(forKey: "date") as? Date
+                let totalDailyDose = (managedObject.value(forKey: "total") as? NSNumber)?.decimalValue
+                return TDD(totalDailyDose: totalDailyDose, timestamp: timestamp)
+            }
+        self[keyPath: keyPath] = tddObjects
+    }
+
+    private func fetchTDDIDs() async throws -> [NSManagedObjectID] {
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: TDDStored.self,
+            onContext: tddFetchContext,
+            predicate: NSPredicate.predicateForOneDayAgo,
+            key: "date",
+            ascending: false,
+            fetchLimit: 1,
+            propertiesToFetch: ["total", "date", "objectID"]
+        )
+
+        return await tddFetchContext.perform {
+            guard let fetchedResults = results as? [[String: Any]] else {
+                return []
+            }
+            return fetchedResults.compactMap { $0["objectID"] as? NSManagedObjectID }
+        }
+    }
+}

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

@@ -124,9 +124,9 @@ extension Home.StateModel {
         let deviationFromNormal = halfBasalTargetValue - normalTarget
 
         let adjustmentFactor = deviationFromNormal + (tempTargetValue - normalTarget)
-        let adjustmentRatio: Decimal = (deviationFromNormal * adjustmentFactor <= 0) ? maxValue : deviationFromNormal /
+        let adjustmentRatio: Decimal = (deviationFromNormal * adjustmentFactor <= 0) ? autosensMax : deviationFromNormal /
             adjustmentFactor
 
-        return Int(Double(min(adjustmentRatio, maxValue) * 100).rounded())
+        return Int(Double(min(adjustmentRatio, autosensMax) * 100).rounded())
     }
 }

+ 19 - 65
Trio/Sources/Modules/Home/HomeStateModel.swift

@@ -63,12 +63,12 @@ extension Home {
         var alarm: GlucoseAlarm?
         var manualTempBasal = false
         var isSmoothingEnabled = false
-        var maxValue: Decimal = 1.2
+        var autosensMax: Decimal = 1.2
         var lowGlucose: Decimal = 70
         var highGlucose: Decimal = 180
         var currentGlucoseTarget: Decimal = 100
         var glucoseColorScheme: GlucoseColorScheme = .staticColor
-        var hbA1cDisplayUnit: HbA1cDisplayUnit = .percent
+        var eA1cDisplayUnit: EstimatedA1cDisplayUnit = .percent
         var displayXgridLines: Bool = false
         var displayYgridLines: Bool = false
         var thresholdLines: Bool = false
@@ -76,7 +76,6 @@ extension Home {
         var totalBolus: Decimal = 0
         var isLoopStatusPresented: Bool = false
         var isLegendPresented: Bool = false
-        var totalInsulinDisplayType: TotalInsulinDisplayType = .totalDailyDose
         var roundedTotalBolus: String = ""
         var selectedTab: Int = 0
         var waitForSuggestion: Bool = false
@@ -86,6 +85,7 @@ extension Home {
         var fpusFromPersistence: [CarbEntryStored] = []
         var determinationsFromPersistence: [OrefDetermination] = []
         var enactedAndNonEnactedDeterminations: [OrefDetermination] = []
+        var fetchedTDDs: [TDD] = []
         var insulinFromPersistence: [PumpEventStored] = []
         var tempBasals: [PumpEventStored] = []
         var suspensions: [PumpEventStored] = []
@@ -125,6 +125,7 @@ extension Home {
         let carbsFetchContext = CoreDataStack.shared.newTaskContext()
         let fpuFetchContext = CoreDataStack.shared.newTaskContext()
         let determinationFetchContext = CoreDataStack.shared.newTaskContext()
+        let tddFetchContext = CoreDataStack.shared.newTaskContext()
         let pumpHistoryFetchContext = CoreDataStack.shared.newTaskContext()
         let overrideFetchContext = CoreDataStack.shared.newTaskContext()
         let tempTargetFetchContext = CoreDataStack.shared.newTaskContext()
@@ -179,6 +180,9 @@ extension Home {
                         self.setupDeterminationsArray()
                     }
                     group.addTask {
+                        self.setupTDDArray()
+                    }
+                    group.addTask {
                         self.setupInsulinArray()
                     }
                     group.addTask {
@@ -237,6 +241,11 @@ extension Home {
                 self.setupDeterminationsArray()
             }.store(in: &subscriptions)
 
+            coreDataPublisher?.filteredByEntityName("TDDStored").sink { [weak self] _ in
+                guard let self = self else { return }
+                self.setupTDDArray()
+            }.store(in: &subscriptions)
+
             coreDataPublisher?.filteredByEntityName("GlucoseStored").sink { [weak self] _ in
                 guard let self = self else { return }
                 self.setupGlucoseArray()
@@ -372,21 +381,19 @@ extension Home {
             manualTempBasal = apsManager.isManualTempBasal
             isSmoothingEnabled = settingsManager.settings.smoothGlucose
             glucoseColorScheme = settingsManager.settings.glucoseColorScheme
-            maxValue = settingsManager.preferences.autosensMax
+            autosensMax = settingsManager.preferences.autosensMax
             lowGlucose = settingsManager.settings.low
             highGlucose = settingsManager.settings.high
-            hbA1cDisplayUnit = settingsManager.settings.hbA1cDisplayUnit
+            eA1cDisplayUnit = settingsManager.settings.eA1cDisplayUnit
             displayXgridLines = settingsManager.settings.xGridLines
             displayYgridLines = settingsManager.settings.yGridLines
             thresholdLines = settingsManager.settings.rulerMarks
-            totalInsulinDisplayType = settingsManager.settings.totalInsulinDisplayType
             showCarbsRequiredBadge = settingsManager.settings.showCarbsRequiredBadge
             forecastDisplayType = settingsManager.settings.forecastDisplayType
             isExerciseModeActive = settingsManager.preferences.exerciseMode
             highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
             lowTTlowersSens = settingsManager.preferences.lowTemptargetLowersSensitivity
             settingHalfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
-            maxValue = settingsManager.preferences.autosensMax
         }
 
         @MainActor private func setupCGMSettings() async {
@@ -516,55 +523,6 @@ extension Home {
             }
         }
 
-        func calculateTINS() -> String {
-            let startTime = calculateStartTime(hours: Int(hours))
-
-            let totalBolus = calculateTotalBolus(from: insulinFromPersistence, since: startTime)
-            let totalBasal = calculateTotalBasal(from: insulinFromPersistence, since: startTime)
-
-            let totalInsulin = totalBolus + totalBasal
-
-            return formatInsulinAmount(totalInsulin)
-        }
-
-        private func calculateStartTime(hours: Int) -> Date {
-            let date = Date()
-            let calendar = Calendar.current
-            var offsetComponents = DateComponents()
-            offsetComponents.hour = -hours
-            return calendar.date(byAdding: offsetComponents, to: date)!
-        }
-
-        private func calculateTotalBolus(from events: [PumpEventStored], since startTime: Date) -> Double {
-            let bolusEvents = events.filter { $0.timestamp ?? .distantPast >= startTime && $0.type == PumpEvent.bolus.rawValue }
-            return bolusEvents.compactMap { $0.bolus?.amount?.doubleValue }.reduce(0, +)
-        }
-
-        private func calculateTotalBasal(from events: [PumpEventStored], since startTime: Date) -> Double {
-            let basalEvents = events
-                .filter { $0.timestamp ?? .distantPast >= startTime && $0.type == PumpEvent.tempBasal.rawValue }
-                .sorted { $0.timestamp ?? .distantPast < $1.timestamp ?? .distantPast }
-
-            var basalDurations: [Double] = []
-            for (index, basalEntry) in basalEvents.enumerated() {
-                if index + 1 < basalEvents.count {
-                    let nextEntry = basalEvents[index + 1]
-                    let durationInSeconds = nextEntry.timestamp?.timeIntervalSince(basalEntry.timestamp ?? Date()) ?? 0
-                    basalDurations.append(durationInSeconds / 3600) // Conversion to hours
-                }
-            }
-
-            return zip(basalEvents, basalDurations).map { entry, duration in
-                guard let rate = entry.tempBasal?.rate?.doubleValue else { return 0 }
-                return rate * duration
-            }.reduce(0, +)
-        }
-
-        private func formatInsulinAmount(_ amount: Double) -> String {
-            let roundedAmount = Decimal(round(100 * amount) / 100)
-            return roundedAmount.formatted()
-        }
-
         private func setupPumpSettings() async {
             let maxBasal = await provider.pumpSettings().maxBasal
             await MainActor.run {
@@ -600,15 +558,12 @@ extension Home {
         private func getCurrentGlucoseTarget() async {
             let now = Date()
             let calendar = Calendar.current
-            let dateFormatter = DateFormatter()
-            dateFormatter.dateFormat = "HH:mm"
-            dateFormatter.timeZone = TimeZone.current
 
             let entries: [(start: String, value: Decimal)] = bgTargets.targets.map { ($0.start, $0.low) }
 
             for (index, entry) in entries.enumerated() {
-                guard let entryTime = dateFormatter.date(from: entry.start) else {
-                    print("Invalid entry start time: \(entry.start)")
+                guard let entryTime = TherapySettingsUtil.parseTime(entry.start) else {
+                    debug(.default, "Invalid entry start time: \(entry.start)")
                     continue
                 }
 
@@ -622,7 +577,7 @@ extension Home {
 
                 let entryEndTime: Date
                 if index < entries.count - 1,
-                   let nextEntryTime = dateFormatter.date(from: entries[index + 1].start)
+                   let nextEntryTime = TherapySettingsUtil.parseTime(entries[index + 1].start)
                 {
                     let nextEntryComponents = calendar.dateComponents([.hour, .minute, .second], from: nextEntryTime)
                     entryEndTime = calendar.date(
@@ -672,12 +627,11 @@ extension Home.StateModel:
             await getCurrentGlucoseTarget()
             await setupGlucoseTargets()
         }
-        hbA1cDisplayUnit = settingsManager.settings.hbA1cDisplayUnit
+        eA1cDisplayUnit = settingsManager.settings.eA1cDisplayUnit
         glucoseColorScheme = settingsManager.settings.glucoseColorScheme
         displayXgridLines = settingsManager.settings.xGridLines
         displayYgridLines = settingsManager.settings.yGridLines
         thresholdLines = settingsManager.settings.rulerMarks
-        totalInsulinDisplayType = settingsManager.settings.totalInsulinDisplayType
         showCarbsRequiredBadge = settingsManager.settings.showCarbsRequiredBadge
         forecastDisplayType = settingsManager.settings.forecastDisplayType
         cgmAvailable = (fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none)
@@ -690,7 +644,7 @@ extension Home.StateModel:
     }
 
     func preferencesDidChange(_: Preferences) {
-        maxValue = settingsManager.preferences.autosensMax
+        autosensMax = settingsManager.preferences.autosensMax
         settingHalfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
         highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
         isExerciseModeActive = settingsManager.preferences.exerciseMode

+ 41 - 16
Trio/Sources/Modules/Home/View/Chart/ChartElements/GlucoseChartView.swift

@@ -78,23 +78,48 @@ 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
-                )
+    struct PreviewWrapper: View {
+        @State private var previewStack: CoreDataStack? = nil
+        @State private var glucoseData: [GlucoseStored] = []
+        @State private var isLoading = true
+
+        var body: some View {
+            NavigationView {
+                Group {
+                    if isLoading {
+                        ProgressView("Loading data...")
+                    } else {
+                        VStack {
+                            Chart {
+                                GlucoseChartView(
+                                    glucoseData: glucoseData,
+                                    units: .mgdL,
+                                    highGlucose: 180,
+                                    lowGlucose: 70,
+                                    currentGlucoseTarget: 100,
+                                    isSmoothingEnabled: false,
+                                    glucoseColorScheme: .dynamicColor
+                                )
+                            }
+                            .frame(height: 200)
+                            .padding()
+                        }
+                    }
+                }
+                .navigationTitle("Glucose Chart")
+                .task {
+                    // Use the preview stack that's initialized asynchronously in CoreDataStack
+                    previewStack = try? await CoreDataStack.preview()
+
+                    // Now you can safely create preview data
+                    if let stack = previewStack {
+                        glucoseData = GlucoseStored.makePreviewGlucose(count: 24, provider: stack)
+                        isLoading = false
+                    }
+                }
             }
-            .frame(height: 200)
-            .padding()
         }
-        .navigationTitle("Glucose Chart")
     }
+
+    return PreviewWrapper()
 }

+ 9 - 2
Trio/Sources/Modules/Home/View/Chart/ChartElements/OverrideView.swift

@@ -23,8 +23,15 @@ struct OverrideView: ChartContent {
                 attribute: "duration",
                 context: viewContext
             ) ?? 0
-            let end: Date = duration != 0 ? start.addingTimeInterval(duration) : start
-                .addingTimeInterval(60 * 60 * 24 * 30) // handle infinite overrides -> 60s x 60m x 24h x 30d = 30 days duration
+            let end: Date = {
+                if override.indefinite {
+                    return start.addingTimeInterval(60 * 60 * 24 * 30)
+                } else if duration != 0 {
+                    return start.addingTimeInterval(duration)
+                } else {
+                    return start.addingTimeInterval(60 * 60 * 24 * 30)
+                }
+            }()
 
             let target = getOverrideTarget(override: override)
 

+ 0 - 3
Trio/Sources/Modules/Home/View/Chart/MainChartView.swift

@@ -186,9 +186,6 @@ extension MainChartView {
                 }
             }
             .id("MainChart")
-            .onChange(of: state.insulinFromPersistence) {
-                state.roundedTotalBolus = state.calculateTINS()
-            }
             .frame(
                 minHeight: geo.size.height * (0.28 - safeAreaSize)
             )

+ 18 - 22
Trio/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift

@@ -39,14 +39,6 @@ struct CurrentGlucoseView: View {
         return formatter
     }
 
-    private var timaAgoFormatter: NumberFormatter {
-        let formatter = NumberFormatter()
-        formatter.numberStyle = .decimal
-        formatter.maximumFractionDigits = 0
-        formatter.negativePrefix = ""
-        return formatter
-    }
-
     var body: some View {
         let triangleColor = Color(red: 0.262745098, green: 0.7333333333, blue: 0.9137254902)
 
@@ -91,20 +83,24 @@ struct CurrentGlucoseView: View {
                     }
                     HStack {
                         let minutesAgo = -1 * (glucose.last?.date?.timeIntervalSinceNow ?? 0) / 60
-                        let text = timaAgoFormatter.string(for: Double(minutesAgo)) ?? ""
-                        Text(
-                            minutesAgo <= 1 ? "< 1 " + String(localized: "min", comment: "Short form for minutes") : (
-                                text + " " +
-                                    String(localized: "min", comment: "Short form for minutes") + " "
-                            )
-                        )
-                        .font(.caption2).foregroundStyle(colorScheme == .dark ? Color.white.opacity(0.9) : Color.secondary)
-
-                        Text(
-                            delta
-                        )
-                        .font(.caption2).foregroundStyle(colorScheme == .dark ? Color.white.opacity(0.9) : Color.secondary)
-                    }.frame(alignment: .top)
+                        var minutesAgoString: String {
+                            if minutesAgo > 1 {
+                                let minuteString = Formatter.timaAgoFormatter.string(for: Double(minutesAgo)) ?? ""
+                                return minuteString + "\u{00A0}" + String(localized: "m", comment: "Abbreviation for Minutes")
+                            } else {
+                                return "<" + "\u{00A0}" + "1" + "\u{00A0}" +
+                                    String(localized: "m", comment: "Abbreviation for Minutes")
+                            }
+                        }
+
+                        Group {
+                            Text(minutesAgoString)
+                            Text(delta)
+                        }
+                        .font(.callout).fontWeight(.bold)
+                        .foregroundStyle(colorScheme == .dark ? Color.white.opacity(0.9) : Color.secondary)
+                    }
+                    .frame(alignment: .top)
                 }
             }
             .onChange(of: glucose.last?.directionEnum) {

+ 15 - 13
Trio/Sources/Modules/Home/View/Header/LoopStatusView.swift

@@ -41,16 +41,6 @@ struct LoopStatusView: View {
                         }
                     )
                 }.padding(.top, 20)
-//                Text("Current Loop Status").bold().padding(.top, 20)
-//
-//                Text(statusTitle)
-//                    .font(.headline)
-//                    .bold()
-//                    .padding(.horizontal, 12)
-//                    .padding(.vertical, 6)
-//                    .foregroundColor(statusBadgeTextColor)
-//                    .background(statusBadgeColor)
-//                    .clipShape(Capsule())
 
                 if let errorMessage = state.errorMessage, let date = state.errorDate {
                     Group {
@@ -85,10 +75,8 @@ struct LoopStatusView: View {
                         .multilineTextAlignment(.leading)
                         .fixedSize(horizontal: false, vertical: true)
 
-                        let tags = !state.isSmoothingEnabled ? determination.reasonParts : determination
-                            .reasonParts + ["Smoothing: On"]
                         TagCloudView(
-                            tags: tags,
+                            tags: getComputedTags(determination),
                             shouldParseToMmolL: state.units == .mmolL
                         )
 
@@ -274,6 +262,20 @@ struct LoopStatusView: View {
 
         return updatedConclusion.capitalizingFirstLetter()
     }
+
+    private func getComputedTags(_ determination: OrefDetermination) -> [String] {
+        var tags: [String] = determination.reasonParts
+
+        if state.isSmoothingEnabled {
+            tags.append("Smoothing: On")
+        }
+
+        if let currentTDD = state.fetchedTDDs.first?.totalDailyDose, currentTDD != 0 {
+            tags.append("TDD: \(currentTDD)")
+        }
+
+        return tags
+    }
 }
 
 struct ContentSizeKey: PreferenceKey {

+ 8 - 3
Trio/Sources/Modules/Home/View/Header/LoopView.swift

@@ -57,11 +57,16 @@ struct LoopView: View {
     }
 
     private var timeString: String {
-        let minAgo = Int((timerDate.timeIntervalSince(lastLoopDate) - Config.lag) / 60) + 1
-        if minAgo > 1440 {
+        let minutesAgo = -1 * lastLoopDate.timeIntervalSinceNow / 60
+        let minuteString = Formatter.timaAgoFormatter.string(for: Double(minutesAgo)) ?? ""
+
+        if minutesAgo > 1440 {
             return "--"
+        } else if minutesAgo <= 1 {
+            return "<" + "\u{00A0}" + "1" + "\u{00A0}" + String(localized: "m", comment: "Abbreviation for Minutes")
+        } else {
+            return minuteString + "\u{00A0}" + String(localized: "m", comment: "Abbreviation for Minutes")
         }
-        return "\(minAgo) " + String(localized: "min", comment: "Minutes ago since last loop")
     }
 
     private var color: Color {

+ 8 - 37
Trio/Sources/Modules/Home/View/HomeRootView.swift

@@ -255,7 +255,7 @@ extension Home {
             } else { halfBasalTarget = state.settingHalfBasalTarget }
             var showPercentage = false
             if target > 100, state.isExerciseModeActive || state.highTTraisesSens { showPercentage = true }
-            if target < 100, state.lowTTlowersSens { showPercentage = true }
+            if target < 100, state.lowTTlowersSens, state.autosensMax > 1 { showPercentage = true }
             if showPercentage {
                 percentageString =
                     " \(state.computeAdjustedPercentage(halfBasalTargetValue: halfBasalTarget, tempTargetValue: target))%" }
@@ -292,7 +292,7 @@ extension Home {
                         Group {
                             if button.active {
                                 Text(
-                                    button.hours.description + " " +
+                                    button.hours.description + "\u{00A0}" +
                                         String(localized: "h", comment: "h")
                                 )
                             } else {
@@ -488,37 +488,7 @@ extension Home {
                             .font(.callout).fontWeight(.bold).fontDesign(.rounded)
                     }
                 }
-                if state.totalInsulinDisplayType == .totalDailyDose {
-                    Spacer()
-                    Text(
-                        "TDD: " +
-                            (
-                                Formatter.decimalFormatterWithTwoFractionDigits
-                                    .string(from: (state.determinationsFromPersistence.first?.totalDailyDose ?? 0) as NSNumber) ??
-                                    "0"
-                            ) +
-                            String(localized: " U", comment: "Insulin unit")
-                    )
-                    .font(.callout).fontWeight(.bold).fontDesign(.rounded)
-                } else {
-                    Spacer()
-                    HStack {
-                        Text(
-                            "TINS: \(state.roundedTotalBolus)" +
-                                String(localized: " U", comment: "Unit in number of units delivered (keep the space character!)")
-                        )
-                        .font(.callout).fontWeight(.bold).fontDesign(.rounded)
-                        .onChange(of: state.hours) {
-                            state.roundedTotalBolus = state.calculateTINS()
-                        }
-                        .onAppear {
-                            DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
-                                state.roundedTotalBolus = state.calculateTINS()
-                            }
-                        }
-                    }
-                }
-            }.padding(.horizontal, 10)
+            }.padding(.horizontal)
         }
 
         @ViewBuilder func adjustmentsOverrideView(_ overrideString: String) -> some View {
@@ -1150,18 +1120,19 @@ func is24HourFormat() -> Bool {
     return !dateString.contains("AM") && !dateString.contains("PM")
 }
 
-/// Converts a duration in minutes to a formatted string (e.g., "1 hr 30 min").
+/// Converts a duration in minutes to a formatted string (e.g., "1 h 30 m").
 func formatHrMin(_ durationInMinutes: Int) -> String {
     let hours = durationInMinutes / 60
     let minutes = durationInMinutes % 60
 
     switch (hours, minutes) {
     case let (0, m):
-        return "\(m) min"
+        return "\(m)\u{00A0}" + String(localized: "m", comment: "Abbreviation for Minutes")
     case let (h, 0):
-        return "\(h) hr"
+        return "\(h)\u{00A0}" + String(localized: "h", comment: "h")
     default:
-        return "\(hours) hr \(minutes) min"
+        return hours.description + "\u{00A0}" + String(localized: "h", comment: "h") + "\u{00A0}" + minutes
+            .description + "\u{00A0}" + String(localized: "m", comment: "Abbreviation for Minutes")
     }
 }
 

+ 2 - 2
Trio/Sources/Modules/ISFEditor/View/ISFEditorRootView.swift

@@ -124,7 +124,7 @@ extension ISFEditor {
                         ForEach(0 ..< state.rateValues.count, id: \.self) { i in
                             Text(
                                 state.units == .mgdL ? state.rateValues[i].description : state.rateValues[i]
-                                    .formattedAsMmolL + " \(state.units.rawValue)/U"
+                                    .formattedAsMmolL + String(localized: " \(state.units.rawValue)/U")
                             ).tag(i)
                         }
                     }
@@ -162,7 +162,7 @@ extension ISFEditor {
                             Text("Rate").foregroundColor(.secondary)
 
                             Text(
-                                displayValue + " \(state.units.rawValue)/U"
+                                displayValue + String(localized: " \(state.units.rawValue)/U")
                             )
                             Spacer()
                             Text("starts at").foregroundColor(.secondary)

+ 1 - 1
Trio/Sources/Modules/LiveActivitySettings/View/LiveActivityWidgetConfiguration.swift

@@ -78,7 +78,7 @@ struct LiveActivityWidgetConfiguration: BaseView {
         VStack {
             Group {
                 VStack(alignment: .trailing, spacing: 0) {
-                    Text("Live Activity Personalization".uppercased())
+                    Text(String(localized: "Live Activity Personalization").uppercased())
                         .frame(maxWidth: .infinity, alignment: .leading)
                         .foregroundColor(.secondary)
                         .font(.footnote)

+ 35 - 2
Trio/Sources/Modules/Main/MainStateModel.swift

@@ -201,6 +201,37 @@ extension Main {
             SwiftMessages.show(config: config, view: view)
         }
 
+        /*
+          Reclassification is needed for Medtronic pumps for 'Pump error:' RileyLink related messages.
+          For details, see https://discord.com/channels/1020905149037813862/1338245444186279946/1343469793013141525.
+          Reclassification of Info type messages is based on APSManager.APSError enum values.
+          Currently, we only re-classify APSError.pumpError 'Pump error:' type to MessageType.error.
+          MessageType.error messagges are always displayed to the user and the user cannot disable them.
+          Other APSManager.APSError remain as MessageType.info which allows users to disable them
+          using the 'Trio Notification' -> 'Always Notify Algorithm' setting.
+         */
+        func reclassifyInfoNotification(_ message: inout MessageContent) {
+            if message.title == "" {
+                switch message.type {
+                case .info:
+                    if let errorIndex = message.content.range(of: "error", options: .caseInsensitive) {
+                        message.title = String(localized: "Error", comment: "Error title")
+                        if let errorPumpIndex = message.content.range(of: "Pump error:", options: .caseInsensitive) {
+                            message.type = .error
+                        }
+                    } else {
+                        message.title = String(localized: "Info", comment: "Info title")
+                    }
+                case .warning:
+                    message.title = String(localized: "Warning", comment: "Warning title")
+                case .error:
+                    message.title = String(localized: "Error", comment: "Error title")
+                case .other:
+                    message.title = String(localized: "Info", comment: "Info title")
+                }
+            }
+        }
+
         override func subscribe() {
             router.mainModalScreen
                 .map { $0?.modal(resolver: self.resolver!) }
@@ -223,8 +254,10 @@ extension Main {
                 .receive(on: DispatchQueue.main)
                 .sink { message in
                     guard !self.isApnPumpConfigAction(message) else { return }
-                    guard self.router.allowNotify(message, self.settingsManager.settings) else { return }
-                    self.showAlertMessage(message)
+                    var reclassifyMessage = message
+                    self.reclassifyInfoNotification(&reclassifyMessage)
+                    guard self.router.allowNotify(reclassifyMessage, self.settingsManager.settings) else { return }
+                    self.showAlertMessage(reclassifyMessage)
                 }
                 .store(in: &lifetime)
 

+ 91 - 0
Trio/Sources/Modules/Main/View/MainLoadingView.swift

@@ -0,0 +1,91 @@
+import SwiftUI
+
+extension Main {
+    struct LoadingView: View {
+        @Binding var showError: Bool
+        let retry: () -> Void
+
+        private let versionNumber = Bundle.main.releaseVersionNumber ?? String(localized: "Unknown")
+
+        var body: some View {
+            ZStack {
+                LinearGradient(
+                    gradient: Gradient(colors: [Color.bgDarkBlue, Color.bgDarkerDarkBlue]),
+                    startPoint: .top,
+                    endPoint: .bottom
+                )
+                .ignoresSafeArea()
+
+                VStack {
+                    Spacer().frame(maxHeight: 92)
+
+                    Image(.trioCircledNoBackground)
+                        .resizable()
+                        .scaledToFit()
+                        .frame(width: 92, height: 92)
+                        .shadow(color: Color.white.opacity(0.1), radius: 5, x: 0, y: 0)
+
+                    Text("Trio v\(versionNumber)")
+                        .fontWeight(.heavy)
+                        .foregroundStyle(Color(red: 148 / 255, green: 102 / 255, blue: 234 / 255))
+                        .padding(.vertical)
+
+                    if showError {
+                        Spacer().frame(maxHeight: 60)
+
+                        VStack(alignment: .leading, spacing: 12) {
+                            Text("Oops, there was an issue!").font(.title3).bold()
+
+                            Text("Something went wrong while loading your data. Please try again in a few moments.")
+                                .foregroundStyle(.white)
+                        }
+                        .padding(.horizontal, 24)
+                        .foregroundStyle(.white)
+
+                        Spacer()
+
+                        RetryButton(action: retry).padding(.bottom, 60)
+                    } else {
+                        Spacer().frame(maxHeight: 100)
+
+                        CustomProgressView(text: String(localized: "Getting everything ready for you...")).foregroundStyle(.white)
+
+                        Spacer()
+                    }
+                }
+            }
+        }
+    }
+
+    struct RetryButton: View {
+        var action: () -> Void
+
+        var body: some View {
+            Button(action: action) {
+                HStack(spacing: 8) {
+                    Image(systemName: "arrow.clockwise")
+                    Text("Retry")
+                }
+                .frame(width: UIScreen.main.bounds.width - 60, height: 50)
+                .font(.title3).bold()
+                .background(
+                    Capsule()
+                        .fill(Color.tabBar)
+                )
+                .foregroundColor(.white)
+                .shadow(color: Color.black.opacity(0.1), radius: 5, x: 0, y: 2)
+            }
+        }
+    }
+}
+
+struct LoadingView_Previews: PreviewProvider {
+    static var previews: some View {
+        Group {
+            Main.LoadingView(showError: .constant(false), retry: {})
+                .previewDisplayName("Loading")
+            Main.LoadingView(showError: .constant(true), retry: {})
+                .previewDisplayName("Error")
+        }
+    }
+}

+ 3 - 2
Trio/Sources/Modules/ManualTempBasal/View/ManualTempBasalRootView.swift

@@ -12,6 +12,7 @@ extension ManualTempBasal {
         private var formatter: NumberFormatter {
             let formatter = NumberFormatter()
             formatter.numberStyle = .decimal
+            formatter.maximumIntegerDigits = 2
             formatter.maximumFractionDigits = 2
             return formatter
         }
@@ -25,8 +26,8 @@ extension ManualTempBasal {
                         TextFieldWithToolBar(
                             text: $state.rate,
                             placeholder: "0",
-                            shouldBecomeFirstResponder: true,
-                            numberFormatter: formatter
+                            numberFormatter: formatter,
+                            initialFocus: true
                         )
                         Text("U/hr").foregroundColor(.secondary)
                     }

+ 1 - 5
Trio/Sources/Modules/Settings/SettingItems.swift

@@ -205,10 +205,7 @@ enum SettingItems {
                 "Low Threshold",
                 "High Threshold",
                 "X-Axis Interval Step",
-                "Total Insulin Display Type",
-                "Total Daily Dose (TDD)",
-                "Total Insulin in Scope (TINS)",
-                "Override HbA1c Unit",
+                "Override eA1c Unit",
                 "Standing / Laying TIR Chart",
                 "Show Carbs Required Badge",
                 "Carbs Required Threshold",
@@ -233,7 +230,6 @@ enum SettingItems {
             title: "Trio Notifications",
             view: .glucoseNotificationSettings,
             searchContents: [
-                "Play Alarm Sound",
                 "Always Notify Pump",
                 "Always Notify CGM",
                 "Always Notify Carb",

+ 0 - 1
Trio/Sources/Modules/Snooze/View/SnoozeRootView.swift

@@ -90,7 +90,6 @@ extension Snooze {
                     state.snoozeUntilDate = untilDate < Date() ? .distantPast : untilDate
                     debug(.default, "will snooze for \(snoozeFor) until \(dateFormatter.string(from: untilDate))")
                     snoozeDescription = getSnoozeDescription()
-                    BaseUserNotificationsManager.stopSound()
                     state.hideModal()
                 } label: {
                     Text("Click to Snooze Alerts")

+ 144 - 0
Trio/Sources/Modules/Stat/StatStateModel+Setup/AreaChartSetup.swift

@@ -0,0 +1,144 @@
+import CoreData
+import Foundation
+
+/// Represents statistical values for glucose readings grouped by hour of the day.
+///
+/// This struct contains various percentile calculations that help visualize
+/// glucose distribution patterns throughout the day:
+///
+/// - The median (50th percentile) shows the central tendency
+/// - The 25th and 75th percentiles form the interquartile range (IQR)
+/// - The 10th and 90th percentiles show the wider range of values
+///
+/// Example usage in visualization:
+/// ```
+/// let stats = HourlyStats(
+///     hour: 14,        // 2 PM
+///     median: 120,     // Center line
+///     percentile25: 100, // Lower bound of dark band
+///     percentile75: 140, // Upper bound of dark band
+///     percentile10: 80,  // Lower bound of light band
+///     percentile90: 160  // Upper bound of light band
+/// )
+/// ```
+///
+/// This data structure is used to create area charts showing glucose
+/// variability patterns across different times of day.
+public struct HourlyStats: Equatable {
+    /// The hour of day (0-23) these statistics represent
+    let hour: Int
+    /// The median (50th percentile) glucose value for this hour
+    let median: Double
+    /// The 25th percentile glucose value (lower quartile)
+    let percentile25: Double
+    /// The 75th percentile glucose value (upper quartile)
+    let percentile75: Double
+    /// The 10th percentile glucose value (lower whisker)
+    let percentile10: Double
+    /// The 90th percentile glucose value (upper whisker)
+    let percentile90: Double
+}
+
+extension Double {
+    var isEven: Bool {
+        truncatingRemainder(dividingBy: 2) == 0
+    }
+}
+
+extension Stat.StateModel {
+    /// Calculates hourly statistical values (median, percentiles) from glucose readings.
+    /// The calculation runs asynchronously using the CoreData context.
+    ///
+    /// The calculation works as follows:
+    /// 1. Group readings by hour of day (0-23)
+    /// 2. For each hour:
+    ///    - Sort glucose values
+    ///    - Calculate median (50th percentile)
+    ///    - Calculate 10th, 25th, 75th, and 90th percentiles
+    ///
+    /// Example:
+    /// For readings at 6:00 AM across multiple days:
+    /// ```
+    /// Readings: [80, 100, 120, 140, 160, 180, 200]
+    /// Results:
+    /// - 10th percentile: 84 (lower whisker)
+    /// - 25th percentile: 110 (lower band)
+    /// - median: 140 (center line)
+    /// - 75th percentile: 170 (upper band)
+    /// - 90th percentile: 196 (upper whisker)
+    /// ```
+    ///
+    /// The resulting statistics are used to show:
+    /// - A dark blue area for the interquartile range (25th-75th percentile)
+    /// - A light blue area for the wider range (10th-90th percentile)
+    /// - A solid blue line for the median
+    func calculateHourlyStatsForGlucoseAreaChart(from ids: [NSManagedObjectID]) async {
+        let taskContext = CoreDataStack.shared.newTaskContext()
+
+        let calendar = Calendar.current
+
+        let stats = await taskContext.perform {
+            // Convert IDs to GlucoseStored objects using the context
+            let readings = ids.compactMap { id -> GlucoseStored? in
+                do {
+                    return try taskContext.existingObject(with: id) as? GlucoseStored
+                } catch {
+                    debugPrint("\(DebuggingIdentifiers.failed) Error fetching glucose: \(error)")
+                    return nil
+                }
+            }
+
+            // Group readings by hour of day (0-23)
+            // Example: [8: [reading1, reading2], 9: [reading3, reading4, reading5], ...]
+            let groupedByHour = Dictionary(grouping: readings) { reading in
+                calendar.component(.hour, from: reading.date ?? Date())
+            }
+
+            // Process each hour of the day (0-23)
+            return (0 ... 23).map { hour in
+                // Get all readings for this hour (or empty if none)
+                let readings = groupedByHour[hour] ?? []
+
+                // Extract and sort glucose values for percentile calculations
+                // Example: [100, 120, 130, 140, 150, 160, 180]
+                let values = readings.map { Double($0.glucose) }.sorted()
+                let count = Double(values.count)
+
+                // Handle hours with no readings
+                guard !values.isEmpty else {
+                    return HourlyStats(
+                        hour: hour,
+                        median: 0,
+                        percentile25: 0,
+                        percentile75: 0,
+                        percentile10: 0,
+                        percentile90: 0
+                    )
+                }
+
+                // Calculate median
+                // For even count: average of two middle values
+                // For odd count: middle value
+                let median = count.isEven ?
+                    (values[Int(count / 2) - 1] + values[Int(count / 2)]) / 2 :
+                    values[Int(count / 2)]
+
+                // Create statistics object with all percentiles
+                // Index calculation: multiply count by desired percentile (0.25 for 25th)
+                return HourlyStats(
+                    hour: hour,
+                    median: median,
+                    percentile25: values[Int(count * 0.25)], // Lower quartile
+                    percentile75: values[Int(count * 0.75)], // Upper quartile
+                    percentile10: values[Int(count * 0.10)], // Lower whisker
+                    percentile90: values[Int(count * 0.90)] // Upper whisker
+                )
+            }
+        }
+
+        // Update stats on main thread
+        await MainActor.run {
+            self.hourlyStats = stats
+        }
+    }
+}

+ 274 - 0
Trio/Sources/Modules/Stat/StatStateModel+Setup/BolusStatsSetup.swift

@@ -0,0 +1,274 @@
+import CoreData
+import Foundation
+
+/// Represents statistical data about bolus insulin for a specific time period
+struct BolusStats: Identifiable {
+    let id = UUID()
+    /// The date representing this time period
+    let date: Date
+    /// Total manual bolus insulin in units
+    let manualBolus: Double
+    /// Total SMB insulin in units
+    let smb: Double
+    /// Total external bolus insulin in units
+    let external: Double
+}
+
+extension Stat.StateModel {
+    /// Sets up bolus statistics by fetching and processing bolus data
+    ///
+    /// This function:
+    /// 1. Fetches hourly and daily bolus statistics asynchronously
+    /// 2. Updates the state model with the fetched statistics on the main actor
+    /// 3. Calculates and caches initial daily averages
+    func setupBolusStats() {
+        Task {
+            do {
+                let (hourly, daily) = try await fetchBolusStats()
+
+                await MainActor.run {
+                    self.hourlyBolusStats = hourly
+                    self.dailyBolusStats = daily
+                }
+
+                // Initially calculate and cache daily averages
+                await calculateAndCacheBolusAveragesAndTotals()
+            } catch {
+                debug(.default, "\(DebuggingIdentifiers.failed) failed to setup bolus stats: \(error.localizedDescription)")
+            }
+        }
+    }
+
+    /// Fetches and processes bolus statistics from Core Data
+    /// - Returns: A tuple containing hourly and daily bolus statistics arrays
+    ///
+    /// This function:
+    /// 1. Fetches bolus entries from Core Data
+    /// 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 throws -> (hourly: [BolusStats], daily: [BolusStats]) {
+        // Fetch PumpEventStored entries from Core Data
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: BolusStored.self,
+            onContext: bolusTaskContext,
+            predicate: NSPredicate.pumpHistoryForStats,
+            key: "pumpEvent.timestamp",
+            ascending: true,
+            batchSize: 100
+        )
+
+        // Variables to hold the results
+        var hourlyStats: [BolusStats] = []
+        var dailyStats: [BolusStats] = []
+
+        // Process CoreData results within the context's thread
+        await bolusTaskContext.perform {
+            guard let fetchedResults = results as? [BolusStored] else {
+                return
+            }
+
+            let calendar = Calendar.current
+
+            // Group entries by hour for hourly statistics
+            let now = Date()
+            let twentyDaysAgo = Calendar.current.date(byAdding: .day, value: -20, to: now) ?? now
+
+            let hourlyGrouped = Dictionary(grouping: fetchedResults.filter { entry in
+                guard let date = entry.pumpEvent?.timestamp else { return false }
+                return date >= twentyDaysAgo && date <= now
+            }) { entry in
+                let components = calendar.dateComponents(
+                    [.year, .month, .day, .hour],
+                    from: entry.pumpEvent?.timestamp ?? Date()
+                )
+                return calendar.date(from: components) ?? Date()
+            }
+
+            // Group entries by day for daily statistics
+            let dailyGrouped = Dictionary(grouping: fetchedResults) { entry in
+                calendar.startOfDay(for: entry.pumpEvent?.timestamp ?? Date())
+            }
+
+            // Process hourly stats
+            hourlyStats = hourlyGrouped.keys.sorted().map { timePoint in
+                let entries = hourlyGrouped[timePoint, default: []]
+                return BolusStats(
+                    date: timePoint,
+                    manualBolus: entries.reduce(0.0) { sum, entry in
+                        if !entry.isSMB, !entry.isExternal {
+                            return sum + (entry.amount?.doubleValue ?? 0)
+                        }
+                        return sum
+                    },
+                    smb: entries.reduce(0.0) { sum, entry in
+                        if entry.isSMB {
+                            return sum + (entry.amount?.doubleValue ?? 0)
+                        }
+                        return sum
+                    },
+                    external: entries.reduce(0.0) { sum, entry in
+                        if entry.isExternal {
+                            return sum + (entry.amount?.doubleValue ?? 0)
+                        }
+                        return sum
+                    }
+                )
+            }
+
+            // Process daily stats
+            dailyStats = dailyGrouped.keys.sorted().map { timePoint in
+                let entries = dailyGrouped[timePoint, default: []]
+                return BolusStats(
+                    date: timePoint,
+                    manualBolus: entries.reduce(0.0) { sum, entry in
+                        if !entry.isSMB, !entry.isExternal {
+                            return sum + (entry.amount?.doubleValue ?? 0)
+                        }
+                        return sum
+                    },
+                    smb: entries.reduce(0.0) { sum, entry in
+                        if entry.isSMB {
+                            return sum + (entry.amount?.doubleValue ?? 0)
+                        }
+                        return sum
+                    },
+                    external: entries.reduce(0.0) { sum, entry in
+                        if entry.isExternal {
+                            return sum + (entry.amount?.doubleValue ?? 0)
+                        }
+                        return sum
+                    }
+                )
+            }
+        }
+
+        return (hourlyStats, dailyStats)
+    }
+
+    /// Calculates and caches the daily averages of bolus insulin
+    ///
+    /// This function:
+    /// 1. Groups bolus statistics by day
+    /// 2. Calculates average total, carb and correction bolus for each day
+    /// 3. Caches the results for later use
+    ///
+    /// This only needs to be called once during subscribe.
+    private func calculateAndCacheBolusAveragesAndTotals() async {
+        let calendar = Calendar.current
+
+        // Calculate averages in context
+        let dailyAverages = await bolusTaskContext.perform { [dailyBolusStats] in
+            // Group by days
+            let groupedByDay = Dictionary(grouping: dailyBolusStats) { stat in
+                calendar.startOfDay(for: stat.date)
+            }
+
+            // Calculate averages for each day
+            var averages: [Date: (Double, Double, Double)] = [:]
+            for (day, stats) in groupedByDay {
+                let total = stats.reduce((0.0, 0.0, 0.0)) { acc, stat in
+                    (acc.0 + stat.manualBolus, acc.1 + stat.smb, acc.2 + stat.external)
+                }
+                let count = Double(stats.count)
+                averages[day] = (total.0 / count, total.1 / count, total.2 / count)
+            }
+            return averages
+        }
+
+        // Calculate averages in context
+        let dailyTotals = await bolusTaskContext.perform { [dailyBolusStats] in
+            // Group by days
+            let groupedByDay = Dictionary(grouping: dailyBolusStats) { stat in
+                calendar.startOfDay(for: stat.date)
+            }
+
+            // Calculate totals for each day
+            var totals: [(Date, Double)] = []
+            for (day, stats) in groupedByDay {
+                let total = stats.reduce(0.0) { _, stat in
+                    stat.manualBolus + stat.smb + stat.external
+                }
+            }
+            return totals
+        }
+
+        // Update cache on main thread
+        await MainActor.run {
+            self.bolusAveragesCache = dailyAverages
+            self.bolusTotalsCache = dailyTotals
+        }
+    }
+
+    /// Returns the average bolus values for the given date range from the cache
+    /// - Parameter range: A tuple containing the start and end dates to get averages for
+    /// - Returns: A tuple containing the average total, carb and correction bolus values for the date range
+    func getCachedBolusAverages(for range: (start: Date, end: Date)) -> (manual: Double, smb: Double, external: Double) {
+        return calculateBolusAveragesForDateRange(from: range.start, to: range.end)
+    }
+
+    /// Returns the total bolus values for the given date range from the cache
+    /// - Parameter range: A tuple containing the start and end dates to get averages for
+    /// - Returns: Totals for bolus (sum of manual, smb and external) for the date range
+    func getCachedBolusTotals(for range: (start: Date, end: Date)) -> Double {
+        calculateBolusTotalsForDateRange(from: range.start, to: range.end)
+    }
+
+    /// Calculates the average bolus values for a given date range
+    /// - Parameters:
+    ///   - startDate: The start date of the range to calculate averages for
+    ///   - endDate: The end date of the range to calculate averages for
+    /// - Returns: A tuple containing the average total, carb and correction bolus values for the date range
+    func calculateBolusAveragesForDateRange(
+        from startDate: Date,
+        to endDate: Date
+    ) -> (manual: Double, smb: Double, external: Double) {
+        // Filter cached values to only include those within the date range
+        let relevantStats = bolusAveragesCache.filter { date, _ in
+            date >= startDate && date <= endDate
+        }
+
+        // Return zeros if no data exists for the range
+        guard !relevantStats.isEmpty else { return (0, 0, 0) }
+
+        // Calculate total bolus across all days
+        let total = relevantStats.values.reduce((0.0, 0.0, 0.0)) { acc, avg in
+            (acc.0 + avg.0, acc.1 + avg.1, acc.2 + avg.2)
+        }
+
+        // Calculate averages by dividing totals by number of days
+        let count = Double(relevantStats.count)
+
+        return (total.0 / count, total.1 / count, total.2 / count)
+    }
+
+    /// Calculates the total bolus values for a given date range
+    /// - Parameters:
+    ///   - startDate: The start date of the range to calculate averages for
+    ///   - endDate: The end date of the range to calculate averages for
+    /// - Returns: A total bolus (sum of manual, smb and external) for the date range
+    func calculateBolusTotalsForDateRange(
+        from startDate: Date,
+        to endDate: Date
+    ) -> Double {
+        // Filter cached values to only include those within the date range
+        let relevantStats = bolusAveragesCache.filter { date, _ in
+            date >= startDate && date <= endDate
+        }
+
+        // Return zeros if no data exists for the range
+        guard !relevantStats.isEmpty else { return 0 }
+
+        // Calculate total bolus across all days
+        return relevantStats.values.reduce(0.0) { _, totalPerCategory in
+            totalPerCategory.0 + totalPerCategory.1 + totalPerCategory.2
+        }
+    }
+}
+
+/// Extension to convert Decimal to Double
+private extension Decimal {
+    var doubleValue: Double {
+        NSDecimalNumber(decimal: self).doubleValue
+    }
+}

+ 246 - 0
Trio/Sources/Modules/Stat/StatStateModel+Setup/LoopChartSetup.swift

@@ -0,0 +1,246 @@
+import CoreData
+import Foundation
+
+/// Represents statistical data about loop execution success/failure for a specific time period
+struct LoopStatsByPeriod: Identifiable {
+    /// The date representing this time period
+    let period: Date
+    /// Number of successful loop executions in this period
+    let successful: Int
+    /// Number of failed loop executions in this period
+    let failed: Int
+    /// Median duration of loop executions in this period
+    let medianDuration: Double
+    /// Number of glucose measurements in this period
+    let glucoseCount: Int
+    /// Total number of loop executions in this period
+    var total: Int { successful + failed }
+    /// Percentage of successful loops (0-100)
+    var successPercentage: Double { total > 0 ? Double(successful) / Double(total) * 100 : 0 }
+    /// Percentage of failed loops (0-100)
+    var failurePercentage: Double { total > 0 ? Double(failed) / Double(total) * 100 : 0 }
+    /// Unique identifier for this period, using the period date
+    var id: Date { period }
+}
+
+enum LoopStatsDataType: String {
+    case successfulLoop
+    case glucoseCount
+
+    var displayName: String {
+        switch self {
+        case .successfulLoop: return String(localized: "Successful Loop")
+        case .glucoseCount: return String(localized: "Glucose Count")
+        }
+    }
+}
+
+extension Stat.StateModel {
+    /// Initiates the process of fetching and processing loop statistics
+    /// This function coordinates three main tasks:
+    /// 1. Fetching loop stat record IDs for the selected duration
+    /// 2. Calculating grouped statistics for the Loop stats chart
+    /// 3. Updating loop stat records on the main thread (!) for the Loop duration chart
+    func setupLoopStatRecords() {
+        Task {
+            do {
+                let (recordIDs, failedRecordIDs) = try await self.fetchLoopStatRecords(for: selectedIntervalForLoopStats)
+
+                // Update loop records for duration chart
+                await self.updateLoopStatRecords(allLoopIds: recordIDs)
+
+                // Calculate statistics and update on main thread
+                let stats = try await self.getLoopStats(
+                    allLoopIds: recordIDs,
+                    failedLoopIds: failedRecordIDs,
+                    interval: selectedIntervalForLoopStats
+                )
+
+                await MainActor.run {
+                    self.loopStats = stats
+                }
+            } catch {
+                debug(.default, "\(DebuggingIdentifiers.failed) failed to fetch loop stats: \(error.localizedDescription)")
+            }
+        }
+    }
+
+    /// Fetches loop statistics records for the specified duration
+    /// - Parameter interval: The time period to fetch records for
+    /// - Returns: A tuple containing arrays of NSManagedObjectIDs for (all loops, failed loops)
+    func fetchLoopStatRecords(for interval: StatsTimeIntervalWithToday) async throws
+        -> ([NSManagedObjectID], [NSManagedObjectID])
+    {
+        // Calculate the date range based on selected duration
+        let now = Date()
+        let startDate: Date
+        switch interval {
+        case .day:
+            startDate = now.addingTimeInterval(-24.hours.timeInterval)
+        case .today:
+            startDate = Calendar.current.startOfDay(for: now)
+        case .week:
+            startDate = now.addingTimeInterval(-7.days.timeInterval)
+        case .month:
+            startDate = now.addingTimeInterval(-30.days.timeInterval)
+        case .total:
+            startDate = now.addingTimeInterval(-90.days.timeInterval)
+        }
+
+        // Perform both fetches asynchronously
+        async let allLoopsResult = CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: LoopStatRecord.self,
+            onContext: loopTaskContext,
+            predicate: NSPredicate(format: "start > %@", startDate as NSDate),
+            key: "start",
+            ascending: false
+        )
+
+        async let failedLoopsResult = CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: LoopStatRecord.self,
+            onContext: loopTaskContext,
+            predicate: NSPredicate(
+                format: "start > %@ AND loopStatus != %@",
+                startDate as NSDate,
+                "Success"
+            ),
+            key: "start",
+            ascending: false
+        )
+
+        // Wait for both results and convert to object IDs
+        let (allLoops, failedLoops) = try await (allLoopsResult, failedLoopsResult)
+
+        return (
+            (allLoops as? [LoopStatRecord] ?? []).map(\.objectID),
+            (failedLoops as? [LoopStatRecord] ?? []).map(\.objectID)
+        )
+    }
+
+    /// Updates the loopStatRecords array on the main thread with records from the provided IDs
+    /// - Parameters:
+    ///   - allLoopIds: Array of NSManagedObjectIDs for all loop records
+    @MainActor func updateLoopStatRecords(allLoopIds: [NSManagedObjectID]) {
+        loopStatRecords = allLoopIds.compactMap { id -> LoopStatRecord? in
+            do {
+                return try viewContext.existingObject(with: id) as? LoopStatRecord
+            } catch {
+                debugPrint("\(DebuggingIdentifiers.failed) Error fetching loop stat: \(error)")
+                return nil
+            }
+        }
+    }
+
+    /// Calculates loop and glucose statistics based on the provided record IDs
+    /// - Parameters:
+    ///   - allLoopIds: Array of NSManagedObjectIDs for all loop records
+    ///   - failedLoopIds: Array of NSManagedObjectIDs for failed loop records
+    ///   - interval: The time period for statistics calculation
+    /// - Returns: Array of tuples containing category, count and percentage for each statistic
+    func getLoopStats(
+        allLoopIds: [NSManagedObjectID],
+        failedLoopIds: [NSManagedObjectID],
+        interval: StatsTimeIntervalWithToday
+    ) async throws
+        -> [(category: LoopStatsDataType, count: Int, percentage: Double, medianDuration: Double, medianInterval: Double)]
+    {
+        // Calculate the date range for glucose readings
+        let now = Date()
+        let startDate: Date
+        switch interval {
+        case .day:
+            startDate = now.addingTimeInterval(-24.hours.timeInterval)
+        case .today:
+            startDate = Calendar.current.startOfDay(for: now)
+        case .week:
+            startDate = now.addingTimeInterval(-7.days.timeInterval)
+        case .month:
+            startDate = now.addingTimeInterval(-30.days.timeInterval)
+        case .total:
+            startDate = now.addingTimeInterval(-90.days.timeInterval)
+        }
+
+        // Get glucose statistics
+        let totalGlucose = try await calculateGlucoseStats(from: startDate, to: now)
+
+        // Get NSManagedObject
+        let allLoops = try await CoreDataStack.shared
+            .getNSManagedObject(with: allLoopIds, context: loopTaskContext) as? [LoopStatRecord] ?? []
+        let failedLoops = try await CoreDataStack.shared
+            .getNSManagedObject(with: failedLoopIds, context: loopTaskContext) as? [LoopStatRecord] ?? []
+
+        return await loopTaskContext.perform {
+            let totalLoopsCount = allLoops.count
+            let failedLoopsCount = failedLoops.count
+            let successfulLoops = totalLoopsCount - failedLoopsCount
+            let maxLoopsPerDay = 288.0 // Maximum possible loops per day (every 5 minutes)
+
+            let numberOfDays = max(1, Calendar.current.dateComponents([.day], from: startDate, to: now).day ?? 1)
+            let averageLoopsPerDay = Double(successfulLoops) / Double(numberOfDays)
+            let averageGlucosePerDay = Double(totalGlucose) / Double(numberOfDays)
+
+            // Calculate median duration (time from start to end of each loop)
+            let sortedDurations: [TimeInterval] = allLoops.compactMap { loop in
+                guard let start = loop.start, let end = loop.end else { return nil }
+                return end.timeIntervalSince(start)
+            }.sorted()
+            let medianDuration = sortedDurations.isEmpty ? 0.0 : sortedDurations[sortedDurations.count / 2]
+
+            // Calculate median interval (time between end of n-th loop and start of n+1th loop)
+            let sortedIntervals: [TimeInterval] = zip(allLoops.dropLast(), allLoops.dropFirst()).compactMap { previous, next in
+                guard let previousEnd = previous.end, let nextStart = next.start else { return nil }
+                return previousEnd.timeIntervalSince(nextStart)
+            }.sorted()
+            let medianInterval = sortedIntervals.isEmpty ? 0.0 : sortedIntervals[sortedIntervals.count / 2]
+
+            let loopPercentage = (averageLoopsPerDay / maxLoopsPerDay) * 100
+            let glucosePercentage = (averageGlucosePerDay / maxLoopsPerDay) * 100
+
+            return [
+                (
+                    LoopStatsDataType.successfulLoop,
+                    Int(round(averageLoopsPerDay)),
+                    loopPercentage,
+                    medianDuration,
+                    medianInterval
+                ),
+                (
+                    LoopStatsDataType.glucoseCount,
+                    Int(round(averageGlucosePerDay)),
+                    glucosePercentage,
+                    medianDuration,
+                    medianInterval
+                )
+            ]
+        }
+    }
+
+    /// Fetches and calculates glucose statistics for the given time period
+    /// - Parameters:
+    ///   - startDate: The start date of the period to analyze
+    ///   - now: The current date (end of period)
+    /// - Returns: Number of glucose readings in the period
+    private func calculateGlucoseStats(
+        from startDate: Date,
+        to _: Date
+    ) async throws -> Int {
+        // Create predicate for glucose readings
+        let glucosePredicate = NSPredicate(format: "date >= %@", startDate as NSDate)
+
+        // Fetch glucose readings asynchronously
+        let glucoseResult = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: GlucoseStored.self,
+            onContext: loopTaskContext,
+            predicate: glucosePredicate,
+            key: "date",
+            ascending: false
+        )
+
+        return await loopTaskContext.perform {
+            guard let readings = glucoseResult as? [GlucoseStored] else {
+                return 0
+            }
+            return readings.count
+        }
+    }
+}

+ 177 - 0
Trio/Sources/Modules/Stat/StatStateModel+Setup/MealStatsSetup.swift

@@ -0,0 +1,177 @@
+import CoreData
+import Foundation
+
+/// Represents statistical data about meal macronutrients for a specific day
+struct MealStats: Identifiable {
+    let id = UUID()
+    /// The date representing this time period
+    let date: Date
+    /// Total carbohydrates in grams
+    let carbs: Double
+    /// Total fat in grams
+    let fat: Double
+    /// Total protein in grams
+    let protein: Double
+}
+
+extension Stat.StateModel {
+    /// Sets up meal statistics by fetching and processing meal data
+    ///
+    /// This function:
+    /// 1. Fetches hourly and daily meal statistics asynchronously
+    /// 2. Updates the state model with the fetched statistics on the main actor
+    /// 3. Calculates and caches initial daily averages
+    func setupMealStats() {
+        Task {
+            do {
+                let (hourly, daily) = try await fetchMealStats()
+
+                await MainActor.run {
+                    self.hourlyMealStats = hourly
+                    self.dailyMealStats = daily
+                }
+
+                // Initially calculate and cache daily averages
+                await calculateAndCacheDailyAverages()
+            } catch {
+                debug(.default, "\(DebuggingIdentifiers.failed) failed to fetch meal stats: \(error)")
+            }
+        }
+    }
+
+    /// Fetches and processes meal statistics from Core Data
+    /// - Returns: A tuple containing hourly and daily meal statistics arrays
+    ///
+    /// This function:
+    /// 1. Fetches carbohydrate entries from Core Data
+    /// 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 throws -> (hourly: [MealStats], daily: [MealStats]) {
+        // Fetch CarbEntryStored entries from Core Data
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: CarbEntryStored.self,
+            onContext: mealTaskContext,
+            predicate: NSPredicate.carbsForStats,
+            key: "date",
+            ascending: true,
+            batchSize: 100
+        )
+
+        return await mealTaskContext.perform {
+            // Safely unwrap the fetched results, return empty arrays if nil
+            guard let fetchedResults = results as? [CarbEntryStored] else { return ([], []) }
+
+            let calendar = Calendar.current
+
+            // Group entries by hour for hourly statistics
+            let now = Date()
+            let twentyDaysAgo = Calendar.current.date(byAdding: .day, value: -20, to: now) ?? now
+
+            let hourlyGrouped = Dictionary(grouping: fetchedResults.filter { entry in
+                guard let date = entry.date else { return false }
+                return date >= twentyDaysAgo && date <= now
+            }) { entry in
+                let components = calendar.dateComponents([.year, .month, .day, .hour], from: entry.date ?? Date())
+                return calendar.date(from: components) ?? Date()
+            }
+
+            // Group entries by day for daily statistics
+            let dailyGrouped = Dictionary(grouping: fetchedResults) { entry in
+                calendar.startOfDay(for: entry.date ?? Date())
+            }
+
+            // Calculate statistics for each hour
+            let hourlyStats = hourlyGrouped.keys.sorted().map { timePoint in
+                let entries = hourlyGrouped[timePoint, default: []]
+                return MealStats(
+                    date: timePoint,
+                    carbs: entries.reduce(0.0) { $0 + $1.carbs },
+                    fat: entries.reduce(0.0) { $0 + $1.fat },
+                    protein: entries.reduce(0.0) { $0 + $1.protein }
+                )
+            }
+
+            // Calculate statistics for each day
+            let dailyStats = dailyGrouped.keys.sorted().map { timePoint in
+                let entries = dailyGrouped[timePoint, default: []]
+                return MealStats(
+                    date: timePoint,
+                    carbs: entries.reduce(0.0) { $0 + $1.carbs },
+                    fat: entries.reduce(0.0) { $0 + $1.fat },
+                    protein: entries.reduce(0.0) { $0 + $1.protein }
+                )
+            }
+
+            return (hourlyStats, dailyStats)
+        }
+    }
+
+    /// Calculates and caches the daily averages of macronutrients
+    ///
+    /// This function:
+    /// 1. Groups meal statistics by day
+    /// 2. Calculates average carbs, fat and protein for each day
+    /// 3. Caches the results for later use
+    ///
+    /// This only needs to be called once during subscribe.
+    private func calculateAndCacheDailyAverages() async {
+        let calendar = Calendar.current
+
+        // Calculate averages in context
+        let dailyAverages = await mealTaskContext.perform { [dailyMealStats] in
+            // Group by days
+            let groupedByDay = Dictionary(grouping: dailyMealStats) { stat in
+                calendar.startOfDay(for: stat.date)
+            }
+
+            // Calculate averages for each day
+            var averages: [Date: (Double, Double, Double)] = [:]
+            for (day, stats) in groupedByDay {
+                let total = stats.reduce((0.0, 0.0, 0.0)) { acc, stat in
+                    (acc.0 + stat.carbs, acc.1 + stat.fat, acc.2 + stat.protein)
+                }
+                let count = Double(stats.count)
+                averages[day] = (total.0 / count, total.1 / count, total.2 / count)
+            }
+            return averages
+        }
+
+        // Update cache on main thread
+        await MainActor.run {
+            self.dailyAveragesCache = dailyAverages
+        }
+    }
+
+    /// Returns the average macronutrient values for the given date range from the cache
+    /// - Parameter range: A tuple containing the start and end dates to get averages for
+    /// - Returns: A tuple containing the average carbs, fat and protein values for the date range
+    func getCachedMealAverages(for range: (start: Date, end: Date)) -> (carbs: Double, fat: Double, protein: Double) {
+        return calculateAveragesForDateRange(from: range.start, to: range.end)
+    }
+
+    /// Calculates the average macronutrient values for a given date range
+    /// - Parameters:
+    ///   - startDate: The start date of the range to calculate averages for
+    ///   - endDate: The end date of the range to calculate averages for
+    /// - Returns: A tuple containing the average carbs, fat and protein values for the date range
+    func calculateAveragesForDateRange(from startDate: Date, to endDate: Date) -> (carbs: Double, fat: Double, protein: Double) {
+        // Filter cached values to only include those within the date range
+        let relevantStats = dailyAveragesCache.filter { date, _ in
+            date >= startDate && date <= endDate
+        }
+
+        // Return zeros if no data exists for the range
+        guard !relevantStats.isEmpty else { return (0, 0, 0) }
+
+        // Calculate total macronutrients across all days
+        let total = relevantStats.values.reduce((0.0, 0.0, 0.0)) { acc, avg in
+            (acc.0 + avg.0, acc.1 + avg.1, acc.2 + avg.2)
+        }
+
+        // Calculate averages by dividing totals by number of days
+        let count = Double(relevantStats.count)
+
+        return (total.0 / count, total.1 / count, total.2 / count)
+    }
+}

+ 132 - 0
Trio/Sources/Modules/Stat/StatStateModel+Setup/StackedChartSetup.swift

@@ -0,0 +1,132 @@
+import CoreData
+import Foundation
+
+/// Represents the distribution of glucose values within specific ranges for each hour.
+///
+/// This struct is used to visualize how glucose values are distributed across different
+/// ranges (e.g., low, normal, high) throughout the day. Each range has a name and
+/// corresponding hourly values showing the percentage of readings in that range.
+///
+/// Example ranges and their meanings:
+/// - "<54": Urgent low
+/// - "54-70": Low
+/// - "70-140": Target range
+/// - "140-180": High
+/// - "180-200": Very high
+/// - "200-220": Very high+
+/// - ">220": Urgent high
+///
+/// Example usage:
+/// ```swift
+/// let range = GlucoseRangeStats(
+///     name: "70-140",           // Target range
+///     values: [
+///         (hour: 8, count: 75), // 75% of readings at 8 AM were in range
+///         (hour: 9, count: 80)  // 80% of readings at 9 AM were in range
+///     ]
+/// )
+/// ```
+///
+/// This data structure is used to create stacked area charts showing the
+/// distribution of glucose values across different ranges for each hour of the day.
+public struct GlucoseRangeStats: Identifiable {
+    /// The name of the glucose range (e.g., "70-140", "<54")
+    let name: String
+
+    /// Array of tuples containing the hour and percentage of readings in this range
+    /// - hour: Hour of the day (0-23)
+    /// - count: Percentage of readings in this range for the given hour (0-100)
+    let values: [(hour: Int, count: Int)]
+
+    /// Unique identifier for the range, derived from its name
+    public var id: String { name }
+}
+
+extension Stat.StateModel {
+    /// Calculates hourly glucose range distribution statistics.
+    /// The calculation runs asynchronously using the CoreData context.
+    ///
+    /// The calculation works as follows:
+    /// 1. Count unique days for each hour to handle missing data
+    /// 2. For each glucose range and hour:
+    ///    - Count readings in that range
+    ///    - Calculate percentage based on number of days with readings
+    ///
+    /// Example:
+    /// If we have data for 7 days and at 6:00 AM:
+    /// - 3 days had readings in range 70-140
+    /// - 2 days had readings in range 140-180
+    /// - 2 day had a reading in range 180-200
+    /// Then for 6:00 AM:
+    /// - 70-140 = (3/7)*100 = 42.9%
+    /// - 140-180 = (2/7)*100 = 28.6%
+    /// - 180-200 = (2/7)*100 = 28.6%
+    func calculateGlucoseRangeStatsForStackedChart(from ids: [NSManagedObjectID]) async {
+        let taskContext = CoreDataStack.shared.newTaskContext()
+
+        let calendar = Calendar.current
+
+        let stats = await taskContext.perform {
+            // Convert IDs to GlucoseStored objects using the context
+            let readings = ids.compactMap { id -> GlucoseStored? in
+                do {
+                    return try taskContext.existingObject(with: id) as? GlucoseStored
+                } catch {
+                    debugPrint("\(DebuggingIdentifiers.failed) Error fetching glucose: \(error)")
+                    return nil
+                }
+            }
+
+            // Count unique days for each hour
+            let daysPerHour = (0 ... 23).map { hour in
+                let uniqueDays = Set(readings.compactMap { reading -> Date? in
+                    guard let date = reading.date else { return nil }
+                    if calendar.component(.hour, from: date) == hour {
+                        return calendar.startOfDay(for: date)
+                    }
+                    return nil
+                })
+                return (hour: hour, days: uniqueDays.count)
+            }
+
+            // Define glucose ranges and their conditions
+            // Ranges are processed from bottom to top in the stacked chart
+            let ranges: [(name: String, condition: (Int) -> Bool)] = [
+                ("<54", { g in g <= 54 }),
+                ("54-70", { g in g > 54 && g < 70 }),
+                ("70-140", { g in g >= 70 && g <= 140 }),
+                ("140-180", { g in g > 140 && g <= 180 }),
+                ("180-200", { g in g > 180 && g <= 200 }),
+                ("200-220", { g in g > 200 && g <= 220 }),
+                (">220", { g in g > 220 })
+            ]
+
+            // Process each range to create the chart data
+            return ranges.map { rangeName, condition in
+                // Calculate values for each hour within this range
+                let hourlyValues = (0 ... 23).map { hour in
+                    let totalDaysForHour = Double(daysPerHour[hour].days)
+                    // Skip if no data for this hour
+                    guard totalDaysForHour > 0 else { return (hour: hour, count: 0) }
+
+                    // Count readings that match the range condition for this hour
+                    let readingsInRange = readings.filter { reading in
+                        guard let date = reading.date else { return false }
+                        return calendar.component(.hour, from: date) == hour &&
+                            condition(Int(reading.glucose))
+                    }.count
+
+                    // Convert to percentage based on number of days with data
+                    let percentage = (Double(readingsInRange) / totalDaysForHour) * 100.0
+                    return (hour: hour, count: Int(percentage))
+                }
+                return GlucoseRangeStats(name: rangeName, values: hourlyValues)
+            }
+        }
+
+        // Update stats on main thread
+        await MainActor.run {
+            self.glucoseRangeStats = stats
+        }
+    }
+}

+ 559 - 0
Trio/Sources/Modules/Stat/StatStateModel+Setup/TDDSetup.swift

@@ -0,0 +1,559 @@
+import CoreData
+import Foundation
+
+/// Represents statistical data about Total Daily Dose for a specific time period
+struct TDDStats: Identifiable {
+    let id = UUID()
+    /// The date representing this time period
+    let date: Date
+    /// Total insulin in units
+    let amount: Double
+}
+
+extension Stat.StateModel {
+    /// Sets up TDD statistics by fetching and processing insulin data
+    func setupTDDStats() {
+        Task {
+            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)")
+            }
+        }
+    }
+
+    /// 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 throws -> (hourly: [TDDStats], daily: [TDDStats]) {
+        // MARK: - Fetch Required Data
+
+        // Fetch data for daily statistics (TDDStored for week, month, total views)
+        let tddResults = try await fetchTDDStoredRecords()
+
+        // Fetch data for hourly statistics (BolusStored and TempBasalStored for day view)
+        let (bolusResults, tempBasalResults, suspendEvents, resumeEvents) = try await fetchHourlyInsulinRecords()
+
+        // MARK: - Process Data on Background Context
+
+        var hourlyStats: [TDDStats] = []
+        var dailyStats: [TDDStats] = []
+
+        await tddTaskContext.perform {
+            let calendar = Calendar.current
+
+            // Process daily statistics from TDDStored
+            if let fetchedTDDs = tddResults as? [TDDStored] {
+                dailyStats = self.processDailyTDDs(fetchedTDDs, calendar: calendar)
+            }
+
+            // Process hourly statistics from BolusStored and TempBasalStored
+            if let fetchedBoluses = bolusResults as? [BolusStored],
+               let fetchedTempBasals = tempBasalResults as? [TempBasalStored],
+               let fetchedSuspendEvents = suspendEvents as? [PumpEventStored],
+               let fetchedResumeEvents = resumeEvents as? [PumpEventStored]
+            {
+                hourlyStats = self.processHourlyInsulinData(
+                    boluses: fetchedBoluses,
+                    tempBasals: fetchedTempBasals,
+                    suspendEvents: fetchedSuspendEvents,
+                    resumeEvents: fetchedResumeEvents,
+                    calendar: calendar
+                )
+            }
+        }
+
+        return (hourlyStats, dailyStats)
+    }
+
+    /// Fetches TDDStored records from CoreData for daily statistics
+    /// - Returns: The results of the fetch request containing TDDStored records
+    /// - Note: Fetches records from the last 3 months for week, month, and total views
+    private func fetchTDDStoredRecords() async throws -> Any {
+        // Create a predicate to fetch TDD records from the last 3 months
+        let threeMonthsAgo = Date().addingTimeInterval(-3.months.timeInterval)
+        let predicate = NSPredicate(format: "date >= %@", threeMonthsAgo as NSDate)
+
+        // Fetch TDD records from CoreData
+        return try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: TDDStored.self,
+            onContext: tddTaskContext,
+            predicate: predicate,
+            key: "date",
+            ascending: true,
+            batchSize: 100
+        )
+    }
+
+    /// Fetches BolusStored and TempBasalStored records from CoreData for hourly statistics
+    /// - Returns: A tuple containing the results of both fetch requests
+    /// - Note: Fetches records from the last 20 days for detailed hourly view
+    private func fetchHourlyInsulinRecords() async throws -> (bolus: Any, tempBasal: Any, suspendEvents: Any, resumeEvents: Any) {
+        // Calculate date range for hourly statistics (last 20 days)
+        let now = Date()
+        let twentyDaysAgo = Calendar.current.date(byAdding: .day, value: -20, to: now) ?? now
+
+        // Create a predicate for the date range
+        let datePredicate = NSPredicate(
+            format: "pumpEvent.timestamp >= %@ AND pumpEvent.timestamp <= %@",
+            twentyDaysAgo as NSDate,
+            now as NSDate
+        )
+
+        // Fetch bolus records for hourly stats
+        let bolusResults = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: BolusStored.self,
+            onContext: tddTaskContext,
+            predicate: datePredicate,
+            key: "pumpEvent.timestamp",
+            ascending: true,
+            batchSize: 100
+        )
+
+        // Fetch temp basal records for hourly stats
+        let tempBasalResults = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: TempBasalStored.self,
+            onContext: tddTaskContext,
+            predicate: datePredicate,
+            key: "pumpEvent.timestamp",
+            ascending: true,
+            batchSize: 100
+        )
+
+        // Create a combined predicate for suspension and resume events
+        let suspendResumeTypes = [
+            PumpEventStored.EventType.pumpSuspend.rawValue,
+            PumpEventStored.EventType.pumpResume.rawValue
+        ]
+
+        let suspendResumePredicate = NSPredicate(
+            format: "timestamp >= %@ AND timestamp <= %@ AND type IN %@",
+            twentyDaysAgo as NSDate,
+            now as NSDate,
+            suspendResumeTypes
+        )
+
+        // Fetch both suspension and resume events in a single query
+        let suspendResumeResults = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: PumpEventStored.self,
+            onContext: tddTaskContext,
+            predicate: suspendResumePredicate,
+            key: "timestamp",
+            ascending: true,
+            batchSize: 100
+        )
+
+        // Filter the results within the context's perform closure to ensure thread safety
+        let (suspendEvents, resumeEvents) = await tddTaskContext.perform {
+            var suspendEventsArray: [PumpEventStored] = []
+            var resumeEventsArray: [PumpEventStored] = []
+
+            if let pumpEvents = suspendResumeResults as? [PumpEventStored] {
+                for event in pumpEvents {
+                    if event.type == PumpEventStored.EventType.pumpSuspend.rawValue {
+                        suspendEventsArray.append(event)
+                    } else if event.type == PumpEventStored.EventType.pumpResume.rawValue {
+                        resumeEventsArray.append(event)
+                    }
+                }
+            }
+
+            return (suspendEventsArray, resumeEventsArray)
+        }
+
+        return (bolusResults, tempBasalResults, suspendEvents, resumeEvents)
+    }
+
+    /// Processes bolus and temporary basal data to create hourly insulin statistics
+    /// - Parameters:
+    ///   - boluses: Array of BolusStored objects containing bolus insulin data
+    ///   - tempBasals: Array of TempBasalStored objects containing temporary basal rate data
+    ///   - suspendEvents: Array of PumpEventStored objects with type pumpSuspend
+    ///   - resumeEvents: Array of PumpEventStored objects with type pumpResume
+    ///   - calendar: Calendar instance used for date calculations and grouping
+    /// - Returns: Array of TDDStats objects representing hourly insulin amounts
+    /// - Note: This method calculates the actual duration of temporary basal rates by using the time
+    ///         difference between consecutive events, rather than relying on the planned duration.
+    ///         It also properly distributes insulin amounts across hour boundaries for accurate hourly statistics.
+    ///         Suspension events are taken into account to prevent counting insulin during pump suspensions.
+    private func processHourlyInsulinData(
+        boluses: [BolusStored],
+        tempBasals: [TempBasalStored],
+        suspendEvents: [PumpEventStored],
+        resumeEvents: [PumpEventStored],
+        calendar: Calendar
+    ) -> [TDDStats] {
+        // Dictionary to store insulin amounts indexed by hour
+        var insulinByHour: [Date: Double] = [:]
+
+        // MARK: - Process Bolus Insulin
+
+        // Iterate through all bolus records and add their amounts to the appropriate hourly totals
+        for bolus in boluses {
+            guard let timestamp = bolus.pumpEvent?.timestamp,
+                  let amount = bolus.amount?.doubleValue
+            else {
+                continue // Skip entries with missing timestamp or amount
+            }
+
+            // Create a date representing the hour of this bolus (truncating minutes/seconds)
+            let components = calendar.dateComponents([.year, .month, .day, .hour], from: timestamp)
+            guard let hourDate = calendar.date(from: components) else { continue }
+
+            // Add this bolus amount to the running total for this hour
+            insulinByHour[hourDate, default: 0] += amount
+        }
+
+        // MARK: - Create Suspend-Resume Pairs
+
+        // Create pairs of suspend and resume events
+        let suspendResumePairs = createSuspendResumePairs(suspendEvents: suspendEvents, resumeEvents: resumeEvents)
+
+        // MARK: - Process Temporary Basal Insulin
+
+        // Sort temp basals chronologically for accurate duration calculation
+        let sortedTempBasals = tempBasals.sorted {
+            ($0.pumpEvent?.timestamp ?? Date.distantPast) < ($1.pumpEvent?.timestamp ?? Date.distantPast)
+        }
+
+        // Process each temporary basal event
+        for (index, tempBasal) in sortedTempBasals.enumerated() {
+            guard let timestamp = tempBasal.pumpEvent?.timestamp,
+                  let rate = tempBasal.rate?.doubleValue
+            else {
+                continue // Skip entries with missing timestamp or rate
+            }
+
+            // MARK: Calculate Actual Duration
+
+            // Determine the actual duration based on the time until the next temp basal event
+            var actualDurationInMinutes: Double
+
+            if index < sortedTempBasals.count - 1 {
+                // For all but the last event, calculate duration as time until next event
+                if let nextTimestamp = sortedTempBasals[index + 1].pumpEvent?.timestamp {
+                    // Calculate time difference in minutes between this event and the next
+                    actualDurationInMinutes = nextTimestamp.timeIntervalSince(timestamp) / 60.0
+                } else {
+                    // Fallback to planned duration if next timestamp is missing (unlikely)
+                    actualDurationInMinutes = Double(tempBasal.duration)
+                }
+            } else {
+                // For the last event, use the planned duration as there's no next event
+                actualDurationInMinutes = Double(tempBasal.duration)
+            }
+
+            // Convert duration from minutes to hours for insulin calculation
+            let durationInHours = actualDurationInMinutes / 60.0
+
+            // MARK: Distribute Insulin Across Hours
+
+            // Handle temp basals that span multiple hours by distributing insulin appropriately
+            // taking into account suspension periods
+            distributeInsulinAcrossHours(
+                startTime: timestamp,
+                durationInHours: durationInHours,
+                rate: rate,
+                suspendResumePairs: suspendResumePairs,
+                insulinByHour: &insulinByHour,
+                calendar: calendar
+            )
+        }
+
+        // MARK: - Convert Results to TDDStats Array
+
+        // Transform the dictionary into a sorted array of TDDStats objects
+        return insulinByHour.keys.sorted().map { hourDate in
+            TDDStats(
+                date: hourDate,
+                amount: insulinByHour[hourDate, default: 0]
+            )
+        }
+    }
+
+    /// Creates pairs of suspend and resume events
+    /// - Parameters:
+    ///   - suspendEvents: Array of PumpEventStored objects with type pumpSuspend
+    ///   - resumeEvents: Array of PumpEventStored objects with type pumpResume
+    /// - Returns: Array of tuples containing suspend and resume event pairs
+    /// - Note: This method pairs suspend events with the next resume event chronologically
+    private func createSuspendResumePairs(
+        suspendEvents: [PumpEventStored],
+        resumeEvents: [PumpEventStored]
+    ) -> [(suspend: PumpEventStored, resume: PumpEventStored)] {
+        // Sort events chronologically
+        let sortedSuspendEvents = suspendEvents.sorted { ($0.timestamp ?? Date.distantPast) < ($1.timestamp ?? Date.distantPast) }
+        let sortedResumeEvents = resumeEvents.sorted { ($0.timestamp ?? Date.distantPast) < ($1.timestamp ?? Date.distantPast) }
+
+        // Create pairs of suspend + resume events
+        var pairs: [(suspend: PumpEventStored, resume: PumpEventStored)] = []
+
+        // Iterate through suspend events and find matching resume events
+        for suspendEvent in sortedSuspendEvents {
+            guard let suspendTime = suspendEvent.timestamp else { continue }
+
+            // Find the first resume event that occurs after this suspend event
+            if let resumeEvent = sortedResumeEvents.first(where: {
+                guard let resumeTime = $0.timestamp else { return false }
+                return resumeTime > suspendTime
+            }) {
+                // Create a pair and add it to the array
+                pairs.append((suspend: suspendEvent, resume: resumeEvent))
+            }
+        }
+
+        return pairs
+    }
+
+    /// Distributes insulin from a temporary basal rate across multiple hours
+    /// - Parameters:
+    ///   - startTime: The start time of the temporary basal rate
+    ///   - durationInHours: The duration of the temporary basal rate in hours
+    ///   - rate: The insulin rate in units per hour (U/h)
+    ///   - suspendResumePairs: Array of suspend-resume event pairs to account for suspension periods
+    ///   - insulinByHour: Dictionary to store insulin amounts by hour (modified in-place)
+    ///   - calendar: Calendar instance used for date calculations
+    /// - Note: This method handles the case where a temporary basal spans multiple hours by
+    ///         calculating the exact amount of insulin delivered in each hour. It accounts for
+    ///         partial hours at the beginning and end of the temporary basal period, as well as
+    ///         suspension periods where no insulin is delivered.
+    private func distributeInsulinAcrossHours(
+        startTime: Date,
+        durationInHours: Double,
+        rate: Double,
+        suspendResumePairs: [(suspend: PumpEventStored, resume: PumpEventStored)],
+        insulinByHour: inout [Date: Double],
+        calendar: Calendar
+    ) {
+        // Extract time components to calculate partial hours
+        let startComponents = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: startTime)
+
+        // Create a date representing just the hour of the start time (truncating minutes/seconds)
+        guard let startHourDate = calendar
+            .date(from: Calendar.current.dateComponents([.year, .month, .day, .hour], from: startTime))
+        else {
+            return // Exit if we can't create a valid hour date
+        }
+
+        // Calculate end time of the temp basal
+        let endTime = startTime.addingTimeInterval(durationInHours * 3600)
+
+        // MARK: - Handle First Hour (Partial)
+
+        // Calculate how many minutes remain in the first hour after the start time
+        let minutesInFirstHour = 60.0 - Double(startComponents.minute ?? 0) - (Double(startComponents.second ?? 0) / 60.0)
+
+        // Calculate how many hours of the temp basal occur in the first hour (capped at remaining time)
+        let hoursInFirstHour = min(durationInHours, minutesInFirstHour / 60.0)
+
+        // Add insulin for the first partial hour, accounting for any suspensions
+        if hoursInFirstHour > 0 {
+            // Calculate the end time of the first hour segment
+            let firstHourEndTime = startTime.addingTimeInterval(hoursInFirstHour * 3600)
+
+            // Calculate effective duration excluding suspension periods
+            let effectiveDuration = calculateEffectiveDuration(
+                from: startTime,
+                to: firstHourEndTime,
+                suspendResumePairs: suspendResumePairs
+            )
+
+            // Insulin = rate (U/h) * effective duration (h)
+            insulinByHour[startHourDate, default: 0] += rate * effectiveDuration
+        }
+
+        // MARK: - Handle Subsequent Hours
+
+        // Calculate remaining duration after the first hour
+        var remainingDuration = durationInHours - hoursInFirstHour
+
+        // Start with the next hour
+        var currentHourDate = calendar.date(byAdding: .hour, value: 1, to: startHourDate) ?? startHourDate
+
+        // Distribute remaining insulin across subsequent hours
+        while remainingDuration > 0 {
+            // Calculate how much of this hour is covered (max 1 hour)
+            let hoursToAdd = min(remainingDuration, 1.0)
+
+            // Calculate the start and end times for this hour segment
+            let hourStartTime = calendar
+                .date(from: calendar.dateComponents([.year, .month, .day, .hour], from: currentHourDate)) ?? currentHourDate
+            let hourEndTime = hourStartTime.addingTimeInterval(hoursToAdd * 3600)
+
+            // Calculate effective duration excluding suspension periods
+            let effectiveDuration = calculateEffectiveDuration(
+                from: hourStartTime,
+                to: hourEndTime,
+                suspendResumePairs: suspendResumePairs
+            )
+
+            // Add insulin for this hour: rate (U/h) * effective duration (h)
+            insulinByHour[currentHourDate, default: 0] += rate * effectiveDuration
+
+            // Reduce remaining duration and move to next hour
+            remainingDuration -= hoursToAdd
+            currentHourDate = calendar.date(byAdding: .hour, value: 1, to: currentHourDate) ?? currentHourDate
+        }
+    }
+
+    /// Calculates the effective duration of insulin delivery, excluding suspension periods
+    /// - Parameters:
+    ///   - startTime: The start time of the period
+    ///   - endTime: The end time of the period
+    ///   - suspendResumePairs: Array of suspend-resume event pairs
+    /// - Returns: The effective duration in hours, excluding suspension periods
+    /// - Note: This method calculates how much of a time period was not affected by pump suspensions
+    private func calculateEffectiveDuration(
+        from startTime: Date,
+        to endTime: Date,
+        suspendResumePairs: [(suspend: PumpEventStored, resume: PumpEventStored)]
+    ) -> Double {
+        // Total duration in hours
+        let totalDuration = endTime.timeIntervalSince(startTime) / 3600.0
+
+        // Calculate total suspended time within this period
+        var suspendedDuration = 0.0
+
+        for pair in suspendResumePairs {
+            guard let suspendTime = pair.suspend.timestamp,
+                  let resumeTime = pair.resume.timestamp
+            else {
+                continue
+            }
+
+            // Check if this suspension overlaps with our period
+            if suspendTime < endTime, resumeTime > startTime {
+                // Calculate overlap start and end
+                let overlapStart = max(startTime, suspendTime)
+                let overlapEnd = min(endTime, resumeTime)
+
+                // Add the overlapping duration to our suspended time
+                suspendedDuration += overlapEnd.timeIntervalSince(overlapStart) / 3600.0
+            }
+        }
+
+        // Return effective duration (total minus suspended)
+        return max(0.0, totalDuration - suspendedDuration)
+    }
+
+    /// Processes TDDStored records to create daily Total Daily Dose statistics
+    /// - Parameters:
+    ///   - tdds: Array of TDDStored objects containing daily insulin data
+    ///   - calendar: Calendar instance used for date calculations and grouping
+    /// - Returns: Array of TDDStats objects representing daily insulin amounts
+    /// - Note: This method groups TDD records by day and uses only the last (most recent) entry
+    ///         for each day, as this represents the complete TDD value for that day. This approach
+    ///         is appropriate for week, month, and total views where we want the final daily totals.
+    private func processDailyTDDs(_ tdds: [TDDStored], calendar: Calendar) -> [TDDStats] {
+        // MARK: - Group TDDs by Calendar Day
+
+        // Create a dictionary where keys are start-of-day dates and values are arrays of TDD entries for that day
+        let dailyGrouped = Dictionary(grouping: tdds) { tdd in
+            guard let timestamp = tdd.date else { return Date() }
+            // Use start of day (midnight) as the key for grouping
+            return calendar.startOfDay(for: timestamp)
+        }
+
+        // MARK: - Process Each Day's Entries
+
+        // Create a TDDStats object for each day using the most recent TDD entry
+        return dailyGrouped.keys.sorted().map { dayDate in
+            // Get all TDD entries for this day
+            let entries = dailyGrouped[dayDate, default: []]
+
+            // MARK: - Sort and Select Most Recent Entry
+
+            // Sort entries chronologically to find the most recent one for the day
+            let sortedEntries = entries.sorted {
+                ($0.date ?? Date.distantPast) < ($1.date ?? Date.distantPast)
+            }
+
+            // MARK: - Create TDDStats from Most Recent Entry
+
+            // The last entry in the sorted array contains the complete TDD for the day
+            if let lastEntry = sortedEntries.last, let total = lastEntry.total?.doubleValue {
+                // Create TDDStats with the day's date and the total insulin amount
+                return TDDStats(
+                    date: dayDate,
+                    amount: total
+                )
+            } else {
+                // Fallback if no valid entry exists for this day
+                return TDDStats(
+                    date: dayDate,
+                    amount: 0.0
+                )
+            }
+        }
+    }
+
+    /// Calculates and caches the daily averages of Total Daily Dose (TDD) insulin values
+    /// - Note: This function runs asynchronously and updates the tddAveragesCache on the main actor
+    private func calculateAndCacheTDDAverages() async {
+        // Get calendar for date calculations
+        let calendar = Calendar.current
+
+        // Calculate daily averages on background context
+        let dailyAverages = await tddTaskContext.perform { [dailyTDDStats] in
+            // Group TDD stats by calendar day
+            let groupedByDay = Dictionary(grouping: dailyTDDStats) { stat in
+                calendar.startOfDay(for: stat.date)
+            }
+
+            // Calculate average TDD for each day
+            var averages: [Date: Double] = [:]
+            for (day, stats) in groupedByDay {
+                // Sum up all TDD values for the day
+                let total = stats.reduce(0.0) { $0 + $1.amount }
+                let count = Double(stats.count)
+                // Store average in dictionary
+                averages[day] = total / count
+            }
+            return averages
+        }
+
+        // Update cache on main actor
+        await MainActor.run {
+            self.tddAveragesCache = dailyAverages
+        }
+    }
+
+    /// Gets the cached average Total Daily Dose (TDD) of insulin for a specified date range
+    /// - Parameter range: A tuple containing the start and end dates to get averages for
+    /// - Returns: The average TDD in units for the specified date range
+    func getCachedTDDAverages(for range: (start: Date, end: Date)) -> Double {
+        // Calculate and return the TDD averages for the given date range using cached values
+        calculateTDDAveragesForDateRange(from: range.start, to: range.end)
+    }
+
+    /// Calculates the average Total Daily Dose (TDD) of insulin for a specified date range
+    /// - Parameters:
+    ///   - startDate: The start date of the range to calculate averages for
+    ///   - endDate: The end date of the range to calculate averages for
+    /// - Returns: The average TDD in units for the specified date range. Returns 0.0 if no data exists.
+    private func calculateTDDAveragesForDateRange(from startDate: Date, to endDate: Date) -> Double {
+        // Filter cached TDD values to only include those within the date range
+        let relevantStats = tddAveragesCache.filter { date, _ in
+            date >= startDate && date <= endDate
+        }
+
+        // Return 0 if no data exists for the specified range
+        guard !relevantStats.isEmpty else { return 0.0 }
+
+        // Calculate total TDD by summing all values
+        let total = relevantStats.values.reduce(0.0, +)
+        // Convert count to Double for floating point division
+        let count = Double(relevantStats.count)
+
+        // Return average TDD
+        return total / count
+    }
+}

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

@@ -7,58 +7,120 @@ import Swinject
 extension Stat {
     @Observable final class StateModel: BaseStateModel<Provider> {
         @ObservationIgnored @Injected() var settings: SettingsManager!
-        var highLimit: Decimal = 10 / 0.0555
-        var lowLimit: Decimal = 4 / 0.0555
-        var hbA1cDisplayUnit: HbA1cDisplayUnit = .percent
+        var highLimit: Decimal = 180
+        var lowLimit: Decimal = 70
+        var eA1cDisplayUnit: EstimatedA1cDisplayUnit = .percent
         var timeInRangeChartStyle: TimeInRangeChartStyle = .vertical
         var units: GlucoseUnits = .mgdL
+        var useFPUconversion: Bool = false
         var glucoseFromPersistence: [GlucoseStored] = []
+        var loopStatRecords: [LoopStatRecord] = []
+        var loopStats: [(
+            category: LoopStatsDataType,
+            count: Int,
+            percentage: Double,
+            medianDuration: Double,
+            medianInterval: Double
+        )] = []
+        var groupedLoopStats: [LoopStatsByPeriod] = []
+        var bolusStats: [BolusStats] = []
+        var hourlyStats: [HourlyStats] = []
+        var glucoseRangeStats: [GlucoseRangeStats] = []
 
-        var selectedDuration: Duration = .Today
+        // Cache for Meal Stats
+        var hourlyMealStats: [MealStats] = []
+        var dailyMealStats: [MealStats] = []
+        var dailyAveragesCache: [Date: (carbs: Double, fat: Double, protein: Double)] = [:]
 
-        private let context = CoreDataStack.shared.newTaskContext()
-        private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
+        // Cache for TDD Stats
+        var hourlyTDDStats: [TDDStats] = []
+        var dailyTDDStats: [TDDStats] = []
+        var tddAveragesCache: [Date: Double] = [:]
 
-        enum Duration: String, CaseIterable, Identifiable {
-            case Today
-            case Day
-            case Week
-            case Month
-            case Total
-            var id: Self { self }
+        // Cache for Bolus Stats
+        var hourlyBolusStats: [BolusStats] = []
+        var dailyBolusStats: [BolusStats] = []
+        var bolusAveragesCache: [Date: (manual: Double, smb: Double, external: Double)] = [:]
+        var bolusTotalsCache: [(Date, total: Double)] = []
+
+        // Selected Duration for Glucose Stats
+        var selectedIntervalForGlucoseStats: StatsTimeIntervalWithToday = .today {
+            didSet {
+                setupGlucoseArray(for: selectedIntervalForGlucoseStats)
+            }
+        }
+
+        // Selected Duration for Insulin Stats
+        var selectedIntervalForInsulinStats: StatsTimeInterval = .day
+
+        // Selected Duration for Meal Stats
+        var selectedIntervalForMealStats: StatsTimeInterval = .day
+
+        // Selected Duration for Loop Stats
+        var selectedIntervalForLoopStats: StatsTimeIntervalWithToday = .today {
+            didSet {
+                setupLoopStatRecords()
+            }
         }
 
+        // Selected Glucose Chart Type
+        var selectedGlucoseChartType: GlucoseChartType = .percentile
+
+        // Selected Insulin Chart Type
+        var selectedInsulinChartType: InsulinChartType = .totalDailyDose
+
+        // Selected Looping Chart Type
+        var selectedLoopingChartType: LoopingChartType = .loopingPerformance
+
+        // Selected Meal Chart Type
+        var selectedMealChartType: MealChartType = .totalMeals
+
+        // Fetching Contexts
+        let context = CoreDataStack.shared.newTaskContext()
+        let viewContext = CoreDataStack.shared.persistentContainer.viewContext
+        let tddTaskContext = CoreDataStack.shared.newTaskContext()
+        let loopTaskContext = CoreDataStack.shared.newTaskContext()
+        let mealTaskContext = CoreDataStack.shared.newTaskContext()
+        let bolusTaskContext = CoreDataStack.shared.newTaskContext()
+
         override func subscribe() {
-            /// Default is today
-            setupGlucoseArray(for: .Today)
-            highLimit = settingsManager.settings.high
-            lowLimit = settingsManager.settings.low
+            setupGlucoseArray(for: .today)
+            setupTDDStats()
+            setupBolusStats()
+            setupLoopStatRecords()
+            setupMealStats()
             units = settingsManager.settings.units
-            hbA1cDisplayUnit = settingsManager.settings.hbA1cDisplayUnit
+            eA1cDisplayUnit = settingsManager.settings.eA1cDisplayUnit
             timeInRangeChartStyle = settingsManager.settings.timeInRangeChartStyle
+            useFPUconversion = settingsManager.settings.useFPUconversion
         }
 
-        func setupGlucoseArray(for duration: Duration) {
+        func setupGlucoseArray(for interval: StatsTimeIntervalWithToday) {
             Task {
-                let ids = await self.fetchGlucose(for: duration)
+                let ids = await fetchGlucose(for: interval)
                 await updateGlucoseArray(with: ids)
+
+                // Calculate hourly stats and glucose range stats asynchronously with fetched glucose IDs
+                async let hourlyStats: () = calculateHourlyStatsForGlucoseAreaChart(from: ids)
+                async let glucoseRangeStats: () = calculateGlucoseRangeStatsForStackedChart(from: ids)
+                _ = await (hourlyStats, glucoseRangeStats)
             }
         }
 
-        private func fetchGlucose(for duration: Duration) async -> [NSManagedObjectID] {
+        private func fetchGlucose(for interval: StatsTimeIntervalWithToday) async -> [NSManagedObjectID] {
             do {
                 let predicate: NSPredicate
 
-                switch duration {
-                case .Day:
+                switch interval {
+                case .day:
                     predicate = NSPredicate.glucoseForStatsDay
-                case .Week:
+                case .week:
                     predicate = NSPredicate.glucoseForStatsWeek
-                case .Today:
+                case .today:
                     predicate = NSPredicate.glucoseForStatsToday
-                case .Month:
+                case .month:
                     predicate = NSPredicate.glucoseForStatsMonth
-                case .Total:
+                case .total:
                     predicate = NSPredicate.glucoseForStatsTotal
                 }
 
@@ -97,4 +159,179 @@ extension Stat {
             }
         }
     }
+
+    @Observable final class UpdateTimer {
+        private var workItem: DispatchWorkItem?
+
+        /// Schedules a delayed update action
+        /// - Parameter action: The closure to execute after the delay
+        /// Cancels any previously scheduled update before scheduling a new one
+        func scheduleUpdate(action: @escaping () -> Void) {
+            workItem?.cancel()
+
+            let newWorkItem = DispatchWorkItem {
+                action()
+            }
+            workItem = newWorkItem
+
+            DispatchQueue.main.asyncAfter(deadline: .now() + 0.05, execute: newWorkItem)
+        }
+    }
+}
+
+// MARK: Stats Types + Enums
+
+extension Stat.StateModel {
+    /// Defines the available types of glucose charts
+    enum GlucoseChartType: String, CaseIterable {
+        /// Ambulatory Glucose Profile showing percentile ranges
+        case percentile = "Percentile"
+        /// Time-based distribution of glucose ranges
+        case distribution = "Distribution"
+
+        var displayName: String {
+            switch self {
+            case .percentile:
+                return String(localized: "Percentile")
+            case .distribution:
+                return String(localized: "Distribution")
+            }
+        }
+    }
+
+    /// Defines the available types of insulin charts
+    enum InsulinChartType: String, CaseIterable {
+        /// Shows total daily insulin doses
+        case totalDailyDose = "Total Daily Dose"
+        /// Shows distribution of bolus types
+        case bolusDistribution = "Bolus Distribution"
+
+        var displayName: String {
+            switch self {
+            case .totalDailyDose:
+                return String(localized: "Total Daily Dose")
+            case .bolusDistribution:
+                return String(localized: "Bolus Distribution")
+            }
+        }
+    }
+
+    /// Defines the available types of looping charts
+    enum LoopingChartType: String, CaseIterable {
+        /// Shows loop completion and success rates
+        case loopingPerformance = "Looping Performance"
+        /// Shows CGM connection status over time
+        case cgmConnectionTrace = "CGM Connection Trace"
+        /// Shows Trio pump uptime statistics
+        case trioUpTime = "Trio Up-Time"
+
+        var displayName: String {
+            switch self {
+            case .loopingPerformance:
+                return String(localized: "Looping Performance")
+            case .cgmConnectionTrace:
+                return String(localized: "CGM Connection Trace")
+            case .trioUpTime:
+                return String(localized: "Trio Up-Time")
+            }
+        }
+    }
+
+    /// Defines the available types of meal charts
+    enum MealChartType: String, CaseIterable {
+        /// Shows total meal statistics
+        case totalMeals = "Total Meals"
+        /// Shows correlation between meals and glucose excursions
+        case mealToHypoHyperDistribution = "Meal to Hypo/Hyper"
+
+        var displayName: String {
+            switch self {
+            case .totalMeals:
+                return String(localized: "Total Meals")
+            case .mealToHypoHyperDistribution:
+                return String(localized: "Meal to Hypo/Hyper")
+            }
+        }
+    }
+
+    /// Defines the available time periods for duration-based statistics including 'Today' (time since midnight until now)
+    enum StatsTimeIntervalWithToday: String, CaseIterable, Identifiable {
+        /// Current day
+        case today
+        /// Single day view
+        case day = "D"
+        /// Week view
+        case week = "W"
+        /// Month view
+        case month = "M"
+        /// Three month view
+        case total = "3 M"
+
+        var id: Self { self }
+
+        var displayName: String {
+            switch self {
+            case .today:
+                return String(localized: "Today")
+            case .day:
+                return String(localized: "D", comment: "Abbreviation for day")
+            case .week:
+                return String(localized: "W", comment: "Abbreviation for week")
+            case .month:
+                return String(localized: "M", comment: "Abbreviation for month")
+            case .total:
+                return String(localized: "3 M", comment: "Abbreviation for three months")
+            }
+        }
+    }
+
+    /// Defines the available time periods for duration-based statistics
+    enum StatsTimeInterval: String, CaseIterable, Identifiable {
+        /// Single day interval
+        case day = "D"
+        /// Week interval
+        case week = "W"
+        /// Month interval
+        case month = "M"
+        /// Three month interval
+        case total = "3 M"
+
+        var id: Self { self }
+
+        var displayName: String {
+            switch self {
+            case .day:
+                return String(localized: "D", comment: "Abbreviation for day")
+            case .week:
+                return String(localized: "W", comment: "Abbreviation for week")
+            case .month:
+                return String(localized: "M", comment: "Abbreviation for month")
+            case .total:
+                return String(localized: "3 M", comment: "Abbreviation for three months")
+            }
+        }
+    }
+
+    /// Defines the main categories of statistics available in the app
+    enum StatisticViewType: String, CaseIterable, Identifiable {
+        /// Glucose-related statistics including AGP and distributions
+        case glucose
+        /// Insulin delivery statistics including TDD and bolus distributions
+        case insulin
+        /// Loop performance and system status statistics
+        case looping
+        /// Meal-related statistics and correlations
+        case meals
+
+        var id: String { rawValue }
+
+        var displayName: String {
+            switch self {
+            case .glucose: return "Glucose"
+            case .insulin: return "Insulin"
+            case .looping: return "Looping"
+            case .meals: return "Meals"
+            }
+        }
+    }
 }

+ 192 - 0
Trio/Sources/Modules/Stat/View/StatChartUtils.swift

@@ -0,0 +1,192 @@
+import Charts
+import Foundation
+import SwiftUI
+
+struct StatChartUtils {
+    /// Returns the time interval length for the visible domain based on the selected duration.
+    /// - Parameter selectedInterval: The selected time interval for statistics.
+    /// - Returns: The time interval in seconds.
+    static func visibleDomainLength(for selectedInterval: Stat.StateModel.StatsTimeInterval) -> TimeInterval {
+        switch selectedInterval {
+        case .day: return 24 * 3600
+        case .week: return 7 * 24 * 3600
+        case .month: return 30 * 24 * 3600
+        case .total: return 90 * 24 * 3600
+        }
+    }
+
+    /// Computes the visible date range based on the scroll position and selected duration.
+    /// - Parameters:
+    ///   - scrollPosition: The current scroll position in the chart.
+    ///   - selectedInterval: The selected time interval for statistics.
+    /// - Returns: A tuple containing the start and end dates of the visible range.
+    static func visibleDateRange(
+        from scrollPosition: Date,
+        for selectedInterval: Stat.StateModel.StatsTimeInterval
+    ) -> (start: Date, end: Date) {
+        let end = scrollPosition.addingTimeInterval(visibleDomainLength(for: selectedInterval))
+        return (scrollPosition, end)
+    }
+
+    /// Returns the appropriate date format style based on the selected time interval.
+    /// - Parameter selectedInterval: The selected time interval for statistics.
+    /// - Returns: A Date.FormatStyle configured for the current time interval.
+    static func dateFormat(for selectedInterval: Stat.StateModel.StatsTimeInterval) -> Date.FormatStyle {
+        switch selectedInterval {
+        case .day: return .dateTime.hour()
+        case .week: return .dateTime.weekday(.abbreviated)
+        case .month: return .dateTime.day()
+        case .total: return .dateTime.month(.abbreviated)
+        }
+    }
+
+    /// Returns DateComponents for aligning dates based on the selected duration.
+    /// - Parameter selectedInterval: The selected time interval for statistics.
+    /// - Returns: DateComponents configured for the appropriate alignment.
+    static func alignmentComponents(for selectedInterval: Stat.StateModel.StatsTimeInterval) -> DateComponents {
+        switch selectedInterval {
+        case .day: return DateComponents(hour: 0)
+        case .week: return DateComponents(weekday: 2)
+        case .month,
+             .total: return DateComponents(day: 1)
+        }
+    }
+
+    /// Returns the initial scroll position date based on the selected duration.
+    /// - Parameter selectedInterval: The selected time interval for statistics.
+    /// - Returns: A Date representing the initial scroll position.
+    static func getInitialScrollPosition(for selectedInterval: Stat.StateModel.StatsTimeInterval) -> Date {
+        let calendar = Calendar.current
+        let now = Date()
+
+        switch selectedInterval {
+//        case .day: return calendar.date(byAdding: .day, value: -1, to: now)!
+        case .day: return calendar.startOfDay(for: now)
+        case .week: return calendar.date(byAdding: .day, value: -7, to: now)!
+        case .month: return calendar.date(byAdding: .month, value: -1, to: now)!
+        case .total: return calendar.date(byAdding: .month, value: -3, to: now)!
+        }
+    }
+
+    /// Checks if two dates belong to the same time unit based on the selected duration.
+    /// - Parameters:
+    ///   - date1: The first date.
+    ///   - date2: The second date.
+    ///   - selectedInterval: The selected time interval for statistics.
+    /// - Returns: A Boolean indicating whether the two dates are in the same time unit.
+    static func isSameTimeUnit(_ date1: Date, _ date2: Date, for selectedInterval: Stat.StateModel.StatsTimeInterval) -> Bool {
+        let calendar = Calendar.current
+        switch selectedInterval {
+        case .day:
+            return calendar.isDate(date1, equalTo: date2, toGranularity: .hour)
+        default:
+            return calendar.isDate(date1, inSameDayAs: date2)
+        }
+    }
+
+    /// Formats the visible date range into a human-readable string.
+    /// - Parameters:
+    ///   - start: The start date of the range.
+    ///   - end: The end date of the range.
+    ///   - selectedInterval: The selected time interval for statistics.
+    /// - Returns: A formatted string representing the visible date range.
+    static func formatVisibleDateRange(
+        from start: Date,
+        to end: Date,
+        for selectedInterval: Stat.StateModel.StatsTimeInterval
+    ) -> String {
+        let calendar = Calendar.current
+
+        // If not .day, we just return "startText - endText", e.g. "Jan 1 - Jan 8"
+        guard selectedInterval == .day else {
+            let formatDate: (Date) -> String = { date in
+                date.formatted(.dateTime.day().month())
+            }
+            let startText = formatDate(start)
+            let endText = formatDate(end)
+            return "\(startText) - \(endText)"
+        }
+
+        // For .day mode, we figure out if we are near the boundaries for a "full day" (00:00 - 23:59)
+        let dayStart = calendar.startOfDay(for: start)
+        let nextDayStart = calendar.date(byAdding: .day, value: 1, to: dayStart)!
+
+        // Allow +/- 15 minutes from midnight as buffer, so slow scrolling doesn't break the "full day"
+        let tolerance: TimeInterval = 60 * 15
+
+        let isStartNearMidnight = abs(start.timeIntervalSince(dayStart)) < tolerance
+        let isEndNearNextMidnight = abs(end.timeIntervalSince(nextDayStart)) < tolerance
+
+        let formatDay: (Date) -> String = { date in
+            date.formatted(.dateTime.day().month(.abbreviated))
+        }
+
+        if isStartNearMidnight, isEndNearNextMidnight {
+            // Full day: show just start as "Mon, Jan 1"
+            return dayStart.formatted(.dateTime.weekday(.abbreviated).day().month(.abbreviated))
+        } else {
+            // Partial day: show start and end
+            let startText = formatDay(start)
+            let endText = formatDay(end)
+            return "\(startText) - \(endText)"
+        }
+    }
+
+    /// A helper function to create a `VStack` for each statistic.
+    ///
+    /// - Parameters:
+    ///   - title: The title of the statistic.
+    ///   - value: The formatted value to display.
+    /// - Returns: A `VStack` with the title and value.
+    static func statView(title: String, value: String) -> some View {
+        VStack(spacing: 5) {
+            Text(title)
+                .font(.subheadline)
+                .foregroundStyle(Color.secondary)
+            Text(value)
+        }
+    }
+
+    /// Computes the median value of an array of integers.
+    ///
+    /// - Parameter array: An array of integers.
+    /// - Returns: The median value as a `Double`. Returns `0` if the array is empty.
+    static func medianCalculation(array: [Int]) -> Double {
+        guard !array.isEmpty else { return 0 }
+        let sorted = array.sorted()
+        let length = array.count
+
+        if length % 2 == 0 {
+            return Double((sorted[length / 2 - 1] + sorted[length / 2]) / 2)
+        }
+        return Double(sorted[length / 2])
+    }
+
+    /// Computes the median value of an array of doubles.
+    ///
+    /// - Parameter array: An array of `Double` values.
+    /// - Returns: The median value. Returns `0` if the array is empty.
+    static func medianCalculationDouble(array: [Double]) -> Double {
+        guard !array.isEmpty else { return 0 }
+        let sorted = array.sorted()
+        let length = array.count
+
+        if length % 2 == 0 {
+            return (sorted[length / 2 - 1] + sorted[length / 2]) / 2
+        }
+        return sorted[length / 2]
+    }
+
+    /// Creates a legend item view for use in a chart legend.
+    ///
+    /// - Parameters:
+    ///   - label: The text label for the legend item.
+    ///   - color: The color associated with the legend item.
+    /// - Returns: A SwiftUI view displaying a colored symbol and a label.
+    @ViewBuilder static func legendItem(label: String, color: Color) -> some View {
+        HStack(spacing: 4) {
+            Image(systemName: "circle.fill").foregroundStyle(color)
+            Text(label).foregroundStyle(Color.secondary)
+        }.font(.caption)
+    }
+}

+ 346 - 113
Trio/Sources/Modules/Stat/View/StatRootView.swift

@@ -1,150 +1,383 @@
 import Charts
-import CoreData
 import SwiftDate
 import SwiftUI
 import Swinject
 
 extension Stat {
     struct RootView: BaseView {
-        let resolver: Resolver
-        @State var state = StateModel()
+        enum Constants {
+            static let spacing: CGFloat = 16
+            static let cornerRadius: CGFloat = 10
+            static let backgroundOpacity = 0.1
+        }
 
+        let resolver: Resolver
         @Environment(\.colorScheme) var colorScheme
         @Environment(AppState.self) var appState
 
-        @State var paddingAmount: CGFloat? = 10
-        @State var headline: Color = .secondary
-        @State var days: Double = 0
-        @State var pointSize: CGFloat = 3
-        @State var conversionFactor = 0.0555
-
-        @ViewBuilder func stats() -> some View {
-            ZStack {
-                Color.gray.opacity(0.05).ignoresSafeArea(.all)
-                let filter = DateFilter()
-                switch state.selectedDuration {
-                case .Today:
-                    StatsView(
-                        filter: filter.today,
-                        highLimit: state.highLimit,
-                        lowLimit: state.lowLimit,
-                        units: state.units,
-                        hbA1cDisplayUnit: state.hbA1cDisplayUnit
-                    )
-                case .Day:
-                    StatsView(
-                        filter: filter.day,
+        @State var state = StateModel()
+        @State private var selectedView: StateModel.StatisticViewType = .glucose
+
+        var body: some View {
+            VStack {
+                Picker("View", selection: $selectedView) {
+                    ForEach(StateModel.StatisticViewType.allCases) { viewType in
+                        Text(viewType.displayName).tag(viewType)
+                    }
+                }
+                .pickerStyle(.segmented)
+                .padding(.horizontal)
+
+                ScrollView {
+                    VStack(spacing: Constants.spacing) {
+                        switch selectedView {
+                        case .glucose:
+                            glucoseView
+                        case .insulin:
+                            insulinView
+                        case .looping:
+                            loopingView
+                        case .meals:
+                            mealsView
+                        }
+                    }
+                    .padding()
+                }
+            }
+            .background(appState.trioBackgroundColor(for: colorScheme))
+            .onAppear(perform: configureView)
+            .navigationBarTitleDisplayMode(.inline)
+            .navigationTitle("Statistics")
+            .toolbar {
+                ToolbarItem(placement: .topBarLeading) {
+                    Button(action: state.hideModal) {
+                        Text("Close")
+                            .foregroundColor(.tabBar)
+                    }
+                }
+            }
+        }
+
+        // MARK: - Stats View
+
+        @ViewBuilder var glucoseView: some View {
+            HStack {
+                Text("Chart Type")
+                    .font(.headline)
+
+                Spacer()
+
+                Picker("Glucose Chart Type", selection: $state.selectedGlucoseChartType) {
+                    ForEach(StateModel.GlucoseChartType.allCases, id: \.self) { type in
+                        Text(type.displayName)
+                    }
+                }
+                .pickerStyle(.menu)
+            }.padding(.horizontal)
+
+            Picker("Duration", selection: $state.selectedIntervalForGlucoseStats) {
+                ForEach(StateModel.StatsTimeIntervalWithToday.allCases, id: \.self) { timeInterval in
+                    Text(timeInterval.displayName)
+                }
+            }
+            .pickerStyle(.segmented)
+
+            if state.glucoseFromPersistence.isEmpty {
+                ContentUnavailableView(
+                    String(localized: "No Glucose Data"),
+                    systemImage: "chart.bar.fill",
+                    description: Text("Glucose statistics will appear here once data is available.")
+                )
+            } else {
+                timeInRangeCard
+                glucoseStatsCard
+
+                HStack {
+                    var hintText: String {
+                        switch state.selectedGlucoseChartType {
+                        case .percentile:
+                            String(localized: "Tap and hold the AGP graph or Time-in-Range ring to reveal more details.")
+                        case .distribution:
+                            String(localized: "Tap and hold the Time-in-Range ring to reveal more details.")
+                        }
+                    }
+                    Image(systemName: "hand.draw.fill")
+                        .foregroundStyle(Color.primary)
+                        .padding(.leading)
+                    Text(hintText)
+                        .foregroundStyle(Color.secondary)
+                        .padding(.trailing)
+                }.font(.footnote)
+            }
+        }
+
+        private var timeInRangeCard: some View {
+            StatCard {
+                VStack(spacing: Constants.spacing) {
+                    switch state.selectedGlucoseChartType {
+                    case .percentile:
+                        GlucosePercentileChart(
+                            glucose: state.glucoseFromPersistence,
+                            highLimit: state.highLimit,
+                            lowLimit: state.lowLimit,
+                            units: state.units,
+                            hourlyStats: state.hourlyStats,
+                            isToday: state.selectedIntervalForGlucoseStats == .today
+                        )
+                    case .distribution:
+                        GlucoseDistributionChart(
+                            glucose: state.glucoseFromPersistence,
+                            highLimit: state.highLimit,
+                            lowLimit: state.lowLimit,
+                            units: state.units,
+                            glucoseRangeStats: state.glucoseRangeStats
+                        )
+                    }
+                }
+            }
+        }
+
+        private var glucoseStatsCard: some View {
+            StatCard {
+                VStack(spacing: Constants.spacing) {
+                    GlucoseSectorChart(
                         highLimit: state.highLimit,
                         lowLimit: state.lowLimit,
                         units: state.units,
-                        hbA1cDisplayUnit: state.hbA1cDisplayUnit
+                        glucose: state.glucoseFromPersistence
                     )
-                case .Week:
-                    StatsView(
-                        filter: filter.week,
+
+                    Divider()
+
+                    GlucoseMetricsView(
                         highLimit: state.highLimit,
                         lowLimit: state.lowLimit,
                         units: state.units,
-                        hbA1cDisplayUnit: state.hbA1cDisplayUnit
+                        eA1cDisplayUnit: state.eA1cDisplayUnit,
+                        glucose: state.glucoseFromPersistence
                     )
-                case .Month:
-                    StatsView(
-                        filter: filter.month,
-                        highLimit: state.highLimit,
-                        lowLimit: state.lowLimit,
-                        units: state.units,
-                        hbA1cDisplayUnit: state.hbA1cDisplayUnit
+                }
+            }
+        }
+
+        @ViewBuilder var insulinView: some View {
+            HStack {
+                Text("Chart Type")
+                    .font(.headline)
+
+                Spacer()
+
+                Picker("Insulin Chart Type", selection: $state.selectedInsulinChartType) {
+                    ForEach(StateModel.InsulinChartType.allCases, id: \.self) { type in
+                        Text(type.displayName)
+                    }
+                }.pickerStyle(.menu)
+            }.padding(.horizontal)
+
+            Picker("Duration", selection: $state.selectedIntervalForInsulinStats) {
+                ForEach(StateModel.StatsTimeInterval.allCases) { timeInterval in
+                    Text(timeInterval.rawValue).tag(timeInterval)
+                }
+            }
+            .pickerStyle(.segmented)
+
+            StatCard {
+                switch state.selectedInsulinChartType {
+                case .totalDailyDose:
+                    if state.dailyTDDStats.isEmpty {
+                        ContentUnavailableView(
+                            String(localized: "No TDD Data"),
+                            systemImage: "chart.bar.xaxis",
+                            description: Text("Total Daily Doses will appear here once data is available.")
+                        )
+                    } else {
+                        TotalDailyDoseChart(
+                            selectedInterval: $state.selectedIntervalForInsulinStats,
+                            tddStats: state.selectedIntervalForInsulinStats == .day ?
+                                state.hourlyTDDStats : state.dailyTDDStats,
+                            state: state
+                        )
+                    }
+
+                case .bolusDistribution:
+                    var hasBolusData: Bool {
+                        state.dailyBolusStats.contains { $0.manualBolus > 0 || $0.smb > 0 || $0.external > 0 }
+                    }
+
+                    if state.dailyBolusStats.isEmpty || !hasBolusData {
+                        ContentUnavailableView(
+                            String(localized: "No Bolus Data"),
+                            systemImage: "cross.vial",
+                            description: Text("Bolus statistics will appear here once data is available.")
+                        )
+                    } else {
+                        BolusStatsView(
+                            selectedInterval: $state.selectedIntervalForInsulinStats,
+                            bolusStats: state.selectedIntervalForInsulinStats == .day ?
+                                state.hourlyBolusStats : state.dailyBolusStats,
+                            state: state
+                        )
+                    }
+                }
+            }
+
+            HStack {
+                Image(systemName: "hand.draw.fill").foregroundStyle(Color.primary)
+                VStack(alignment: .leading) {
+                    Text("Swipe the chart to scroll through time.")
+                    Text("Tap and hold a bar to reveal more details.")
+                }.foregroundStyle(Color.secondary)
+            }.font(.footnote)
+        }
+
+        @ViewBuilder var loopingView: some View {
+            HStack {
+                Text("Chart Type")
+                    .font(.headline)
+
+                Spacer()
+
+                Picker("Looping Chart Type", selection: $state.selectedLoopingChartType) {
+                    ForEach(StateModel.LoopingChartType.allCases, id: \.self) { type in
+                        Text(type.displayName)
+                    }
+                }.pickerStyle(.menu)
+            }.padding(.horizontal)
+
+            Picker("Duration", selection: $state.selectedIntervalForLoopStats) {
+                ForEach(StateModel.StatsTimeIntervalWithToday.allCases, id: \.self) { interval in
+                    Text(interval.displayName)
+                }
+            }
+            .pickerStyle(.segmented)
+
+            StatCard {
+                switch state.selectedLoopingChartType {
+                case .loopingPerformance:
+                    if state.loopStatRecords.isEmpty {
+                        ContentUnavailableView(
+                            String(localized: "No Loop Data"),
+                            systemImage: "clock.arrow.2.circlepath",
+                            description: Text("Loop statistics will appear here once data is available.")
+                        )
+                    } else {
+                        loopingChartView
+                        loopStats
+                    }
+                case .trioUpTime:
+                    // TODO: Trio Up-Time Chart
+                    ContentUnavailableView(
+                        String(localized: "Coming soon."),
+                        systemImage: "hourglass",
+                        description: Text("Trio Up-Time Chart")
                     )
-                case .Total:
-                    StatsView(
-                        filter: filter.total,
-                        highLimit: state.highLimit,
-                        lowLimit: state.lowLimit,
-                        units: state.units,
-                        hbA1cDisplayUnit: state.hbA1cDisplayUnit
+                case .cgmConnectionTrace:
+                    // TODO: CGM Connection Trace Chart
+                    ContentUnavailableView(
+                        String(localized: "Coming soon."),
+                        systemImage: "hourglass",
+                        description: Text("CGM Connection Trace Chart")
                     )
                 }
             }
         }
 
-        @ViewBuilder func chart() -> some View {
-            switch state.selectedDuration {
-            case .Today:
-                ChartsView(
-                    highLimit: state.highLimit,
-                    lowLimit: state.lowLimit,
-                    units: state.units,
-                    hbA1cDisplayUnit: state.hbA1cDisplayUnit,
-                    timeInRangeChartStyle: state.timeInRangeChartStyle,
-                    glucose: state.glucoseFromPersistence
-                )
-            case .Day:
-                ChartsView(
-                    highLimit: state.highLimit,
-                    lowLimit: state.lowLimit,
-                    units: state.units,
-                    hbA1cDisplayUnit: state.hbA1cDisplayUnit,
-                    timeInRangeChartStyle: state.timeInRangeChartStyle,
-                    glucose: state.glucoseFromPersistence
-                )
-            case .Week:
-                ChartsView(
-                    highLimit: state.highLimit,
-                    lowLimit: state.lowLimit,
-                    units: state.units,
-                    hbA1cDisplayUnit: state.hbA1cDisplayUnit,
-                    timeInRangeChartStyle: state.timeInRangeChartStyle,
-                    glucose: state.glucoseFromPersistence
+        private var loopingChartView: some View {
+            VStack(spacing: Constants.spacing) {
+                LoopBarChartView(
+                    loopStatRecords: state.loopStatRecords,
+                    selectedInterval: state.selectedIntervalForLoopStats,
+                    statsData: state.loopStats
                 )
-            case .Month:
-                ChartsView(
-                    highLimit: state.highLimit,
-                    lowLimit: state.lowLimit,
-                    units: state.units,
-                    hbA1cDisplayUnit: state.hbA1cDisplayUnit,
-                    timeInRangeChartStyle: state.timeInRangeChartStyle,
-                    glucose: state.glucoseFromPersistence
-                )
-            case .Total:
-                ChartsView(
-                    highLimit: state.highLimit,
-                    lowLimit: state.lowLimit,
-                    units: state.units,
-                    hbA1cDisplayUnit: state.hbA1cDisplayUnit,
-                    timeInRangeChartStyle: state.timeInRangeChartStyle,
-                    glucose: state.glucoseFromPersistence
+            }
+        }
+
+        private var loopStats: some View {
+            VStack(spacing: Constants.spacing) {
+                LoopStatsView(
+                    statsData: state.loopStats
                 )
             }
         }
 
-        var body: some View {
-            VStack(alignment: .center) {
-                chart().padding(.top, 20)
-                Picker("Duration", selection: $state.selectedDuration) {
-                    ForEach(Stat.StateModel.Duration.allCases) { duration in
-                        Text(duration.rawValue).tag(Optional(duration))
+        @ViewBuilder var mealsView: some View {
+            HStack {
+                Text("Chart Type")
+                    .font(.headline)
+
+                Spacer()
+
+                Picker("Meal Chart Type", selection: $state.selectedMealChartType) {
+                    ForEach(StateModel.MealChartType.allCases, id: \.self) { type in
+                        Text(type.displayName)
                     }
-                }.onChange(of: state.selectedDuration) { _, newValue in
-                    state.setupGlucoseArray(for: newValue)
+                }.pickerStyle(.menu)
+            }.padding(.horizontal)
+
+            Picker("Duration", selection: $state.selectedIntervalForMealStats) {
+                ForEach(StateModel.StatsTimeInterval.allCases, id: \.self) { timeInterval in
+                    Text(timeInterval.rawValue)
                 }
-                .pickerStyle(.segmented).background(.cyan.opacity(0.2))
-                stats()
-            }.background(appState.trioBackgroundColor(for: colorScheme))
-                .onAppear(perform: configureView)
-                .navigationBarTitle("Statistics")
-                .navigationBarTitleDisplayMode(.automatic)
-                .toolbar {
-                    ToolbarItem(placement: .topBarLeading, content: {
-                        Button(
-                            action: { state.hideModal() },
-                            label: {
-                                HStack {
-                                    Text("Close")
-                                }
-                            }
+            }
+            .pickerStyle(.segmented)
+
+            StatCard {
+                switch state.selectedMealChartType {
+                case .totalMeals:
+                    var hasMealData: Bool {
+                        state.dailyMealStats.contains { $0.carbs > 0 || $0.fat > 0 || $0.protein > 0 }
+                    }
+
+                    if state.dailyMealStats.isEmpty || !hasMealData {
+                        ContentUnavailableView(
+                            String(localized: "No Meal Data"),
+                            systemImage: "fork.knife",
+                            description: Text("Meal statistics will appear here once data is available.")
                         )
-                    })
+                    } else {
+                        MealStatsView(
+                            selectedInterval: $state.selectedIntervalForMealStats,
+                            mealStats: state.selectedIntervalForMealStats == .day ?
+                                state.hourlyMealStats : state.dailyMealStats,
+                            state: state
+                        )
+                    }
+                case .mealToHypoHyperDistribution:
+                    // TODO: Meal to Hypoglycemia/Hyperglycemia Distribution
+                    ContentUnavailableView(
+                        String(localized: "Coming soon."),
+                        systemImage: "hourglass",
+                        description: Text("Meal to Hypoglycemia/Hyperglycemia Distribution Chart")
+                    )
                 }
+            }
+
+            HStack {
+                Image(systemName: "hand.draw.fill").foregroundStyle(Color.primary)
+                VStack(alignment: .leading) {
+                    Text("Swipe the chart to scroll through time.")
+                    Text("Tap and hold a bar to reveal more details.")
+                }.foregroundStyle(Color.secondary)
+            }.font(.footnote)
         }
     }
 }
+
+// MARK: - Supporting Views
+
+struct StatCard<Content: View>: View {
+    let content: Content
+
+    init(@ViewBuilder content: () -> Content) {
+        self.content = content()
+    }
+
+    var body: some View {
+        content
+            .padding()
+            .background(
+                RoundedRectangle(cornerRadius: Stat.RootView.Constants.cornerRadius)
+                    .fill(Color.secondary.opacity(Stat.RootView.Constants.backgroundOpacity))
+            )
+    }
+}

+ 0 - 287
Trio/Sources/Modules/Stat/View/StatsView.swift

@@ -1,287 +0,0 @@
-import CoreData
-import SwiftDate
-import SwiftUI
-
-struct StatsView: View {
-    @FetchRequest var fetchRequest: FetchedResults<LoopStatRecord>
-    @FetchRequest var glucose: FetchedResults<GlucoseStored>
-
-    @State var headline: Color = .secondary
-
-    var highLimit: Decimal
-    var lowLimit: Decimal
-    var units: GlucoseUnits
-    var hbA1cDisplayUnit: HbA1cDisplayUnit
-
-    private let conversionFactor = 0.0555
-
-    var body: some View {
-        VStack(spacing: 10) {
-            loops
-            Divider()
-            hba1c
-            Divider()
-            bloodGlucose
-        }
-    }
-
-    init(
-        filter: NSDate,
-        highLimit: Decimal,
-        lowLimit: Decimal,
-        units: GlucoseUnits,
-        hbA1cDisplayUnit: HbA1cDisplayUnit
-    ) {
-        _fetchRequest = FetchRequest<LoopStatRecord>(
-            sortDescriptors: [NSSortDescriptor(key: "start", ascending: false)],
-            predicate: NSPredicate(format: "interval > 0 AND start > %@", filter)
-        )
-
-        _glucose = FetchRequest<GlucoseStored>(
-            sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)],
-            predicate: NSPredicate(format: "glucose > 0 AND date > %@", filter)
-        )
-
-        self.highLimit = highLimit
-        self.lowLimit = lowLimit
-        self.units = units
-        self.hbA1cDisplayUnit = hbA1cDisplayUnit
-    }
-
-    var loops: some View {
-        let loops = fetchRequest
-        // First date
-        let previous = loops.last?.end ?? Date()
-        // Last date (recent)
-        let current = loops.first?.start ?? Date()
-        // Total time in days
-        let totalTime = (current - previous).timeInterval / 8.64E4
-
-        let durationArray = loops.compactMap({ each in each.duration })
-        let durationArrayCount = durationArray.count
-        // var durationAverage = durationArray.reduce(0, +) / Double(durationArrayCount)
-        let medianDuration = medianCalculationDouble(array: durationArray)
-        let successsNR = loops.compactMap({ each in each.loopStatus }).filter({ each in each!.contains("Success") }).count
-        let errorNR = durationArrayCount - successsNR
-        let total = Double(successsNR + errorNR) == 0 ? 1 : Double(successsNR + errorNR)
-        let successRate: Double? = (Double(successsNR) / total) * 100
-        let loopNr = totalTime <= 1 ? total : round(total / (totalTime != 0 ? totalTime : 1))
-        let intervalArray = loops.compactMap({ each in each.interval as Double })
-        let count = intervalArray.count != 0 ? intervalArray.count : 1
-        let intervalAverage = intervalArray.reduce(0, +) / Double(count)
-        // let maximumInterval = intervalArray.max()
-        // let minimumInterval = intervalArray.min()
-        return VStack(spacing: 10) {
-            HStack(spacing: 35) {
-                VStack(spacing: 5) {
-                    Text("Loops").font(.subheadline).foregroundColor(headline)
-                    Text(loopNr.formatted())
-                }
-                VStack(spacing: 5) {
-                    Text("Interval").font(.subheadline).foregroundColor(headline)
-                    Text(intervalAverage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + " min")
-                }
-                VStack(spacing: 5) {
-                    Text("Duration").font(.subheadline).foregroundColor(headline)
-                    Text(
-                        (medianDuration * 60)
-                            .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + " s"
-                    )
-                }
-                VStack(spacing: 5) {
-                    Text("Success").font(.subheadline).foregroundColor(headline)
-                    Text(
-                        ((successRate ?? 100) / 100)
-                            .formatted(.percent.grouping(.never).rounded().precision(.fractionLength(1)))
-                    )
-                }
-            }
-        }
-    }
-
-    private func medianCalculation(array: [Int]) -> Double {
-        guard !array.isEmpty else {
-            return 0
-        }
-        let sorted = array.sorted()
-        let length = array.count
-
-        if length % 2 == 0 {
-            return Double((sorted[length / 2 - 1] + sorted[length / 2]) / 2)
-        }
-        return Double(sorted[length / 2])
-    }
-
-    private func medianCalculationDouble(array: [Double]) -> Double {
-        guard !array.isEmpty else {
-            return 0
-        }
-        let sorted = array.sorted()
-        let length = array.count
-
-        if length % 2 == 0 {
-            return (sorted[length / 2 - 1] + sorted[length / 2]) / 2
-        }
-        return sorted[length / 2]
-    }
-
-    var hba1c: some View {
-        HStack(spacing: 50) {
-            let useUnit: GlucoseUnits = {
-                if hbA1cDisplayUnit == .mmolMol { return .mmolL }
-                else { return .mgdL }
-            }()
-
-            let hba1cs = glucoseStats()
-            // First date
-            let previous = glucose.last?.date ?? Date()
-            // Last date (recent)
-            let current = glucose.first?.date ?? Date()
-            // Total time in days
-            let numberOfDays = (current - previous).timeInterval / 8.64E4
-
-            let hba1cString = (
-                useUnit == .mmolL ? hba1cs.ifcc
-                    .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) : hba1cs.ngsp
-                    .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))
-                    + " %"
-            )
-            VStack(spacing: 5) {
-                Text("HbA1c").font(.subheadline).foregroundColor(headline)
-                Text(hba1cString)
-            }
-            VStack(spacing: 5) {
-                Text("SD").font(.subheadline).foregroundColor(.secondary)
-                Text(
-                    hba1cs.sd
-                        .formatted(
-                            .number.grouping(.never).rounded()
-                                .precision(.fractionLength(units == .mmolL ? 1 : 0))
-                        )
-                )
-            }
-            VStack(spacing: 5) {
-                Text("CV").font(.subheadline).foregroundColor(.secondary)
-                Text(hba1cs.cv.formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))))
-            }
-            VStack(spacing: 5) {
-                Text("Days").font(.subheadline).foregroundColor(.secondary)
-                Text(numberOfDays.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))))
-            }
-        }
-    }
-
-    var bloodGlucose: some View {
-        HStack(spacing: 30) {
-            let bgs = glucoseStats()
-
-            // First date
-            let previous = glucose.last?.date ?? Date()
-            // Last date (recent)
-            let current = glucose.first?.date ?? Date()
-            // Total time in days
-            let numberOfDays = (current - previous).timeInterval / 8.64E4
-
-            VStack(spacing: 5) {
-                Text(numberOfDays < 1 ? "Readings" : "Readings / 24 h").font(.subheadline)
-                    .foregroundColor(.secondary)
-                Text(bgs.readings.formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))))
-            }
-            VStack(spacing: 5) {
-                Text("Average").font(.subheadline).foregroundColor(headline)
-                Text(
-                    bgs.average
-                        .formatted(
-                            .number.grouping(.never).rounded()
-                                .precision(.fractionLength(units == .mmolL ? 1 : 0))
-                        )
-                )
-            }
-            VStack(spacing: 5) {
-                Text("Median").font(.subheadline).foregroundColor(.secondary)
-                Text(
-                    bgs.median
-                        .formatted(
-                            .number.grouping(.never).rounded()
-                                .precision(.fractionLength(units == .mmolL ? 1 : 0))
-                        )
-                )
-            }
-        }
-    }
-
-    private func glucoseStats()
-        -> (ifcc: Double, ngsp: Double, average: Double, median: Double, sd: Double, cv: Double, readings: Double)
-    {
-        // First date
-        let previous = glucose.last?.date ?? Date()
-        // Last date (recent)
-        let current = glucose.first?.date ?? Date()
-        // Total time in days
-        let numberOfDays = (current - previous).timeInterval / 8.64E4
-
-        let denominator = numberOfDays < 1 ? 1 : numberOfDays
-
-        let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
-        let sumReadings = justGlucoseArray.reduce(0, +)
-        let countReadings = justGlucoseArray.count
-
-        let glucoseAverage = Double(sumReadings) / Double(countReadings)
-        let medianGlucose = medianCalculation(array: justGlucoseArray)
-
-        var NGSPa1CStatisticValue = 0.0
-        var IFCCa1CStatisticValue = 0.0
-
-        if numberOfDays > 0 {
-            NGSPa1CStatisticValue = (glucoseAverage + 46.7) / 28.7 // NGSP (%)
-            IFCCa1CStatisticValue = 10.929 *
-                (NGSPa1CStatisticValue - 2.152) // IFCC (mmol/mol)  A1C(mmol/mol) = 10.929 * (A1C(%) - 2.15)
-        }
-        var sumOfSquares = 0.0
-
-        for array in justGlucoseArray {
-            sumOfSquares += pow(Double(array) - Double(glucoseAverage), 2)
-        }
-        var sd = 0.0
-        var cv = 0.0
-
-        // Avoid division by zero
-        if glucoseAverage > 0 {
-            sd = sqrt(sumOfSquares / Double(countReadings))
-            cv = sd / Double(glucoseAverage) * 100
-        }
-
-        var output: (ifcc: Double, ngsp: Double, average: Double, median: Double, sd: Double, cv: Double, readings: Double)
-        output = (
-            ifcc: IFCCa1CStatisticValue,
-            ngsp: NGSPa1CStatisticValue,
-            average: glucoseAverage * (units == .mmolL ? conversionFactor : 1),
-            median: medianGlucose * (units == .mmolL ? conversionFactor : 1),
-            sd: sd * (units == .mmolL ? conversionFactor : 1), cv: cv,
-            readings: Double(countReadings) / denominator
-        )
-        return output
-    }
-
-    private func tir() -> [(decimal: Decimal, string: String)] {
-        let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
-        let totalReadings = justGlucoseArray.count
-
-        let hyperArray = glucose.filter({ $0.glucose >= Int(highLimit) })
-        let hyperReadings = hyperArray.compactMap({ each in each.glucose as Int16 }).count
-        let hyperPercentage = Double(hyperReadings) / Double(totalReadings) * 100
-
-        let hypoArray = glucose.filter({ $0.glucose <= Int(lowLimit) })
-        let hypoReadings = hypoArray.compactMap({ each in each.glucose as Int16 }).count
-        let hypoPercentage = Double(hypoReadings) / Double(totalReadings) * 100
-
-        let tir = 100 - (hypoPercentage + hyperPercentage)
-
-        var array: [(decimal: Decimal, string: String)] = []
-        array.append((decimal: Decimal(hypoPercentage), string: "Low"))
-        array.append((decimal: Decimal(tir), string: "NormaL"))
-        array.append((decimal: Decimal(hyperPercentage), string: "High"))
-
-        return array
-    }
-}

+ 102 - 0
Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseDistributionChart.swift

@@ -0,0 +1,102 @@
+import Charts
+import SwiftUI
+
+struct GlucoseDistributionChart: View {
+    let glucose: [GlucoseStored]
+    let highLimit: Decimal
+    let lowLimit: Decimal
+    let units: GlucoseUnits
+    let glucoseRangeStats: [GlucoseRangeStats]
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 8) {
+            Text("Glucose Distribution")
+                .font(.headline)
+
+            Chart(glucoseRangeStats) { range in
+                ForEach(range.values, id: \.hour) { value in
+                    AreaMark(
+                        x: .value("Hour", Calendar.current.dateForChartHour(value.hour)),
+                        y: .value("Count", value.count),
+                        stacking: .normalized
+                    )
+                    .foregroundStyle(by: .value("Range", range.name))
+                }
+            }
+            .chartForegroundStyleScale([
+                "<54": .purple.opacity(0.7),
+                "54-70": .red.opacity(0.7),
+                "70-140": .green,
+                "140-180": .green.opacity(0.7),
+                "180-200": .yellow.opacity(0.7),
+                "200-220": .orange.opacity(0.7),
+                ">220": .orange.opacity(0.8)
+            ])
+            .chartLegend(position: .bottom, alignment: .leading, spacing: 12) {
+                let legendItems: [(String, Color)] = [
+                    ("<\(units == .mgdL ? Decimal(54) : 54.asMmolL)", .purple.opacity(0.7)),
+                    (
+                        "\(units == .mgdL ? Decimal(54) : 54.asMmolL)-\(units == .mgdL ? Decimal(70) : 70.asMmolL)",
+                        .red.opacity(0.7)
+                    ),
+                    ("\(units == .mgdL ? Decimal(70) : 70.asMmolL)-\(units == .mgdL ? Decimal(140) : 140.asMmolL)", .green),
+                    (
+                        "\(units == .mgdL ? Decimal(140) : 140.asMmolL)-\(units == .mgdL ? Decimal(180) : 180.asMmolL)",
+                        .green.opacity(0.7)
+                    ),
+                    (
+                        "\(units == .mgdL ? Decimal(180) : 180.asMmolL)-\(units == .mgdL ? Decimal(200) : 200.asMmolL)",
+                        .yellow.opacity(0.7)
+                    ),
+                    (
+                        "\(units == .mgdL ? Decimal(200) : 200.asMmolL)-\(units == .mgdL ? Decimal(220) : 220.asMmolL)",
+                        .orange.opacity(0.7)
+                    ),
+                    (">\(units == .mgdL ? Decimal(220) : 220.asMmolL)", .orange.opacity(0.8))
+                ]
+
+                let columns = [GridItem(.adaptive(minimum: 65), spacing: 4)]
+
+                LazyVGrid(columns: columns, alignment: .leading, spacing: 4) {
+                    ForEach(legendItems, id: \.0) { item in
+                        StatChartUtils.legendItem(label: item.0, color: item.1)
+                    }
+                }
+            }
+            .chartYAxis {
+                AxisMarks(position: .trailing) { value in
+                    if let percentage = value.as(Double.self) {
+                        AxisValueLabel {
+                            Text((percentage / 100).formatted(.percent.precision(.fractionLength(0))))
+                                .font(.footnote)
+                        }
+                        AxisGridLine()
+                    }
+                }
+            }
+            .chartYAxisLabel(alignment: .trailing) {
+                Text("Percentage")
+                    .foregroundStyle(.primary)
+                    .font(.footnote)
+                    .padding(.vertical, 3)
+            }
+            .chartXAxis {
+                AxisMarks(values: .stride(by: .hour, count: 3)) { value in
+                    if let date = value.as(Date.self) {
+                        let hour = Calendar.current.component(.hour, from: date)
+                        switch hour {
+                        case 0,
+                             12:
+                            AxisValueLabel(format: .dateTime.hour())
+                        default:
+                            AxisValueLabel(format: .dateTime.hour(.defaultDigits(amPM: .omitted)))
+                        }
+
+                        AxisGridLine()
+                    }
+                }
+            }
+            .frame(height: 200)
+        }
+    }
+}

+ 134 - 0
Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseMetricsView.swift

@@ -0,0 +1,134 @@
+import CoreData
+import SwiftDate
+import SwiftUI
+
+/// A SwiftUI view displaying various glucose-related statistics based on stored glucose readings.
+struct GlucoseMetricsView: View {
+    /// The upper glucose limit for evaluation.
+    let highLimit: Decimal
+    /// The lower glucose limit for evaluation.
+    let lowLimit: Decimal
+    /// The unit of measurement for blood glucose values (e.g., mg/dL or mmol/L).
+    let units: GlucoseUnits
+    /// The display unit for estimated HbA1c values.
+    let eA1cDisplayUnit: EstimatedA1cDisplayUnit
+    /// A list of stored glucose readings.
+    let glucose: [GlucoseStored]
+
+    /// The main body of the `GlucoseMetricsView`, displaying glucose-related statistics.
+    var body: some View {
+        let preferredUnit: GlucoseUnits = eA1cDisplayUnit == .mmolMol ? .mmolL : .mgdL
+
+        let glucoseStats = calculateGlucoseStatistics()
+
+        // Determine the time range of the stored glucose data
+        let earliestDate = glucose.last?.date ?? Date()
+        let latestDate = glucose.first?.date ?? Date()
+        let totalDays = (latestDate - earliestDate).timeInterval / 86400
+
+        // Format glucose statistics based on the selected unit
+        let eA1cString = preferredUnit == .mmolL
+            ? glucoseStats.ifcc.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))
+            : glucoseStats.ngsp.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "%"
+
+        let gmiString = glucoseStats.gmi.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "%"
+
+        // glucoseStats already parsed to units - only format decimals
+        let standardDeviationString = units == .mgdL ? glucoseStats.sd.formatted(
+            .number.grouping(.never).rounded().precision(.fractionLength(0))
+        ) : glucoseStats.sd.formatted(
+            .number.grouping(.never).rounded().precision(.fractionLength(1))
+        )
+        let coefficientOfVariationString = glucoseStats.cv
+            .formatted(.number.grouping(.never).rounded().precision(.fractionLength(0)))
+        let daysTrackedString = totalDays.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))
+
+        VStack(alignment: .leading) {
+            HStack {
+                StatChartUtils.statView(title: String(localized: "eA1c"), value: eA1cString)
+                Spacer()
+                StatChartUtils.statView(title: String(localized: "GMI"), value: gmiString)
+                Spacer()
+                StatChartUtils.statView(title: String(localized: "SD"), value: standardDeviationString)
+                Spacer()
+                StatChartUtils.statView(title: String(localized: "CV"), value: coefficientOfVariationString)
+                Spacer()
+                StatChartUtils.statView(title: String(localized: "Days"), value: daysTrackedString)
+            }
+        }
+    }
+
+    /// Computes various statistical metrics from stored glucose readings, including:
+    /// - Estimated A1c in NGSP (%) and IFCC (mmol/mol)
+    /// - Glucose Management Index (GMI)
+    /// - Average and median glucose levels
+    /// - Standard deviation (SD) and coefficient of variation (CV)
+    /// - Number of readings per day
+    ///
+    /// - Returns: A tuple containing glucose statistics.
+    func calculateGlucoseStatistics() -> (
+        ifcc: Double, ngsp: Double, gmi: Double, average: Double,
+        median: Double, sd: Double, cv: Double, readingsPerDay: Double
+    ) {
+        // Determine the date range of the glucose data
+        let earliestDate = glucose.last?.date ?? Date()
+        let latestDate = glucose.first?.date ?? Date()
+        let totalDays = latestDate.timeIntervalSince(earliestDate) / 86400
+
+        // Ensure at least one day to avoid division by zero
+        let daysCount = max(totalDays, 1)
+
+        // Extract glucose values as an array of integers
+        let glucoseValues = glucose.compactMap { Int($0.glucose as Int16) }
+        let totalReadings = glucoseValues.count
+
+        // Handle empty dataset case
+        guard totalReadings > 1 else {
+            return (ifcc: 0, ngsp: 0, gmi: 0, average: 0, median: 0, sd: 0, cv: 0, readingsPerDay: 0)
+        }
+
+        let sumOfReadings = glucoseValues.reduce(0, +)
+        // Compute mean (average) glucose level
+        let meanGlucose = Double(sumOfReadings) / Double(totalReadings)
+        // Compute median glucose level
+        let medianGlucose = StatChartUtils.medianCalculation(array: glucoseValues)
+
+        // Estimated A1c and Glucose Management Index (GMI) calculations
+        var eA1cNGSP = 0.0 // eA1c NGSP (%)
+        var eA1cIFCC = 0.0 // eA1c IFCC (mmol/mol)
+        var gmiValue = 0.0 // Glucose Management Index (GMI)
+
+        if totalDays > 0 {
+            // **eA1c NGSP Calculation** (CGM-based)
+            // eA1c NGSP (%) = (Average Glucose mg/dL + 46.7) / 28.7
+            eA1cNGSP = (meanGlucose + 46.7) / 28.7
+
+            // **eA1c IFCC Calculation**
+            // eA1c IFCC (mmol/mol) = 10.929 * (eA1c NGSP - 2.152)
+            eA1cIFCC = 10.929 * (eA1cNGSP - 2.152)
+
+            // **Glucose Management Index (GMI)**
+            // GMI = 3.31 + (0.02392 × Average Glucose mg/dL)
+            gmiValue = 3.31 + (0.02392 * meanGlucose)
+        }
+
+        // Compute Standard Deviation (SD) and Coefficient of Variation (CV)
+        let sumOfSquaredDifferences = glucoseValues.reduce(0.0) { sum, value in
+            sum + pow(Double(value) - meanGlucose, 2)
+        }
+
+        let standardDeviation = sqrt(sumOfSquaredDifferences / Double(totalReadings - 1)) // Using N-1 for sample SD
+        let coefficientOfVariation = (meanGlucose > 0) ? (standardDeviation / meanGlucose) * 100 : 0.0
+
+        return (
+            ifcc: eA1cIFCC, // eA1c in IFCC (mmol/mol)
+            ngsp: eA1cNGSP, // eA1c in NGSP (%)
+            gmi: gmiValue, // Glucose Management Index
+            average: Double(units == .mgdL ? Decimal(meanGlucose) : meanGlucose.asMmolL),
+            median: Double(units == .mgdL ? Decimal(medianGlucose) : medianGlucose.asMmolL),
+            sd: Double(units == .mgdL ? Decimal(standardDeviation) : standardDeviation.asMmolL),
+            cv: coefficientOfVariation, // CV is already in percentage format
+            readingsPerDay: Double(totalReadings) / Double(daysCount)
+        )
+    }
+}

+ 242 - 0
Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucosePercentileChart.swift

@@ -0,0 +1,242 @@
+import Charts
+import SwiftUI
+
+/// A view that displays an Ambulatory Glucose Profile (AGP) chart.
+///
+/// This chart visualizes glucose percentile statistics over a 24-hour period.
+/// It includes the 10-90 percentile, 25-75 percentile, median glucose values,
+/// and high/low glucose limits.
+struct GlucosePercentileChart: View {
+    /// The list of stored glucose values.
+    let glucose: [GlucoseStored]
+    /// The upper glucose limit for the chart.
+    let highLimit: Decimal
+    /// The lower glucose limit for the chart.
+    let lowLimit: Decimal
+    /// The units used for glucose measurement (mg/dL or mmol/L).
+    let units: GlucoseUnits
+    /// The hourly glucose statistics.
+    let hourlyStats: [HourlyStats]
+    /// Flag indicating whether the chart represents today's data.
+    let isToday: Bool
+
+    /// The currently selected hour in the chart.
+    @State private var selection: Date? = nil
+
+    /// Retrieves the hourly statistics for the selected time.
+    private var selectedStats: HourlyStats? {
+        guard let selection = selection else { return nil }
+
+        if isToday && selection > Date() {
+            return nil
+        }
+
+        let calendar = Calendar.current
+        let hour = calendar.component(.hour, from: selection)
+        return hourlyStats.first { Int($0.hour) == hour }
+    }
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 8) {
+            Text("Ambulatory Glucose Profile (AGP)")
+                .font(.headline)
+
+            Chart {
+                // Statistical view for longer periods
+                ForEach(hourlyStats, id: \.hour) { stats in
+                    // 10-90 percentile area
+                    AreaMark(
+                        x: .value("Hour", Calendar.current.dateForChartHour(stats.hour)),
+                        yStart: .value("10th Percentile", stats.percentile10),
+                        yEnd: .value("90th Percentile", stats.percentile90),
+                        series: .value("10-90", "10-90")
+                    )
+                    .foregroundStyle(by: .value("Series", "10-90"))
+                    .opacity(stats.median > 0 ? 0.3 : 0)
+
+                    // 25-75 percentile area
+                    AreaMark(
+                        x: .value("Hour", Calendar.current.dateForChartHour(stats.hour)),
+                        yStart: .value("25th Percentile", stats.percentile25),
+                        yEnd: .value("75th Percentile", stats.percentile75),
+                        series: .value("25-75", "25-75")
+                    )
+                    .foregroundStyle(by: .value("Series", "25-75"))
+                    .opacity(stats.median > 0 ? 0.5 : 0)
+
+                    // Median line
+                    if stats.median > 0 {
+                        LineMark(
+                            x: .value("Hour", Calendar.current.dateForChartHour(stats.hour)),
+                            y: .value("Median", stats.median),
+                            series: .value("Median", "Median")
+                        )
+                        .lineStyle(StrokeStyle(lineWidth: 2))
+                        .foregroundStyle(by: .value("Series", "Median"))
+                    }
+                }
+
+                // High/Low limit lines
+                RuleMark(y: .value("High Limit", Double(highLimit)))
+                    .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 5]))
+                    .foregroundStyle(by: .value("Series", "High"))
+
+                RuleMark(y: .value("Low Limit", Double(lowLimit)))
+                    .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 5]))
+                    .foregroundStyle(by: .value("Series", "Low"))
+
+                if let selectedStats, let selection {
+                    RuleMark(x: .value("Selection", selection))
+                        .foregroundStyle(Color.blue.opacity(0.5))
+                        .annotation(
+                            position: .top,
+                            spacing: 0,
+                            overflowResolution: .init(x: .fit, y: .disabled)
+                        ) {
+                            AGPSelectionPopover(
+                                stats: selectedStats,
+                                time: selection,
+                                units: units
+                            )
+                        }
+                }
+            }
+            .chartForegroundStyleScale([
+                "10-90": Color.blue.opacity(0.3),
+                "25-75": Color.blue.opacity(0.5),
+                "Median": Color.blue,
+                "High": Color.orange,
+                "Low": Color.red
+            ])
+            .chartLegend(position: .bottom, alignment: .leading, spacing: 12) {
+                let legendItems: [(String, Color)] = [
+                    ("10-90%", Color.blue.opacity(0.3)),
+                    ("20-75%", Color.blue.opacity(0.5)),
+                    (String(localized: "Median"), Color.blue),
+                    (String(localized: "High Threshold"), Color.orange),
+                    (String(localized: "Low Threshold"), Color.red)
+                ]
+
+                let columns = [GridItem(.adaptive(minimum: 100), spacing: 4)]
+
+                LazyVGrid(columns: columns, alignment: .leading, spacing: 4) {
+                    ForEach(legendItems, id: \.0) { item in
+                        StatChartUtils.legendItem(label: item.0, color: item.1)
+                    }
+                }
+            }
+            .chartYAxis {
+                AxisMarks(position: .trailing) { value in
+                    if let glucose = value.as(Double.self) {
+                        AxisValueLabel {
+                            Text(
+                                units == .mmolL ? glucose.asMmolL.formatted(.number.precision(.fractionLength(0))) : glucose
+                                    .formatted(.number.precision(.fractionLength(0)))
+                            )
+                            .font(.footnote)
+                        }
+                        AxisGridLine()
+                    }
+                }
+            }
+            .chartYAxisLabel(alignment: .trailing) {
+                Text("\(units.rawValue)")
+                    .foregroundStyle(.primary)
+                    .font(.footnote)
+                    .padding(.vertical, 3)
+            }
+            .chartXAxis {
+                AxisMarks(values: .stride(by: .hour, count: 3)) { value in
+                    if let date = value.as(Date.self) {
+                        let hour = Calendar.current.component(.hour, from: date)
+                        switch hour {
+                        case 0,
+                             12:
+                            AxisValueLabel(format: .dateTime.hour())
+                        default:
+                            AxisValueLabel(format: .dateTime.hour(.defaultDigits(amPM: .omitted)))
+                        }
+
+                        AxisGridLine()
+                    }
+                }
+            }
+            .chartXSelection(value: $selection.animation(.easeInOut))
+            .frame(height: 200)
+        }
+    }
+}
+
+/// A popover view displaying detailed glucose statistics for a selected time.
+struct AGPSelectionPopover: View {
+    let stats: HourlyStats
+    let time: Date
+    let units: GlucoseUnits
+
+    @Environment(\.colorScheme) var colorScheme
+
+    private var timeText: String {
+        if let hour = Calendar.current.dateComponents([.hour], from: time).hour {
+            return "\(hour):00-\(hour + 1):00"
+        } else {
+            return time.formatted(.dateTime.hour().minute())
+        }
+    }
+
+    /// A helper function to format glucose values based on the selected unit.
+    private func formattedGlucoseValue(_ value: Double) -> String {
+        units == .mmolL ? value.formattedAsMmolL :
+            value.formatted()
+    }
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 4) {
+            Text(timeText).bold().font(.subheadline)
+
+            Grid(alignment: .leading, horizontalSpacing: 8, verticalSpacing: 4) {
+                GridRow {
+                    Text("Median:").bold()
+                    Text(formattedGlucoseValue(stats.median))
+                    Text(units.rawValue).foregroundStyle(.secondary)
+                }
+                GridRow {
+                    Text("90%:").bold()
+                    Text(formattedGlucoseValue(stats.percentile90))
+                    Text(units.rawValue).foregroundStyle(.secondary)
+                }
+                GridRow {
+                    Text("75%:").bold()
+                    Text(formattedGlucoseValue(stats.percentile75))
+                    Text(units.rawValue).foregroundStyle(.secondary)
+                }
+                GridRow {
+                    Text("25%:").bold()
+                    Text(formattedGlucoseValue(stats.percentile25))
+                    Text(units.rawValue).foregroundStyle(.secondary)
+                }
+                GridRow {
+                    Text("10%:").bold()
+                    Text(formattedGlucoseValue(stats.percentile10))
+                    Text(units.rawValue).foregroundStyle(.secondary)
+                }
+            }.font(.headline)
+        }
+        .padding(20)
+        .background {
+            RoundedRectangle(cornerRadius: 10)
+                .fill(colorScheme == .dark ? Color.bgDarkBlue.opacity(0.9) : Color.white.opacity(0.95))
+                .shadow(color: Color.secondary, radius: 2)
+                .overlay(
+                    RoundedRectangle(cornerRadius: 4)
+                        .stroke(Color.blue, lineWidth: 2)
+                )
+        }
+    }
+}
+
+private extension Calendar {
+    func startOfHour(for date: Date) -> Date {
+        let components = dateComponents([.year, .month, .day, .hour], from: date)
+        return self.date(from: components) ?? date
+    }
+}

+ 374 - 0
Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseSectorChart.swift

@@ -0,0 +1,374 @@
+import Charts
+import CoreData
+import SwiftDate
+import SwiftUI
+
+struct GlucoseSectorChart: View {
+    let highLimit: Decimal
+    let lowLimit: Decimal
+    let units: GlucoseUnits
+    let glucose: [GlucoseStored]
+
+    @State private var selectedCount: Int?
+    @State private var selectedRange: GlucoseRange?
+
+    /// Represents the different ranges of glucose values that can be displayed in the sector chart
+    /// - high: Above target range
+    /// - inRange: Within target range
+    /// - low: Below target range
+    private enum GlucoseRange: String, Plottable {
+        case high = "High"
+        case inRange = "In Range"
+        case low = "Low"
+    }
+
+    var body: some View {
+        HStack(alignment: .center, spacing: 20) {
+            // Calculate total number of glucose readings
+            let total = Decimal(glucose.count)
+            // Count readings between high limit and 250 mg/dL (high)
+            let high = glucose.filter { $0.glucose > Int(highLimit) }.count
+            // Count readings between low limit and 140 mg/dL (tight control)
+            let tight = glucose.filter { $0.glucose >= Int(lowLimit) && $0.glucose <= 140 }.count
+            // Count readings between 140 and high limit (normal range)
+            let normal = glucose.filter { $0.glucose >= Int(lowLimit) && $0.glucose <= Int(highLimit) }.count
+            // Count readings between 54 and low limit (low)
+            let low = glucose.filter { $0.glucose < Int(lowLimit) }.count
+
+            let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
+            let sumReadings = justGlucoseArray.reduce(0, +)
+
+            let glucoseAverage = Decimal(sumReadings) / total
+            let medianGlucose = StatChartUtils.medianCalculation(array: justGlucoseArray)
+
+            let lowPercentage = Decimal(low) / total * 100
+            let tightPercentage = Decimal(tight) / total * 100
+            let inRangePercentage = Decimal(normal) / total * 100
+            let highPercentage = Decimal(high) / total * 100
+
+            VStack(alignment: .leading, spacing: 10) {
+                VStack(alignment: .leading, spacing: 5) {
+                    Text("\(formatValue(lowLimit))-\(formatValue(highLimit))").font(.subheadline).foregroundStyle(Color.secondary)
+                    Text(inRangePercentage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "%")
+                        .foregroundStyle(Color.loopGreen)
+                }
+
+                VStack(alignment: .leading, spacing: 5) {
+                    Text("\(formatValue(lowLimit))-\(formatValue(140))").font(.subheadline).foregroundStyle(Color.secondary)
+                    Text(tightPercentage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "%")
+                        .foregroundStyle(Color.green)
+                }
+            }.padding(.leading, 5)
+
+            VStack(alignment: .leading, spacing: 10) {
+                VStack(alignment: .leading, spacing: 5) {
+                    Text("> \(formatValue(highLimit))").font(.subheadline).foregroundStyle(Color.secondary)
+                    Text(highPercentage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "%")
+                        .foregroundStyle(Color.orange)
+                }
+
+                VStack(alignment: .leading, spacing: 5) {
+                    Text("< \(formatValue(lowLimit))").font(.subheadline).foregroundStyle(Color.secondary)
+                    Text(lowPercentage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "%")
+                        .foregroundStyle(Color.loopRed)
+                }
+            }
+
+            VStack(alignment: .leading, spacing: 10) {
+                VStack(alignment: .leading, spacing: 5) {
+                    Text("Average").font(.subheadline).foregroundStyle(Color.secondary)
+                    Text(
+                        units == .mgdL ? glucoseAverage
+                            .formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))) : glucoseAverage.asMmolL
+                            .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))
+                    )
+                }
+
+                VStack(alignment: .leading, spacing: 5) {
+                    Text("Median").font(.subheadline).foregroundStyle(Color.secondary)
+                    Text(
+                        units == .mgdL ? medianGlucose
+                            .formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))) : medianGlucose.asMmolL
+                            .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))
+                    )
+                }
+            }
+
+            Chart {
+                ForEach(rangeData, id: \.range) { data in
+                    SectorMark(
+                        angle: .value("Percentage", data.count),
+                        innerRadius: .ratio(0.618),
+                        outerRadius: selectedRange == data.range ? 100 : 80,
+                        angularInset: 1.5
+                    )
+                    .foregroundStyle(data.color)
+                }
+            }
+            .chartAngleSelection(value: $selectedCount)
+            .frame(height: 100)
+        }
+        .onChange(of: selectedCount) { _, newValue in
+            if let newValue {
+                withAnimation {
+                    getSelectedRange(value: newValue)
+                }
+            } else {
+                withAnimation {
+                    selectedRange = nil
+                }
+            }
+        }
+        .overlay(alignment: .top) {
+            if let selectedRange {
+                let data = getDetailedData(for: selectedRange)
+                RangeDetailPopover(data: data)
+                    .transition(.scale.combined(with: .opacity))
+                    .offset(y: -150) // TODO: make this dynamic
+            }
+        }
+    }
+
+    /// Calculates statistics about glucose ranges and returns data for the sector chart
+    ///
+    /// This computed property processes glucose readings and categorizes them into high, in-range, and low ranges.
+    /// For each range, it calculates:
+    /// - The count of readings in that range
+    /// - The percentage of total readings
+    /// - The associated color for visualization
+    ///
+    /// - Returns: An array of tuples containing range data, where each tuple has:
+    ///   - range: The glucose range category (high, in-range, or low)
+    ///   - count: Number of readings in that range
+    ///   - percentage: Percentage of total readings in that range
+    ///   - color: Color used to represent that range in the chart
+    private var rangeData: [(range: GlucoseRange, count: Int, percentage: Decimal, color: Color)] {
+        let total = glucose.count
+        // Return empty array if no glucose readings available
+        guard total > 0 else { return [] }
+
+        // Count readings above high limit
+        let highCount = glucose.filter { $0.glucose > Int(highLimit) }.count
+        // Count readings below low limit
+        let lowCount = glucose.filter { $0.glucose < Int(lowLimit) }.count
+        // Calculate in-range readings by subtracting high and low counts from total
+        let inRangeCount = total - highCount - lowCount
+
+        // Return array of tuples with range data
+        return [
+            (.high, highCount, Decimal(highCount) / Decimal(total) * 100, .orange),
+            (.inRange, inRangeCount, Decimal(inRangeCount) / Decimal(total) * 100, .green),
+            (.low, lowCount, Decimal(lowCount) / Decimal(total) * 100, .red)
+        ]
+    }
+
+    /// Determines which glucose range was selected based on a cumulative value
+    ///
+    /// This function takes a value representing a point in the cumulative total of glucose readings
+    /// and determines which range (high, in-range, or low) that point falls into.
+    /// It updates the selectedRange state variable when the appropriate range is found.
+    ///
+    /// - Parameter value: An integer representing a point in the cumulative total of readings
+    private func getSelectedRange(value: Int) {
+        // Keep track of running total as we check each range
+        var cumulativeTotal = 0
+
+        // Find first range where value falls within its cumulative count
+        _ = rangeData.first { data in
+            cumulativeTotal += data.count
+            if value <= cumulativeTotal {
+                selectedRange = data.range
+                return true
+            }
+            return false
+        }
+    }
+
+    /// Gets detailed statistics for a specific glucose range category
+    ///
+    /// This function calculates detailed statistics for a given glucose range (high, in-range, or low),
+    /// breaking down the readings into subcategories and calculating percentages.
+    ///
+    /// - Parameter range: The glucose range category to analyze
+    /// - Returns: A RangeDetail object containing the title, color and detailed statistics
+    private func getDetailedData(for range: GlucoseRange) -> RangeDetail {
+        let total = Decimal(glucose.count)
+
+        switch range {
+        case .high:
+            let veryHigh = glucose.filter { $0.glucose > 250 }.count
+            let high = glucose.filter { $0.glucose > Int(highLimit) && $0.glucose <= 250 }.count
+
+            let highGlucoseValues = glucose.filter { $0.glucose > Int(highLimit) }
+            let highGlucoseValuesAsInt = highGlucoseValues.map { Int($0.glucose) }
+            let (average, median, standardDeviation) = calculateDetailedStatistics(for: highGlucoseValuesAsInt)
+
+            return RangeDetail(
+                title: String(localized: "High Glucose"),
+                color: .orange,
+                items: [
+                    (String(localized: "Very High (>\(formatValue(250)))"), formatPercentage(Decimal(veryHigh) / total * 100)),
+                    (
+                        String(localized: "High (\(formatValue(highLimit))-\(formatValue(250)))"),
+                        formatPercentage(Decimal(high) / total * 100)
+                    ),
+                    (String(localized: "Average"), formatValue(average)),
+                    (String(localized: "Median"), formatValue(median)),
+                    (String(localized: "SD"), formatSD(standardDeviation))
+                ]
+            )
+
+        case .inRange:
+            let tight = glucose.filter { $0.glucose >= Int(lowLimit) && $0.glucose <= 140 }.count
+            let glucoseValues = glucose.filter { $0.glucose >= Int(lowLimit) && $0.glucose <= Int(highLimit) }
+            let glucoseValuesAsInt = glucoseValues.map { Int($0.glucose) }
+            let (average, median, standardDeviation) = calculateDetailedStatistics(for: glucoseValuesAsInt)
+
+            return RangeDetail(
+                title: String(localized: "In Range"),
+                color: .green,
+                items: [
+                    (
+                        String(localized: "Normal (\(formatValue(lowLimit))-\(formatValue(highLimit)))"),
+                        formatPercentage(Decimal(glucoseValues.count) / total * 100)
+                    ),
+                    (
+                        String(localized: "Tight (\(formatValue(lowLimit))-\(formatValue(140)))"),
+                        formatPercentage(Decimal(tight) / total * 100)
+                    ),
+                    (String(localized: "Average"), formatValue(average)),
+                    (String(localized: "Median"), formatValue(median)),
+                    (String(localized: "SD"), formatSD(standardDeviation))
+                ]
+            )
+
+        case .low:
+            let veryLow = glucose.filter { $0.glucose <= 54 }.count
+            let low = glucose.filter { $0.glucose > 54 && $0.glucose < Int(lowLimit) }.count
+
+            let lowGlucoseValues = glucose.filter { $0.glucose < Int(lowLimit) }
+            let lowGlucoseValuesAsInt = lowGlucoseValues.map { Int($0.glucose) }
+            let (average, median, standardDeviation) = calculateDetailedStatistics(for: lowGlucoseValuesAsInt)
+
+            return RangeDetail(
+                title: String(localized: "Low Glucose"),
+                color: .red,
+                items: [
+                    (
+                        String(localized: "Low (\(formatValue(54))-\(formatValue(lowLimit)))"),
+                        formatPercentage(Decimal(low) / total * 100)
+                    ),
+                    (String(localized: "Very Low (<\(formatValue(54)))"), formatPercentage(Decimal(veryLow) / total * 100)),
+                    (String(localized: "Average"), formatValue(average)),
+                    (String(localized: "Median"), formatValue(median)),
+                    (String(localized: "SD"), formatSD(standardDeviation))
+                ]
+            )
+        }
+    }
+
+    /// Formats a percentage value to a string with one decimal place.
+    /// - Parameter value: A decimal value representing the percentage.
+    /// - Returns: A formatted percentage string
+    private func formatPercentage(_ value: Decimal) -> String {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .percent
+        formatter.maximumFractionDigits = 1
+        return formatter.string(from: NSDecimalNumber(decimal: value / 100)) ?? "0%"
+    }
+
+    /// Calculates statistical values for a given array of glucose readings.
+    /// - Parameter values: An array of glucose readings as integers.
+    /// - Returns: A tuple containing the average, median, and standard deviation.
+    private func calculateDetailedStatistics(for values: [Int]) -> (Decimal, Decimal, Double) {
+        guard !values.isEmpty else { return (0, 0, 0) }
+
+        let total = values.reduce(0, +)
+        let average = Decimal(total / values.count)
+        let median = Decimal(StatChartUtils.medianCalculation(array: values))
+
+        let sumOfSquares = values.reduce(0.0) { sum, value in
+            sum + pow(Double(value) - Double(average), 2)
+        }
+
+        let standardDeviation = sqrt(sumOfSquares / Double(values.count))
+        return (average, median, standardDeviation)
+    }
+
+    /// Formats the standard deviation value based on glucose units.
+    /// - Parameter sd: The standard deviation as a Double.
+    /// - Returns: A formatted string representing the standard deviation.
+    private func formatSD(_ sd: Double) -> String {
+        units == .mgdL ? sd.formatted(
+            .number.grouping(.never).rounded().precision(.fractionLength(0))
+        ) : sd.formattedAsMmolL
+    }
+
+    /// Formats a glucose value based on the current units.
+    /// - Parameter value: A decimal value representing the glucose level.
+    /// - Returns: A formatted string of the glucose value.
+    private func formatValue(_ value: Decimal) -> String {
+        units == .mgdL ? value.description : value.formattedAsMmolL
+    }
+}
+
+/// Represents details about a specific glucose range category including title, color and percentage breakdowns
+private struct RangeDetail {
+    /// The title of this range category (e.g. "High Glucose", "In Range", "Low Glucose")
+    let title: String
+    /// The color used to represent this range in the UI
+    let color: Color
+    /// Array of tuples containing label and percentage for each sub-range
+    let items: [(label: String, value: String)]
+}
+
+/// A popover view that displays detailed breakdown of glucose percentages for a range category
+private struct RangeDetailPopover: View {
+    let data: RangeDetail
+
+    @Environment(\.colorScheme) var colorScheme
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 8) {
+            Text(data.title)
+                .font(.subheadline)
+                .fontWeight(.bold)
+                .foregroundStyle(data.color)
+                .padding(.bottom, 4)
+
+            ForEach(Array(data.items.enumerated()), id: \..offset) { index, item in
+                if index < 2 {
+                    HStack {
+                        Text(item.label)
+                        Text(item.value).bold()
+                    }
+                    .font(.footnote)
+                }
+            }
+
+            HStack(spacing: 20) {
+                ForEach(Array(data.items.enumerated()), id: \..offset) { index, item in
+                    if index > 1 {
+                        VStack(alignment: .leading, spacing: 5) {
+                            Text(item.label)
+                            HStack {
+                                Text(item.value).bold()
+                            }
+                        }
+                        .font(.footnote)
+                    }
+                }
+            }
+        }
+        .padding(20)
+        .background {
+            RoundedRectangle(cornerRadius: 10)
+                .fill(colorScheme == .dark ? Color.bgDarkBlue.opacity(0.9) : Color.white.opacity(0.95))
+                .shadow(color: Color.secondary, radius: 2)
+                .overlay(
+                    RoundedRectangle(cornerRadius: 4)
+                        .stroke(data.color, lineWidth: 2)
+                )
+        }
+    }
+}

+ 411 - 0
Trio/Sources/Modules/Stat/View/ViewElements/Insulin/BolusStatsView.swift

@@ -0,0 +1,411 @@
+import Charts
+import SwiftUI
+
+/// A view that displays a bar chart for bolus insulin statistics.
+///
+/// This view presents different types of bolus insulin (manual, SMB, and external) over time,
+/// allowing users to adjust the time interval and scroll through historical data.
+struct BolusStatsView: View {
+    /// The selected time interval for displaying statistics.
+    @Binding var selectedInterval: Stat.StateModel.StatsTimeInterval
+    /// The list of bolus statistics data.
+    let bolusStats: [BolusStats]
+    /// The state model containing cached statistics data.
+    let state: Stat.StateModel
+
+    /// The current scroll position in the chart.
+    @State private var scrollPosition = Date()
+    /// The currently selected date in the chart.
+    @State private var selectedDate: Date?
+    /// The calculated bolus insulin averages for the visible range.
+    @State private var currentAverages: (manual: Double, smb: Double, external: Double) = (0, 0, 0)
+    /// The calculated total bolus insulin for the visible range.
+    @State private var currentTotal: Double = 0
+    /// Timer to throttle updates when scrolling.
+    @State private var updateTimer = Stat.UpdateTimer()
+    /// The actual chart plot's width in pixel
+    @State private var chartWidth: CGFloat = 0
+
+    /// Computes the visible date range based on the current scroll position.
+    private var visibleDateRange: (start: Date, end: Date) {
+        StatChartUtils.visibleDateRange(from: scrollPosition, for: selectedInterval)
+    }
+
+    /// Retrieves the bolus statistic for a given date.
+    /// - Parameter date: The date for which to retrieve bolus data.
+    /// - Returns: The `BolusStats` object if available, otherwise `nil`.
+    private func getBolusForDate(_ date: Date) -> BolusStats? {
+        bolusStats.first { stat in
+            StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedInterval)
+        }
+    }
+
+    /// Updates the bolus insulin averages based on the visible date range.
+    private func updateCalculatedValues() {
+        currentAverages = state.getCachedBolusAverages(for: visibleDateRange)
+        currentTotal = state.getCachedBolusTotals(for: visibleDateRange)
+    }
+
+    /// A view displaying the statistics summary including bolus insulin averages.
+    private var statsView: some View {
+        HStack {
+            Grid(alignment: .leading) {
+                GridRow {
+                    if selectedInterval != .day {
+                        Text("ø") + Text("\u{00A0}") + Text("Manual:")
+                    } else {
+                        Text("Manual:")
+                    }
+                    Text(currentAverages.manual.formatted(.number.precision(.fractionLength(1))))
+                        + Text("\u{00A0}") + Text("U")
+                }
+                GridRow {
+                    if selectedInterval != .day {
+                        Text("ø") + Text("\u{00A0}") + Text("SMB:")
+                    } else {
+                        Text("SMB:")
+                    }
+                    Text(currentAverages.smb.formatted(.number.precision(.fractionLength(1))))
+                        + Text("\u{00A0}") + Text("U")
+                }
+                GridRow {
+                    if selectedInterval != .day {
+                        Text("ø") + Text("\u{00A0}") + Text("External:")
+                    } else {
+                        Text("External:")
+                    }
+                    Text(currentAverages.external.formatted(.number.precision(.fractionLength(1))))
+                        + Text("\u{00A0}") + Text("U")
+                }
+                Divider()
+                GridRow {
+                    Text("Total:")
+                    Text(
+                        currentTotal.formatted(.number.precision(.fractionLength(1)))
+                    )
+                        + Text("\u{00A0}") + Text("U")
+                }
+            }
+            .font(.headline)
+
+            Spacer()
+
+            Text(
+                StatChartUtils
+                    .formatVisibleDateRange(from: visibleDateRange.start, to: visibleDateRange.end, for: selectedInterval)
+            )
+            .font(.callout)
+            .foregroundStyle(.secondary)
+        }
+    }
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 8) {
+            statsView.padding(.bottom)
+
+            VStack(alignment: .trailing) {
+                Text("Bolus Insulin (U)")
+                    .foregroundStyle(.secondary)
+                    .font(.footnote)
+                    .padding(.bottom, 4)
+
+                chartsView
+                    .background(
+                        GeometryReader { geo in
+                            Color.clear
+                                .onAppear { chartWidth = geo.size.width }
+                                .onChange(of: geo.size.width) { _, newValue in chartWidth = newValue }
+                        }
+                    )
+            }
+        }
+        .onAppear {
+            scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedInterval)
+            updateCalculatedValues()
+        }
+        .onChange(of: scrollPosition) {
+            updateTimer.scheduleUpdate {
+                updateCalculatedValues()
+            }
+        }
+        .onChange(of: selectedInterval) {
+            Task {
+                scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedInterval)
+                updateCalculatedValues()
+            }
+        }
+    }
+
+    /// A view displaying the bar chart for bolus insulin statistics.
+    private var chartsView: some View {
+        Chart {
+            ForEach(bolusStats) { stat in
+                // Total Bolus Bar
+                BarMark(
+                    x: .value("Date", stat.date, unit: selectedInterval == .day ? .hour : .day),
+                    y: .value("Amount", stat.manualBolus)
+                )
+                .foregroundStyle(by: .value("Type", "Manual"))
+                .position(by: .value("Type", "Boluses"))
+                .opacity(
+                    selectedDate.map { date in
+                        StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedInterval) ? 1 : 0.3
+                    } ?? 1
+                )
+
+                // Carb Bolus Bar
+                BarMark(
+                    x: .value("Date", stat.date, unit: selectedInterval == .day ? .hour : .day),
+                    y: .value("Amount", stat.smb)
+                )
+                .foregroundStyle(by: .value("Type", "SMB"))
+                .position(by: .value("Type", "Boluses"))
+                .opacity(
+                    selectedDate.map { date in
+                        StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedInterval) ? 1 : 0.3
+                    } ?? 1
+                )
+                // Correction Bolus Bar
+                BarMark(
+                    x: .value("Date", stat.date, unit: selectedInterval == .day ? .hour : .day),
+                    y: .value("Amount", stat.external)
+                )
+                .foregroundStyle(by: .value("Type", "External"))
+                .position(by: .value("Type", "Boluses"))
+                .opacity(
+                    selectedDate.map { date in
+                        StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedInterval) ? 1 : 0.3
+                    } ?? 1
+                )
+            }
+
+            // Dummy PointMark to force SwiftCharts to render a visible domain of 00:00-23:59
+            // i.e. single day from midnight to midnight
+            if selectedInterval == .day {
+                let calendar = Calendar.current
+                let midnight = calendar.startOfDay(for: Date())
+                let nextMidnight = calendar.date(byAdding: .day, value: 1, to: midnight)!
+
+                PointMark(
+                    x: .value("Time", nextMidnight),
+                    y: .value("Dummy", 0)
+                )
+                .opacity(0) // ensures dummy ChartContent is hidden
+            }
+
+            // Selection popover outside of the ForEach loop!
+            if let selectedDate, let selectedBolus = getBolusForDate(selectedDate)
+            {
+                RuleMark(
+                    x: .value("Selected Date", selectedDate)
+                )
+                .foregroundStyle(Color.insulin.opacity(0.5))
+                .annotation(
+                    position: .overlay,
+                    alignment: .top,
+                    spacing: 0,
+                    overflowResolution: .init(x: .fit(to: .chart), y: .fit(to: .chart))
+                ) { _ in
+                    BolusSelectionPopover(
+                        selectedDate: selectedDate,
+                        bolus: selectedBolus,
+                        selectedInterval: selectedInterval,
+                        domain: visibleDateRange,
+                        chartWidth: chartWidth
+                    )
+                }
+            }
+        }
+        .chartForegroundStyleScale([
+            "SMB": Color.blue,
+            "Manual": Color.teal,
+            "External": Color.purple
+        ])
+        .chartLegend(position: .bottom, alignment: .leading, spacing: 12) {
+            let legendItems: [(String, Color)] = [
+                (String(localized: "SMB"), Color.blue),
+                (String(localized: "Manual"), Color.teal),
+                (String(localized: "External"), Color.purple)
+            ]
+
+            let columns = [GridItem(.adaptive(minimum: 65), spacing: 4)]
+
+            LazyVGrid(columns: columns, alignment: .leading, spacing: 4) {
+                ForEach(legendItems, id: \.0) { item in
+                    StatChartUtils.legendItem(label: item.0, color: item.1)
+                }
+            }
+        }
+        .chartYAxis {
+            AxisMarks(position: .trailing) { value in
+                if let amount = value.as(Double.self) {
+                    AxisValueLabel {
+                        Text(amount.formatted(.number.precision(.fractionLength(0))))
+                            .font(.footnote)
+                    }
+                    AxisGridLine()
+                }
+            }
+        }
+        .chartXAxis {
+            AxisMarks(preset: .aligned, values: .stride(by: selectedInterval == .day ? .hour : .day)) { value in
+                if let date = value.as(Date.self) {
+                    let day = Calendar.current.component(.day, from: date)
+                    let hour = Calendar.current.component(.hour, from: date)
+
+                    switch selectedInterval {
+                    case .day:
+                        if hour % 6 == 0 { // Show only every 6 hours
+                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
+                                .font(.footnote)
+                            AxisGridLine()
+                        }
+                    case .month:
+                        if day % 3 == 0 { // Only show every 3rd day
+                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
+                                .font(.footnote)
+                            AxisGridLine()
+                        }
+                    case .total:
+                        // Only show every other month
+                        if day == 1 && Calendar.current.component(.month, from: date) % 2 == 1 {
+                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
+                                .font(.footnote)
+                            AxisGridLine()
+                        }
+                    default:
+                        AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
+                            .font(.footnote)
+                        AxisGridLine()
+                    }
+                }
+            }
+        }
+        .chartXSelection(value: $selectedDate.animation(.easeInOut))
+        .chartScrollableAxes(.horizontal)
+        .chartScrollPosition(x: $scrollPosition)
+        .chartScrollTargetBehavior(
+            .valueAligned(
+                matching:
+                selectedInterval == .day ?
+                    DateComponents(minute: 0) : // Align to next hour for Day view
+                    DateComponents(hour: 0), // Align to start of day for other views
+                majorAlignment: .matching(
+                    StatChartUtils.alignmentComponents(for: selectedInterval)
+                )
+            )
+        )
+        .chartXVisibleDomain(length: StatChartUtils.visibleDomainLength(for: selectedInterval))
+        .frame(height: 280)
+    }
+}
+
+private struct BolusSelectionPopover: View {
+    let selectedDate: Date
+    let bolus: BolusStats
+    let selectedInterval: Stat.StateModel.StatsTimeInterval
+    let domain: (start: Date, end: Date)
+    let chartWidth: CGFloat
+
+    @State private var popoverSize: CGSize = .zero
+
+    @Environment(\.colorScheme) var colorScheme
+
+    private var timeText: String {
+        if selectedInterval == .day {
+            let hour = Calendar.current.component(.hour, from: selectedDate)
+            return selectedDate.formatted(.dateTime.month().day().weekday()) + "\n" + "\(hour):00-\(hour + 1):00"
+        } else {
+            return selectedDate.formatted(.dateTime.month().day().weekday())
+        }
+    }
+
+    private func xOffset() -> CGFloat {
+        let domainDuration = domain.end.timeIntervalSince(domain.start)
+        guard domainDuration > 0, chartWidth > 0 else { return 0 }
+
+        let popoverWidth = popoverSize.width
+
+        // Convert dates to pixel'd x-condition
+        let dateFraction = selectedDate.timeIntervalSince(domain.start) / domainDuration
+        let x_selected = dateFraction * chartWidth
+
+        // TODO: this is semi hacky, can this be improved?
+        let x_left = x_selected - (popoverWidth / 2) // Left edge of popover
+        let x_right = x_selected + (popoverWidth / 2) // Right edge of popover
+
+        var offset: CGFloat = 0 // Default = no shift
+
+        // Push popover to right if its left edge is (nearing) out-of-bounds
+        if x_left < 0 {
+            offset = abs(x_left) // push to right
+        }
+
+        // Push popover to left if its right edge is (nearing) out-of-bounds)
+        if x_right > chartWidth {
+            offset = -(x_right - chartWidth) // push to left
+        }
+
+        return offset
+    }
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 4) {
+            Text(timeText)
+                .font(.subheadline)
+                .bold()
+                .foregroundStyle(Color.secondary)
+
+            Grid(alignment: .leading) {
+                Divider()
+                GridRow {
+                    Text("Manual:")
+                    Text(bolus.manualBolus.formatted(.number.precision(.fractionLength(1))))
+                        .gridColumnAlignment(.trailing).bold()
+                    Text("U").foregroundStyle(Color.secondary)
+                }
+                GridRow {
+                    Text("SMB:")
+                    Text(bolus.smb.formatted(.number.precision(.fractionLength(1))))
+                        .gridColumnAlignment(.trailing).bold()
+                    Text("U").foregroundStyle(Color.secondary)
+                }
+                GridRow {
+                    Text("External:")
+                    Text(bolus.external.formatted(.number.precision(.fractionLength(1))))
+                        .gridColumnAlignment(.trailing).bold()
+                    Text("U").foregroundStyle(Color.secondary)
+                }
+                Divider()
+                GridRow {
+                    Text("Total:")
+                    Text(
+                        (bolus.manualBolus + bolus.smb + bolus.external).formatted(.number.precision(.fractionLength(1)))
+                    ).bold()
+                    Text("U").foregroundStyle(Color.secondary)
+                }
+            }
+            .font(.headline)
+        }
+        .padding(20)
+        .background {
+            RoundedRectangle(cornerRadius: 10)
+                .fill(colorScheme == .dark ? Color.bgDarkBlue.opacity(0.9) : Color.white.opacity(0.95))
+                .shadow(color: Color.secondary, radius: 2)
+                .overlay(
+                    RoundedRectangle(cornerRadius: 4)
+                        .stroke(Color.blue, lineWidth: 2)
+                )
+        }
+        .frame(minWidth: 180, maxWidth: .infinity) // Ensures proper width
+        .background(
+            GeometryReader { geo in
+                Color.clear
+                    .onAppear { popoverSize = geo.size }
+                    .onChange(of: geo.size) { _, newValue in popoverSize = newValue }
+            }
+        )
+        // Apply calculated xOffset to keep within bounds
+        .offset(x: xOffset(), y: 0)
+    }
+}

+ 342 - 0
Trio/Sources/Modules/Stat/View/ViewElements/Insulin/TotalDailyDoseChart.swift

@@ -0,0 +1,342 @@
+import Charts
+import SwiftUI
+
+/// A view that displays a bar chart for Total Daily Dose (TDD) statistics.
+///
+/// This view presents insulin usage over time, with the ability to adjust the time interval
+/// and scroll through historical data.
+struct TotalDailyDoseChart: View {
+    /// The selected time interval for displaying statistics.
+    @Binding var selectedInterval: Stat.StateModel.StatsTimeInterval
+    /// The list of TDD statistics data.
+    let tddStats: [TDDStats]
+    /// The state model containing cached statistics data.
+    let state: Stat.StateModel
+
+    /// The current scroll position in the chart.
+    @State private var scrollPosition = Date()
+    /// The currently selected date in the chart.
+    @State private var selectedDate: Date?
+    /// The calculated average TDD for the visible range.
+    @State private var currentAverage: Double = 0
+    /// Timer to throttle updates when scrolling.
+    @State private var updateTimer = Stat.UpdateTimer()
+    /// Sum of hourly doses for `Day` view
+    @State private var sumOfHourlyDoses: Double = 0
+    /// The actual chart plot's width in pixel
+    @State private var chartWidth: CGFloat = 0
+
+    /// Computes the visible date range based on the current scroll position.
+    private var visibleDateRange: (start: Date, end: Date) {
+        StatChartUtils.visibleDateRange(from: scrollPosition, for: selectedInterval)
+    }
+
+    /// Retrieves the TDD statistic for a given date.
+    /// - Parameter date: The date for which to retrieve TDD data.
+    /// - Returns: The `TDDStats` object if available, otherwise `nil`.
+    private func getTDDForDate(_ date: Date) -> TDDStats? {
+        tddStats.first { stat in
+            StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedInterval)
+        }
+    }
+
+    /// Updates the average TDD value based on the visible date range.
+    private func updateAverages() {
+        currentAverage = state.getCachedTDDAverages(for: visibleDateRange)
+    }
+
+    /// Updates the total of hourly doses for `Day` view
+    private func updateTotalDoses() {
+        sumOfHourlyDoses = tddStats.filter({ $0.date >= visibleDateRange.start && $0.date <= visibleDateRange.end })
+            .reduce(0, { result, stat in
+                result + stat.amount
+            })
+    }
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 8) {
+            statsView.padding(.bottom)
+
+            VStack(alignment: .trailing) {
+                Text("Total Daily Dose (U)")
+                    .foregroundStyle(.secondary)
+                    .font(.footnote)
+                    .padding(.bottom, 4)
+
+                chartsView
+                    .background(
+                        GeometryReader { geo in
+                            Color.clear
+                                .onAppear { chartWidth = geo.size.width }
+                                .onChange(of: geo.size.width) { _, newValue in chartWidth = newValue }
+                        }
+                    )
+            }
+        }
+        .onAppear {
+            scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedInterval)
+            updateAverages()
+            updateTotalDoses()
+        }
+        .onChange(of: scrollPosition) {
+            updateTimer.scheduleUpdate {
+                updateAverages()
+                if selectedInterval == .day {
+                    updateTotalDoses()
+                }
+            }
+        }
+        .onChange(of: selectedInterval) {
+            Task {
+                scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedInterval)
+                updateAverages()
+                if selectedInterval == .day {
+                    updateTotalDoses()
+                }
+            }
+        }
+    }
+
+    /// A view displaying the statistics summary including average TDD.
+    private var statsView: some View {
+        HStack {
+            if selectedInterval == .day {
+                Grid(alignment: .leading) {
+                    GridRow {
+                        Text("Average:")
+                        Text(currentAverage.formatted(.number.precision(.fractionLength(1))))
+                            + Text("\u{00A0}") + Text("U")
+                    }
+                    GridRow {
+                        Text("Total:")
+                        Text(sumOfHourlyDoses.formatted(.number.precision(.fractionLength(1))))
+                            + Text("\u{00A0}") + Text("U")
+                    }
+                }
+                .font(.headline)
+            } else {
+                Group {
+                    Text("Average:")
+                    Text(currentAverage.formatted(.number.precision(.fractionLength(1))))
+                        + Text("\u{00A0}") + Text("U")
+                }
+                .font(.headline)
+            }
+            Spacer()
+
+            Text(
+                StatChartUtils
+                    .formatVisibleDateRange(from: visibleDateRange.start, to: visibleDateRange.end, for: selectedInterval)
+            )
+            .font(.callout)
+            .foregroundStyle(.secondary)
+        }
+    }
+
+    /// A view displaying the bar chart for TDD statistics.
+    private var chartsView: some View {
+        Chart {
+            ForEach(tddStats) { stat in
+                BarMark(
+                    x: .value("Date", stat.date, unit: selectedInterval == .day ? .hour : .day),
+                    y: .value("Amount", stat.amount)
+                )
+                .foregroundStyle(Color.insulin)
+                .opacity(
+                    selectedDate.map { date in
+                        StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedInterval) ? 1 : 0.3
+                    } ?? 1
+                )
+            }
+
+            // Selection popover outside of the ForEach loop!
+            if let selectedDate,
+               let selectedTDD = getTDDForDate(selectedDate)
+            {
+                RuleMark(
+                    x: .value("Selected Date", selectedDate)
+                )
+                .foregroundStyle(Color.insulin.opacity(0.5))
+                .annotation(
+                    position: .top,
+                    spacing: 0,
+                    overflowResolution: .init(x: .fit(to: .chart), y: .fit(to: .chart))
+                ) {
+                    TDDSelectionPopover(
+                        selectedDate: selectedDate,
+                        tdd: selectedTDD,
+                        selectedInterval: selectedInterval,
+                        domain: visibleDateRange,
+                        chartWidth: chartWidth
+                    )
+                }
+            }
+
+            // Dummy PointMark to force SwiftCharts to render a visible domain of 00:00-23:59
+            // i.e. single day from midnight to midnight
+            if selectedInterval == .day {
+                let calendar = Calendar.current
+                let midnight = calendar.startOfDay(for: Date())
+                let nextMidnight = calendar.date(byAdding: .day, value: 1, to: midnight)!
+
+                PointMark(
+                    x: .value("Time", nextMidnight),
+                    y: .value("Dummy", 0)
+                )
+                .opacity(0) // ensures dummy ChartContent is hidden
+            }
+        }
+        .chartYAxis {
+            AxisMarks(position: .trailing) { value in
+                if let amount = value.as(Double.self) {
+                    AxisValueLabel {
+                        Text(amount.formatted(.number.precision(.fractionLength(0))))
+                            .font(.footnote)
+                    }
+                    AxisGridLine()
+                }
+            }
+        }
+        .chartXAxis {
+            AxisMarks(preset: .aligned, values: .stride(by: selectedInterval == .day ? .hour : .day)) { value in
+                if let date = value.as(Date.self) {
+                    let day = Calendar.current.component(.day, from: date)
+                    let hour = Calendar.current.component(.hour, from: date)
+
+                    switch selectedInterval {
+                    case .day:
+                        if hour % 6 == 0 { // Show only every 6 hours
+                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
+                                .font(.footnote)
+                            AxisGridLine()
+                        }
+                    case .month:
+                        if day % 3 == 0 { // Only show every 3rd day
+                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
+                                .font(.footnote)
+                            AxisGridLine()
+                        }
+                    case .total:
+                        // Only show every other month
+                        if day == 1 && Calendar.current.component(.month, from: date) % 2 == 1 {
+                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
+                                .font(.footnote)
+                            AxisGridLine()
+                        }
+                    default:
+                        AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
+                            .font(.footnote)
+                        AxisGridLine()
+                    }
+                }
+            }
+        }
+        .chartScrollableAxes(.horizontal)
+        .chartXSelection(value: $selectedDate.animation(.easeInOut))
+        .chartScrollPosition(x: $scrollPosition)
+        .chartScrollTargetBehavior(
+            .valueAligned(
+                matching: selectedInterval == .day ?
+                    DateComponents(minute: 0) :
+                    DateComponents(hour: 0),
+                majorAlignment: .matching(StatChartUtils.alignmentComponents(for: selectedInterval))
+            )
+        )
+        .chartXVisibleDomain(length: StatChartUtils.visibleDomainLength(for: selectedInterval))
+        .frame(height: 250)
+    }
+}
+
+/// A popover view displaying TDD (Total Daily Dose) for a given time period.
+/// Shows the insulin amount in units (U) for an hourly or daily interval, depending on `selectedInterval`.
+///
+/// - Parameters:
+///   - date: The reference date for determining the displayed time range.
+///   - tdd: The TDDStats containing insulin usage data.
+///   - selectedInterval: The selected time interval (hourly or daily).
+private struct TDDSelectionPopover: View {
+    let selectedDate: Date
+    let tdd: TDDStats
+    let selectedInterval: Stat.StateModel.StatsTimeInterval
+    let domain: (start: Date, end: Date)
+    let chartWidth: CGFloat
+
+    @State private var popoverSize: CGSize = .zero
+
+    @Environment(\.colorScheme) var colorScheme
+
+    private var timeText: String {
+        if selectedInterval == .day {
+            let hour = Calendar.current.component(.hour, from: selectedDate)
+            return selectedDate.formatted(.dateTime.month().day().weekday()) + "\n" + "\(hour):00-\(hour + 1):00"
+        } else {
+            return selectedDate.formatted(.dateTime.month().day().weekday())
+        }
+    }
+
+    private func xOffset() -> CGFloat {
+        let domainDuration = domain.end.timeIntervalSince(domain.start)
+        guard domainDuration > 0, chartWidth > 0 else { return 0 }
+
+        let popoverWidth = popoverSize.width
+
+        // Convert dates to pixel'd x-condition
+        let dateFraction = selectedDate.timeIntervalSince(domain.start) / domainDuration
+        let x_selected = dateFraction * chartWidth
+
+        // TODO: this is semi hacky, can this be improved?
+        let x_left = x_selected - (popoverWidth / 2) // Left edge of popover
+        let x_right = x_selected + (popoverWidth / 2) // Right edge of popover
+
+        var offset: CGFloat = 0 // Default = no shift
+
+        // Push popover to right if its left edge is (nearing) out-of-bounds
+        if x_left < 0 {
+            offset = abs(x_left) // push to right
+        }
+
+        // Push popover to left if its right edge is (nearing) out-of-bounds)
+        if x_right > chartWidth {
+            offset = -(x_right - chartWidth) // push to left
+        }
+
+        return offset
+    }
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 4) {
+            Text(timeText)
+                .font(.subheadline)
+                .bold()
+                .foregroundStyle(Color.secondary)
+
+            Divider()
+
+            HStack {
+                Text(tdd.amount.formatted(.number.precision(.fractionLength(1))))
+                Text("U").foregroundStyle(Color.secondary)
+            }
+            .font(.headline)
+        }
+        .padding(20)
+        .background {
+            RoundedRectangle(cornerRadius: 10)
+                .fill(colorScheme == .dark ? Color.bgDarkBlue.opacity(0.9) : Color.white.opacity(0.95))
+                .shadow(color: Color.secondary, radius: 2)
+                .overlay(
+                    RoundedRectangle(cornerRadius: 4)
+                        .stroke(Color.blue, lineWidth: 2)
+                )
+        }
+        .frame(minWidth: 100, maxWidth: .infinity) // Ensures proper width
+        .background(
+            GeometryReader { geo in
+                Color.clear
+                    .onAppear { popoverSize = geo.size }
+                    .onChange(of: geo.size) { _, newValue in popoverSize = newValue }
+            }
+        )
+        // Apply calculated xOffset to keep within bounds
+        .offset(x: xOffset(), y: 0)
+    }
+}

+ 83 - 0
Trio/Sources/Modules/Stat/View/ViewElements/Looping/LoopBarChartView.swift

@@ -0,0 +1,83 @@
+import Charts
+import SwiftUI
+
+struct LoopBarChartView: View {
+    let loopStatRecords: [LoopStatRecord]
+    let selectedInterval: Stat.StateModel.StatsTimeIntervalWithToday
+    let statsData: [(category: LoopStatsDataType, count: Int, percentage: Double, medianDuration: Double, medianInterval: Double)]
+
+    var body: some View {
+        VStack(spacing: 20) {
+            Chart(statsData, id: \.category) { data in
+                BarMark(
+                    x: .value("Percentage", data.percentage),
+                    y: .value("Category", data.category.displayName)
+                )
+                .cornerRadius(5)
+                .foregroundStyle(data.category == .successfulLoop ? Color.blue : Color.green)
+                .annotation(position: .overlay) {
+                    HStack {
+                        Text(annotationText(for: data))
+                            .font(.callout)
+                            .foregroundStyle(.white)
+                    }
+                }
+            }
+            .chartYAxis {
+                AxisMarks { value in
+                    if let category = value.as(String.self) {
+                        AxisValueLabel {
+                            Text(category)
+                                .font(.footnote)
+                        }
+                    }
+                }
+            }
+            .chartXAxis {
+                AxisMarks(position: .bottom) { value in
+                    if let percentage = value.as(Double.self) {
+                        AxisValueLabel {
+                            Text("\(Int(percentage))%")
+                                .font(.footnote)
+                        }
+                        AxisGridLine()
+                    }
+                }
+            }
+            .chartXScale(domain: 0 ... 100)
+            .frame(height: 200)
+            .padding()
+        }
+    }
+
+    private func annotationText(for data: (
+        category: LoopStatsDataType,
+        count: Int,
+        percentage: Double,
+        medianDuration: Double,
+        medianInterval: Double
+    )) -> String {
+        if data.category == .successfulLoop {
+            switch selectedInterval {
+            case .day,
+                 .today:
+                return "\(data.count) " + String(localized: "Loops")
+            case .month,
+                 .total,
+                 .week:
+                return "\(data.count) " + String(localized: "Loops per Day")
+            }
+        } else {
+            // For Glucose Count, show different text based on duration
+            switch selectedInterval {
+            case .day,
+                 .today:
+                return "\(data.count) " + String(localized: "Readings")
+            case .month,
+                 .total,
+                 .week:
+                return "\(data.count) " + String(localized: "Readings per Day")
+            }
+        }
+    }
+}

+ 39 - 0
Trio/Sources/Modules/Stat/View/ViewElements/Looping/LoopStatsView.swift

@@ -0,0 +1,39 @@
+import SwiftDate
+import SwiftUI
+
+/// A SwiftUI view displaying statistics about the looping process in an Automated Insulin Delivery (AID) system.
+struct LoopStatsView: View {
+    /// The list of loop statistics records used to generate the statistics.
+    let statsData: [(category: LoopStatsDataType, count: Int, percentage: Double, medianDuration: Double, medianInterval: Double)]
+
+    /// The main body of the `LoopStatsView`, displaying loop statistics.
+    var body: some View {
+        if let successfulStats = statsData.first(where: { $0.category == .successfulLoop }) {
+            HStack {
+                StatChartUtils.statView(
+                    title: String(localized: "Loops"),
+                    value: successfulStats.count.formatted()
+                )
+                Spacer()
+                StatChartUtils.statView(
+                    title: String(localized: "Interval"),
+                    value: (successfulStats.medianInterval / 60)
+                        .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "m"
+                )
+                Spacer()
+                StatChartUtils.statView(
+                    title: String(localized: "Duration"),
+                    value: successfulStats.medianDuration
+                        .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "s"
+                )
+                Spacer()
+                StatChartUtils.statView(
+                    title: String(localized: "Success"),
+                    value: (successfulStats.percentage / 100)
+                        .formatted(.percent.grouping(.never).rounded().precision(.fractionLength(1)))
+                )
+            }
+            .padding()
+        }
+    }
+}

+ 400 - 0
Trio/Sources/Modules/Stat/View/ViewElements/Meal/MealStatsView.swift

@@ -0,0 +1,400 @@
+import Charts
+import SwiftUI
+
+/// A view that displays a bar chart for meal statistics.
+///
+/// This view presents macronutrient intake (carbohydrates, fats, and proteins) over time,
+/// allowing users to adjust the time interval and scroll through historical data.
+struct MealStatsView: View {
+    /// The selected time interval for displaying statistics.
+    @Binding var selectedInterval: Stat.StateModel.StatsTimeInterval
+    /// The list of meal statistics data.
+    let mealStats: [MealStats]
+    /// The state model containing cached statistics data.
+    let state: Stat.StateModel
+
+    /// The current scroll position in the chart.
+    @State private var scrollPosition = Date()
+    /// The currently selected date in the chart.
+    @State private var selectedDate: Date?
+    /// The calculated macronutrient averages for the visible range.
+    @State private var currentAverages: (carbs: Double, fat: Double, protein: Double) = (0, 0, 0)
+    /// Timer to throttle updates when scrolling.
+    @State private var updateTimer = Stat.UpdateTimer()
+    /// The actual chart plot's width in pixel
+    @State private var chartWidth: CGFloat = 0
+
+    /// Computes the visible date range based on the current scroll position.
+    private var visibleDateRange: (start: Date, end: Date) {
+        StatChartUtils.visibleDateRange(from: scrollPosition, for: selectedInterval)
+    }
+
+    /// Retrieves the meal statistic for a given date.
+    /// - Parameter date: The date for which to retrieve meal data.
+    /// - Returns: The `MealStats` object if available, otherwise `nil`.
+    private func getMealForDate(_ date: Date) -> MealStats? {
+        mealStats.first { stat in
+            StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedInterval)
+        }
+    }
+
+    /// Updates the macronutrient averages based on the visible date range.
+    private func updateAverages() {
+        currentAverages = state.getCachedMealAverages(for: visibleDateRange)
+    }
+
+    /// A view displaying the statistics summary including macronutrient averages.
+    private var statsView: some View {
+        HStack {
+            Grid(alignment: .leading) {
+                GridRow {
+                    Text("Carbs:")
+                    Text(currentAverages.carbs.formatted(.number.precision(.fractionLength(1))))
+                        + Text("\u{00A0}") + Text("g")
+                }
+                if state.useFPUconversion {
+                    GridRow {
+                        Text("Fat:")
+                        Text(currentAverages.fat.formatted(.number.precision(.fractionLength(1))))
+                            + Text("\u{00A0}") + Text("g")
+                    }
+                    GridRow {
+                        Text("Protein:")
+                        Text(currentAverages.protein.formatted(.number.precision(.fractionLength(1))))
+                            + Text("\u{00A0}") + Text("g")
+                    }
+                }
+            }
+            .font(.headline)
+
+            Spacer()
+
+            Text(
+                StatChartUtils
+                    .formatVisibleDateRange(from: visibleDateRange.start, to: visibleDateRange.end, for: selectedInterval)
+            )
+            .font(.callout)
+            .foregroundStyle(.secondary)
+        }
+    }
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 8) {
+            statsView.padding(.bottom)
+
+            VStack(alignment: .trailing) {
+                Text("Macro Nutrients (g)")
+                    .foregroundStyle(.secondary)
+                    .font(.footnote)
+                    .padding(.bottom, 4)
+
+                chartsView
+                    .background(
+                        GeometryReader { geo in
+                            Color.clear
+                                .onAppear { chartWidth = geo.size.width }
+                                .onChange(of: geo.size.width) { _, newValue in chartWidth = newValue }
+                        }
+                    )
+            }
+        }
+        .onAppear {
+            scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedInterval)
+            updateAverages()
+        }
+        .onChange(of: scrollPosition) {
+            updateTimer.scheduleUpdate {
+                updateAverages()
+            }
+        }
+        .onChange(of: selectedInterval) {
+            Task {
+                scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedInterval)
+                updateAverages()
+            }
+        }
+    }
+
+    /// A view displaying the bar chart for meal statistics.
+    private var chartsView: some View {
+        Chart {
+            ForEach(mealStats) { stat in
+                // Carbs Bar (bottom)
+                BarMark(
+                    x: .value("Date", stat.date, unit: selectedInterval == .day ? .hour : .day),
+                    y: .value("Amount", stat.carbs)
+                )
+                .foregroundStyle(by: .value("Type", "Carbs"))
+                .position(by: .value("Type", "Macros"))
+                .opacity(
+                    selectedDate.map { date in
+                        StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedInterval) ? 1 : 0.3
+                    } ?? 1
+                )
+                if state.useFPUconversion {
+                    // Fat Bar (middle)
+                    BarMark(
+                        x: .value("Date", stat.date, unit: selectedInterval == .day ? .hour : .day),
+                        y: .value("Amount", stat.fat)
+                    )
+                    .foregroundStyle(by: .value("Type", "Fat"))
+                    .position(by: .value("Type", "Macros"))
+                    .opacity(
+                        selectedDate.map { date in
+                            StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedInterval) ? 1 : 0.3
+                        } ?? 1
+                    )
+                    // Protein Bar (top)
+                    BarMark(
+                        x: .value("Date", stat.date, unit: selectedInterval == .day ? .hour : .day),
+                        y: .value("Amount", stat.protein)
+                    )
+                    .foregroundStyle(by: .value("Type", "Protein"))
+                    .position(by: .value("Type", "Macros"))
+                    .opacity(
+                        selectedDate.map { date in
+                            StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedInterval) ? 1 : 0.3
+                        } ?? 1
+                    )
+                }
+            }
+
+            // Selection popover outside of the ForEach loop!
+            if let selectedDate,
+               let selectedMeal = getMealForDate(selectedDate)
+            {
+                RuleMark(
+                    x: .value("Selected Date", selectedDate)
+                )
+                .foregroundStyle(Color.orange.opacity(0.5))
+                .annotation(
+                    position: .top,
+                    spacing: 0,
+                    overflowResolution: .init(x: .fit(to: .chart), y: .fit(to: .chart))
+                ) {
+                    MealSelectionPopover(
+                        selectedDate: selectedDate,
+                        selectedMeal: selectedMeal,
+                        selectedInterval: selectedInterval,
+                        isFpuEnabled: state.useFPUconversion,
+                        domain: visibleDateRange,
+                        chartWidth: chartWidth
+                    )
+                }
+            }
+
+            // Dummy PointMark to force SwiftCharts to render a visible domain of 00:00-23:59
+            // i.e. single day from midnight to midnight
+            if selectedInterval == .day {
+                let calendar = Calendar.current
+                let midnight = calendar.startOfDay(for: Date())
+                let nextMidnight = calendar.date(byAdding: .day, value: 1, to: midnight)!
+
+                PointMark(
+                    x: .value("Time", nextMidnight),
+                    y: .value("Dummy", 0)
+                )
+                .opacity(0) // ensures dummy ChartContent is hidden
+            }
+        }
+        .chartForegroundStyleScale([
+            "Carbs": Color.orange,
+            "Protein": Color.blue,
+            "Fat": Color.purple
+        ])
+        .chartLegend(position: .bottom, alignment: .leading, spacing: 12) {
+            let legendItems: [(String, Color)] = state.useFPUconversion ? [
+                (String(localized: "Carbs"), Color.orange),
+                (String(localized: "Protein"), Color.blue),
+                (String(localized: "Fat"), Color.purple)
+            ] : [(String(localized: "Carbs"), Color.orange)]
+
+            let columns = [GridItem(.adaptive(minimum: 65), spacing: 4)]
+
+            LazyVGrid(columns: columns, alignment: .leading, spacing: 4) {
+                ForEach(legendItems, id: \.0) { item in
+                    StatChartUtils.legendItem(label: item.0, color: item.1)
+                }
+            }
+        }
+        .chartYAxis {
+            AxisMarks(position: .trailing) { value in
+                if let amount = value.as(Double.self) {
+                    AxisValueLabel {
+                        Text(amount.formatted(.number.precision(.fractionLength(0))))
+                            .font(.footnote)
+                    }
+                    AxisGridLine()
+                }
+            }
+        }
+        .chartXAxis {
+            AxisMarks(preset: .aligned, values: .stride(by: selectedInterval == .day ? .hour : .day)) { value in
+                if let date = value.as(Date.self) {
+                    let day = Calendar.current.component(.day, from: date)
+                    let hour = Calendar.current.component(.hour, from: date)
+
+                    switch selectedInterval {
+                    case .day:
+                        if hour % 6 == 0 { // Show only every 6 hours
+                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
+                                .font(.footnote)
+                            AxisGridLine()
+                        }
+                    case .month:
+                        if day % 3 == 0 { // Only show every 3rd day
+                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
+                                .font(.footnote)
+                            AxisGridLine()
+                        }
+                    case .total:
+                        // Only show every other month
+                        if day == 1 && Calendar.current.component(.month, from: date) % 2 == 1 {
+                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
+                                .font(.footnote)
+                            AxisGridLine()
+                        }
+                    default:
+                        AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
+                            .font(.footnote)
+                        AxisGridLine()
+                    }
+                }
+            }
+        }
+        .chartScrollableAxes(.horizontal)
+        .chartXSelection(value: $selectedDate.animation(.easeInOut))
+        .chartScrollPosition(x: $scrollPosition)
+        .chartScrollTargetBehavior(
+            .valueAligned(
+                matching: selectedInterval == .day ?
+                    DateComponents(minute: 0) :
+                    DateComponents(hour: 0),
+                majorAlignment: .matching(StatChartUtils.alignmentComponents(for: selectedInterval))
+            )
+        )
+        .chartXVisibleDomain(length: StatChartUtils.visibleDomainLength(for: selectedInterval))
+        .frame(height: 250)
+    }
+}
+
+/// A view that displays detailed meal information in a popover
+///
+/// This view shows a formatted display of meal macronutrients including:
+/// - Date of the meal
+/// - Carbohydrates in grams
+/// - Fat in grams
+/// - Protein in grams
+private struct MealSelectionPopover: View {
+    // The date when the meal was logged
+    let selectedDate: Date
+    // The meal statistics to display
+    let selectedMeal: MealStats
+    // The selected duration in the time picker
+    let selectedInterval: Stat.StateModel.StatsTimeInterval
+    // Setting controlling whether to display fat and protein
+    let isFpuEnabled: Bool
+    let domain: (start: Date, end: Date)
+    let chartWidth: CGFloat
+
+    @State private var popoverSize: CGSize = .zero
+
+    @Environment(\.colorScheme) var colorScheme
+
+    private var timeText: String {
+        if selectedInterval == .day {
+            let hour = Calendar.current.component(.hour, from: selectedDate)
+            return selectedDate.formatted(.dateTime.month().day().weekday()) + "\n" + "\(hour):00-\(hour + 1):00"
+        } else {
+            return selectedDate.formatted(.dateTime.month().day().weekday())
+        }
+    }
+
+    private func xOffset() -> CGFloat {
+        let domainDuration = domain.end.timeIntervalSince(domain.start)
+        guard domainDuration > 0, chartWidth > 0 else { return 0 }
+
+        let popoverWidth = popoverSize.width
+
+        // Convert dates to pixel'd x-condition
+        let dateFraction = selectedDate.timeIntervalSince(domain.start) / domainDuration
+        let x_selected = dateFraction * chartWidth
+
+        // TODO: this is semi hacky, can this be improved?
+        let x_left = x_selected - (popoverWidth / 2) // Left edge of popover
+        let x_right = x_selected + (popoverWidth / 2) // Right edge of popover
+
+        var offset: CGFloat = 0 // Default = no shift
+
+        // Push popover to right if its left edge is (nearing) out-of-bounds
+        if x_left < 0 {
+            offset = abs(x_left) // push to right
+        }
+
+        // Push popover to left if its right edge is (nearing) out-of-bounds)
+        if x_right > chartWidth {
+            offset = -(x_right - chartWidth) // push to left
+        }
+
+        return offset
+    }
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 4) {
+            Text(timeText)
+                .font(.subheadline)
+                .bold()
+                .foregroundStyle(Color.secondary)
+
+            Divider()
+
+            // Grid layout for macronutrient values
+            Grid(alignment: .leading) {
+                // Carbohydrates row
+                GridRow {
+                    Text("Carbs:")
+                    Text(selectedMeal.carbs.formatted(.number.precision(.fractionLength(1))))
+                        .gridColumnAlignment(.trailing)
+                    Text("g").foregroundStyle(Color.secondary)
+                }
+                if isFpuEnabled {
+                    // Fat row
+                    GridRow {
+                        Text("Fat:")
+                        Text(selectedMeal.fat.formatted(.number.precision(.fractionLength(1))))
+                            .gridColumnAlignment(.trailing)
+                        Text("g").foregroundStyle(Color.secondary)
+                    }
+                    // Protein row
+                    GridRow {
+                        Text("Protein:")
+                        Text(selectedMeal.protein.formatted(.number.precision(.fractionLength(1))))
+                            .gridColumnAlignment(.trailing)
+                        Text("g").foregroundStyle(Color.secondary)
+                    }
+                }
+            }
+            .font(.headline.bold())
+        }
+        .padding(20)
+        .background {
+            RoundedRectangle(cornerRadius: 10)
+                .fill(colorScheme == .dark ? Color.bgDarkBlue.opacity(0.9) : Color.white.opacity(0.95))
+                .shadow(color: Color.secondary, radius: 2)
+                .overlay(
+                    RoundedRectangle(cornerRadius: 4)
+                        .stroke(Color.orange, lineWidth: 2)
+                )
+        }
+        .frame(minWidth: 100, maxWidth: .infinity) // Ensures proper width
+        .background(
+            GeometryReader { geo in
+                Color.clear
+                    .onAppear { popoverSize = geo.size }
+                    .onChange(of: geo.size) { _, newValue in popoverSize = newValue }
+            }
+        )
+        // Apply calculated xOffset to keep within bounds
+        .offset(x: xOffset(), y: 0)
+    }
+}

+ 2 - 1
Trio/Sources/Modules/TargetBehavoir/TargetBehavoirStateModel.swift

@@ -12,10 +12,11 @@ extension TargetBehavoir {
         @Published var sensitivityRaisesTarget: Bool = false
         @Published var resistanceLowersTarget: Bool = false
         @Published var halfBasalExerciseTarget: Decimal = 160
+        @Published var autosensMax: Decimal = 1
 
         override func subscribe() {
             units = settingsManager.settings.units
-
+            autosensMax = settingsManager.preferences.autosensMax
             subscribePreferencesSetting(\.highTemptargetRaisesSensitivity, on: $highTemptargetRaisesSensitivity) {
                 highTemptargetRaisesSensitivity = $0 }
             subscribePreferencesSetting(\.lowTemptargetLowersSensitivity, on: $lowTemptargetLowersSensitivity) {

+ 27 - 5
Trio/Sources/Modules/TargetBehavoir/View/TargetBehavoirRootView.swift

@@ -11,6 +11,7 @@ extension TargetBehavoir {
         @State var hintLabel: String?
         @State private var decimalPlaceholder: Decimal = 0.0
         @State private var booleanPlaceholder: Bool = false
+        @State private var showAutosensMaxAlert = false
 
         @Environment(\.colorScheme) var colorScheme
         @EnvironmentObject var appIcons: Icons
@@ -59,7 +60,7 @@ extension TargetBehavoir {
 
                 SettingInputSection(
                     decimalValue: $decimalPlaceholder,
-                    booleanValue: $state.lowTemptargetLowersSensitivity,
+                    booleanValue: effectiveLowTTLowersSensBinding,
                     shouldDisplayHint: $shouldDisplayHint,
                     selectedVerboseHint: Binding(
                         get: { selectedVerboseHint },
@@ -86,7 +87,7 @@ extension TargetBehavoir {
                     VStack(alignment: .leading, spacing: 10) {
                         Text("Default: OFF").bold()
                         Text(
-                            "When this feature is enabled, setting a temporary target below \(state.units == .mgdL ? "100" : 100.formattedAsMmolL) \(state.units.rawValue) will increase the Autosens Ratio used for ISF and basal adjustments, resulting in more insulin delivered overall. This scales with the temporary target set; the lower the Temp Target, the higher the Autosens Ratio used."
+                            "When this feature is enabled, setting a temporary target below \(state.units == .mgdL ? "100" : 100.formattedAsMmolL) \(state.units.rawValue) will increase the Autosens Ratio used for ISF and basal adjustments, resulting in more insulin delivered overall. This scales with the temporary target set; the lower the Temp Target, the higher the Autosens Ratio used. It requires Algorithm Settings > Autosens > Autosens Max to be set to > 100% to work."
                         )
                         Text(
                             "If Half Basal Exercise Target is \(state.units == .mgdL ? "160" : 160.formattedAsMmolL) \(state.units.rawValue), a Temp Target of \(state.units == .mgdL ? "95" : 95.formattedAsMmolL) \(state.units.rawValue) uses an Autosens Ratio of 1.09. A Temp Target of \(state.units == .mgdL ? "85" : 85.formattedAsMmolL) \(state.units.rawValue) uses an Autosens Ratio of 1.33."
@@ -186,11 +187,32 @@ extension TargetBehavoir {
             }
             .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
             .onAppear(perform: configureView)
+            .alert(
+                "Cannot Enable This Setting",
+                isPresented: $showAutosensMaxAlert
+            ) {
+                // Alert button(s). For a single button:
+                Button("Got it!", role: .cancel) {}
+            } message: {
+                Text(
+                    "This feature cannot be enabled unless Algorithm Settings > Autosens > Autosens Max is set higher than 100%."
+                )
+            }
             .navigationTitle("Target Behavior")
             .navigationBarTitleDisplayMode(.automatic)
-//            .onDisappear {
-//                state.saveIfChanged()
-//            }
+        }
+
+        private var effectiveLowTTLowersSensBinding: Binding<Bool> {
+            Binding(
+                get: { state.autosensMax > 1 && state.lowTemptargetLowersSensitivity },
+                set: { newValue in
+                    if newValue, state.autosensMax <= 1 {
+                        showAutosensMaxAlert = true
+                    } else {
+                        state.lowTemptargetLowersSensitivity = newValue
+                    }
+                }
+            )
         }
     }
 }

+ 13 - 40
Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift

@@ -286,11 +286,6 @@ extension Treatments {
         private func getCurrentSettingValue(for type: SettingType) async {
             let now = Date()
             let calendar = Calendar.current
-            let dateFormatter = DateFormatter()
-            dateFormatter.timeZone = TimeZone.current
-
-            let regexWithSeconds = #"^\d{2}:\d{2}:\d{2}$"#
-
             let entries: [(start: String, value: Decimal)]
 
             switch type {
@@ -309,15 +304,8 @@ extension Treatments {
             }
 
             for (index, entry) in entries.enumerated() {
-                // Dynamically set the format based on whether it matches the regex
-                if entry.start.range(of: regexWithSeconds, options: .regularExpression) != nil {
-                    dateFormatter.dateFormat = "HH:mm:ss"
-                } else {
-                    dateFormatter.dateFormat = "HH:mm"
-                }
-
-                guard let entryTime = dateFormatter.date(from: entry.start) else {
-                    print("Invalid entry start time: \(entry.start)")
+                guard let entryTime = TherapySettingsUtil.parseTime(entry.start) else {
+                    debug(.default, "Invalid entry start time: \(entry.start)")
                     continue
                 }
 
@@ -331,14 +319,7 @@ extension Treatments {
 
                 let entryEndTime: Date
                 if index < entries.count - 1 {
-                    // Dynamically set the format again for the next element
-                    if entries[index + 1].start.range(of: regexWithSeconds, options: .regularExpression) != nil {
-                        dateFormatter.dateFormat = "HH:mm:ss"
-                    } else {
-                        dateFormatter.dateFormat = "HH:mm"
-                    }
-
-                    if let nextEntryTime = dateFormatter.date(from: entries[index + 1].start) {
+                    if let nextEntryTime = TherapySettingsUtil.parseTime(entries[index + 1].start) {
                         let nextEntryComponents = calendar.dateComponents([.hour, .minute, .second], from: nextEntryTime)
                         entryEndTime = calendar.date(
                             bySettingHour: nextEntryComponents.hour!,
@@ -727,14 +708,11 @@ extension Treatments.StateModel {
             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 determination = await determinationFetchContext.perform {
+                let determinationObject = determinationObjects.first
+                let eventualBG = determinationObject?.eventualBG?.intValue
 
-                let forecastsSet = determinationObject.forecasts ?? []
+                let forecastsSet = determinationObject?.forecasts ?? []
                 let predictions = Predictions(
                     iob: forecastsSet.extractValues(for: "iob"),
                     zt: forecastsSet.extractValues(for: "zt"),
@@ -747,7 +725,6 @@ extension Treatments.StateModel {
                     reason: "",
                     units: 0,
                     insulinReq: 0,
-                    eventualBG: eventualBG,
                     sensitivityRatio: 0,
                     rate: 0,
                     duration: 0,
@@ -756,23 +733,19 @@ extension Treatments.StateModel {
                     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
                 )
             }
+
+            guard !determinationObjects.isEmpty else {
+                return nil
+            }
+
+            return determination
         } catch {
             debug(
                 .default,

+ 0 - 0
Trio/Sources/Modules/Treatments/View/MealPreset/AddMealPresetView.swift


Некоторые файлы не были показаны из-за большого количества измененных файлов