فهرست منبع

Merge branch 'dev' into maxIOB-maxCOB

Mike Plante 1 سال پیش
والد
کامیت
774ab8bb2a
98فایلهای تغییر یافته به همراه52515 افزوده شده و 32897 حذف شده
  1. 0 1
      Model/Classes+Properties/OrefDetermination+CoreDataProperties.swift
  2. 5 0
      Model/Helper/CarbEntryStored+helper.swift
  3. 5 0
      Model/Helper/Determination+helper.swift
  4. 5 0
      Model/Helper/PumpEvent+helper.swift
  5. 0 1
      Model/TrioCoreDataPersistentContainer.xcdatamodeld/TrioCoreDataPersistentContainer.xcdatamodel/contents
  6. 1 0
      Trio Watch App Extension/WatchState.swift
  7. 132 16
      Trio.xcodeproj/project.pbxproj
  8. 1 1
      Trio/Resources/javascript/bundle/determine-basal.js
  9. 1 2
      Trio/Resources/json/defaults/freeaps/freeaps_settings.json
  10. 20 54
      Trio/Sources/APS/APSManager.swift
  11. 63 31
      Trio/Sources/APS/CGM/PluginSource.swift
  12. 15 24
      Trio/Sources/APS/FetchGlucoseManager.swift
  13. 14 12
      Trio/Sources/APS/OpenAPS/OpenAPS.swift
  14. 0 2
      Trio/Sources/APS/Storage/DeterminationStorage.swift
  15. 61 0
      Trio/Sources/APS/Storage/OverrideStorage.swift
  16. 1 1
      Trio/Sources/APS/Storage/TDDStorage.swift
  17. 1 1
      Trio/Sources/Config/Config.swift
  18. 29 0
      Trio/Sources/Helpers/BackgroundTask+Helper.swift
  19. 19 0
      Trio/Sources/Helpers/Calendar+GlucoseStatsChart.swift
  20. 56 0
      Trio/Sources/Helpers/CustomDatePicker.swift
  21. 14 0
      Trio/Sources/Helpers/Formatters.swift
  22. 46913 31406
      Trio/Sources/Localizations/Main/Localizable.xcstrings
  23. 3 3
      Trio/Sources/Models/ColorSchemeOption.swift
  24. 0 20
      Trio/Sources/Models/Determination.swift
  25. 1 1
      Trio/Sources/Models/HbA1cDisplayUnit.swift
  26. 2 2
      Trio/Sources/Models/GlucoseColorScheme.swift
  27. 9 4
      Trio/Sources/Models/GlucoseNotificationsOption.swift
  28. 4 0
      Trio/Sources/Models/Oref2_variables.swift
  29. 3 3
      Trio/Sources/Models/Statistics.swift
  30. 6 0
      Trio/Sources/Models/TDD.swift
  31. 0 22
      Trio/Sources/Models/TotalInsulinDisplayType.swift
  32. 3 8
      Trio/Sources/Models/TrioSettings.swift
  33. 6 5
      Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+Helpers.swift
  34. 32 7
      Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+Overrides.swift
  35. 6 5
      Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+TempTargets.swift
  36. 3 3
      Trio/Sources/Modules/Adjustments/AdjustmentsStateModel.swift
  37. 6 6
      Trio/Sources/Modules/Adjustments/View/Overrides/AddOverrideForm.swift
  38. 1 1
      Trio/Sources/Modules/Adjustments/View/Overrides/AdjustmentsRootView+Overrides.swift
  39. 6 6
      Trio/Sources/Modules/Adjustments/View/Overrides/EditOverrideForm.swift
  40. 6 5
      Trio/Sources/Modules/Adjustments/View/TempTargets/AddTempTargetForm.swift
  41. 1 1
      Trio/Sources/Modules/Adjustments/View/TempTargets/EditTempTargetForm.swift
  42. 2 4
      Trio/Sources/Modules/BasalProfileEditor/View/BasalProfileEditorRootView.swift
  43. 4 5
      Trio/Sources/Modules/CarbRatioEditor/View/CarbRatioEditorRootView.swift
  44. 1 1
      Trio/Sources/Modules/DataTable/View/CarbEntryEditorView.swift
  45. 52 0
      Trio/Sources/Modules/Home/HomeStateModel+Setup/CurrentTDDSetup.swift
  46. 2 2
      Trio/Sources/Modules/Home/HomeStateModel+Setup/TempTargetSetup.swift
  47. 16 59
      Trio/Sources/Modules/Home/HomeStateModel.swift
  48. 9 2
      Trio/Sources/Modules/Home/View/Chart/ChartElements/OverrideView.swift
  49. 0 3
      Trio/Sources/Modules/Home/View/Chart/MainChartView.swift
  50. 18 22
      Trio/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift
  51. 8 3
      Trio/Sources/Modules/Home/View/Header/LoopView.swift
  52. 8 37
      Trio/Sources/Modules/Home/View/HomeRootView.swift
  53. 2 2
      Trio/Sources/Modules/ISFEditor/View/ISFEditorRootView.swift
  54. 1 1
      Trio/Sources/Modules/LiveActivitySettings/View/LiveActivityWidgetConfiguration.swift
  55. 35 2
      Trio/Sources/Modules/Main/MainStateModel.swift
  56. 1 4
      Trio/Sources/Modules/Settings/SettingItems.swift
  57. 144 0
      Trio/Sources/Modules/Stat/StatStateModel+Setup/AreaChartSetup.swift
  58. 274 0
      Trio/Sources/Modules/Stat/StatStateModel+Setup/BolusStatsSetup.swift
  59. 246 0
      Trio/Sources/Modules/Stat/StatStateModel+Setup/LoopChartSetup.swift
  60. 177 0
      Trio/Sources/Modules/Stat/StatStateModel+Setup/MealStatsSetup.swift
  61. 132 0
      Trio/Sources/Modules/Stat/StatStateModel+Setup/StackedChartSetup.swift
  62. 559 0
      Trio/Sources/Modules/Stat/StatStateModel+Setup/TDDSetup.swift
  63. 264 27
      Trio/Sources/Modules/Stat/StatStateModel.swift
  64. 192 0
      Trio/Sources/Modules/Stat/View/StatChartUtils.swift
  65. 346 113
      Trio/Sources/Modules/Stat/View/StatRootView.swift
  66. 0 287
      Trio/Sources/Modules/Stat/View/StatsView.swift
  67. 102 0
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseDistributionChart.swift
  68. 134 0
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseMetricsView.swift
  69. 242 0
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucosePercentileChart.swift
  70. 374 0
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseSectorChart.swift
  71. 411 0
      Trio/Sources/Modules/Stat/View/ViewElements/Insulin/BolusStatsView.swift
  72. 342 0
      Trio/Sources/Modules/Stat/View/ViewElements/Insulin/TotalDailyDoseChart.swift
  73. 83 0
      Trio/Sources/Modules/Stat/View/ViewElements/Looping/LoopBarChartView.swift
  74. 39 0
      Trio/Sources/Modules/Stat/View/ViewElements/Looping/LoopStatsView.swift
  75. 400 0
      Trio/Sources/Modules/Stat/View/ViewElements/Meal/MealStatsView.swift
  76. 2 1
      Trio/Sources/Modules/TargetBehavoir/TargetBehavoirStateModel.swift
  77. 27 5
      Trio/Sources/Modules/TargetBehavoir/View/TargetBehavoirRootView.swift
  78. 10 18
      Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift
  79. 6 2
      Trio/Sources/Modules/Treatments/View/TreatmentsRootView.swift
  80. 2 5
      Trio/Sources/Modules/UserInterfaceSettings/UserInterfaceSettingsStateModel.swift
  81. 6 60
      Trio/Sources/Modules/UserInterfaceSettings/View/UserInterfaceSettingsRootView.swift
  82. 25 10
      Trio/Sources/Services/LiveActivity/Data/DataManager.swift
  83. 33 0
      Trio/Sources/Services/Network/Nightscout/NightscoutAPI.swift
  84. 38 3
      Trio/Sources/Services/Network/Nightscout/NightscoutManager.swift
  85. 1 1
      Trio/Sources/Services/WatchManager/AppleWatchManager.swift
  86. 1 1
      Trio/Sources/Services/WatchManager/GarminManager.swift
  87. 3 16
      Trio/Sources/Shortcuts/LiveActivity/RestartLiveActivityIntentRequest.swift
  88. 15 2
      Trio/Sources/Shortcuts/Override/ApplyOverridePresetIntent.swift
  89. 11 8
      Trio/Sources/Shortcuts/Override/CancelOverrideIntent.swift
  90. 17 0
      Trio/Sources/Shortcuts/Override/OverridePresetEntity.swift
  91. 88 65
      Trio/Sources/Shortcuts/Override/OverridePresetsIntentRequest.swift
  92. 22 4
      Trio/Sources/Shortcuts/TempPresets/ApplyTempPresetIntent.swift
  93. 7 2
      Trio/Sources/Shortcuts/TempPresets/CancelTempPresetIntent.swift
  94. 23 0
      Trio/Sources/Shortcuts/TempPresets/TempPresetIntent.swift
  95. 94 92
      Trio/Sources/Shortcuts/TempPresets/TempPresetsIntentRequest.swift
  96. 5 5
      Trio/Sources/Views/SettingInputSection.swift
  97. 3 1
      oref0_source_version.txt
  98. 2 368
      trio-oref/lib/determine-basal/determine-basal.js

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

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

+ 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)
         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 {
     static var carbsNotYetUploadedToNightscout: NSPredicate {
         let date = Date.oneDayAgo
         let date = Date.oneDayAgo
         return NSPredicate(
         return NSPredicate(

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

@@ -64,4 +64,9 @@ extension NSPredicate {
             true as NSNumber
             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)
         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 {
     static var recentPumpHistory: NSPredicate {
         let date = Date.twentyMinutesAgo
         let date = Date.twentyMinutesAgo
         return NSPredicate(
         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="threshold" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
         <attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
         <attribute name="timestampEnacted" 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"/>
         <relationship name="forecasts" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Forecast" inverseName="orefDetermination" inverseEntity="Forecast"/>
         <fetchIndex name="byDate">
         <fetchIndex name="byDate">
             <fetchIndexElement property="deliverAt" type="Binary" order="descending"/>
             <fetchIndexElement property="deliverAt" type="Binary" order="descending"/>

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

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

+ 132 - 16
Trio.xcodeproj/project.pbxproj

@@ -37,9 +37,7 @@
 		1967DFBE29D052C200759F30 /* Icons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1967DFBD29D052C200759F30 /* Icons.swift */; };
 		1967DFBE29D052C200759F30 /* Icons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1967DFBD29D052C200759F30 /* Icons.swift */; };
 		1967DFC029D053AC00759F30 /* IconSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1967DFBF29D053AC00759F30 /* IconSelection.swift */; };
 		1967DFC029D053AC00759F30 /* IconSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1967DFBF29D053AC00759F30 /* IconSelection.swift */; };
 		1967DFC229D053D300759F30 /* IconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1967DFC129D053D300759F30 /* IconImage.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 */; };
 		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 */; };
 		19B0EF2128F6D66200069496 /* Statistics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19B0EF2028F6D66200069496 /* Statistics.swift */; };
 		19D466A329AA2B80004D5F33 /* MealSettingsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19D466A229AA2B80004D5F33 /* MealSettingsDataFlow.swift */; };
 		19D466A329AA2B80004D5F33 /* MealSettingsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19D466A229AA2B80004D5F33 /* MealSettingsDataFlow.swift */; };
 		19D466A529AA2BD4004D5F33 /* MealSettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19D466A429AA2BD4004D5F33 /* MealSettingsProvider.swift */; };
 		19D466A529AA2BD4004D5F33 /* MealSettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19D466A429AA2BD4004D5F33 /* MealSettingsProvider.swift */; };
@@ -292,6 +290,22 @@
 		BD04ECCE2D29952A008C5FEB /* BolusProgressOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD04ECCD2D299522008C5FEB /* BolusProgressOverlay.swift */; };
 		BD04ECCE2D29952A008C5FEB /* BolusProgressOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD04ECCD2D299522008C5FEB /* BolusProgressOverlay.swift */; };
 		BD0B2EF32C5998E600B3298F /* MealPresetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD0B2EF22C5998E600B3298F /* MealPresetView.swift */; };
 		BD0B2EF32C5998E600B3298F /* MealPresetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD0B2EF22C5998E600B3298F /* MealPresetView.swift */; };
 		BD1661312B82ADAB00256551 /* CustomProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1661302B82ADAB00256551 /* CustomProgressView.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 */; };
+		BD249DA52D42FD9700412DEB /* CustomDatePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249DA42D42FD9500412DEB /* CustomDatePicker.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 */; };
 		BD2B464E0745FBE7B79913F4 /* NightscoutConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF768BD6264FF7D71D66767 /* NightscoutConfigProvider.swift */; };
 		BD2FF1A02AE29D43005D1C5D /* CheckboxToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */; };
 		BD2FF1A02AE29D43005D1C5D /* CheckboxToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */; };
 		BD3CC0722B0B89D50013189E /* MainChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD3CC0712B0B89D50013189E /* MainChartView.swift */; };
 		BD3CC0722B0B89D50013189E /* MainChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD3CC0712B0B89D50013189E /* MainChartView.swift */; };
@@ -490,6 +504,7 @@
 		DD498F2D2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
 		DD498F2D2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
 		DD4C57A82D73ADEA001BFF2C /* RestartLiveActivityIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4C57A72D73ADEA001BFF2C /* RestartLiveActivityIntent.swift */; };
 		DD4C57A82D73ADEA001BFF2C /* RestartLiveActivityIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4C57A72D73ADEA001BFF2C /* RestartLiveActivityIntent.swift */; };
 		DD4C57AA2D73B3E2001BFF2C /* RestartLiveActivityIntentRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4C57A92D73B3D9001BFF2C /* RestartLiveActivityIntentRequest.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 */; };
 		DD4FFF332D458EE600B6CFF9 /* GarminWatchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4FFF322D458EE600B6CFF9 /* GarminWatchState.swift */; };
 		DD5DC9F12CF3D97C00AB8703 /* AdjustmentsStateModel+Overrides.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5DC9F02CF3D96E00AB8703 /* AdjustmentsStateModel+Overrides.swift */; };
 		DD5DC9F12CF3D97C00AB8703 /* AdjustmentsStateModel+Overrides.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5DC9F02CF3D96E00AB8703 /* AdjustmentsStateModel+Overrides.swift */; };
 		DD5DC9F32CF3D9DD00AB8703 /* AdjustmentsStateModel+TempTargets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5DC9F22CF3D9D600AB8703 /* AdjustmentsStateModel+TempTargets.swift */; };
 		DD5DC9F32CF3D9DD00AB8703 /* AdjustmentsStateModel+TempTargets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5DC9F22CF3D9D600AB8703 /* AdjustmentsStateModel+TempTargets.swift */; };
@@ -499,15 +514,16 @@
 		DD68889D2C386E17006E3C44 /* NightscoutExercise.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD68889C2C386E17006E3C44 /* NightscoutExercise.swift */; };
 		DD68889D2C386E17006E3C44 /* NightscoutExercise.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD68889C2C386E17006E3C44 /* NightscoutExercise.swift */; };
 		DD6B7CB22C7B6F0800B75029 /* Rounding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB12C7B6F0800B75029 /* Rounding.swift */; };
 		DD6B7CB22C7B6F0800B75029 /* Rounding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB12C7B6F0800B75029 /* Rounding.swift */; };
 		DD6B7CB42C7B71F700B75029 /* ForecastDisplayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.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 */; };
 		DD6B7CB92C7BAC6900B75029 /* NightscoutImportResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB82C7BAC6900B75029 /* NightscoutImportResultView.swift */; };
 		DD6B7CBB2C7FBBFA00B75029 /* ReviewInsulinActionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CBA2C7FBBFA00B75029 /* ReviewInsulinActionView.swift */; };
 		DD6B7CBB2C7FBBFA00B75029 /* ReviewInsulinActionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CBA2C7FBBFA00B75029 /* ReviewInsulinActionView.swift */; };
 		DD6D67E42C9C253500660C9B /* ColorSchemeOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6D67E32C9C253500660C9B /* ColorSchemeOption.swift */; };
 		DD6D67E42C9C253500660C9B /* ColorSchemeOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6D67E32C9C253500660C9B /* ColorSchemeOption.swift */; };
 		DD6F63CC2D27F615007D94CF /* TreatmentMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F63CB2D27F606007D94CF /* TreatmentMenuView.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 */; };
 		DD8262CB2D289297009F6F62 /* BolusConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8262CA2D289297009F6F62 /* BolusConfirmationView.swift */; };
 		DD88C8E22C50420800F2D558 /* DefinitionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD88C8E12C50420800F2D558 /* DefinitionRow.swift */; };
 		DD88C8E22C50420800F2D558 /* DefinitionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD88C8E12C50420800F2D558 /* DefinitionRow.swift */; };
 		DD940BAA2CA7585D000830A5 /* GlucoseColorScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */; };
 		DD940BAA2CA7585D000830A5 /* GlucoseColorScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */; };
 		DD940BAC2CA75889000830A5 /* DynamicGlucoseColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.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 */; };
 		DD9ECB682CA99F4500AA7C45 /* TrioRemoteControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB672CA99F4500AA7C45 /* TrioRemoteControl.swift */; };
 		DD9ECB6A2CA99F6C00AA7C45 /* PushMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB692CA99F6C00AA7C45 /* PushMessage.swift */; };
 		DD9ECB6A2CA99F6C00AA7C45 /* PushMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB692CA99F6C00AA7C45 /* PushMessage.swift */; };
 		DD9ECB702CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6D2CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift */; };
 		DD9ECB702CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6D2CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift */; };
@@ -533,7 +549,7 @@
 		DDD1631A2C4C695E00CD525A /* EditOverrideForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163192C4C695E00CD525A /* EditOverrideForm.swift */; };
 		DDD1631A2C4C695E00CD525A /* EditOverrideForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163192C4C695E00CD525A /* EditOverrideForm.swift */; };
 		DDD1631C2C4C697400CD525A /* AddOverrideForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD1631B2C4C697400CD525A /* AddOverrideForm.swift */; };
 		DDD1631C2C4C697400CD525A /* AddOverrideForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD1631B2C4C697400CD525A /* AddOverrideForm.swift */; };
 		DDD1631F2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DDD1631D2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodeld */; };
 		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 */; };
 		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 */; };
 		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 */; };
 		DDE179542C910127003CDDB7 /* LoopStatRecord+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179342C910127003CDDB7 /* LoopStatRecord+CoreDataClass.swift */; };
@@ -721,9 +737,7 @@
 		1967DFBF29D053AC00759F30 /* IconSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconSelection.swift; sourceTree = "<group>"; };
 		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>"; };
 		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; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		19D466A429AA2BD4004D5F33 /* MealSettingsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealSettingsProvider.swift; sourceTree = "<group>"; };
@@ -1000,6 +1014,22 @@
 		BD0B2EF22C5998E600B3298F /* MealPresetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealPresetView.swift; sourceTree = "<group>"; };
 		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>"; };
 		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>"; };
 		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>"; };
+		BD249DA42D42FD9500412DEB /* CustomDatePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDatePicker.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>"; };
 		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>"; };
 		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>"; };
 		BD4064D02C4ED26900582F43 /* CoreDataObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataObserver.swift; sourceTree = "<group>"; };
@@ -1198,6 +1228,7 @@
 		DD3A3CE82D29C97800AE478E /* Helper+ButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Helper+ButtonStyles.swift"; sourceTree = "<group>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		DD5DC9F22CF3D9D600AB8703 /* AdjustmentsStateModel+TempTargets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentsStateModel+TempTargets.swift"; sourceTree = "<group>"; };
@@ -1207,15 +1238,16 @@
 		DD68889C2C386E17006E3C44 /* NightscoutExercise.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutExercise.swift; sourceTree = "<group>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		DD9ECB6D2CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigStateModel.swift; sourceTree = "<group>"; };
@@ -1244,7 +1276,7 @@
 		DDD163192C4C695E00CD525A /* EditOverrideForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditOverrideForm.swift; sourceTree = "<group>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		DDE179342C910127003CDDB7 /* LoopStatRecord+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LoopStatRecord+CoreDataClass.swift"; sourceTree = "<group>"; };
@@ -1571,6 +1603,7 @@
 		19F95FF129F10F9C00314DDC /* Stat */ = {
 		19F95FF129F10F9C00314DDC /* Stat */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				BD249D952D42FCA800412DEB /* StatStateModel+Setup */,
 				19F95FF229F10FBC00314DDC /* StatDataFlow.swift */,
 				19F95FF229F10FBC00314DDC /* StatDataFlow.swift */,
 				19F95FF429F10FCF00314DDC /* StatProvider.swift */,
 				19F95FF429F10FCF00314DDC /* StatProvider.swift */,
 				19F95FF629F10FEE00314DDC /* StatStateModel.swift */,
 				19F95FF629F10FEE00314DDC /* StatStateModel.swift */,
@@ -1582,9 +1615,9 @@
 		19F95FF829F10FF600314DDC /* View */ = {
 		19F95FF829F10FF600314DDC /* View */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				BD249D842D42FBD200412DEB /* ViewElements */,
 				19F95FF929F1102A00314DDC /* StatRootView.swift */,
 				19F95FF929F1102A00314DDC /* StatRootView.swift */,
-				19A9102F2A24BF6300C8951B /* StatsView.swift */,
-				19A910372A24EF3200C8951B /* ChartsView.swift */,
+				DD98ACBF2D71013200C0778F /* StatChartUtils.swift */,
 			);
 			);
 			path = View;
 			path = View;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -2059,6 +2092,7 @@
 		388E5A5925B6F0250019842D /* Models */ = {
 		388E5A5925B6F0250019842D /* Models */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				49249B372D46E76A000F4866 /* TDD.swift */,
 				DD4FFF322D458EE600B6CFF9 /* GarminWatchState.swift */,
 				DD4FFF322D458EE600B6CFF9 /* GarminWatchState.swift */,
 				DD3078692D42F94000DE0490 /* GarminDevice.swift */,
 				DD3078692D42F94000DE0490 /* GarminDevice.swift */,
 				DD3078672D42F5CE00DE0490 /* WatchGlucoseObject.swift */,
 				DD3078672D42F5CE00DE0490 /* WatchGlucoseObject.swift */,
@@ -2111,9 +2145,8 @@
 				BDC2EA462C3045AD00E5BBD0 /* Override.swift */,
 				BDC2EA462C3045AD00E5BBD0 /* Override.swift */,
 				DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */,
 				DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */,
 				DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */,
 				DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */,
-				DD6B7CB52C7B748B00B75029 /* TotalInsulinDisplayType.swift */,
 				DD9ECB692CA99F6C00AA7C45 /* PushMessage.swift */,
 				DD9ECB692CA99F6C00AA7C45 /* PushMessage.swift */,
-				DDD6D4D22CDE90720029439A /* HbA1cDisplayUnit.swift */,
+				DDD6D4D22CDE90720029439A /* EstimatedA1cDisplayUnit.swift */,
 			);
 			);
 			path = Models;
 			path = Models;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -2121,6 +2154,9 @@
 		388E5A5A25B6F05F0019842D /* Helpers */ = {
 		388E5A5A25B6F05F0019842D /* Helpers */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				BD249DA62D42FE3800412DEB /* Calendar+GlucoseStatsChart.swift */,
+				BD249DA42D42FD9500412DEB /* CustomDatePicker.swift */,
+				DD73FA0E2D74F57300D19D1E /* BackgroundTask+Helper.swift */,
 				CEF1ED6A2D58FB4600FAF41E /* CGMOptions.swift */,
 				CEF1ED6A2D58FB4600FAF41E /* CGMOptions.swift */,
 				C2A0A42E2CE0312C003B98E8 /* ConstantValues.swift */,
 				C2A0A42E2CE0312C003B98E8 /* ConstantValues.swift */,
 				DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */,
 				DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */,
@@ -2385,6 +2421,7 @@
 				58645B982CA2D1A4008AFCE7 /* GlucoseSetup.swift */,
 				58645B982CA2D1A4008AFCE7 /* GlucoseSetup.swift */,
 				58645B9A2CA2D24F008AFCE7 /* CarbSetup.swift */,
 				58645B9A2CA2D24F008AFCE7 /* CarbSetup.swift */,
 				58645B9C2CA2D275008AFCE7 /* DeterminationSetup.swift */,
 				58645B9C2CA2D275008AFCE7 /* DeterminationSetup.swift */,
+				49249B1B2D46E45E000F4866 /* CurrentTDDSetup.swift */,
 				58645B9E2CA2D2BE008AFCE7 /* PumpHistorySetup.swift */,
 				58645B9E2CA2D2BE008AFCE7 /* PumpHistorySetup.swift */,
 				58645BA02CA2D2F8008AFCE7 /* OverrideSetup.swift */,
 				58645BA02CA2D2F8008AFCE7 /* OverrideSetup.swift */,
 				58645BA22CA2D325008AFCE7 /* BatterySetup.swift */,
 				58645BA22CA2D325008AFCE7 /* BatterySetup.swift */,
@@ -2493,6 +2530,30 @@
 			path = View;
 			path = View;
 			sourceTree = "<group>";
 			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 */ = {
 		BD793CAD2CE7660C00D669AC /* Overrides */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
@@ -3024,6 +3085,43 @@
 			path = Nightscout;
 			path = Nightscout;
 			sourceTree = "<group>";
 			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 */ = {
 		DDD163032C4C67B400CD525A /* Adjustments */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
@@ -3595,6 +3693,7 @@
 				CE7CA3552A064973004BE681 /* ListStateIntent.swift in Sources */,
 				CE7CA3552A064973004BE681 /* ListStateIntent.swift in Sources */,
 				BDF530D82B40F8AC002CAF43 /* LockScreenView.swift in Sources */,
 				BDF530D82B40F8AC002CAF43 /* LockScreenView.swift in Sources */,
 				195D80B72AF697B800D25097 /* DynamicSettingsDataFlow.swift in Sources */,
 				195D80B72AF697B800D25097 /* DynamicSettingsDataFlow.swift in Sources */,
+				DD98ACC02D71013200C0778F /* StatChartUtils.swift in Sources */,
 				3862CC2E2743F9F700BF832C /* CalendarManager.swift in Sources */,
 				3862CC2E2743F9F700BF832C /* CalendarManager.swift in Sources */,
 				CEA4F62329BE10F70011ADF7 /* SavitzkyGolayFilter.swift in Sources */,
 				CEA4F62329BE10F70011ADF7 /* SavitzkyGolayFilter.swift in Sources */,
 				38B4F3C325E2A20B00E76A18 /* PumpSetupView.swift in Sources */,
 				38B4F3C325E2A20B00E76A18 /* PumpSetupView.swift in Sources */,
@@ -3609,6 +3708,7 @@
 				38AEE75225F022080013F05B /* SettingsManager.swift in Sources */,
 				38AEE75225F022080013F05B /* SettingsManager.swift in Sources */,
 				3894873A2614928B004DF424 /* DispatchTimer.swift in Sources */,
 				3894873A2614928B004DF424 /* DispatchTimer.swift in Sources */,
 				3895E4C625B9E00D00214B37 /* Preferences.swift in Sources */,
 				3895E4C625B9E00D00214B37 /* Preferences.swift in Sources */,
+				BD249DA52D42FD9700412DEB /* CustomDatePicker.swift in Sources */,
 				CE94598429E9E3E60047C9C6 /* WatchConfigStateModel.swift in Sources */,
 				CE94598429E9E3E60047C9C6 /* WatchConfigStateModel.swift in Sources */,
 				DD6B7CB92C7BAC6900B75029 /* NightscoutImportResultView.swift in Sources */,
 				DD6B7CB92C7BAC6900B75029 /* NightscoutImportResultView.swift in Sources */,
 				38DF1786276A73D400B3528F /* TagCloudView.swift in Sources */,
 				38DF1786276A73D400B3528F /* TagCloudView.swift in Sources */,
@@ -3654,7 +3754,6 @@
 				3811DE4125C9D4A100A708ED /* SettingsRootView.swift in Sources */,
 				3811DE4125C9D4A100A708ED /* SettingsRootView.swift in Sources */,
 				38192E04261B82FA0094D973 /* ReachabilityManager.swift in Sources */,
 				38192E04261B82FA0094D973 /* ReachabilityManager.swift in Sources */,
 				38E44539274E411700EC9A94 /* Disk+UIImage.swift in Sources */,
 				38E44539274E411700EC9A94 /* Disk+UIImage.swift in Sources */,
-				DD6B7CB62C7B748B00B75029 /* TotalInsulinDisplayType.swift in Sources */,
 				388E595C25AD948C0019842D /* TrioApp.swift in Sources */,
 				388E595C25AD948C0019842D /* TrioApp.swift in Sources */,
 				38FEF3FC2737E53800574A46 /* MainStateModel.swift in Sources */,
 				38FEF3FC2737E53800574A46 /* MainStateModel.swift in Sources */,
 				DD1745352C55AE7E00211FAC /* TargetBehavoirRootView.swift in Sources */,
 				DD1745352C55AE7E00211FAC /* TargetBehavoirRootView.swift in Sources */,
@@ -3672,13 +3771,15 @@
 				3811DE3125C9D49500A708ED /* HomeProvider.swift in Sources */,
 				3811DE3125C9D49500A708ED /* HomeProvider.swift in Sources */,
 				FE41E4D629463EE20047FD55 /* NightscoutPreferences.swift in Sources */,
 				FE41E4D629463EE20047FD55 /* NightscoutPreferences.swift in Sources */,
 				E013D872273AC6FE0014109C /* GlucoseSimulatorSource.swift in Sources */,
 				E013D872273AC6FE0014109C /* GlucoseSimulatorSource.swift in Sources */,
+				BD249D862D42FBEC00412DEB /* GlucoseMetricsView.swift in Sources */,
 				58645BA32CA2D325008AFCE7 /* BatterySetup.swift in Sources */,
 				58645BA32CA2D325008AFCE7 /* BatterySetup.swift in Sources */,
 				388E5A5C25B6F0770019842D /* JSON.swift in Sources */,
 				388E5A5C25B6F0770019842D /* JSON.swift in Sources */,
 				3811DF0225CA9FEA00A708ED /* Credentials.swift in Sources */,
 				3811DF0225CA9FEA00A708ED /* Credentials.swift in Sources */,
 				5837A5302BD2E3C700A5DC04 /* CarbEntryStored+helper.swift in Sources */,
 				5837A5302BD2E3C700A5DC04 /* CarbEntryStored+helper.swift in Sources */,
 				389A572026079BAA00BC102F /* Interpolation.swift in Sources */,
 				389A572026079BAA00BC102F /* Interpolation.swift in Sources */,
 				DD9ECB702CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.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 */,
 				DD32CF9A2CC8247B003686D6 /* TrioRemoteControl+Meal.swift in Sources */,
 				BDF34F832C10C5B600D51995 /* DataManager.swift in Sources */,
 				BDF34F832C10C5B600D51995 /* DataManager.swift in Sources */,
 				38B4F3C625E5017E00E76A18 /* NotificationCenter.swift in Sources */,
 				38B4F3C625E5017E00E76A18 /* NotificationCenter.swift in Sources */,
@@ -3691,6 +3792,7 @@
 				38A13D3225E28B4B00EAA382 /* PumpHistoryEvent.swift in Sources */,
 				38A13D3225E28B4B00EAA382 /* PumpHistoryEvent.swift in Sources */,
 				E00EEC0627368630002FF094 /* UIAssembly.swift in Sources */,
 				E00EEC0627368630002FF094 /* UIAssembly.swift in Sources */,
 				3811DE1825C9D40400A708ED /* Router.swift in Sources */,
 				3811DE1825C9D40400A708ED /* Router.swift in Sources */,
+				BD249DA12D42FD1200412DEB /* TDDSetup.swift in Sources */,
 				CE7950262998056D00FA576E /* CGMSetupView.swift in Sources */,
 				CE7950262998056D00FA576E /* CGMSetupView.swift in Sources */,
 				582FAE432C05102C00D1C13F /* CoreDataError.swift in Sources */,
 				582FAE432C05102C00D1C13F /* CoreDataError.swift in Sources */,
 				38A0363B25ECF07E00FCBB52 /* GlucoseStorage.swift in Sources */,
 				38A0363B25ECF07E00FCBB52 /* GlucoseStorage.swift in Sources */,
@@ -3709,7 +3811,7 @@
 				DDA6E3202D258E0500C2988C /* OverrideHelpView.swift in Sources */,
 				DDA6E3202D258E0500C2988C /* OverrideHelpView.swift in Sources */,
 				DDA6E2502D22187500C2988C /* ChartLegendView.swift in Sources */,
 				DDA6E2502D22187500C2988C /* ChartLegendView.swift in Sources */,
 				3811DEAF25C9D88300A708ED /* KeyValueStorage.swift in Sources */,
 				3811DEAF25C9D88300A708ED /* KeyValueStorage.swift in Sources */,
-				DDD6D4D32CDE90720029439A /* HbA1cDisplayUnit.swift in Sources */,
+				DDD6D4D32CDE90720029439A /* EstimatedA1cDisplayUnit.swift in Sources */,
 				DDA6E3572D25988500C2988C /* ContactImageHelpView.swift in Sources */,
 				DDA6E3572D25988500C2988C /* ContactImageHelpView.swift in Sources */,
 				38FE826D25CC8461001FF17A /* NightscoutAPI.swift in Sources */,
 				38FE826D25CC8461001FF17A /* NightscoutAPI.swift in Sources */,
 				388358C825EEF6D200E024B2 /* BasalProfileEntry.swift in Sources */,
 				388358C825EEF6D200E024B2 /* BasalProfileEntry.swift in Sources */,
@@ -3724,6 +3826,7 @@
 				CE1F6DE92BAF37C90064EB8D /* TidepoolConfigView.swift in Sources */,
 				CE1F6DE92BAF37C90064EB8D /* TidepoolConfigView.swift in Sources */,
 				3811DE5D25C9D4D500A708ED /* Publisher.swift in Sources */,
 				3811DE5D25C9D4D500A708ED /* Publisher.swift in Sources */,
 				E00EEC0727368630002FF094 /* APSAssembly.swift in Sources */,
 				E00EEC0727368630002FF094 /* APSAssembly.swift in Sources */,
+				BD249D8A2D42FC1200412DEB /* GlucosePercentileChart.swift in Sources */,
 				38B4F3AF25E2979F00E76A18 /* IndexedCollection.swift in Sources */,
 				38B4F3AF25E2979F00E76A18 /* IndexedCollection.swift in Sources */,
 				58D08B222C8DAA8E00AA37D3 /* OverrideView.swift in Sources */,
 				58D08B222C8DAA8E00AA37D3 /* OverrideView.swift in Sources */,
 				BD0B2EF32C5998E600B3298F /* MealPresetView.swift in Sources */,
 				BD0B2EF32C5998E600B3298F /* MealPresetView.swift in Sources */,
@@ -3731,6 +3834,7 @@
 				DD6D67E42C9C253500660C9B /* ColorSchemeOption.swift in Sources */,
 				DD6D67E42C9C253500660C9B /* ColorSchemeOption.swift in Sources */,
 				582DF9752C8CDB92001F516D /* GlucoseChartView.swift in Sources */,
 				582DF9752C8CDB92001F516D /* GlucoseChartView.swift in Sources */,
 				BD432CA12D2F4E3600D1EB79 /* WatchMessageKeys.swift in Sources */,
 				BD432CA12D2F4E3600D1EB79 /* WatchMessageKeys.swift in Sources */,
+				DD4C581F2D73C43D001BFF2C /* LoopStatsView.swift in Sources */,
 				58A3D53A2C96D4DE003F90FC /* AddTempTargetForm.swift in Sources */,
 				58A3D53A2C96D4DE003F90FC /* AddTempTargetForm.swift in Sources */,
 				DD1745302C55AE5300211FAC /* TargetBehaviorProvider.swift in Sources */,
 				DD1745302C55AE5300211FAC /* TargetBehaviorProvider.swift in Sources */,
 				58D08B382C8DFB6000AA37D3 /* BasalChart.swift in Sources */,
 				58D08B382C8DFB6000AA37D3 /* BasalChart.swift in Sources */,
@@ -3761,16 +3865,17 @@
 				3811DE6125C9D4D500A708ED /* ViewModifiers.swift in Sources */,
 				3811DE6125C9D4D500A708ED /* ViewModifiers.swift in Sources */,
 				3811DEAC25C9D88300A708ED /* NightscoutManager.swift in Sources */,
 				3811DEAC25C9D88300A708ED /* NightscoutManager.swift in Sources */,
 				BD793CB22CE8033500D669AC /* TempTargetRunStored.swift in Sources */,
 				BD793CB22CE8033500D669AC /* TempTargetRunStored.swift in Sources */,
-				19A910302A24BF6300C8951B /* StatsView.swift in Sources */,
 				BD7DA9A92AE06E9200601B20 /* BolusCalculatorStateModel.swift in Sources */,
 				BD7DA9A92AE06E9200601B20 /* BolusCalculatorStateModel.swift in Sources */,
 				CEB434E528B8FF5D00B70274 /* UIColor.swift in Sources */,
 				CEB434E528B8FF5D00B70274 /* UIColor.swift in Sources */,
 				190EBCCB29FF13CB00BA767D /* UserInterfaceSettingsRootView.swift in Sources */,
 				190EBCCB29FF13CB00BA767D /* UserInterfaceSettingsRootView.swift in Sources */,
 				3811DEA925C9D88300A708ED /* AppearanceManager.swift in Sources */,
 				3811DEA925C9D88300A708ED /* AppearanceManager.swift in Sources */,
+				BD249D8E2D42FC3900412DEB /* LoopBarChartView.swift in Sources */,
 				CE7950242997D81700FA576E /* CGMSettingsView.swift in Sources */,
 				CE7950242997D81700FA576E /* CGMSettingsView.swift in Sources */,
 				58237D9E2BCF0A6B00A47A79 /* PopupView.swift in Sources */,
 				58237D9E2BCF0A6B00A47A79 /* PopupView.swift in Sources */,
 				BD793CB02CE7C61500D669AC /* OverrideRunStored+helper.swift in Sources */,
 				BD793CB02CE7C61500D669AC /* OverrideRunStored+helper.swift in Sources */,
 				38D0B3D925EC07C400CB6E88 /* CarbsEntry.swift in Sources */,
 				38D0B3D925EC07C400CB6E88 /* CarbsEntry.swift in Sources */,
 				DD32CF9C2CC82499003686D6 /* TrioRemoteControl+TempTarget.swift in Sources */,
 				DD32CF9C2CC82499003686D6 /* TrioRemoteControl+TempTarget.swift in Sources */,
+				BD249D882D42FC0000412DEB /* BolusStatsView.swift in Sources */,
 				38A9260525F012D8009E3739 /* CarbRatios.swift in Sources */,
 				38A9260525F012D8009E3739 /* CarbRatios.swift in Sources */,
 				38FCF3D625E8FDF40078B0D1 /* MD5.swift in Sources */,
 				38FCF3D625E8FDF40078B0D1 /* MD5.swift in Sources */,
 				DDD163142C4C68D300CD525A /* AdjustmentsProvider.swift in Sources */,
 				DDD163142C4C68D300CD525A /* AdjustmentsProvider.swift in Sources */,
@@ -3778,6 +3883,7 @@
 				191F62682AD6B05A004D7911 /* NightscoutSettings.swift in Sources */,
 				191F62682AD6B05A004D7911 /* NightscoutSettings.swift in Sources */,
 				3811DEAB25C9D88300A708ED /* HTTPResponseStatus.swift in Sources */,
 				3811DEAB25C9D88300A708ED /* HTTPResponseStatus.swift in Sources */,
 				3811DE5F25C9D4D500A708ED /* ProgressBar.swift in Sources */,
 				3811DE5F25C9D4D500A708ED /* ProgressBar.swift in Sources */,
+				BD249D8C2D42FC2C00412DEB /* GlucoseDistributionChart.swift in Sources */,
 				38E87408274F9AD000975559 /* UserNotificationsManager.swift in Sources */,
 				38E87408274F9AD000975559 /* UserNotificationsManager.swift in Sources */,
 				CE82E02528E867BA00473A9C /* AlertStorage.swift in Sources */,
 				CE82E02528E867BA00473A9C /* AlertStorage.swift in Sources */,
 				DD1745372C55B74200211FAC /* AlgorithmSettings.swift in Sources */,
 				DD1745372C55B74200211FAC /* AlgorithmSettings.swift in Sources */,
@@ -3833,6 +3939,7 @@
 				1935364028496F7D001E0B16 /* Oref2_variables.swift in Sources */,
 				1935364028496F7D001E0B16 /* Oref2_variables.swift in Sources */,
 				CE2FAD3A297D93F0001A872C /* BloodGlucoseExtensions.swift in Sources */,
 				CE2FAD3A297D93F0001A872C /* BloodGlucoseExtensions.swift in Sources */,
 				38E4453A274E411700EC9A94 /* Disk+[UIImage].swift in Sources */,
 				38E4453A274E411700EC9A94 /* Disk+[UIImage].swift in Sources */,
+				DD73FA0F2D74F58E00D19D1E /* BackgroundTask+Helper.swift in Sources */,
 				72F1BD388F42FCA6C52E4500 /* ConfigEditorProvider.swift in Sources */,
 				72F1BD388F42FCA6C52E4500 /* ConfigEditorProvider.swift in Sources */,
 				E39E418C56A5A46B61D960EE /* ConfigEditorStateModel.swift in Sources */,
 				E39E418C56A5A46B61D960EE /* ConfigEditorStateModel.swift in Sources */,
 				45717281F743594AA9D87191 /* ConfigEditorRootView.swift in Sources */,
 				45717281F743594AA9D87191 /* ConfigEditorRootView.swift in Sources */,
@@ -3856,9 +3963,11 @@
 				491D6FBF2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift in Sources */,
 				491D6FBF2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift in Sources */,
 				491D6FC02D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift in Sources */,
 				491D6FC02D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift in Sources */,
 				DD1745442C55C60E00211FAC /* AutosensSettingsDataFlow.swift in Sources */,
 				DD1745442C55C60E00211FAC /* AutosensSettingsDataFlow.swift in Sources */,
+				BD249DA72D42FE4600412DEB /* Calendar+GlucoseStatsChart.swift in Sources */,
 				BDCAF2382C639F35002DC907 /* SettingItems.swift in Sources */,
 				BDCAF2382C639F35002DC907 /* SettingItems.swift in Sources */,
 				58D08B342C8DF9A700AA37D3 /* CobIobChart.swift in Sources */,
 				58D08B342C8DF9A700AA37D3 /* CobIobChart.swift in Sources */,
 				642F76A05A4FF530463A9FD0 /* NightscoutConfigRootView.swift in Sources */,
 				642F76A05A4FF530463A9FD0 /* NightscoutConfigRootView.swift in Sources */,
+				BD249D9B2D42FCDB00412DEB /* LoopChartSetup.swift in Sources */,
 				BD7DA9AC2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift in Sources */,
 				BD7DA9AC2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift in Sources */,
 				AD3D2CD42CD01B9EB8F26522 /* PumpConfigDataFlow.swift in Sources */,
 				AD3D2CD42CD01B9EB8F26522 /* PumpConfigDataFlow.swift in Sources */,
 				DD17452E2C55AE4800211FAC /* TargetBehavoirDataFlow.swift in Sources */,
 				DD17452E2C55AE4800211FAC /* TargetBehavoirDataFlow.swift in Sources */,
@@ -3923,6 +4032,7 @@
 				BD7DA9A72AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift in Sources */,
 				BD7DA9A72AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift in Sources */,
 				38192E0D261BAF980094D973 /* ConvenienceExtensions.swift in Sources */,
 				38192E0D261BAF980094D973 /* ConvenienceExtensions.swift in Sources */,
 				88AB39B23C9552BD6E0C9461 /* ISFEditorRootView.swift in Sources */,
 				88AB39B23C9552BD6E0C9461 /* ISFEditorRootView.swift in Sources */,
+				BD249D9D2D42FCF500412DEB /* MealStatsSetup.swift in Sources */,
 				BD6EB2D62C7D049B0086BBB6 /* LiveActivityWidgetConfiguration.swift in Sources */,
 				BD6EB2D62C7D049B0086BBB6 /* LiveActivityWidgetConfiguration.swift in Sources */,
 				DD32CF982CC82463003686D6 /* TrioRemoteControl+Bolus.swift in Sources */,
 				DD32CF982CC82463003686D6 /* TrioRemoteControl+Bolus.swift in Sources */,
 				F816825E28DB441200054060 /* HeartBeatManager.swift in Sources */,
 				F816825E28DB441200054060 /* HeartBeatManager.swift in Sources */,
@@ -3935,6 +4045,7 @@
 				38E44538274E411700EC9A94 /* Disk+[Data].swift in Sources */,
 				38E44538274E411700EC9A94 /* Disk+[Data].swift in Sources */,
 				98641AF4F92123DA668AB931 /* CarbRatioEditorRootView.swift in Sources */,
 				98641AF4F92123DA668AB931 /* CarbRatioEditorRootView.swift in Sources */,
 				BDF34F902C10CF8C00D51995 /* CoreDataStack.swift in Sources */,
 				BDF34F902C10CF8C00D51995 /* CoreDataStack.swift in Sources */,
+				49249B1C2D46E45E000F4866 /* CurrentTDDSetup.swift in Sources */,
 				CEE9A65C2BBB41C800EB5194 /* CalibrationService.swift in Sources */,
 				CEE9A65C2BBB41C800EB5194 /* CalibrationService.swift in Sources */,
 				110AEDED2C51A0AE00615CC9 /* ShortcutsConfigProvider.swift in Sources */,
 				110AEDED2C51A0AE00615CC9 /* ShortcutsConfigProvider.swift in Sources */,
 				38E4453D274E411700EC9A94 /* Disk+Errors.swift in Sources */,
 				38E4453D274E411700EC9A94 /* Disk+Errors.swift in Sources */,
@@ -3948,9 +4059,11 @@
 				DD17453A2C55BFA600211FAC /* AlgorithmAdvancedSettingsDataFlow.swift in Sources */,
 				DD17453A2C55BFA600211FAC /* AlgorithmAdvancedSettingsDataFlow.swift in Sources */,
 				5075C1608E6249A51495C422 /* TargetsEditorProvider.swift in Sources */,
 				5075C1608E6249A51495C422 /* TargetsEditorProvider.swift in Sources */,
 				E13B7DAB2A435F57066AF02E /* TargetsEditorStateModel.swift in Sources */,
 				E13B7DAB2A435F57066AF02E /* TargetsEditorStateModel.swift in Sources */,
+				BD249D992D42FCCD00412DEB /* BolusStatsSetup.swift in Sources */,
 				9702FF92A09C53942F20D7EA /* TargetsEditorRootView.swift in Sources */,
 				9702FF92A09C53942F20D7EA /* TargetsEditorRootView.swift in Sources */,
 				1967DFBE29D052C200759F30 /* Icons.swift in Sources */,
 				1967DFBE29D052C200759F30 /* Icons.swift in Sources */,
 				DDD163182C4C694000CD525A /* AdjustmentsRootView.swift in Sources */,
 				DDD163182C4C694000CD525A /* AdjustmentsRootView.swift in Sources */,
+				49249B382D46E76A000F4866 /* TDD.swift in Sources */,
 				389ECE052601144100D86C4F /* ConcurrentMap.swift in Sources */,
 				389ECE052601144100D86C4F /* ConcurrentMap.swift in Sources */,
 				110AEDEC2C51A0AE00615CC9 /* ShortcutsConfigDataFlow.swift in Sources */,
 				110AEDEC2C51A0AE00615CC9 /* ShortcutsConfigDataFlow.swift in Sources */,
 				CE7CA3562A064973004BE681 /* StateIntentRequest.swift in Sources */,
 				CE7CA3562A064973004BE681 /* StateIntentRequest.swift in Sources */,
@@ -3966,6 +4079,7 @@
 				DDAA29852D2D1D9E006546A1 /* AdjustmentsRootView+TempTargets.swift in Sources */,
 				DDAA29852D2D1D9E006546A1 /* AdjustmentsRootView+TempTargets.swift in Sources */,
 				E592A3792CEEC038009A472C /* ContactImageRootView.swift in Sources */,
 				E592A3792CEEC038009A472C /* ContactImageRootView.swift in Sources */,
 				BDC531182D1062F200088832 /* ContactImageState.swift in Sources */,
 				BDC531182D1062F200088832 /* ContactImageState.swift in Sources */,
+				BD249D9F2D42FD0600412DEB /* StackedChartSetup.swift in Sources */,
 				E592A37A2CEEC038009A472C /* ContactImageProvider.swift in Sources */,
 				E592A37A2CEEC038009A472C /* ContactImageProvider.swift in Sources */,
 				CE82E02728E869DF00473A9C /* AlertEntry.swift in Sources */,
 				CE82E02728E869DF00473A9C /* AlertEntry.swift in Sources */,
 				DD30786A2D42F94000DE0490 /* GarminDevice.swift in Sources */,
 				DD30786A2D42F94000DE0490 /* GarminDevice.swift in Sources */,
@@ -3977,6 +4091,7 @@
 				38AAF85525FFF846004AF583 /* CurrentGlucoseView.swift in Sources */,
 				38AAF85525FFF846004AF583 /* CurrentGlucoseView.swift in Sources */,
 				041D1E995A6AE92E9289DC49 /* TreatmentsDataFlow.swift in Sources */,
 				041D1E995A6AE92E9289DC49 /* TreatmentsDataFlow.swift in Sources */,
 				DD32CF9E2CC824C5003686D6 /* TrioRemoteControl+Override.swift in Sources */,
 				DD32CF9E2CC824C5003686D6 /* TrioRemoteControl+Override.swift in Sources */,
+				BD249D922D42FC5300412DEB /* GlucoseSectorChart.swift in Sources */,
 				23888883D4EA091C88480FF2 /* TreatmentsProvider.swift in Sources */,
 				23888883D4EA091C88480FF2 /* TreatmentsProvider.swift in Sources */,
 				38E98A2D25F52DC400C0CED0 /* NSLocking+Extensions.swift in Sources */,
 				38E98A2D25F52DC400C0CED0 /* NSLocking+Extensions.swift in Sources */,
 				715120D22D3C2BB4005D9FB6 /* GlucoseNotificationsOption.swift in Sources */,
 				715120D22D3C2BB4005D9FB6 /* GlucoseNotificationsOption.swift in Sources */,
@@ -4029,6 +4144,7 @@
 				6EADD581738D64431902AC0A /* (null) in Sources */,
 				6EADD581738D64431902AC0A /* (null) in Sources */,
 				CE94598729E9E4110047C9C6 /* WatchConfigRootView.swift in Sources */,
 				CE94598729E9E4110047C9C6 /* WatchConfigRootView.swift in Sources */,
 				DD5DC9FB2CF3E1B100AB8703 /* AdjustmentsStateModel+Helpers.swift in Sources */,
 				DD5DC9FB2CF3E1B100AB8703 /* AdjustmentsStateModel+Helpers.swift in Sources */,
+				BD249D972D42FCBF00412DEB /* AreaChartSetup.swift in Sources */,
 				DDF847E42C5C288F0049BB3B /* LiveActivitySettingsRootView.swift in Sources */,
 				DDF847E42C5C288F0049BB3B /* LiveActivitySettingsRootView.swift in Sources */,
 				DD88C8E22C50420800F2D558 /* DefinitionRow.swift in Sources */,
 				DD88C8E22C50420800F2D558 /* DefinitionRow.swift in Sources */,
 				B7C465E9472624D8A2BE2A6A /* (null) in Sources */,
 				B7C465E9472624D8A2BE2A6A /* (null) in Sources */,

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 1
Trio/Resources/javascript/bundle/determine-basal.js


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

@@ -21,14 +21,13 @@
   "carbsRequiredThreshold" : 10,
   "carbsRequiredThreshold" : 10,
   "showCarbsRequiredBadge" : true,
   "showCarbsRequiredBadge" : true,
   "useFPUconversion" : true,
   "useFPUconversion" : true,
-  "totalInsulinDisplayType": "totalDailyDose",
   "individualAdjustmentFactor" : 0.5,
   "individualAdjustmentFactor" : 0.5,
   "timeCap" : 8,
   "timeCap" : 8,
   "minuteInterval" : 30,
   "minuteInterval" : 30,
   "delay" : 60,
   "delay" : 60,
   "useAppleHealth" : false,
   "useAppleHealth" : false,
   "smoothGlucose" : false,
   "smoothGlucose" : false,
-  "hbA1cDisplayUnit" : "percent",
+  "eA1cDisplayUnit" : "percent",
   "high" : 180,
   "high" : 180,
   "low" : 70,
   "low" : 70,
   "hours" : 6,
   "hours" : 6,

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

@@ -81,7 +81,7 @@ final class BaseAPSManager: APSManager, Injectable {
 
 
     private var lifetime = Lifetime()
     private var lifetime = Lifetime()
 
 
-    private var backGroundTaskID: UIBackgroundTaskIdentifier?
+    private var backgroundTaskID: UIBackgroundTaskIdentifier?
 
 
     var pumpManager: PumpManagerUI? {
     var pumpManager: PumpManagerUI? {
         get { deviceDataManager.pumpManager }
         get { deviceDataManager.pumpManager }
@@ -224,7 +224,7 @@ final class BaseAPSManager: APSManager, Injectable {
             // Cleanup background task
             // Cleanup background task
             if let backgroundTask = backgroundTask {
             if let backgroundTask = backgroundTask {
                 await UIApplication.shared.endBackgroundTask(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?) {
     private func setupLoop() async -> (LoopStats, UIBackgroundTaskIdentifier?) {
         // Start background task
         // Start background task
         let backgroundTask = await UIApplication.shared.beginBackgroundTask(withName: "Loop starting") { [weak self] in
         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 {
             Task {
                 UIApplication.shared.endBackgroundTask(backgroundTask)
                 UIApplication.shared.endBackgroundTask(backgroundTask)
             }
             }
-            self.backGroundTaskID = .invalid
+            self.backgroundTaskID = .invalid
         }
         }
-        backGroundTaskID = backgroundTask
+        backgroundTaskID = backgroundTask
 
 
         // Set loop start time
         // Set loop start time
         lastLoopStartDate = Date()
         lastLoopStartDate = Date()
@@ -325,9 +325,9 @@ final class BaseAPSManager: APSManager, Injectable {
 
 
         if let error = error {
         if let error = error {
             warning(.apsManager, "Loop failed with error: \(error.localizedDescription)")
             warning(.apsManager, "Loop failed with error: \(error.localizedDescription)")
-            if let backgroundTask = backGroundTaskID {
+            if let backgroundTask = backgroundTaskID {
                 await UIApplication.shared.endBackgroundTask(backgroundTask)
                 await UIApplication.shared.endBackgroundTask(backgroundTask)
-                backGroundTaskID = .invalid
+                backgroundTaskID = .invalid
             }
             }
             processError(error)
             processError(error)
         } else {
         } else {
@@ -343,9 +343,9 @@ final class BaseAPSManager: APSManager, Injectable {
         }
         }
 
 
         // End of the BG tasks
         // End of the BG tasks
-        if let backgroundTask = backGroundTaskID {
+        if let backgroundTask = backgroundTaskID {
             await UIApplication.shared.endBackgroundTask(backgroundTask)
             await UIApplication.shared.endBackgroundTask(backgroundTask)
-            backGroundTaskID = .invalid
+            backgroundTaskID = .invalid
         }
         }
     }
     }
 
 
@@ -973,7 +973,8 @@ final class BaseAPSManager: APSManager, Injectable {
                 total_average: 0
                 total_average: 0
             )
             )
             guard let processedGlucoseStats = await glucoseStats else { return }
             guard let processedGlucoseStats = await glucoseStats else { return }
-            let hbA1cDisplayUnit = processedGlucoseStats.hbA1cDisplayUnit
+
+            let eA1cDisplayUnit = processedGlucoseStats.eA1cDisplayUnit
 
 
             let dailystat = await Statistics(
             let dailystat = await Statistics(
                 created_at: Date(),
                 created_at: Date(),
@@ -995,8 +996,8 @@ final class BaseAPSManager: APSManager, Injectable {
                 Statistics: Stats(
                 Statistics: Stats(
                     Distribution: processedGlucoseStats.TimeInRange,
                     Distribution: processedGlucoseStats.TimeInRange,
                     Glucose: processedGlucoseStats.avg,
                     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,
                     LoopCycles: loopStats,
                     Insulin: insulin,
                     Insulin: insulin,
                     Variance: processedGlucoseStats.variance
                     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 -> (
     private func glucoseForStats() async -> (
         oneDayGlucose: (ifcc: Double, ngsp: Double, average: Double, median: Double, sd: Double, cv: Double, readings: Double),
         oneDayGlucose: (ifcc: Double, ngsp: Double, average: Double, median: Double, sd: Double, cv: Double, readings: Double),
-        hbA1cDisplayUnit: HbA1cDisplayUnit,
+        eA1cDisplayUnit: EstimatedA1cDisplayUnit,
         numberofDays: Double,
         numberofDays: Double,
         TimeInRange: TIRs,
         TimeInRange: TIRs,
         avg: Averages,
         avg: Averages,
@@ -1202,19 +1168,19 @@ final class BaseAPSManager: APSManager, Injectable {
                     total: self.roundDecimal(Decimal(totalDaysGlucose.median), 1)
                     total: self.roundDecimal(Decimal(totalDaysGlucose.median), 1)
                 )
                 )
 
 
-                let hbA1cDisplayUnit = self.settingsManager.settings.hbA1cDisplayUnit
+                let eA1cDisplayUnit = self.settingsManager.settings.eA1cDisplayUnit
 
 
                 let hbs = Durations(
                 let hbs = Durations(
-                    day: hbA1cDisplayUnit == .mmolMol ?
+                    day: eA1cDisplayUnit == .mmolMol ?
                         self.roundDecimal(Decimal(oneDayGlucose.ifcc), 1) :
                         self.roundDecimal(Decimal(oneDayGlucose.ifcc), 1) :
                         self.roundDecimal(Decimal(oneDayGlucose.ngsp), 1),
                         self.roundDecimal(Decimal(oneDayGlucose.ngsp), 1),
-                    week: hbA1cDisplayUnit == .mmolMol ?
+                    week: eA1cDisplayUnit == .mmolMol ?
                         self.roundDecimal(Decimal(sevenDaysGlucose.ifcc), 1) :
                         self.roundDecimal(Decimal(sevenDaysGlucose.ifcc), 1) :
                         self.roundDecimal(Decimal(sevenDaysGlucose.ngsp), 1),
                         self.roundDecimal(Decimal(sevenDaysGlucose.ngsp), 1),
-                    month: hbA1cDisplayUnit == .mmolMol ?
+                    month: eA1cDisplayUnit == .mmolMol ?
                         self.roundDecimal(Decimal(thirtyDaysGlucose.ifcc), 1) :
                         self.roundDecimal(Decimal(thirtyDaysGlucose.ifcc), 1) :
                         self.roundDecimal(Decimal(thirtyDaysGlucose.ngsp), 1),
                         self.roundDecimal(Decimal(thirtyDaysGlucose.ngsp), 1),
-                    total: hbA1cDisplayUnit == .mmolMol ?
+                    total: eA1cDisplayUnit == .mmolMol ?
                         self.roundDecimal(Decimal(totalDaysGlucose.ifcc), 1) :
                         self.roundDecimal(Decimal(totalDaysGlucose.ifcc), 1) :
                         self.roundDecimal(Decimal(totalDaysGlucose.ngsp), 1)
                         self.roundDecimal(Decimal(totalDaysGlucose.ngsp), 1)
                 )
                 )
@@ -1289,7 +1255,7 @@ final class BaseAPSManager: APSManager, Injectable {
                 )
                 )
                 let variance = Variance(SD: standardDeviations, CV: cvs)
                 let variance = Variance(SD: standardDeviations, CV: cvs)
 
 
-                return (oneDayGlucose, hbA1cDisplayUnit, numberOfDays, TimeInRange, avg, hbs, variance)
+                return (oneDayGlucose, eA1cDisplayUnit, numberOfDays, TimeInRange, avg, hbs, variance)
             }
             }
         } catch {
         } catch {
             debug(
             debug(

+ 63 - 31
Trio/Sources/APS/CGM/PluginSource.swift

@@ -105,53 +105,81 @@ extension PluginSource: CGMManagerDelegate {
     func recordRetractedAlert(_: LoopKit.Alert, at _: Date) {}
     func recordRetractedAlert(_: LoopKit.Alert, at _: Date) {}
 
 
     func cgmManagerWantsDeletion(_ manager: CGMManager) {
     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) {
     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]) {
     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? {
     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) {
     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 {
     func credentialStoragePrefix(for _: CGMManager) -> String {
@@ -162,7 +190,11 @@ extension PluginSource: CGMManagerDelegate {
     func cgmManager(_: CGMManager, didUpdate status: CGMManagerStatus) {
     func cgmManager(_: CGMManager, didUpdate status: CGMManagerStatus) {
         debug(.deviceManager, "CGM Manager did update state to \(status)")
         debug(.deviceManager, "CGM Manager did update state to \(status)")
 
 
-        processQueue.async {
+        processQueue.async { [weak self] in
+            guard let self = self else { return }
+
+            dispatchPrecondition(condition: .onQueue(self.processQueue))
+
             if self.cgmHasValidSensorSession != status.hasValidSensorSession {
             if self.cgmHasValidSensorSession != status.hasValidSensorSession {
                 self.cgmHasValidSensorSession = status.hasValidSensorSession
                 self.cgmHasValidSensorSession = status.hasValidSensorSession
             }
             }

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

@@ -164,13 +164,9 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         debug(.apsManager, "plugin : \(String(describing: cgmManager?.pluginIdentifier))")
         debug(.apsManager, "plugin : \(String(describing: cgmManager?.pluginIdentifier))")
 
 
         if let manager = newManager {
         if let manager = newManager {
-            // 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
-                removeCalibrations()
-                cgmManager = manager
-                glucoseSource = nil
-            }
+            removeCalibrations()
+            cgmManager = manager
+            glucoseSource = nil
         } else if self.cgmGlucoseSourceType == .plugin, cgmManager == nil, let rawCGMManager = rawCGMManager {
         } else if self.cgmGlucoseSourceType == .plugin, cgmManager == nil, let rawCGMManager = rawCGMManager {
             cgmManager = cgmManagerFromRawValue(rawCGMManager)
             cgmManager = cgmManagerFromRawValue(rawCGMManager)
             updateManagerUnits(cgmManager)
             updateManagerUnits(cgmManager)
@@ -248,29 +244,22 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         var filteredByDate: [BloodGlucose] = []
         var filteredByDate: [BloodGlucose] = []
         var filtered: [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 }
         filteredByDate = newGlucose.filter { $0.dateString > syncDate }
         filtered = glucoseStorage.filterTooFrequentGlucose(filteredByDate, at: 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")
         debug(.deviceManager, "New glucose found")
 
 
         // filter the data if it is the case
         // filter the data if it is the case
@@ -289,6 +278,8 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
 
 
         try await glucoseStorage.storeGlucose(filtered)
         try await glucoseStorage.storeGlucose(filtered)
         deviceDataManager.heartbeat(date: Date())
         deviceDataManager.heartbeat(date: Date())
+
+        endBackgroundTaskSafely(&backgroundTaskID, taskName: "Glucose Store and Heartbeat Decision")
     }
     }
 
 
     func sourceInfo() -> [String: Any]? {
     func sourceInfo() -> [String: Any]? {

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

@@ -34,7 +34,6 @@ final class OpenAPS {
         await context.perform {
         await context.perform {
             let newOrefDetermination = OrefDetermination(context: self.context)
             let newOrefDetermination = OrefDetermination(context: self.context)
             newOrefDetermination.id = UUID()
             newOrefDetermination.id = UUID()
-            newOrefDetermination.totalDailyDose = self.decimalToNSDecimalNumber(determination.tdd)
             newOrefDetermination.insulinSensitivity = self.decimalToNSDecimalNumber(determination.isf)
             newOrefDetermination.insulinSensitivity = self.decimalToNSDecimalNumber(determination.isf)
             newOrefDetermination.currentTarget = self.decimalToNSDecimalNumber(determination.current_target)
             newOrefDetermination.currentTarget = self.decimalToNSDecimalNumber(determination.current_target)
             newOrefDetermination.eventualBG = determination.eventualBG.map(NSDecimalNumber.init)
             newOrefDetermination.eventualBG = determination.eventualBG.map(NSDecimalNumber.init)
@@ -55,9 +54,6 @@ final class OpenAPS {
             newOrefDetermination.expectedDelta = self.decimalToNSDecimalNumber(determination.expectedDelta)
             newOrefDetermination.expectedDelta = self.decimalToNSDecimalNumber(determination.expectedDelta)
             newOrefDetermination.cob = Int16(Int(determination.cob ?? 0))
             newOrefDetermination.cob = Int16(Int(determination.cob ?? 0))
             newOrefDetermination.manualBolusErrorString = self.decimalToNSDecimalNumber(determination.manualBolusErrorString)
             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.smbToDeliver = determination.units.map { NSDecimalNumber(decimal: $0) }
             newOrefDetermination.carbsRequired = Int16(Int(determination.carbsReq ?? 0))
             newOrefDetermination.carbsRequired = Int16(Int(determination.carbsReq ?? 0))
             newOrefDetermination.isUploadedToNS = false
             newOrefDetermination.isUploadedToNS = false
@@ -392,16 +388,16 @@ final class OpenAPS {
             let overrideTargetBG = activeOverrides.first?.target?.decimalValue ?? 0
             let overrideTargetBG = activeOverrides.first?.target?.decimalValue ?? 0
 
 
             // Calculate averages for Total Daily Dose (TDD)
             // 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)
             let totalDaysCount = max(historicalTDDData.count, 1)
 
 
             // Fetch recent TDD data for the past two hours
             // 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 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, +)
                 .reduce(0, +)
 
 
-            let currentTDD = historicalTDDData.last?["totalDailyDose"] as? Decimal ?? 0
+            let currentTDD = historicalTDDData.last?["total"] as? Decimal ?? 0
             let averageTDDLastTwoHours = recentTotalTDD / Decimal(recentDataCount)
             let averageTDDLastTwoHours = recentTotalTDD / Decimal(recentDataCount)
             let averageTDDLastTenDays = totalTDD / Decimal(totalDaysCount)
             let averageTDDLastTenDays = totalTDD / Decimal(totalDaysCount)
             let weightedTDD = weightPercentage * averageTDDLastTwoHours + (1 - weightPercentage) * averageTDDLastTenDays
             let weightedTDD = weightPercentage * averageTDDLastTwoHours + (1 - weightPercentage) * averageTDDLastTenDays
@@ -410,6 +406,7 @@ final class OpenAPS {
             let oref2Data = Oref2_variables(
             let oref2Data = Oref2_variables(
                 average_total_data: currentTDD > 0 ? averageTDDLastTenDays : 0,
                 average_total_data: currentTDD > 0 ? averageTDDLastTenDays : 0,
                 weightedAverage: currentTDD > 0 ? weightedTDD : 1,
                 weightedAverage: currentTDD > 0 ? weightedTDD : 1,
+                currentTDD: currentTDD,
                 past2hoursAverage: currentTDD > 0 ? averageTDDLastTwoHours : 0,
                 past2hoursAverage: currentTDD > 0 ? averageTDDLastTwoHours : 0,
                 date: Date(),
                 date: Date(),
                 overridePercentage: overridePercentage,
                 overridePercentage: overridePercentage,
@@ -518,6 +515,11 @@ final class OpenAPS {
                 adjustedPreferences.halfBasalExerciseTarget = activeHBT
                 adjustedPreferences.halfBasalExerciseTarget = activeHBT
                 debug(.openAPS, "Updated halfBasalExerciseTarget to active Temp Target value: \(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 {
         do {
@@ -831,12 +833,12 @@ extension OpenAPS {
 
 
     func fetchHistoricalTDDData(from date: Date) throws -> [[String: Any]] {
     func fetchHistoricalTDDData(from date: Date) throws -> [[String: Any]] {
         try CoreDataStack.shared.fetchEntities(
         try CoreDataStack.shared.fetchEntities(
-            ofType: OrefDetermination.self,
+            ofType: TDDStored.self,
             onContext: context,
             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,
             ascending: true,
-            propertiesToFetch: ["timestamp", "totalDailyDose"]
+            propertiesToFetch: ["date", "total"]
         ) as? [[String: Any]] ?? []
         ) 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),
                         reservoir: self.decimal(from: orefDetermination.reservoir),
                         isf: self.decimal(from: orefDetermination.insulinSensitivity),
                         isf: self.decimal(from: orefDetermination.insulinSensitivity),
                         timestamp: orefDetermination.timestamp,
                         timestamp: orefDetermination.timestamp,
-                        tdd: self.decimal(from: orefDetermination.totalDailyDose),
-                        insulin: nil,
                         current_target: self.decimal(from: orefDetermination.currentTarget),
                         current_target: self.decimal(from: orefDetermination.currentTarget),
                         insulinForManualBolus: self.decimal(from: orefDetermination.insulinForManualBolus),
                         insulinForManualBolus: self.decimal(from: orefDetermination.insulinForManualBolus),
                         manualBolusErrorString: self.decimal(from: orefDetermination.manualBolusErrorString),
                         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 deleteOverridePreset(_ objectID: NSManagedObjectID) async
     func getOverridesNotYetUploadedToNightscout() async throws -> [NightscoutExercise]
     func getOverridesNotYetUploadedToNightscout() async throws -> [NightscoutExercise]
     func getOverrideRunsNotYetUploadedToNightscout() 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 getPresetOverridesForNightscout() async throws -> [NightscoutPresetOverride]
     func fetchLatestActiveOverride() async throws -> NSManagedObjectID?
     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] {
     func getPresetOverridesForNightscout() async throws -> [NightscoutPresetOverride] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
             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 bolusString = String(format: "%.2f", NSDecimalNumber(decimal: bolus.rounded(toPlaces: 2)).doubleValue)
         let tempBasalString = String(format: "%.2f", NSDecimalNumber(decimal: temp.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 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)
         let hoursString = String(format: "%.5f", NSDecimalNumber(decimal: Decimal(hours).truncated(toPlaces: 5)).doubleValue)
 
 
         debug(.apsManager, """
         debug(.apsManager, """

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

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

+ 56 - 0
Trio/Sources/Helpers/CustomDatePicker.swift

@@ -0,0 +1,56 @@
+import SwiftUI
+
+struct CustomDatePicker: UIViewRepresentable {
+    @Binding var selection: Date
+
+    // Coordinator to handle date changes
+    class Coordinator: NSObject {
+        var parent: CustomDatePicker
+
+        init(_ parent: CustomDatePicker) {
+            self.parent = parent
+        }
+
+        @objc func dateChanged(_ sender: UIDatePicker) {
+            let calendar = Calendar.current
+            // Set the time of the selected date to 23:59:59 for any selected date
+            if let adjustedDate = calendar.date(bySettingHour: 23, minute: 59, second: 59, of: sender.date) {
+                parent.selection = adjustedDate
+            } else {
+                parent.selection = sender.date // Fallback in case something goes wrong
+            }
+        }
+    }
+
+    func makeUIView(context: Context) -> UIDatePicker {
+        let datePicker = UIDatePicker()
+        datePicker.datePickerMode = .date
+
+        // Calculate yesterday's date at 23:59:59
+        let today = Date()
+        let calendar = Calendar.current
+        if let yesterday = calendar.date(byAdding: .day, value: -1, to: today),
+           let adjustedYesterday = calendar.date(bySettingHour: 23, minute: 59, second: 59, of: yesterday)
+        {
+            datePicker.maximumDate = adjustedYesterday // Set maximum date to yesterday at 23:59:59
+            datePicker.date = adjustedYesterday // Set default date to yesterday at 23:59:59
+        }
+
+        // Set up the date change action
+        datePicker.addTarget(context.coordinator, action: #selector(Coordinator.dateChanged(_:)), for: .valueChanged)
+
+        return datePicker
+    }
+
+    func updateUIView(_ uiView: UIDatePicker, context _: Context) {
+        // Ensure the displayed date is also adjusted to 23:59:59
+        let calendar = Calendar.current
+        if let adjustedDate = calendar.date(bySettingHour: 23, minute: 59, second: 59, of: selection) {
+            uiView.date = adjustedDate
+        }
+    }
+
+    func makeCoordinator() -> Coordinator {
+        Coordinator(self)
+    }
+}

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

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

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 46913 - 31406
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 {
     var displayName: String {
         switch self {
         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?
     let reservoir: Decimal?
     var isf: Decimal?
     var isf: Decimal?
     var timestamp: Date?
     var timestamp: Date?
-    let tdd: Decimal?
-    let insulin: Insulin?
     var current_target: Decimal?
     var current_target: Decimal?
     let insulinForManualBolus: Decimal?
     let insulinForManualBolus: Decimal?
     let manualBolusErrorString: Decimal?
     let manualBolusErrorString: Decimal?
@@ -40,13 +38,6 @@ struct Predictions: JSON, Equatable {
     let uam: [Int]?
     let uam: [Int]?
 }
 }
 
 
-struct Insulin: JSON, Equatable {
-    let TDD: Decimal?
-    let bolus: Decimal?
-    let temp_basal: Decimal?
-    let scheduled_basal: Decimal?
-}
-
 extension Determination {
 extension Determination {
     private enum CodingKeys: String, CodingKey {
     private enum CodingKeys: String, CodingKey {
         case id
         case id
@@ -67,8 +58,6 @@ extension Determination {
         case reservoir
         case reservoir
         case timestamp
         case timestamp
         case isf = "ISF"
         case isf = "ISF"
-        case tdd = "TDD"
-        case insulin
         case current_target
         case current_target
         case insulinForManualBolus
         case insulinForManualBolus
         case manualBolusErrorString
         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 {
 protocol DeterminationObserver {
     func determinationDidUpdate(_ determination: Determination)
     func determinationDidUpdate(_ determination: Determination)
 }
 }

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

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

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

@@ -10,9 +10,9 @@ public enum GlucoseColorScheme: String, JSON, CaseIterable, Identifiable, Codabl
     var displayName: String {
     var displayName: String {
         switch self {
         switch self {
         case .staticColor:
         case .staticColor:
-            return "Static"
+            return String(localized: "Static")
         case .dynamicColor:
         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.
 //  Created by Kimberlie Skandis on 1/18/25.
 //
 //
 import Foundation
 import Foundation
+import SwiftUI
 
 
 public enum GlucoseNotificationsOption: String, JSON, CaseIterable, Identifiable, Codable, Hashable {
 public enum GlucoseNotificationsOption: String, JSON, CaseIterable, Identifiable, Codable, Hashable {
-    public var id: String { rawValue }
     case disabled
     case disabled
     case alwaysEveryCGM
     case alwaysEveryCGM
     case onlyAlarmLimits
     case onlyAlarmLimits
 
 
+    public var id: String { rawValue }
+
     var displayName: String {
     var displayName: String {
         switch self {
         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")
         }
         }
     }
     }
 }
 }

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

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

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

@@ -117,7 +117,7 @@ struct Durations: JSON, Equatable {
 
 
 struct Units: JSON, Equatable {
 struct Units: JSON, Equatable {
     var Glucose: String
     var Glucose: String
-    var HbA1c: String
+    var EstimatedA1c: String
 }
 }
 
 
 struct Threshold: JSON, Equatable {
 struct Threshold: JSON, Equatable {
@@ -149,7 +149,7 @@ struct Variance: JSON, Equatable {
 struct Stats: JSON, Equatable {
 struct Stats: JSON, Equatable {
     var Distribution: TIRs
     var Distribution: TIRs
     var Glucose: Averages
     var Glucose: Averages
-    var HbA1c: Durations
+    var EstimatedA1c: Durations
     var Units: Units
     var Units: Units
     var LoopCycles: LoopCycles
     var LoopCycles: LoopCycles
     var Insulin: Ins
     var Insulin: Ins
@@ -211,7 +211,7 @@ extension Stats {
     private enum CodingKeys: String, CodingKey {
     private enum CodingKeys: String, CodingKey {
         case Distribution
         case Distribution
         case Glucose
         case Glucose
-        case HbA1c
+        case EstimatedA1c
         case Units
         case Units
         case LoopCycles
         case LoopCycles
         case Insulin
         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 - 8
Trio/Sources/Models/TrioSettings.swift

@@ -42,14 +42,13 @@ struct TrioSettings: JSON, Equatable {
     var carbsRequiredThreshold: Decimal = 10
     var carbsRequiredThreshold: Decimal = 10
     var showCarbsRequiredBadge: Bool = true
     var showCarbsRequiredBadge: Bool = true
     var useFPUconversion: Bool = true
     var useFPUconversion: Bool = true
-    var totalInsulinDisplayType: TotalInsulinDisplayType = .totalDailyDose
     var individualAdjustmentFactor: Decimal = 0.5
     var individualAdjustmentFactor: Decimal = 0.5
     var timeCap: Int = 8
     var timeCap: Int = 8
     var minuteInterval: Int = 30
     var minuteInterval: Int = 30
     var delay: Int = 60
     var delay: Int = 60
     var useAppleHealth: Bool = false
     var useAppleHealth: Bool = false
     var smoothGlucose: Bool = false
     var smoothGlucose: Bool = false
-    var hbA1cDisplayUnit: HbA1cDisplayUnit = .percent
+    var eA1cDisplayUnit: EstimatedA1cDisplayUnit = .percent
     var high: Decimal = 180
     var high: Decimal = 180
     var low: Decimal = 70
     var low: Decimal = 70
     var hours: Int = 6
     var hours: Int = 6
@@ -145,10 +144,6 @@ extension TrioSettings: Decodable {
             settings.useFPUconversion = useFPUconversion
             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) {
         if let individualAdjustmentFactor = try? container.decode(Decimal.self, forKey: .individualAdjustmentFactor) {
             settings.individualAdjustmentFactor = individualAdjustmentFactor
             settings.individualAdjustmentFactor = individualAdjustmentFactor
         }
         }
@@ -275,8 +270,8 @@ extension TrioSettings: Decodable {
             settings.forecastDisplayType = forecastDisplayType
             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) {
         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)
         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 hours = durationInMinutes / 60
         let minutes = durationInMinutes % 60
         let minutes = durationInMinutes % 60
 
 
         switch (hours, minutes) {
         switch (hours, minutes) {
         case let (0, m):
         case let (0, m):
-            return "\(m) min"
+            return "\(m)\u{00A0}" + String(localized: "m", comment: "Abbreviation for Minutes")
         case let (h, 0):
         case let (h, 0):
-            return "\(h) hr"
+            return "\(h)\u{00A0}" + String(localized: "h", comment: "h")
         default:
         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 Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
+import SwiftUICore
 
 
 extension Adjustments.StateModel {
 extension Adjustments.StateModel {
     // MARK: - Enact Overrides
     // MARK: - Enact Overrides
@@ -370,14 +371,38 @@ extension Adjustments.StateModel {
 }
 }
 
 
 enum IsfAndOrCrOptions: String, CaseIterable {
 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 {
 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.
     /// Determines if sensitivity adjustment is enabled based on target.
     func isAdjustSensEnabled(usingTarget initialTarget: Decimal? = nil) -> Bool {
     func isAdjustSensEnabled(usingTarget initialTarget: Decimal? = nil) -> Bool {
         let target = initialTarget ?? tempTargetTarget
         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 }
         if target > normalTarget, highTTraisesSens || isExerciseModeActive { return true }
         return false
         return false
     }
     }
@@ -416,8 +416,9 @@ extension Adjustments.StateModel {
     /// Computes the high value for the slider based on the target.
     /// Computes the high value for the slider based on the target.
     func computeSliderHigh(usingTarget initialTarget: Decimal? = nil) -> Double {
     func computeSliderHigh(usingTarget initialTarget: Decimal? = nil) -> Double {
         let calcTarget = initialTarget ?? tempTargetTarget
         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
         return maxSens
     }
     }
 
 
@@ -431,10 +432,10 @@ extension Adjustments.StateModel {
         let deviationFromNormal = halfBasalTargetValue - normalTarget
         let deviationFromNormal = halfBasalTargetValue - normalTarget
 
 
         let adjustmentFactor = deviationFromNormal + (calcTarget - normalTarget)
         let adjustmentFactor = deviationFromNormal + (calcTarget - normalTarget)
-        let adjustmentRatio: Decimal = (deviationFromNormal * adjustmentFactor <= 0) ? maxValue : deviationFromNormal /
+        let adjustmentRatio: Decimal = (deviationFromNormal * adjustmentFactor <= 0) ? autosensMax : deviationFromNormal /
             adjustmentFactor
             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 tempTargetPresets: [TempTargetStored] = []
         var scheduledTempTargets: [TempTargetStored] = []
         var scheduledTempTargets: [TempTargetStored] = []
         var percentage: Double = 100
         var percentage: Double = 100
-        var maxValue: Decimal = 1.2
+        var autosensMax: Decimal = 1.2
         var halfBasalTarget: Decimal = 160
         var halfBasalTarget: Decimal = 160
         var settingHalfBasalTarget: Decimal = 160
         var settingHalfBasalTarget: Decimal = 160
         var highTTraisesSens: Bool = false
         var highTTraisesSens: Bool = false
@@ -152,7 +152,7 @@ extension Adjustments {
             units = settingsManager.settings.units
             units = settingsManager.settings.units
             defaultSmbMinutes = settingsManager.preferences.maxSMBBasalMinutes
             defaultSmbMinutes = settingsManager.preferences.maxSMBBasalMinutes
             defaultUamMinutes = settingsManager.preferences.maxUAMSMBBasalMinutes
             defaultUamMinutes = settingsManager.preferences.maxUAMSMBBasalMinutes
-            maxValue = settingsManager.preferences.autosensMax
+            autosensMax = settingsManager.preferences.autosensMax
             settingHalfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
             settingHalfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
             halfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
             halfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
             highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
             highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
@@ -262,7 +262,7 @@ extension Adjustments.StateModel: SettingsObserver, PreferencesObserver {
     func preferencesDidChange(_: Preferences) {
     func preferencesDidChange(_: Preferences) {
         defaultSmbMinutes = settingsManager.preferences.maxSMBBasalMinutes
         defaultSmbMinutes = settingsManager.preferences.maxSMBBasalMinutes
         defaultUamMinutes = settingsManager.preferences.maxUAMSMBBasalMinutes
         defaultUamMinutes = settingsManager.preferences.maxUAMSMBBasalMinutes
-        maxValue = settingsManager.preferences.autosensMax
+        autosensMax = settingsManager.preferences.autosensMax
         settingHalfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
         settingHalfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
         halfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
         halfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
         highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
         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 for ISF/CR settings
                 Picker("Also Inversely Change", selection: $selectedIsfCrOption) {
                 Picker("Also Inversely Change", selection: $selectedIsfCrOption) {
                     ForEach(IsfAndOrCrOptions.allCases, id: \.self) { option in
                     ForEach(IsfAndOrCrOptions.allCases, id: \.self) { option in
-                        Text(option.rawValue).tag(option)
+                        Text(option.displayName).tag(option)
                     }
                     }
                 }
                 }
                 .pickerStyle(MenuPickerStyle())
                 .pickerStyle(MenuPickerStyle())
@@ -188,7 +188,7 @@ struct AddOverrideForm: View {
                 // Picker for ISF/CR settings
                 // Picker for ISF/CR settings
                 Picker("Disable SMBs", selection: $selectedDisableSmbOption) {
                 Picker("Disable SMBs", selection: $selectedDisableSmbOption) {
                     ForEach(DisableSmbOptions.allCases, id: \.self) { option in
                     ForEach(DisableSmbOptions.allCases, id: \.self) { option in
-                        Text(option.rawValue).tag(option)
+                        Text(option.displayName).tag(option)
                     }
                     }
                 }
                 }
                 .pickerStyle(MenuPickerStyle())
                 .pickerStyle(MenuPickerStyle())
@@ -343,7 +343,7 @@ struct AddOverrideForm: View {
                     HStack {
                     HStack {
                         Text("Duration")
                         Text("Duration")
                         Spacer()
                         Spacer()
-                        Text(state.formatHrMin(Int(state.overrideDuration)))
+                        Text(state.formatHoursAndMinutes(Int(state.overrideDuration)))
                             .foregroundColor(!displayPickerDuration ? .primary : .accentColor)
                             .foregroundColor(!displayPickerDuration ? .primary : .accentColor)
                             .onTapGesture {
                             .onTapGesture {
                                 displayPickerDuration = toggleScrollWheel(displayPickerDuration)
                                 displayPickerDuration = toggleScrollWheel(displayPickerDuration)
@@ -448,15 +448,15 @@ struct AddOverrideForm: View {
             !state.advancedSettings && !state.smbIsOff && !state.smbIsScheduledOff
             !state.advancedSettings && !state.smbIsOff && !state.smbIsScheduledOff
 
 
         if noDurationSpecified {
         if noDurationSpecified {
-            return (true, "Enable indefinitely or set a duration.")
+            return (true, String(localized: "Enable indefinitely or set a duration."))
         }
         }
 
 
         if targetZeroWithOverride {
         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 {
         if allSettingsDefault {
-            return (true, "All settings are at default values.")
+            return (true, String(localized: "All settings are at default values."))
         }
         }
 
 
         return (false, nil)
         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 targetString = target.isEmpty ? "" : "\(target) \(state.units.rawValue)"
 
 
-        let durationString = indefinite ? "" : "\(state.formatHrMin(Int(duration)))"
+        let durationString = indefinite ? "" : "\(state.formatHoursAndMinutes(Int(duration)))"
 
 
         let scheduledSMBString: String = {
         let scheduledSMBString: String = {
             guard preset.smbIsScheduledOff, preset.start != preset.end else { return "" }
             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 for ISF/CR settings
                 Picker("Also Change", selection: $selectedIsfCrOption) {
                 Picker("Also Change", selection: $selectedIsfCrOption) {
                     ForEach(IsfAndOrCrOptions.allCases, id: \.self) { option in
                     ForEach(IsfAndOrCrOptions.allCases, id: \.self) { option in
-                        Text(option.rawValue).tag(option)
+                        Text(option.displayName).tag(option)
                     }
                     }
                 }
                 }
                 .pickerStyle(MenuPickerStyle())
                 .pickerStyle(MenuPickerStyle())
@@ -257,7 +257,7 @@ struct EditOverrideForm: View {
                 // Picker for Disable SMB settings
                 // Picker for Disable SMB settings
                 Picker("Disable SMBs", selection: $selectedDisableSmbOption) {
                 Picker("Disable SMBs", selection: $selectedDisableSmbOption) {
                     ForEach(DisableSmbOptions.allCases, id: \.self) { option in
                     ForEach(DisableSmbOptions.allCases, id: \.self) { option in
-                        Text(option.rawValue).tag(option)
+                        Text(option.displayName).tag(option)
                     }
                     }
                 }
                 }
                 .pickerStyle(MenuPickerStyle())
                 .pickerStyle(MenuPickerStyle())
@@ -440,7 +440,7 @@ struct EditOverrideForm: View {
                     HStack {
                     HStack {
                         Text("Duration")
                         Text("Duration")
                         Spacer()
                         Spacer()
-                        Text(state.formatHrMin(Int(truncating: duration as NSNumber)))
+                        Text(state.formatHoursAndMinutes(Int(truncating: duration as NSNumber)))
                             .foregroundColor(!displayPickerDuration ? .primary : .accentColor)
                             .foregroundColor(!displayPickerDuration ? .primary : .accentColor)
                             .onTapGesture {
                             .onTapGesture {
                                 displayPickerDuration = toggleScrollWheel(displayPickerDuration)
                                 displayPickerDuration = toggleScrollWheel(displayPickerDuration)
@@ -557,15 +557,15 @@ struct EditOverrideForm: View {
             !smbIsOff && !smbIsScheduledOff
             !smbIsOff && !smbIsScheduledOff
 
 
         if noDurationSpecified {
         if noDurationSpecified {
-            return (true, "Enable indefinitely or set a duration.")
+            return (true, String(localized: "Enable indefinitely or set a duration."))
         }
         }
 
 
         if targetZeroWithOverride {
         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 {
         if allSettingsDefault {
-            return (true, "All settings are at default values.")
+            return (true, String(localized: "All settings are at default values."))
         }
         }
 
 
         if !hasChanges {
         if !hasChanges {

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

@@ -84,7 +84,7 @@ struct AddTempTargetForm: View {
                 let settingsProvider = PickerSettingsProvider.shared
                 let settingsProvider = PickerSettingsProvider.shared
                 let glucoseSetting = PickerSetting(value: 0, step: targetStep, min: 80, max: 200, type: .glucose)
                 let glucoseSetting = PickerSetting(value: 0, step: targetStep, min: 80, max: 200, type: .glucose)
                 TargetPicker(
                 TargetPicker(
-                    label: "Target Glucose",
+                    label: String(localized: "Target Glucose"),
                     selection: Binding(
                     selection: Binding(
                         get: { state.tempTargetTarget },
                         get: { state.tempTargetTarget },
                         set: { state.tempTargetTarget = $0 }
                         set: { state.tempTargetTarget = $0 }
@@ -160,7 +160,7 @@ struct AddTempTargetForm: View {
                     HStack {
                     HStack {
                         Text("Duration")
                         Text("Duration")
                         Spacer()
                         Spacer()
-                        Text(state.formatHrMin(Int(state.tempTargetDuration)))
+                        Text(state.formatHoursAndMinutes(Int(state.tempTargetDuration)))
                             .foregroundColor(
                             .foregroundColor(
                                 !displayPickerDuration ?
                                 !displayPickerDuration ?
                                     (state.tempTargetDuration > 0 ? .primary : .secondary) : .accentColor
                                     (state.tempTargetDuration > 0 ? .primary : .secondary) : .accentColor
@@ -205,13 +205,14 @@ struct AddTempTargetForm: View {
         let targetZero = state.tempTargetTarget < 80
         let targetZero = state.tempTargetTarget < 80
 
 
         if noDurationSpecified {
         if noDurationSpecified {
-            return (true, "Set a duration!")
+            return (true, String(localized: "Set a duration!"))
         }
         }
 
 
         if targetZero {
         if targetZero {
             return (
             return (
                 true,
                 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 {
         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)
         return (false, nil)

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

@@ -232,7 +232,7 @@ struct EditTempTargetForm: View {
                     HStack {
                     HStack {
                         Text("Duration")
                         Text("Duration")
                         Spacer()
                         Spacer()
-                        Text(state.formatHrMin(Int(duration)))
+                        Text(state.formatHoursAndMinutes(Int(duration)))
                             .foregroundColor(!displayPickerDuration ? (duration > 0 ? .primary : .secondary) : .accentColor)
                             .foregroundColor(!displayPickerDuration ? (duration > 0 ? .primary : .secondary) : .accentColor)
                             .onTapGesture {
                             .onTapGesture {
                                 displayPickerDuration = toggleScrollWheel(displayPickerDuration)
                                 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")) {
                     Picker(selection: $state.items[index].rateIndex, label: Text("Rate")) {
                         ForEach(0 ..< state.rateValues.count, id: \.self) { i in
                         ForEach(0 ..< state.rateValues.count, id: \.self) { i in
                             Text(
                             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)
                             ).tag(i)
                         }
                         }
                     }
                     }

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

@@ -125,10 +125,8 @@ extension CarbRatioEditor {
                     Picker(selection: $state.items[index].rateIndex, label: Text("Ratio")) {
                     Picker(selection: $state.items[index].rateIndex, label: Text("Ratio")) {
                         ForEach(0 ..< state.rateValues.count, id: \.self) { i in
                         ForEach(0 ..< state.rateValues.count, id: \.self) { i in
                             Text(
                             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)
                             ).tag(i)
                         }
                         }
                     }
                     }
@@ -163,7 +161,8 @@ extension CarbRatioEditor {
                         HStack {
                         HStack {
                             Text("Ratio").foregroundColor(.secondary)
                             Text("Ratio").foregroundColor(.secondary)
                             Text(
                             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()
                             Spacer()
                             Text("starts at").foregroundColor(.secondary)
                             Text("starts at").foregroundColor(.secondary)

+ 1 - 1
Trio/Sources/Modules/DataTable/View/CarbEntryEditorView.swift

@@ -161,7 +161,7 @@ struct CarbEntryEditorView: View {
 
 
                     HStack {
                     HStack {
                         Image(systemName: "square.and.pencil")
                         Image(systemName: "square.and.pencil")
-                        TextFieldWithToolBarString(text: $editedNote, placeholder: "Note...", maxLength: 25)
+                        TextFieldWithToolBarString(text: $editedNote, placeholder: String(localized: "Note..."), maxLength: 25)
                     }
                     }
                 }.listRowBackground(Color.chart)
                 }.listRowBackground(Color.chart)
 
 

+ 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 deviationFromNormal = halfBasalTargetValue - normalTarget
 
 
         let adjustmentFactor = deviationFromNormal + (tempTargetValue - normalTarget)
         let adjustmentFactor = deviationFromNormal + (tempTargetValue - normalTarget)
-        let adjustmentRatio: Decimal = (deviationFromNormal * adjustmentFactor <= 0) ? maxValue : deviationFromNormal /
+        let adjustmentRatio: Decimal = (deviationFromNormal * adjustmentFactor <= 0) ? autosensMax : deviationFromNormal /
             adjustmentFactor
             adjustmentFactor
 
 
-        return Int(Double(min(adjustmentRatio, maxValue) * 100).rounded())
+        return Int(Double(min(adjustmentRatio, autosensMax) * 100).rounded())
     }
     }
 }
 }

+ 16 - 59
Trio/Sources/Modules/Home/HomeStateModel.swift

@@ -63,12 +63,12 @@ extension Home {
         var alarm: GlucoseAlarm?
         var alarm: GlucoseAlarm?
         var manualTempBasal = false
         var manualTempBasal = false
         var isSmoothingEnabled = false
         var isSmoothingEnabled = false
-        var maxValue: Decimal = 1.2
+        var autosensMax: Decimal = 1.2
         var lowGlucose: Decimal = 70
         var lowGlucose: Decimal = 70
         var highGlucose: Decimal = 180
         var highGlucose: Decimal = 180
         var currentGlucoseTarget: Decimal = 100
         var currentGlucoseTarget: Decimal = 100
         var glucoseColorScheme: GlucoseColorScheme = .staticColor
         var glucoseColorScheme: GlucoseColorScheme = .staticColor
-        var hbA1cDisplayUnit: HbA1cDisplayUnit = .percent
+        var eA1cDisplayUnit: EstimatedA1cDisplayUnit = .percent
         var displayXgridLines: Bool = false
         var displayXgridLines: Bool = false
         var displayYgridLines: Bool = false
         var displayYgridLines: Bool = false
         var thresholdLines: Bool = false
         var thresholdLines: Bool = false
@@ -76,7 +76,6 @@ extension Home {
         var totalBolus: Decimal = 0
         var totalBolus: Decimal = 0
         var isLoopStatusPresented: Bool = false
         var isLoopStatusPresented: Bool = false
         var isLegendPresented: Bool = false
         var isLegendPresented: Bool = false
-        var totalInsulinDisplayType: TotalInsulinDisplayType = .totalDailyDose
         var roundedTotalBolus: String = ""
         var roundedTotalBolus: String = ""
         var selectedTab: Int = 0
         var selectedTab: Int = 0
         var waitForSuggestion: Bool = false
         var waitForSuggestion: Bool = false
@@ -86,6 +85,7 @@ extension Home {
         var fpusFromPersistence: [CarbEntryStored] = []
         var fpusFromPersistence: [CarbEntryStored] = []
         var determinationsFromPersistence: [OrefDetermination] = []
         var determinationsFromPersistence: [OrefDetermination] = []
         var enactedAndNonEnactedDeterminations: [OrefDetermination] = []
         var enactedAndNonEnactedDeterminations: [OrefDetermination] = []
+        var fetchedTDDs: [TDD] = []
         var insulinFromPersistence: [PumpEventStored] = []
         var insulinFromPersistence: [PumpEventStored] = []
         var tempBasals: [PumpEventStored] = []
         var tempBasals: [PumpEventStored] = []
         var suspensions: [PumpEventStored] = []
         var suspensions: [PumpEventStored] = []
@@ -125,6 +125,7 @@ extension Home {
         let carbsFetchContext = CoreDataStack.shared.newTaskContext()
         let carbsFetchContext = CoreDataStack.shared.newTaskContext()
         let fpuFetchContext = CoreDataStack.shared.newTaskContext()
         let fpuFetchContext = CoreDataStack.shared.newTaskContext()
         let determinationFetchContext = CoreDataStack.shared.newTaskContext()
         let determinationFetchContext = CoreDataStack.shared.newTaskContext()
+        let tddFetchContext = CoreDataStack.shared.newTaskContext()
         let pumpHistoryFetchContext = CoreDataStack.shared.newTaskContext()
         let pumpHistoryFetchContext = CoreDataStack.shared.newTaskContext()
         let overrideFetchContext = CoreDataStack.shared.newTaskContext()
         let overrideFetchContext = CoreDataStack.shared.newTaskContext()
         let tempTargetFetchContext = CoreDataStack.shared.newTaskContext()
         let tempTargetFetchContext = CoreDataStack.shared.newTaskContext()
@@ -179,6 +180,9 @@ extension Home {
                         self.setupDeterminationsArray()
                         self.setupDeterminationsArray()
                     }
                     }
                     group.addTask {
                     group.addTask {
+                        self.setupTDDArray()
+                    }
+                    group.addTask {
                         self.setupInsulinArray()
                         self.setupInsulinArray()
                     }
                     }
                     group.addTask {
                     group.addTask {
@@ -237,6 +241,11 @@ extension Home {
                 self.setupDeterminationsArray()
                 self.setupDeterminationsArray()
             }.store(in: &subscriptions)
             }.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
             coreDataPublisher?.filteredByEntityName("GlucoseStored").sink { [weak self] _ in
                 guard let self = self else { return }
                 guard let self = self else { return }
                 self.setupGlucoseArray()
                 self.setupGlucoseArray()
@@ -372,21 +381,19 @@ extension Home {
             manualTempBasal = apsManager.isManualTempBasal
             manualTempBasal = apsManager.isManualTempBasal
             isSmoothingEnabled = settingsManager.settings.smoothGlucose
             isSmoothingEnabled = settingsManager.settings.smoothGlucose
             glucoseColorScheme = settingsManager.settings.glucoseColorScheme
             glucoseColorScheme = settingsManager.settings.glucoseColorScheme
-            maxValue = settingsManager.preferences.autosensMax
+            autosensMax = settingsManager.preferences.autosensMax
             lowGlucose = settingsManager.settings.low
             lowGlucose = settingsManager.settings.low
             highGlucose = settingsManager.settings.high
             highGlucose = settingsManager.settings.high
-            hbA1cDisplayUnit = settingsManager.settings.hbA1cDisplayUnit
+            eA1cDisplayUnit = settingsManager.settings.eA1cDisplayUnit
             displayXgridLines = settingsManager.settings.xGridLines
             displayXgridLines = settingsManager.settings.xGridLines
             displayYgridLines = settingsManager.settings.yGridLines
             displayYgridLines = settingsManager.settings.yGridLines
             thresholdLines = settingsManager.settings.rulerMarks
             thresholdLines = settingsManager.settings.rulerMarks
-            totalInsulinDisplayType = settingsManager.settings.totalInsulinDisplayType
             showCarbsRequiredBadge = settingsManager.settings.showCarbsRequiredBadge
             showCarbsRequiredBadge = settingsManager.settings.showCarbsRequiredBadge
             forecastDisplayType = settingsManager.settings.forecastDisplayType
             forecastDisplayType = settingsManager.settings.forecastDisplayType
             isExerciseModeActive = settingsManager.preferences.exerciseMode
             isExerciseModeActive = settingsManager.preferences.exerciseMode
             highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
             highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
             lowTTlowersSens = settingsManager.preferences.lowTemptargetLowersSensitivity
             lowTTlowersSens = settingsManager.preferences.lowTemptargetLowersSensitivity
             settingHalfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
             settingHalfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
-            maxValue = settingsManager.preferences.autosensMax
         }
         }
 
 
         @MainActor private func setupCGMSettings() async {
         @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 {
         private func setupPumpSettings() async {
             let maxBasal = await provider.pumpSettings().maxBasal
             let maxBasal = await provider.pumpSettings().maxBasal
             await MainActor.run {
             await MainActor.run {
@@ -672,12 +630,11 @@ extension Home.StateModel:
             await getCurrentGlucoseTarget()
             await getCurrentGlucoseTarget()
             await setupGlucoseTargets()
             await setupGlucoseTargets()
         }
         }
-        hbA1cDisplayUnit = settingsManager.settings.hbA1cDisplayUnit
+        eA1cDisplayUnit = settingsManager.settings.eA1cDisplayUnit
         glucoseColorScheme = settingsManager.settings.glucoseColorScheme
         glucoseColorScheme = settingsManager.settings.glucoseColorScheme
         displayXgridLines = settingsManager.settings.xGridLines
         displayXgridLines = settingsManager.settings.xGridLines
         displayYgridLines = settingsManager.settings.yGridLines
         displayYgridLines = settingsManager.settings.yGridLines
         thresholdLines = settingsManager.settings.rulerMarks
         thresholdLines = settingsManager.settings.rulerMarks
-        totalInsulinDisplayType = settingsManager.settings.totalInsulinDisplayType
         showCarbsRequiredBadge = settingsManager.settings.showCarbsRequiredBadge
         showCarbsRequiredBadge = settingsManager.settings.showCarbsRequiredBadge
         forecastDisplayType = settingsManager.settings.forecastDisplayType
         forecastDisplayType = settingsManager.settings.forecastDisplayType
         cgmAvailable = (fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none)
         cgmAvailable = (fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none)
@@ -690,7 +647,7 @@ extension Home.StateModel:
     }
     }
 
 
     func preferencesDidChange(_: Preferences) {
     func preferencesDidChange(_: Preferences) {
-        maxValue = settingsManager.preferences.autosensMax
+        autosensMax = settingsManager.preferences.autosensMax
         settingHalfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
         settingHalfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
         highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
         highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
         isExerciseModeActive = settingsManager.preferences.exerciseMode
         isExerciseModeActive = settingsManager.preferences.exerciseMode

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

@@ -23,8 +23,15 @@ struct OverrideView: ChartContent {
                 attribute: "duration",
                 attribute: "duration",
                 context: viewContext
                 context: viewContext
             ) ?? 0
             ) ?? 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)
             let target = getOverrideTarget(override: override)
 
 

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

@@ -186,9 +186,6 @@ extension MainChartView {
                 }
                 }
             }
             }
             .id("MainChart")
             .id("MainChart")
-            .onChange(of: state.insulinFromPersistence) {
-                state.roundedTotalBolus = state.calculateTINS()
-            }
             .frame(
             .frame(
                 minHeight: geo.size.height * (0.28 - safeAreaSize)
                 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
         return formatter
     }
     }
 
 
-    private var timaAgoFormatter: NumberFormatter {
-        let formatter = NumberFormatter()
-        formatter.numberStyle = .decimal
-        formatter.maximumFractionDigits = 0
-        formatter.negativePrefix = ""
-        return formatter
-    }
-
     var body: some View {
     var body: some View {
         let triangleColor = Color(red: 0.262745098, green: 0.7333333333, blue: 0.9137254902)
         let triangleColor = Color(red: 0.262745098, green: 0.7333333333, blue: 0.9137254902)
 
 
@@ -91,20 +83,24 @@ struct CurrentGlucoseView: View {
                     }
                     }
                     HStack {
                     HStack {
                         let minutesAgo = -1 * (glucose.last?.date?.timeIntervalSinceNow ?? 0) / 60
                         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) {
             .onChange(of: glucose.last?.directionEnum) {

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

@@ -57,11 +57,16 @@ struct LoopView: View {
     }
     }
 
 
     private var timeString: String {
     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 "--"
             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 {
     private var color: Color {

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

@@ -255,7 +255,7 @@ extension Home {
             } else { halfBasalTarget = state.settingHalfBasalTarget }
             } else { halfBasalTarget = state.settingHalfBasalTarget }
             var showPercentage = false
             var showPercentage = false
             if target > 100, state.isExerciseModeActive || state.highTTraisesSens { showPercentage = true }
             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 {
             if showPercentage {
                 percentageString =
                 percentageString =
                     " \(state.computeAdjustedPercentage(halfBasalTargetValue: halfBasalTarget, tempTargetValue: target))%" }
                     " \(state.computeAdjustedPercentage(halfBasalTargetValue: halfBasalTarget, tempTargetValue: target))%" }
@@ -292,7 +292,7 @@ extension Home {
                         Group {
                         Group {
                             if button.active {
                             if button.active {
                                 Text(
                                 Text(
-                                    button.hours.description + " " +
+                                    button.hours.description + "\u{00A0}" +
                                         String(localized: "h", comment: "h")
                                         String(localized: "h", comment: "h")
                                 )
                                 )
                             } else {
                             } else {
@@ -488,37 +488,7 @@ extension Home {
                             .font(.callout).fontWeight(.bold).fontDesign(.rounded)
                             .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 {
         @ViewBuilder func adjustmentsOverrideView(_ overrideString: String) -> some View {
@@ -1150,18 +1120,19 @@ func is24HourFormat() -> Bool {
     return !dateString.contains("AM") && !dateString.contains("PM")
     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 {
 func formatHrMin(_ durationInMinutes: Int) -> String {
     let hours = durationInMinutes / 60
     let hours = durationInMinutes / 60
     let minutes = durationInMinutes % 60
     let minutes = durationInMinutes % 60
 
 
     switch (hours, minutes) {
     switch (hours, minutes) {
     case let (0, m):
     case let (0, m):
-        return "\(m) min"
+        return "\(m)\u{00A0}" + String(localized: "m", comment: "Abbreviation for Minutes")
     case let (h, 0):
     case let (h, 0):
-        return "\(h) hr"
+        return "\(h)\u{00A0}" + String(localized: "h", comment: "h")
     default:
     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
                         ForEach(0 ..< state.rateValues.count, id: \.self) { i in
                             Text(
                             Text(
                                 state.units == .mgdL ? state.rateValues[i].description : state.rateValues[i]
                                 state.units == .mgdL ? state.rateValues[i].description : state.rateValues[i]
-                                    .formattedAsMmolL + " \(state.units.rawValue)/U"
+                                    .formattedAsMmolL + String(localized: " \(state.units.rawValue)/U")
                             ).tag(i)
                             ).tag(i)
                         }
                         }
                     }
                     }
@@ -162,7 +162,7 @@ extension ISFEditor {
                             Text("Rate").foregroundColor(.secondary)
                             Text("Rate").foregroundColor(.secondary)
 
 
                             Text(
                             Text(
-                                displayValue + " \(state.units.rawValue)/U"
+                                displayValue + String(localized: " \(state.units.rawValue)/U")
                             )
                             )
                             Spacer()
                             Spacer()
                             Text("starts at").foregroundColor(.secondary)
                             Text("starts at").foregroundColor(.secondary)

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

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

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

@@ -201,6 +201,37 @@ extension Main {
             SwiftMessages.show(config: config, view: view)
             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() {
         override func subscribe() {
             router.mainModalScreen
             router.mainModalScreen
                 .map { $0?.modal(resolver: self.resolver!) }
                 .map { $0?.modal(resolver: self.resolver!) }
@@ -223,8 +254,10 @@ extension Main {
                 .receive(on: DispatchQueue.main)
                 .receive(on: DispatchQueue.main)
                 .sink { message in
                 .sink { message in
                     guard !self.isApnPumpConfigAction(message) else { return }
                     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)
                 .store(in: &lifetime)
 
 

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

@@ -205,10 +205,7 @@ enum SettingItems {
                 "Low Threshold",
                 "Low Threshold",
                 "High Threshold",
                 "High Threshold",
                 "X-Axis Interval Step",
                 "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",
                 "Standing / Laying TIR Chart",
                 "Show Carbs Required Badge",
                 "Show Carbs Required Badge",
                 "Carbs Required Threshold",
                 "Carbs Required Threshold",

+ 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 {
 extension Stat {
     @Observable final class StateModel: BaseStateModel<Provider> {
     @Observable final class StateModel: BaseStateModel<Provider> {
         @ObservationIgnored @Injected() var settings: SettingsManager!
         @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 timeInRangeChartStyle: TimeInRangeChartStyle = .vertical
         var units: GlucoseUnits = .mgdL
         var units: GlucoseUnits = .mgdL
+        var useFPUconversion: Bool = false
         var glucoseFromPersistence: [GlucoseStored] = []
         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() {
         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
             units = settingsManager.settings.units
-            hbA1cDisplayUnit = settingsManager.settings.hbA1cDisplayUnit
+            eA1cDisplayUnit = settingsManager.settings.eA1cDisplayUnit
             timeInRangeChartStyle = settingsManager.settings.timeInRangeChartStyle
             timeInRangeChartStyle = settingsManager.settings.timeInRangeChartStyle
+            useFPUconversion = settingsManager.settings.useFPUconversion
         }
         }
 
 
-        func setupGlucoseArray(for duration: Duration) {
+        func setupGlucoseArray(for interval: StatsTimeIntervalWithToday) {
             Task {
             Task {
-                let ids = await self.fetchGlucose(for: duration)
+                let ids = await fetchGlucose(for: interval)
                 await updateGlucoseArray(with: ids)
                 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 {
             do {
                 let predicate: NSPredicate
                 let predicate: NSPredicate
 
 
-                switch duration {
-                case .Day:
+                switch interval {
+                case .day:
                     predicate = NSPredicate.glucoseForStatsDay
                     predicate = NSPredicate.glucoseForStatsDay
-                case .Week:
+                case .week:
                     predicate = NSPredicate.glucoseForStatsWeek
                     predicate = NSPredicate.glucoseForStatsWeek
-                case .Today:
+                case .today:
                     predicate = NSPredicate.glucoseForStatsToday
                     predicate = NSPredicate.glucoseForStatsToday
-                case .Month:
+                case .month:
                     predicate = NSPredicate.glucoseForStatsMonth
                     predicate = NSPredicate.glucoseForStatsMonth
-                case .Total:
+                case .total:
                     predicate = NSPredicate.glucoseForStatsTotal
                     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 Charts
-import CoreData
 import SwiftDate
 import SwiftDate
 import SwiftUI
 import SwiftUI
 import Swinject
 import Swinject
 
 
 extension Stat {
 extension Stat {
     struct RootView: BaseView {
     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(\.colorScheme) var colorScheme
         @Environment(AppState.self) var appState
         @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,
                         highLimit: state.highLimit,
                         lowLimit: state.lowLimit,
                         lowLimit: state.lowLimit,
                         units: state.units,
                         units: state.units,
-                        hbA1cDisplayUnit: state.hbA1cDisplayUnit
+                        glucose: state.glucoseFromPersistence
                     )
                     )
-                case .Week:
-                    StatsView(
-                        filter: filter.week,
+
+                    Divider()
+
+                    GlucoseMetricsView(
                         highLimit: state.highLimit,
                         highLimit: state.highLimit,
                         lowLimit: state.lowLimit,
                         lowLimit: state.lowLimit,
                         units: state.units,
                         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 sensitivityRaisesTarget: Bool = false
         @Published var resistanceLowersTarget: Bool = false
         @Published var resistanceLowersTarget: Bool = false
         @Published var halfBasalExerciseTarget: Decimal = 160
         @Published var halfBasalExerciseTarget: Decimal = 160
+        @Published var autosensMax: Decimal = 1
 
 
         override func subscribe() {
         override func subscribe() {
             units = settingsManager.settings.units
             units = settingsManager.settings.units
-
+            autosensMax = settingsManager.preferences.autosensMax
             subscribePreferencesSetting(\.highTemptargetRaisesSensitivity, on: $highTemptargetRaisesSensitivity) {
             subscribePreferencesSetting(\.highTemptargetRaisesSensitivity, on: $highTemptargetRaisesSensitivity) {
                 highTemptargetRaisesSensitivity = $0 }
                 highTemptargetRaisesSensitivity = $0 }
             subscribePreferencesSetting(\.lowTemptargetLowersSensitivity, on: $lowTemptargetLowersSensitivity) {
             subscribePreferencesSetting(\.lowTemptargetLowersSensitivity, on: $lowTemptargetLowersSensitivity) {

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

@@ -11,6 +11,7 @@ extension TargetBehavoir {
         @State var hintLabel: String?
         @State var hintLabel: String?
         @State private var decimalPlaceholder: Decimal = 0.0
         @State private var decimalPlaceholder: Decimal = 0.0
         @State private var booleanPlaceholder: Bool = false
         @State private var booleanPlaceholder: Bool = false
+        @State private var showAutosensMaxAlert = false
 
 
         @Environment(\.colorScheme) var colorScheme
         @Environment(\.colorScheme) var colorScheme
         @EnvironmentObject var appIcons: Icons
         @EnvironmentObject var appIcons: Icons
@@ -59,7 +60,7 @@ extension TargetBehavoir {
 
 
                 SettingInputSection(
                 SettingInputSection(
                     decimalValue: $decimalPlaceholder,
                     decimalValue: $decimalPlaceholder,
-                    booleanValue: $state.lowTemptargetLowersSensitivity,
+                    booleanValue: effectiveLowTTLowersSensBinding,
                     shouldDisplayHint: $shouldDisplayHint,
                     shouldDisplayHint: $shouldDisplayHint,
                     selectedVerboseHint: Binding(
                     selectedVerboseHint: Binding(
                         get: { selectedVerboseHint },
                         get: { selectedVerboseHint },
@@ -86,7 +87,7 @@ extension TargetBehavoir {
                     VStack(alignment: .leading, spacing: 10) {
                     VStack(alignment: .leading, spacing: 10) {
                         Text("Default: OFF").bold()
                         Text("Default: OFF").bold()
                         Text(
                         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(
                         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."
                             "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))
             .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
             .onAppear(perform: configureView)
             .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")
             .navigationTitle("Target Behavior")
             .navigationBarTitleDisplayMode(.automatic)
             .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
+                    }
+                }
+            )
         }
         }
     }
     }
 }
 }

+ 10 - 18
Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift

@@ -741,14 +741,11 @@ extension Treatments.StateModel {
             let determinationObjects: [OrefDetermination] = try await CoreDataStack.shared
             let determinationObjects: [OrefDetermination] = try await CoreDataStack.shared
                 .getNSManagedObject(with: determinationObjectIDs, context: determinationFetchContext)
                 .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(
                 let predictions = Predictions(
                     iob: forecastsSet.extractValues(for: "iob"),
                     iob: forecastsSet.extractValues(for: "iob"),
                     zt: forecastsSet.extractValues(for: "zt"),
                     zt: forecastsSet.extractValues(for: "zt"),
@@ -761,7 +758,6 @@ extension Treatments.StateModel {
                     reason: "",
                     reason: "",
                     units: 0,
                     units: 0,
                     insulinReq: 0,
                     insulinReq: 0,
-                    eventualBG: eventualBG,
                     sensitivityRatio: 0,
                     sensitivityRatio: 0,
                     rate: 0,
                     rate: 0,
                     duration: 0,
                     duration: 0,
@@ -770,23 +766,19 @@ extension Treatments.StateModel {
                     predictions: predictions.isEmpty ? nil : predictions,
                     predictions: predictions.isEmpty ? nil : predictions,
                     carbsReq: 0,
                     carbsReq: 0,
                     temp: nil,
                     temp: nil,
-                    bg: 0,
                     reservoir: 0,
                     reservoir: 0,
-                    isf: 0,
-                    tdd: 0,
-                    insulin: nil,
-                    current_target: 0,
                     insulinForManualBolus: 0,
                     insulinForManualBolus: 0,
                     manualBolusErrorString: 0,
                     manualBolusErrorString: 0,
-                    minDelta: 0,
-                    expectedDelta: 0,
-                    minGuardBG: 0,
-                    minPredBG: 0,
-                    threshold: 0,
                     carbRatio: 0,
                     carbRatio: 0,
                     received: false
                     received: false
                 )
                 )
             }
             }
+
+            guard !determinationObjects.isEmpty else {
+                return nil
+            }
+
+            return determination
         } catch {
         } catch {
             debug(
             debug(
                 .default,
                 .default,

+ 6 - 2
Trio/Sources/Modules/Treatments/View/TreatmentsRootView.swift

@@ -228,7 +228,11 @@ extension Treatments {
                             // Notes
                             // Notes
                             HStack {
                             HStack {
                                 Image(systemName: "square.and.pencil")
                                 Image(systemName: "square.and.pencil")
-                                TextFieldWithToolBarString(text: $state.note, placeholder: "Note...", maxLength: 25)
+                                TextFieldWithToolBarString(
+                                    text: $state.note,
+                                    placeholder: String(localized: "Note..."),
+                                    maxLength: 25
+                                )
                             }
                             }
                         }.listRowBackground(Color.chart)
                         }.listRowBackground(Color.chart)
 
 
@@ -505,7 +509,7 @@ extension Treatments {
             let hasInsulin = state.amount > 0
             let hasInsulin = state.amount > 0
             let hasCarbs = state.carbs > 0
             let hasCarbs = state.carbs > 0
             let hasFatOrProtein = state.fat > 0 || state.protein > 0
             let hasFatOrProtein = state.fat > 0 || state.protein > 0
-            let bolusString = state.externalInsulin ? "External Insulin" : "Enact Bolus"
+            let bolusString = state.externalInsulin ? String(localized: "External Insulin") : String(localized: "Enact Bolus")
 
 
             if state.isBolusInProgress && hasInsulin && !state.externalInsulin && (!hasCarbs || !hasFatOrProtein) {
             if state.isBolusInProgress && hasInsulin && !state.externalInsulin && (!hasCarbs || !hasFatOrProtein) {
                 return Text("Bolus In Progress...")
                 return Text("Bolus In Progress...")

+ 2 - 5
Trio/Sources/Modules/UserInterfaceSettings/UserInterfaceSettingsStateModel.swift

@@ -8,11 +8,10 @@ extension UserInterfaceSettings {
         @Published var yGridLines: Bool = false
         @Published var yGridLines: Bool = false
         @Published var rulerMarks: Bool = true
         @Published var rulerMarks: Bool = true
         @Published var forecastDisplayType: ForecastDisplayType = .cone
         @Published var forecastDisplayType: ForecastDisplayType = .cone
-        @Published var totalInsulinDisplayType: TotalInsulinDisplayType = .totalDailyDose
         @Published var showCarbsRequiredBadge: Bool = true
         @Published var showCarbsRequiredBadge: Bool = true
         @Published var carbsRequiredThreshold: Decimal = 0
         @Published var carbsRequiredThreshold: Decimal = 0
         @Published var glucoseColorScheme: GlucoseColorScheme = .staticColor
         @Published var glucoseColorScheme: GlucoseColorScheme = .staticColor
-        @Published var hbA1cDisplayUnit: HbA1cDisplayUnit = .percent
+        @Published var eA1cDisplayUnit: EstimatedA1cDisplayUnit = .percent
         @Published var timeInRangeChartStyle: TimeInRangeChartStyle = .vertical
         @Published var timeInRangeChartStyle: TimeInRangeChartStyle = .vertical
 
 
         var units: GlucoseUnits = .mgdL
         var units: GlucoseUnits = .mgdL
@@ -27,8 +26,6 @@ extension UserInterfaceSettings {
 
 
             subscribeSetting(\.forecastDisplayType, on: $forecastDisplayType) { forecastDisplayType = $0 }
             subscribeSetting(\.forecastDisplayType, on: $forecastDisplayType) { forecastDisplayType = $0 }
 
 
-            subscribeSetting(\.totalInsulinDisplayType, on: $totalInsulinDisplayType) { totalInsulinDisplayType = $0 }
-
             subscribeSetting(\.low, on: $low) { low = $0 }
             subscribeSetting(\.low, on: $low) { low = $0 }
 
 
             subscribeSetting(\.high, on: $high) { high = $0 }
             subscribeSetting(\.high, on: $high) { high = $0 }
@@ -42,7 +39,7 @@ extension UserInterfaceSettings {
 
 
             subscribeSetting(\.glucoseColorScheme, on: $glucoseColorScheme) { glucoseColorScheme = $0 }
             subscribeSetting(\.glucoseColorScheme, on: $glucoseColorScheme) { glucoseColorScheme = $0 }
 
 
-            subscribeSetting(\.hbA1cDisplayUnit, on: $hbA1cDisplayUnit) { hbA1cDisplayUnit = $0 }
+            subscribeSetting(\.eA1cDisplayUnit, on: $eA1cDisplayUnit) { eA1cDisplayUnit = $0 }
 
 
             subscribeSetting(\.timeInRangeChartStyle, on: $timeInRangeChartStyle) { timeInRangeChartStyle = $0 }
             subscribeSetting(\.timeInRangeChartStyle, on: $timeInRangeChartStyle) { timeInRangeChartStyle = $0 }
         }
         }

+ 6 - 60
Trio/Sources/Modules/UserInterfaceSettings/View/UserInterfaceSettingsRootView.swift

@@ -376,76 +376,22 @@ extension UserInterfaceSettings {
                     }.padding(.bottom)
                     }.padding(.bottom)
                 }.listRowBackground(Color.chart)
                 }.listRowBackground(Color.chart)
 
 
-                Section {
-                    VStack(alignment: .leading) {
-                        Picker(
-                            selection: $state.totalInsulinDisplayType,
-                            label: Text("Total Insulin Display Type").multilineTextAlignment(.leading)
-                        ) {
-                            ForEach(TotalInsulinDisplayType.allCases) { selection in
-                                Text(selection.displayName).tag(selection)
-                            }
-                        }.padding(.top)
-
-                        HStack(alignment: .center) {
-                            Text(
-                                "Choose which total insulin calculation is displayed on the home screen. See hint for more details."
-                            )
-                            .font(.footnote)
-                            .foregroundColor(.secondary)
-                            .lineLimit(nil)
-                            Spacer()
-                            Button(
-                                action: {
-                                    hintLabel = String(localized: "Total Insulin Display Type")
-                                    selectedVerboseHint =
-                                        AnyView(
-                                            VStack(alignment: .leading, spacing: 10) {
-                                                Text(
-                                                    "Choose between Total Daily Dose (TDD) or Total Insulin in Scope (TINS) to be displayed above the main glucose graph. Descriptions for each option found below."
-                                                )
-                                                VStack(alignment: .leading, spacing: 5) {
-                                                    Text("Total Daily Dose:").bold()
-                                                    Text(
-                                                        "Displays the last 24 hours of total insulin administered, both basal and bolus."
-                                                    )
-                                                }
-                                                VStack(alignment: .leading, spacing: 5) {
-                                                    Text("Total Insulin in Scope:").bold()
-                                                    Text(
-                                                        "Displays the total amount of insulin given as a bolus (manual or SMB) and through temporary basal rates above zero during the selected timeframe of the main chart."
-                                                    )
-                                                }
-                                            }
-                                        )
-                                    shouldDisplayHint.toggle()
-                                },
-                                label: {
-                                    HStack {
-                                        Image(systemName: "questionmark.circle")
-                                    }
-                                }
-                            ).buttonStyle(BorderlessButtonStyle())
-                        }.padding(.top)
-                    }.padding(.bottom)
-                }.listRowBackground(Color.chart)
-
                 Section(
                 Section(
                     header: Text("Trio Statistics"),
                     header: Text("Trio Statistics"),
                     content: {
                     content: {
                         VStack {
                         VStack {
                             Picker(
                             Picker(
-                                selection: $state.hbA1cDisplayUnit,
-                                label: Text("HbA1c Display Unit")
+                                selection: $state.eA1cDisplayUnit,
+                                label: Text("eA1c Display Unit")
                             ) {
                             ) {
-                                ForEach(HbA1cDisplayUnit.allCases) { selection in
+                                ForEach(EstimatedA1cDisplayUnit.allCases) { selection in
                                     Text(selection.displayName).tag(selection)
                                     Text(selection.displayName).tag(selection)
                                 }
                                 }
                             }.padding(.top)
                             }.padding(.top)
 
 
                             HStack(alignment: .center) {
                             HStack(alignment: .center) {
                                 Text(
                                 Text(
-                                    "Choose to display HbA1c in percent or mmol/mol."
+                                    "Choose to display eA1c in percent or mmol/mol."
                                 )
                                 )
                                 .font(.footnote)
                                 .font(.footnote)
                                 .foregroundColor(.secondary)
                                 .foregroundColor(.secondary)
@@ -453,11 +399,11 @@ extension UserInterfaceSettings {
                                 Spacer()
                                 Spacer()
                                 Button(
                                 Button(
                                     action: {
                                     action: {
-                                        hintLabel = String(localized: "HbA1c Display Unit")
+                                        hintLabel = String(localized: "eA1c Display Unit")
                                         selectedVerboseHint =
                                         selectedVerboseHint =
                                             AnyView(
                                             AnyView(
                                                 Text(
                                                 Text(
-                                                    "Choose which format you'd prefer the HbA1c value in the statistics view as a percentage (Example: 6.5%) or mmol/mol (Example: 48 mmol/mol)."
+                                                    "Choose which format you'd prefer the eA1c (estimated A1c) value in the statistics view as a percentage (Example: 6.5%) or mmol/mol (Example: 48 mmol/mol)."
                                                 )
                                                 )
                                             )
                                             )
                                         shouldDisplayHint.toggle()
                                         shouldDisplayHint.toggle()

+ 25 - 10
Trio/Sources/Services/LiveActivity/Data/DataManager.swift

@@ -25,6 +25,7 @@ extension LiveActivityManager {
         }
         }
     }
     }
 
 
+    // TODO: extract logic or at least rename function appropiately
     func fetchAndMapDetermination() async throws -> DeterminationData? {
     func fetchAndMapDetermination() async throws -> DeterminationData? {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OrefDetermination.self,
             ofType: OrefDetermination.self,
@@ -33,23 +34,37 @@ extension LiveActivityManager {
             key: "deliverAt",
             key: "deliverAt",
             ascending: false,
             ascending: false,
             fetchLimit: 1,
             fetchLimit: 1,
-            propertiesToFetch: ["iob", "cob", "totalDailyDose", "currentTarget", "deliverAt"]
+            propertiesToFetch: ["iob", "cob", "currentTarget", "deliverAt"]
+        )
+
+        let tddResults = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: TDDStored.self,
+            onContext: context,
+            predicate: NSPredicate.predicateFor30MinAgo,
+            key: "date",
+            ascending: false,
+            fetchLimit: 1,
+            propertiesToFetch: ["total"]
         )
         )
 
 
         return try await context.perform {
         return try await context.perform {
-            guard let determinationResults = results as? [[String: Any]] else {
+            guard let determinationResults = results as? [[String: Any]], let tddResults = tddResults as? [[String: Any]] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
             }
 
 
-            return determinationResults.first.map {
-                DeterminationData(
-                    cob: ($0["cob"] as? Int) ?? 0,
-                    iob: ($0["iob"] as? NSDecimalNumber)?.decimalValue ?? 0,
-                    tdd: ($0["totalDailyDose"] as? NSDecimalNumber)?.decimalValue ?? 0,
-                    target: ($0["currentTarget"] as? NSDecimalNumber)?.decimalValue ?? 0,
-                    date: $0["deliverAt"] as? Date ?? nil
-                )
+            guard let determination = determinationResults.first else {
+                return nil
             }
             }
+
+            let tddValue = (tddResults.first?["total"] as? NSDecimalNumber)?.decimalValue ?? 0
+
+            return DeterminationData(
+                cob: (determination["cob"] as? Int) ?? 0,
+                iob: (determination["iob"] as? NSDecimalNumber)?.decimalValue ?? 0,
+                tdd: tddValue,
+                target: (determination["currentTarget"] as? NSDecimalNumber)?.decimalValue ?? 0,
+                date: determination["deliverAt"] as? Date ?? nil
+            )
         }
         }
     }
     }
 
 

+ 33 - 0
Trio/Sources/Services/Network/Nightscout/NightscoutAPI.swift

@@ -435,6 +435,39 @@ extension NightscoutAPI {
         }
         }
     }
     }
 
 
+    /// The delete func is needed to force re-rendering of overrides with changed durations in Nightscout main chart
+    /// since just updating durations in existing entries doesn't trigger re-rendering.
+    func deleteNightscoutOverride(withCreatedAt createdAt: String) async throws {
+        var components = URLComponents()
+        components.scheme = url.scheme
+        components.host = url.host
+        components.port = url.port
+        components.path = Config.treatmentsPath
+        components.queryItems = [
+            URLQueryItem(name: "find[created_at][$eq]", value: createdAt)
+        ]
+
+        guard let url = components.url else {
+            throw URLError(.badURL)
+        }
+
+        var request = URLRequest(url: url)
+        request.timeoutInterval = Config.timeout
+        request.httpMethod = "DELETE"
+
+        if let secret = secret {
+            request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
+        }
+
+        let (_, response) = try await URLSession.shared.data(for: request)
+        if let httpResponse = response as? HTTPURLResponse, (200 ... 299).contains(httpResponse.statusCode) {
+        } else {
+            let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0
+            debug(.nightscout, "Failed to delete override with created_at: \(createdAt). HTTP status code: \(statusCode)")
+            throw URLError(.badServerResponse)
+        }
+    }
+
     func uploadOverrides(_ overrides: [NightscoutExercise]) async throws {
     func uploadOverrides(_ overrides: [NightscoutExercise]) async throws {
         var components = URLComponents()
         var components = URLComponents()
         components.scheme = url.scheme
         components.scheme = url.scheme

+ 38 - 3
Trio/Sources/Services/Network/Nightscout/NightscoutManager.swift

@@ -1090,12 +1090,30 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         }
         }
 
 
         do {
         do {
-            for chunk in overrides.chunks(ofCount: 100) {
+            var processedOverrides: [NightscoutExercise] = []
+
+            for override in overrides {
+                guard let createdAtString = override.created_at as? String else {
+                    continue
+                }
+
+                /// Check for an existing stored override and delete if needed
+                /// This is neccessary to delete original entry in NS when a running override gets customized with a new duration.
+                try await overridesStorage.checkIfShouldDeleteNightscoutOverrideEntry(
+                    forCreatedAt: createdAtString,
+                    newDuration: override.duration,
+                    using: nightscout
+                )
+
+                processedOverrides.append(override)
+            }
+
+            for chunk in processedOverrides.chunks(ofCount: 100) {
                 try await nightscout.uploadOverrides(Array(chunk))
                 try await nightscout.uploadOverrides(Array(chunk))
             }
             }
 
 
             // If successful, update the isUploadedToNS property of the OverrideStored objects
             // If successful, update the isUploadedToNS property of the OverrideStored objects
-            await updateOverridesAsUploaded(overrides)
+            await updateOverridesAsUploaded(processedOverrides)
 
 
             debug(.nightscout, "Overrides uploaded")
             debug(.nightscout, "Overrides uploaded")
         } catch {
         } catch {
@@ -1131,7 +1149,24 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         }
         }
 
 
         do {
         do {
-            for chunk in overrideRuns.chunks(ofCount: 100) {
+            var processedOverrideRuns: [NightscoutExercise] = []
+            for overrideRun in overrideRuns {
+                guard let createdAtString = overrideRun.created_at as? String else {
+                    continue
+                }
+
+                /// Check for an existing stored override and delete if needed
+                /// This is neccessary when a running override is cancelled, or replaced with a new override, before its duration is over.
+                try await overridesStorage.checkIfShouldDeleteNightscoutOverrideEntry(
+                    forCreatedAt: createdAtString,
+                    newDuration: overrideRun.duration,
+                    using: nightscout
+                )
+
+                processedOverrideRuns.append(overrideRun)
+            }
+
+            for chunk in processedOverrideRuns.chunks(ofCount: 100) {
                 try await nightscout.uploadOverrides(Array(chunk))
                 try await nightscout.uploadOverrides(Array(chunk))
             }
             }
 
 

+ 1 - 1
Trio/Sources/Services/WatchManager/AppleWatchManager.swift

@@ -60,7 +60,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
         // Observer for OrefDetermination and adjustments
         // Observer for OrefDetermination and adjustments
         coreDataPublisher =
         coreDataPublisher =
             changedObjectsOnManagedObjectContextDidSavePublisher()
             changedObjectsOnManagedObjectContextDidSavePublisher()
-                .receive(on: DispatchQueue.global(qos: .background))
+                .receive(on: queue)
                 .share()
                 .share()
                 .eraseToAnyPublisher()
                 .eraseToAnyPublisher()
 
 

+ 1 - 1
Trio/Sources/Services/WatchManager/GarminManager.swift

@@ -125,7 +125,7 @@ final class BaseGarminManager: NSObject, GarminManager, Injectable {
 
 
         coreDataPublisher =
         coreDataPublisher =
             changedObjectsOnManagedObjectContextDidSavePublisher()
             changedObjectsOnManagedObjectContextDidSavePublisher()
-                .receive(on: DispatchQueue.global(qos: .background))
+                .receive(on: queue)
                 .share()
                 .share()
                 .eraseToAnyPublisher()
                 .eraseToAnyPublisher()
 
 

+ 3 - 16
Trio/Sources/Shortcuts/LiveActivity/RestartLiveActivityIntentRequest.swift

@@ -11,18 +11,9 @@ import UIKit
     /// - Throws: An error if the restart process fails.
     /// - Throws: An error if the restart process fails.
     /// - Returns: Void upon successful restart.
     /// - Returns: Void upon successful restart.
     @MainActor func performRestart() async throws {
     @MainActor func performRestart() async throws {
-        var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
-
         // Start background task
         // Start background task
-        backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "Restart Live Activity") {
-            Task { @MainActor in
-                if backgroundTaskID != .invalid {
-                    UIApplication.shared.endBackgroundTask(backgroundTaskID)
-                    backgroundTaskID = .invalid
-                    debug(.default, "Background task expired and ended.")
-                }
-            }
-        }
+        var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
+        backgroundTaskID = startBackgroundTask(withName: "Restart Live Activity")
 
 
         guard backgroundTaskID != .invalid else {
         guard backgroundTaskID != .invalid else {
             debug(.default, "Failed to start background task.")
             debug(.default, "Failed to start background task.")
@@ -34,10 +25,6 @@ import UIKit
         await liveActivityManager.restartActivityFromLiveActivityIntent()
         await liveActivityManager.restartActivityFromLiveActivityIntent()
 
 
         // Ensure background task ends properly
         // Ensure background task ends properly
-        if backgroundTaskID != .invalid {
-            UIApplication.shared.endBackgroundTask(backgroundTaskID)
-            debug(.default, "Background task ended successfully.")
-            backgroundTaskID = .invalid
-        }
+        endBackgroundTaskSafely(&backgroundTaskID, taskName: "Restart Live Activity")
     }
     }
 }
 }

+ 15 - 2
Trio/Sources/Shortcuts/Override/ApplyOverridePresetIntent.swift

@@ -1,24 +1,28 @@
 import AppIntents
 import AppIntents
 import Foundation
 import Foundation
 
 
+/// An App Intent that allows users to activate an override preset through the Shortcuts app.
 struct ApplyOverridePresetIntent: AppIntent {
 struct ApplyOverridePresetIntent: AppIntent {
-    // Title of the action in the Shortcuts app
+    /// The title displayed for this action in the Shortcuts app.
     static var title = LocalizedStringResource("Activate an override", table: "ShortcutsDetail")
     static var title = LocalizedStringResource("Activate an override", table: "ShortcutsDetail")
 
 
-    // Description of the action in the Shortcuts app
+    /// The description displayed for this action in the Shortcuts app.
     static var description = IntentDescription(.init("Activate an override", table: "ShortcutsDetail"))
     static var description = IntentDescription(.init("Activate an override", table: "ShortcutsDetail"))
 
 
+    /// The override preset to be applied.
     @Parameter(
     @Parameter(
         title: LocalizedStringResource("Override", table: "ShortcutsDetail"),
         title: LocalizedStringResource("Override", table: "ShortcutsDetail"),
         description: LocalizedStringResource("Override choice", table: "ShortcutsDetail")
         description: LocalizedStringResource("Override choice", table: "ShortcutsDetail")
     ) var preset: OverridePreset?
     ) var preset: OverridePreset?
 
 
+    /// A boolean parameter that determines whether confirmation is required before applying the override.
     @Parameter(
     @Parameter(
         title: LocalizedStringResource("Confirm Before applying", table: "ShortcutsDetail"),
         title: LocalizedStringResource("Confirm Before applying", table: "ShortcutsDetail"),
         description: LocalizedStringResource("If toggled, you will need to confirm before applying", table: "ShortcutsDetail"),
         description: LocalizedStringResource("If toggled, you will need to confirm before applying", table: "ShortcutsDetail"),
         default: true
         default: true
     ) var confirmBeforeApplying: Bool
     ) var confirmBeforeApplying: Bool
 
 
+    /// Defines the summary format shown in the Shortcuts app when configuring this intent.
     static var parameterSummary: some ParameterSummary {
     static var parameterSummary: some ParameterSummary {
         When(\ApplyOverridePresetIntent.$confirmBeforeApplying, .equalTo, true, {
         When(\ApplyOverridePresetIntent.$confirmBeforeApplying, .equalTo, true, {
             Summary("Applying \(\.$preset) override", table: "ShortcutsDetail") {
             Summary("Applying \(\.$preset) override", table: "ShortcutsDetail") {
@@ -31,12 +35,18 @@ struct ApplyOverridePresetIntent: AppIntent {
         })
         })
     }
     }
 
 
+    /// Executes the intent to apply the selected override preset.
+    ///
+    /// - Returns: A dialog indicating whether the override was successfully applied or failed.
+    /// - Throws: An error if an issue occurs during execution.
     @MainActor func perform() async throws -> some ProvidesDialog {
     @MainActor func perform() async throws -> some ProvidesDialog {
         do {
         do {
+            // Determine which preset to apply
             let presetToApply: OverridePreset
             let presetToApply: OverridePreset
             if let preset = preset {
             if let preset = preset {
                 presetToApply = preset
                 presetToApply = preset
             } else {
             } else {
+                // Request user selection if no preset is provided
                 presetToApply = try await $preset.requestDisambiguation(
                 presetToApply = try await $preset.requestDisambiguation(
                     among: await OverridePresetsIntentRequest().fetchAndProcessOverrides(),
                     among: await OverridePresetsIntentRequest().fetchAndProcessOverrides(),
                     dialog: IntentDialog(LocalizedStringResource("Select override", table: "ShortcutsDetail"))
                     dialog: IntentDialog(LocalizedStringResource("Select override", table: "ShortcutsDetail"))
@@ -44,6 +54,8 @@ struct ApplyOverridePresetIntent: AppIntent {
             }
             }
 
 
             let displayName: String = presetToApply.name
             let displayName: String = presetToApply.name
+
+            // Request confirmation before applying if required
             if confirmBeforeApplying {
             if confirmBeforeApplying {
                 try await requestConfirmation(
                 try await requestConfirmation(
                     result: .result(
                     result: .result(
@@ -55,6 +67,7 @@ struct ApplyOverridePresetIntent: AppIntent {
                 )
                 )
             }
             }
 
 
+            // Apply the override and return the appropriate dialog message
             if await OverridePresetsIntentRequest().enactOverride(presetToApply) {
             if await OverridePresetsIntentRequest().enactOverride(presetToApply) {
                 return .result(
                 return .result(
                     dialog: IntentDialog(
                     dialog: IntentDialog(

+ 11 - 8
Trio/Sources/Shortcuts/Override/CancelOverrideIntent.swift

@@ -1,19 +1,22 @@
 import AppIntents
 import AppIntents
 import Foundation
 import Foundation
 
 
+/// An App Intent that allows users to cancel an active override through the Shortcuts app.
 struct CancelOverrideIntent: AppIntent {
 struct CancelOverrideIntent: AppIntent {
-    // Title of the action in the Shortcuts app
+    /// The title displayed for this action in the Shortcuts app.
     static var title = LocalizedStringResource("Cancel override", table: "ShortcutsDetail")
     static var title = LocalizedStringResource("Cancel override", table: "ShortcutsDetail")
 
 
-    // Description of the action in the Shortcuts app
+    /// The description displayed for this action in the Shortcuts app.
     static var description = IntentDescription(.init("Cancel an active override", table: "ShortcutsDetail"))
     static var description = IntentDescription(.init("Cancel an active override", table: "ShortcutsDetail"))
 
 
+    /// Performs the intent action to cancel an active override.
+    ///
+    /// - Returns: A confirmation dialog indicating the override has been canceled.
+    /// - Throws: An error if the cancellation process fails.
     @MainActor func perform() async throws -> some ProvidesDialog {
     @MainActor func perform() async throws -> some ProvidesDialog {
-        do {
-            await OverridePresetsIntentRequest().cancelOverride()
-            return .result(
-                dialog: IntentDialog(LocalizedStringResource("Override canceled", table: "ShortcutsDetail"))
-            )
-        }
+        await OverridePresetsIntentRequest().cancelOverride()
+        return .result(
+            dialog: IntentDialog(LocalizedStringResource("Override canceled", table: "ShortcutsDetail"))
+        )
     }
     }
 }
 }

+ 17 - 0
Trio/Sources/Shortcuts/Override/OverridePresetEntity.swift

@@ -3,24 +3,41 @@ import Foundation
 import Intents
 import Intents
 import Swinject
 import Swinject
 
 
+/// Represents an override preset that can be used in the app.
 struct OverridePreset: AppEntity, Identifiable {
 struct OverridePreset: AppEntity, Identifiable {
+    /// Default query instance for fetching override presets.
     static var defaultQuery = OverridePresetsQuery()
     static var defaultQuery = OverridePresetsQuery()
 
 
+    /// Unique identifier for the override preset.
     var id: String
     var id: String
+
+    /// Name of the override preset.
     var name: String
     var name: String
 
 
+    /// Provides a display representation for the override preset.
     var displayRepresentation: DisplayRepresentation {
     var displayRepresentation: DisplayRepresentation {
         DisplayRepresentation(title: "\(name)")
         DisplayRepresentation(title: "\(name)")
     }
     }
 
 
+    /// Representation for the entity type when displayed in UI.
     static var typeDisplayRepresentation: TypeDisplayRepresentation = "Override"
     static var typeDisplayRepresentation: TypeDisplayRepresentation = "Override"
 }
 }
 
 
+/// Query structure for fetching override presets in an App Intent.
 struct OverridePresetsQuery: EntityQuery {
 struct OverridePresetsQuery: EntityQuery {
+    /// Fetches a list of override presets matching the given identifiers.
+    ///
+    /// - Parameter identifiers: A list of override preset IDs to fetch.
+    /// - Returns: An array of `OverridePreset` objects matching the given IDs.
+    /// - Throws: An error if the fetch operation fails.
     func entities(for identifiers: [OverridePreset.ID]) async throws -> [OverridePreset] {
     func entities(for identifiers: [OverridePreset.ID]) async throws -> [OverridePreset] {
         try await OverridePresetsIntentRequest().fetchIDs(identifiers)
         try await OverridePresetsIntentRequest().fetchIDs(identifiers)
     }
     }
 
 
+    /// Fetches a list of suggested override presets.
+    ///
+    /// - Returns: An array of available `OverridePreset` objects.
+    /// - Throws: An error if fetching fails.
     func suggestedEntities() async throws -> [OverridePreset] {
     func suggestedEntities() async throws -> [OverridePreset] {
         try await OverridePresetsIntentRequest().fetchAndProcessOverrides()
         try await OverridePresetsIntentRequest().fetchAndProcessOverrides()
     }
     }

+ 88 - 65
Trio/Sources/Shortcuts/Override/OverridePresetsIntentRequest.swift

@@ -9,6 +9,14 @@ import UIKit
         case noActiveOverride
         case noActiveOverride
     }
     }
 
 
+    private var intentSuccess: Bool = false
+
+    /**
+     Fetches and processes override presets from Core Data.
+
+     - Returns: An array of `OverridePreset` objects.
+     - Throws: An error if fetching fails or Core Data operations fail.
+     */
     func fetchAndProcessOverrides() async throws -> [OverridePreset] {
     func fetchAndProcessOverrides() async throws -> [OverridePreset] {
         do {
         do {
             // Fetch all Override Presets via OverrideStorage
             // Fetch all Override Presets via OverrideStorage
@@ -35,6 +43,13 @@ import UIKit
         }
         }
     }
     }
 
 
+    /**
+     Fetches override presets by their IDs.
+
+     - Parameter uuid: An array of `OverridePreset.ID` values to fetch.
+     - Returns: An array of `OverridePreset` objects matching the provided IDs.
+     - Throws: `overridePresetsError.noTempOverrideFound` if no presets are found.
+     */
     func fetchIDs(_ uuid: [OverridePreset.ID]) async throws -> [OverridePreset] {
     func fetchIDs(_ uuid: [OverridePreset.ID]) async throws -> [OverridePreset] {
         try await coredataContext.perform {
         try await coredataContext.perform {
             let fetchRequest: NSFetchRequest<OverrideStored> = OverrideStored.fetchRequest()
             let fetchRequest: NSFetchRequest<OverrideStored> = OverrideStored.fetchRequest()
@@ -64,6 +79,13 @@ import UIKit
         }
         }
     }
     }
 
 
+    /**
+     Fetches the Core Data `NSManagedObjectID` for a given `OverridePreset`.
+
+     - Parameter preset: The `OverridePreset` for which to fetch the object ID.
+     - Returns: The corresponding `NSManagedObjectID`.
+     - Throws: `overridePresetsError.noTempOverrideFound` if the preset is not found.
+     */
     private func fetchOverrideID(_ preset: OverridePreset) async throws -> NSManagedObjectID {
     private func fetchOverrideID(_ preset: OverridePreset) async throws -> NSManagedObjectID {
         let fetchRequest: NSFetchRequest<OverrideStored> = OverrideStored.fetchRequest()
         let fetchRequest: NSFetchRequest<OverrideStored> = OverrideStored.fetchRequest()
         fetchRequest.predicate = NSPredicate(format: "id == %@", preset.id)
         fetchRequest.predicate = NSPredicate(format: "id == %@", preset.id)
@@ -81,103 +103,96 @@ import UIKit
         }
         }
     }
     }
 
 
+    /**
+     Enacts an override preset by enabling it in Core Data and notifying the system.
+
+     - Parameter preset: The `OverridePreset` to enact.
+     - Returns: A boolean indicating whether the override was successfully enacted.
+     */
     @MainActor func enactOverride(_ preset: OverridePreset) async -> Bool {
     @MainActor func enactOverride(_ preset: OverridePreset) async -> Bool {
-        // Start background task
+        debug(.default, "Enacting override: \(preset.name)")
+        intentSuccess = false
+
         var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
         var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
-        backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "Override Upload") {
-            guard backgroundTaskID != .invalid else { return }
-            Task {
-                UIApplication.shared.endBackgroundTask(backgroundTaskID)
-            }
-            backgroundTaskID = .invalid
-        }
+        backgroundTaskID = startBackgroundTask(withName: "Override Enact")
 
 
-        defer {
-            if backgroundTaskID != .invalid {
-                Task {
-                    UIApplication.shared.endBackgroundTask(backgroundTaskID)
-                }
-                backgroundTaskID = .invalid
-            }
-        }
+        await disableAllActiveOverrides(shouldStartBackgroundTask: false)
 
 
         do {
         do {
-            // Get NSManagedObjectID of Preset
             let overrideID = try await fetchOverrideID(preset)
             let overrideID = try await fetchOverrideID(preset)
             guard let overrideObject = try viewContext.existingObject(with: overrideID) as? OverrideStored else {
             guard let overrideObject = try viewContext.existingObject(with: overrideID) as? OverrideStored else {
+                endBackgroundTaskSafely(&backgroundTaskID, taskName: "Override Enact")
                 throw overridePresetsError.noTempOverrideFound
                 throw overridePresetsError.noTempOverrideFound
             }
             }
 
 
-            // Enable Override
             overrideObject.enabled = true
             overrideObject.enabled = true
             overrideObject.date = Date()
             overrideObject.date = Date()
             overrideObject.isUploadedToNS = false
             overrideObject.isUploadedToNS = false
 
 
-            // Disable previous overrides if necessary
-            await disableAllActiveOverrides(except: overrideID, createOverrideRunEntry: true, shouldStartBackgroundTask: false)
-
             if viewContext.hasChanges {
             if viewContext.hasChanges {
+                debug(.default, "Saving changes...")
                 try viewContext.save()
                 try viewContext.save()
+                debug(.default, "Waiting for notification...")
                 Foundation.NotificationCenter.default.post(name: .willUpdateOverrideConfiguration, object: nil)
                 Foundation.NotificationCenter.default.post(name: .willUpdateOverrideConfiguration, object: nil)
                 await awaitNotification(.didUpdateOverrideConfiguration)
                 await awaitNotification(.didUpdateOverrideConfiguration)
-                return true
+                debug(.default, "Notification received, continuing...")
+                intentSuccess = true
             }
             }
-            return false
+
+            endBackgroundTaskSafely(&backgroundTaskID, taskName: "Override Enact")
+            debug(.default, "Finished. Override enacted via Shortcut.")
+            return intentSuccess
         } catch {
         } catch {
             debug(
             debug(
                 .default,
                 .default,
                 "\(DebuggingIdentifiers.failed) Failed to enact override: \(error.localizedDescription)"
                 "\(DebuggingIdentifiers.failed) Failed to enact override: \(error.localizedDescription)"
             )
             )
+            endBackgroundTaskSafely(&backgroundTaskID, taskName: "Override Enact")
             return false
             return false
         }
         }
     }
     }
 
 
+    /**
+     Cancels all active overrides asynchronously.
+     */
     func cancelOverride() async {
     func cancelOverride() async {
-        await disableAllActiveOverrides(createOverrideRunEntry: true, shouldStartBackgroundTask: true)
+        await disableAllActiveOverrides(shouldStartBackgroundTask: true)
     }
     }
 
 
-    @MainActor func disableAllActiveOverrides(
-        except overrideID: NSManagedObjectID? = nil,
-        createOverrideRunEntry: Bool,
-        shouldStartBackgroundTask: Bool = true
-    ) async {
-        var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
+    /**
+     Disables all active overrides and optionally starts a background task.
 
 
-        if shouldStartBackgroundTask {
-            // Start background task
-            backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "Override Cancel") {
-                guard backgroundTaskID != .invalid else { return }
-                Task {
-                    // End background task when the time is about to expire
-                    UIApplication.shared.endBackgroundTask(backgroundTaskID)
-                }
-                backgroundTaskID = .invalid
-            }
-        }
+     - Parameter shouldStartBackgroundTask: A boolean indicating whether to start a background task.
+     */
+    @MainActor func disableAllActiveOverrides(shouldStartBackgroundTask: Bool) async {
+        debug(.default, "Disabling all active overrides")
+        var backgroundTaskID: UIBackgroundTaskIdentifier?
 
 
-        // Defer block to end background task when function exits, only if it was started
-        defer {
-            if shouldStartBackgroundTask, backgroundTaskID != .invalid {
-                Task {
-                    UIApplication.shared.endBackgroundTask(backgroundTaskID)
-                }
-                backgroundTaskID = .invalid
-            }
+        if shouldStartBackgroundTask {
+            debug(.default, "Starting background task for override cancel")
+            backgroundTaskID = .invalid
+            backgroundTaskID = startBackgroundTask(withName: "Override Cancel")
         }
         }
 
 
         do {
         do {
             // Get NSManagedObjectID of all active overrides
             // Get NSManagedObjectID of all active overrides
-            let ids = try await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0) // 0 = no fetch limit
-            // Fetch existing OverrideStored objects
+            let ids = try await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0)
             let results = try ids.compactMap { id in
             let results = try ids.compactMap { id in
                 try self.viewContext.existingObject(with: id) as? OverrideStored
                 try self.viewContext.existingObject(with: id) as? OverrideStored
             }
             }
 
 
             // Return early if no results
             // Return early if no results
-            guard !results.isEmpty else { return }
+            guard !results.isEmpty else {
+                debug(.default, "No active overrides to cancel… returning early")
+                if var backgroundTaskID = backgroundTaskID {
+                    debug(.default, "Ending background task for override cancel")
+                    endBackgroundTaskSafely(&backgroundTaskID, taskName: "Override Cancel")
+                }
+                return
+            }
 
 
             // Create OverrideRunStored entry if needed
             // Create OverrideRunStored entry if needed
-            if createOverrideRunEntry, let canceledOverride = results.first {
+            if let canceledOverride = results.first {
                 let newOverrideRunStored = OverrideRunStored(context: viewContext)
                 let newOverrideRunStored = OverrideRunStored(context: viewContext)
                 newOverrideRunStored.id = UUID()
                 newOverrideRunStored.id = UUID()
                 newOverrideRunStored.name = canceledOverride.name
                 newOverrideRunStored.name = canceledOverride.name
@@ -190,30 +205,38 @@ import UIKit
                 newOverrideRunStored.isUploadedToNS = false
                 newOverrideRunStored.isUploadedToNS = false
             }
             }
 
 
-            // Disable all overrides except the one specified
+            // Disable all active overrides
             for overrideToCancel in results {
             for overrideToCancel in results {
-                if overrideToCancel.objectID != overrideID {
-                    overrideToCancel.enabled = false
-                    overrideToCancel.isUploadedToNS = false
-                }
+                let endTime = overrideToCancel.date?
+                    .addingTimeInterval(TimeInterval(truncating: overrideToCancel.duration ?? 0))
+
+                debugPrint(
+                    "Disabling override: \(overrideToCancel.name ?? "Unnamed") with end time: \(endTime?.description ?? "Unknown")"
+                )
+                overrideToCancel.enabled = false
+                overrideToCancel.isUploadedToNS = false
             }
             }
 
 
             if viewContext.hasChanges {
             if viewContext.hasChanges {
                 try viewContext.save()
                 try viewContext.save()
-
-                // Update State variables in OverrideView
+                debug(.default, "Waiting for notification...")
                 Foundation.NotificationCenter.default.post(name: .willUpdateOverrideConfiguration, object: nil)
                 Foundation.NotificationCenter.default.post(name: .willUpdateOverrideConfiguration, object: nil)
+                await awaitNotification(.didUpdateOverrideConfiguration)
+                debug(.default, "Notification received, continuing...")
             }
             }
 
 
-            // Await the notification
-            print("Waiting for notification...")
-            await awaitNotification(.didUpdateOverrideConfiguration)
-            print("Notification received, continuing...")
-
+            if var backgroundTaskID = backgroundTaskID {
+                debug(.default, "Ending background task for override cancel")
+                endBackgroundTaskSafely(&backgroundTaskID, taskName: "Override Cancel")
+            }
         } catch {
         } catch {
             debugPrint(
             debugPrint(
                 "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active Overrides with error: \(error.localizedDescription)"
                 "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active Overrides with error: \(error.localizedDescription)"
             )
             )
+            if var backgroundTaskID = backgroundTaskID {
+                debug(.default, "Ending background task for override cancel")
+                endBackgroundTaskSafely(&backgroundTaskID, taskName: "Override Cancel")
+            }
         }
         }
     }
     }
 }
 }

+ 22 - 4
Trio/Sources/Shortcuts/TempPresets/ApplyTempPresetIntent.swift

@@ -1,21 +1,25 @@
 import AppIntents
 import AppIntents
 import Foundation
 import Foundation
 
 
+/// An App Intent that allows users to apply a temporary target preset through the Shortcuts app.
 struct ApplyTempPresetIntent: AppIntent {
 struct ApplyTempPresetIntent: AppIntent {
-    // Title of the action in the Shortcuts app
+    /// The title displayed for this action in the Shortcuts app.
     static var title: LocalizedStringResource = "Apply a Temporary Target"
     static var title: LocalizedStringResource = "Apply a Temporary Target"
 
 
-    // Description of the action in the Shortcuts app
+    /// The description displayed for this action in the Shortcuts app.
     static var description = IntentDescription("Enable a Temporary Target")
     static var description = IntentDescription("Enable a Temporary Target")
 
 
+    /// The temporary target preset to be applied.
     @Parameter(title: "Preset") var preset: TempPreset?
     @Parameter(title: "Preset") var preset: TempPreset?
 
 
+    /// A boolean parameter that determines whether confirmation is required before applying the temporary target.
     @Parameter(
     @Parameter(
         title: "Confirm Before applying",
         title: "Confirm Before applying",
         description: "If toggled, you will need to confirm before applying",
         description: "If toggled, you will need to confirm before applying",
         default: true
         default: true
     ) var confirmBeforeApplying: Bool
     ) var confirmBeforeApplying: Bool
 
 
+    /// Defines the summary format shown in the Shortcuts app when configuring this intent.
     static var parameterSummary: some ParameterSummary {
     static var parameterSummary: some ParameterSummary {
         When(\ApplyTempPresetIntent.$confirmBeforeApplying, .equalTo, true, {
         When(\ApplyTempPresetIntent.$confirmBeforeApplying, .equalTo, true, {
             Summary("Applying \(\.$preset)") {
             Summary("Applying \(\.$preset)") {
@@ -28,23 +32,34 @@ struct ApplyTempPresetIntent: AppIntent {
         })
         })
     }
     }
 
 
+    /// Converts a decimal duration value into a formatted time string.
+    ///
+    /// - Parameter decimal: The duration value in decimal format.
+    /// - Returns: A string representing the formatted time in hours and minutes.
     private func decimalToTimeFormattedString(decimal: Decimal) -> String {
     private func decimalToTimeFormattedString(decimal: Decimal) -> String {
-        let timeInterval = TimeInterval(decimal * 60) // seconds
+        let timeInterval = TimeInterval(decimal * 60) // Convert minutes to seconds
 
 
         let formatter = DateComponentsFormatter()
         let formatter = DateComponentsFormatter()
         formatter.allowedUnits = [.hour, .minute]
         formatter.allowedUnits = [.hour, .minute]
-        formatter.unitsStyle = .brief // example: 1h 10 min
+        formatter.unitsStyle = .brief // Example: "1h 10m"
 
 
         return formatter.string(from: timeInterval) ?? ""
         return formatter.string(from: timeInterval) ?? ""
     }
     }
 
 
+    /// Executes the intent to apply the selected temporary target preset.
+    ///
+    /// - Returns: A dialog indicating whether the temporary target was successfully applied or failed.
+    /// - Throws: An error if an issue occurs during execution.
     @MainActor func perform() async throws -> some ProvidesDialog {
     @MainActor func perform() async throws -> some ProvidesDialog {
         do {
         do {
             let intentRequest = TempPresetsIntentRequest()
             let intentRequest = TempPresetsIntentRequest()
             let presetToApply: TempPreset
             let presetToApply: TempPreset
+
+            // Determine which preset to apply
             if let preset = preset {
             if let preset = preset {
                 presetToApply = preset
                 presetToApply = preset
             } else {
             } else {
+                // Request user selection if no preset is provided
                 presetToApply = try await $preset.requestDisambiguation(
                 presetToApply = try await $preset.requestDisambiguation(
                     among: intentRequest.fetchAndProcessTempTargets(),
                     among: intentRequest.fetchAndProcessTempTargets(),
                     dialog: "Select Temporary Target"
                     dialog: "Select Temporary Target"
@@ -52,12 +67,15 @@ struct ApplyTempPresetIntent: AppIntent {
             }
             }
 
 
             let displayName: String = presetToApply.name
             let displayName: String = presetToApply.name
+
+            // Request confirmation before applying if required
             if confirmBeforeApplying {
             if confirmBeforeApplying {
                 try await requestConfirmation(
                 try await requestConfirmation(
                     result: .result(dialog: "Confirm to apply Temporary Target '\(displayName)'")
                     result: .result(dialog: "Confirm to apply Temporary Target '\(displayName)'")
                 )
                 )
             }
             }
 
 
+            // Apply the temporary target and return the appropriate dialog message
             if await intentRequest.enactTempTarget(presetToApply) {
             if await intentRequest.enactTempTarget(presetToApply) {
                 return .result(
                 return .result(
                     dialog: IntentDialog(
                     dialog: IntentDialog(

+ 7 - 2
Trio/Sources/Shortcuts/TempPresets/CancelTempPresetIntent.swift

@@ -1,13 +1,18 @@
 import AppIntents
 import AppIntents
 import Foundation
 import Foundation
 
 
+/// An App Intent that allows users to cancel an active temporary target through the Shortcuts app.
 struct CancelTempPresetIntent: AppIntent {
 struct CancelTempPresetIntent: AppIntent {
-    // Title of the action in the Shortcuts app
+    /// The title displayed for this action in the Shortcuts app.
     static var title: LocalizedStringResource = "Cancel a Temporary Target"
     static var title: LocalizedStringResource = "Cancel a Temporary Target"
 
 
-    // Description of the action in the Shortcuts app
+    /// The description displayed for this action in the Shortcuts app.
     static var description = IntentDescription("Cancel Temporary Target.")
     static var description = IntentDescription("Cancel Temporary Target.")
 
 
+    /// Performs the intent action to cancel an active temporary target.
+    ///
+    /// - Returns: A confirmation dialog indicating that the temporary target has been canceled.
+    /// - Throws: An error if the cancellation process fails.
     @MainActor func perform() async throws -> some ProvidesDialog {
     @MainActor func perform() async throws -> some ProvidesDialog {
         await TempPresetsIntentRequest().cancelTempTarget()
         await TempPresetsIntentRequest().cancelTempTarget()
         return .result(
         return .result(

+ 23 - 0
Trio/Sources/Shortcuts/TempPresets/TempPresetIntent.swift

@@ -3,27 +3,50 @@ import Foundation
 import Intents
 import Intents
 import Swinject
 import Swinject
 
 
+/// Represents a temporary target preset that can be used in the app.
 struct TempPreset: AppEntity, Identifiable {
 struct TempPreset: AppEntity, Identifiable {
+    /// Default query instance for fetching temporary presets.
     static var defaultQuery = TempPresetsQuery()
     static var defaultQuery = TempPresetsQuery()
 
 
+    /// Unique identifier for the temporary preset.
     var id: UUID
     var id: UUID
+
+    /// Name of the temporary preset.
     var name: String
     var name: String
+
+    /// The upper target value for the preset, if applicable.
     var targetTop: Decimal?
     var targetTop: Decimal?
+
+    /// The lower target value for the preset, if applicable.
     var targetBottom: Decimal?
     var targetBottom: Decimal?
+
+    /// The duration of the temporary preset in minutes.
     var duration: Decimal
     var duration: Decimal
 
 
+    /// Provides a display representation for the temporary preset.
     var displayRepresentation: DisplayRepresentation {
     var displayRepresentation: DisplayRepresentation {
         DisplayRepresentation(title: "\(name)")
         DisplayRepresentation(title: "\(name)")
     }
     }
 
 
+    /// Representation for the entity type when displayed in UI.
     static var typeDisplayRepresentation: TypeDisplayRepresentation = "Presets"
     static var typeDisplayRepresentation: TypeDisplayRepresentation = "Presets"
 }
 }
 
 
+/// Query structure for fetching temporary target presets in an App Intent.
 struct TempPresetsQuery: EntityQuery {
 struct TempPresetsQuery: EntityQuery {
+    /// Fetches a list of temporary target presets matching the given identifiers.
+    ///
+    /// - Parameter identifiers: A list of preset IDs to fetch.
+    /// - Returns: An array of `TempPreset` objects matching the given IDs.
+    /// - Throws: An error if the fetch operation fails.
     func entities(for identifiers: [TempPreset.ID]) async throws -> [TempPreset] {
     func entities(for identifiers: [TempPreset.ID]) async throws -> [TempPreset] {
         await TempPresetsIntentRequest().fetchIDs(identifiers)
         await TempPresetsIntentRequest().fetchIDs(identifiers)
     }
     }
 
 
+    /// Fetches a list of suggested temporary target presets.
+    ///
+    /// - Returns: An array of available `TempPreset` objects.
+    /// - Throws: An error if fetching fails.
     func suggestedEntities() async throws -> [TempPreset] {
     func suggestedEntities() async throws -> [TempPreset] {
         try await TempPresetsIntentRequest().fetchAndProcessTempTargets()
         try await TempPresetsIntentRequest().fetchAndProcessTempTargets()
     }
     }

+ 94 - 92
Trio/Sources/Shortcuts/TempPresets/TempPresetsIntentRequest.swift

@@ -2,12 +2,21 @@ import CoreData
 import Foundation
 import Foundation
 import UIKit
 import UIKit
 
 
+/// Handles intent requests related to temporary presets, such as fetching, enacting, and canceling temp targets.
 final class TempPresetsIntentRequest: BaseIntentsRequest {
 final class TempPresetsIntentRequest: BaseIntentsRequest {
+    /// Enum representing possible errors related to temporary presets.
     enum TempPresetsError: Error {
     enum TempPresetsError: Error {
         case noTempTargetFound
         case noTempTargetFound
         case noDurationDefined
         case noDurationDefined
     }
     }
 
 
+    /// Tracks whether the intent execution was successful.
+    private var intentSuccess: Bool = false
+
+    /// Fetches and processes all available temporary target presets.
+    ///
+    /// - Returns: An array of `TempPreset` objects.
+    /// - Throws: An error if fetching or processing fails.
     func fetchAndProcessTempTargets() async throws -> [TempPreset] {
     func fetchAndProcessTempTargets() async throws -> [TempPreset] {
         // Fetch all Temp Target Presets via TempTargetStorage
         // Fetch all Temp Target Presets via TempTargetStorage
         let allTempTargetPresetsIDs = try await tempTargetsStorage.fetchForTempTargetPresets()
         let allTempTargetPresetsIDs = try await tempTargetsStorage.fetchForTempTargetPresets()
@@ -38,6 +47,10 @@ final class TempPresetsIntentRequest: BaseIntentsRequest {
         }
         }
     }
     }
 
 
+    /// Fetches temporary target presets based on the given identifiers.
+    ///
+    /// - Parameter uuid: An array of preset IDs to fetch.
+    /// - Returns: An array of `TempPreset` objects.
     func fetchIDs(_ uuid: [TempPreset.ID]) async -> [TempPreset] {
     func fetchIDs(_ uuid: [TempPreset.ID]) async -> [TempPreset] {
         await coredataContext.perform {
         await coredataContext.perform {
             let fetchRequest: NSFetchRequest<TempTargetStored> = TempTargetStored.fetchRequest()
             let fetchRequest: NSFetchRequest<TempTargetStored> = TempTargetStored.fetchRequest()
@@ -67,6 +80,10 @@ final class TempPresetsIntentRequest: BaseIntentsRequest {
         }
         }
     }
     }
 
 
+    /// Fetches the `NSManagedObjectID` for a given `TempPreset`.
+    ///
+    /// - Parameter preset: The `TempPreset` to find.
+    /// - Returns: The `NSManagedObjectID` of the temp target if found, otherwise `nil`.
     private func fetchTempTargetID(_ preset: TempPreset) async -> NSManagedObjectID? {
     private func fetchTempTargetID(_ preset: TempPreset) async -> NSManagedObjectID? {
         let fetchRequest: NSFetchRequest<TempTargetStored> = TempTargetStored.fetchRequest()
         let fetchRequest: NSFetchRequest<TempTargetStored> = TempTargetStored.fetchRequest()
         fetchRequest.predicate = NSPredicate(format: "id == %@", preset.id.uuidString)
         fetchRequest.predicate = NSPredicate(format: "id == %@", preset.id.uuidString)
@@ -84,55 +101,43 @@ final class TempPresetsIntentRequest: BaseIntentsRequest {
         }
         }
     }
     }
 
 
+    /// Enacts a temporary target preset by updating Core Data and notifying relevant components.
+    ///
+    /// - Parameter preset: The `TempPreset` to apply.
+    /// - Returns: `true` if successfully enacted, otherwise `false`.
     @MainActor func enactTempTarget(_ preset: TempPreset) async -> Bool {
     @MainActor func enactTempTarget(_ preset: TempPreset) async -> Bool {
+        debug(.default, "Enacting Temp Target: \(preset.name)")
+        intentSuccess = false
+
         // Start background task
         // Start background task
         var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
         var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
-        backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "TempTarget Upload") {
-            guard backgroundTaskID != .invalid else { return }
-            Task {
-                // End background task when the time is about to expire
-                UIApplication.shared.endBackgroundTask(backgroundTaskID)
-            }
-            backgroundTaskID = .invalid
-        }
+        backgroundTaskID = startBackgroundTask(withName: "TempTarget Enact")
 
 
-        // Defer block to end background task when function exits
-        defer {
-            if backgroundTaskID != .invalid {
-                Task {
-                    UIApplication.shared.endBackgroundTask(backgroundTaskID)
-                }
-                backgroundTaskID = .invalid
-            }
-        }
+        // Disable previous temp targets if necessary, without starting a background task
+        await disableAllActiveTempTargets(shouldStartBackgroundTask: false)
 
 
         do {
         do {
             // Get NSManagedObjectID of Preset
             // Get NSManagedObjectID of Preset
             guard let tempTargetID = await fetchTempTargetID(preset),
             guard let tempTargetID = await fetchTempTargetID(preset),
                   let tempTargetObject = try viewContext.existingObject(with: tempTargetID) as? TempTargetStored
                   let tempTargetObject = try viewContext.existingObject(with: tempTargetID) as? TempTargetStored
-            else { return false }
+            else {
+                endBackgroundTaskSafely(&backgroundTaskID, taskName: "TempTarget Enact")
+                throw TempPresetsError.noTempTargetFound
+            }
 
 
             // Enable TempTarget
             // Enable TempTarget
             tempTargetObject.enabled = true
             tempTargetObject.enabled = true
             tempTargetObject.date = Date()
             tempTargetObject.date = Date()
             tempTargetObject.isUploadedToNS = false
             tempTargetObject.isUploadedToNS = false
 
 
-            // Disable previous overrides if necessary, without starting a background task
-            await disableAllActiveTempTargets(
-                except: tempTargetID,
-                createTempTargetRunEntry: true,
-                shouldStartBackgroundTask: false
-            )
-
             if viewContext.hasChanges {
             if viewContext.hasChanges {
+                debug(.default, "Saving changes...")
                 try viewContext.save()
                 try viewContext.save()
-
+                debug(.default, "Waiting for notification...")
                 // Update State variables in TempTargetView
                 // Update State variables in TempTargetView
                 Foundation.NotificationCenter.default.post(name: .willUpdateTempTargetConfiguration, object: nil)
                 Foundation.NotificationCenter.default.post(name: .willUpdateTempTargetConfiguration, object: nil)
 
 
-                // Await the notification
-                print("Waiting for notification...")
-
+                // Prepare JSON for oref
                 guard let tempTargetDate = tempTargetObject.date, let tempTarget = tempTargetObject.target,
                 guard let tempTargetDate = tempTargetObject.date, let tempTarget = tempTargetObject.target,
                       let tempTargetDuration = tempTargetObject.duration else { return false }
                       let tempTargetDuration = tempTargetObject.duration else { return false }
 
 
@@ -148,108 +153,105 @@ final class TempPresetsIntentRequest: BaseIntentsRequest {
                     enabled: tempTargetObject.enabled,
                     enabled: tempTargetObject.enabled,
                     halfBasalTarget: tempTargetObject.halfBasalTarget as Decimal?
                     halfBasalTarget: tempTargetObject.halfBasalTarget as Decimal?
                 )
                 )
-
                 // Save the temp targets to JSON so that they get used by oref
                 // Save the temp targets to JSON so that they get used by oref
                 tempTargetsStorage.saveTempTargetsToStorage([tempTargetToStoreAsJSON])
                 tempTargetsStorage.saveTempTargetsToStorage([tempTargetToStoreAsJSON])
 
 
                 await awaitNotification(.didUpdateTempTargetConfiguration)
                 await awaitNotification(.didUpdateTempTargetConfiguration)
-                print("Notification received, continuing...")
 
 
-                return true
+                debug(.default, "Notification received, continuing...")
+                intentSuccess = true
             }
             }
+
+            endBackgroundTaskSafely(&backgroundTaskID, taskName: "TempTarget Enact")
+
+            debug(.default, "Finished. Temp Target enacted via Shortcut.")
+
+            return intentSuccess
         } catch {
         } catch {
-            // Handle error and ensure background task is ended
-            debugPrint("Failed to enact TempTarget: \(error.localizedDescription)")
-        }
+            debugPrint(
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to enact Temp Target with error: \(error.localizedDescription)"
+            )
 
 
-        return false
+            endBackgroundTaskSafely(&backgroundTaskID, taskName: "TempTarget Enact")
+
+            intentSuccess = false
+            return intentSuccess
+        }
     }
     }
 
 
+    /// Cancels an active temporary target.
     func cancelTempTarget() async {
     func cancelTempTarget() async {
-        await disableAllActiveTempTargets(createTempTargetRunEntry: true, shouldStartBackgroundTask: true)
+        await disableAllActiveTempTargets(shouldStartBackgroundTask: true)
         tempTargetsStorage.saveTempTargetsToStorage([TempTarget.cancel(at: Date().addingTimeInterval(-1))])
         tempTargetsStorage.saveTempTargetsToStorage([TempTarget.cancel(at: Date().addingTimeInterval(-1))])
     }
     }
 
 
-    @MainActor func disableAllActiveTempTargets(
-        except tempTargetID: NSManagedObjectID? = nil,
-        createTempTargetRunEntry: Bool,
-        shouldStartBackgroundTask: Bool = true
-    ) async {
-        var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
+    /// Disables all active temporary targets.
+    ///
+    /// - Parameter shouldStartBackgroundTask: A flag indicating whether a background task should be started.
+    @MainActor func disableAllActiveTempTargets(shouldStartBackgroundTask: Bool) async {
+        var backgroundTaskID: UIBackgroundTaskIdentifier?
 
 
         if shouldStartBackgroundTask {
         if shouldStartBackgroundTask {
-            // Start background task
-            backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "TempTarget Cancel") {
-                guard backgroundTaskID != .invalid else { return }
-                Task {
-                    // End background task when the time is about to expire
-                    UIApplication.shared.endBackgroundTask(backgroundTaskID)
-                }
-                backgroundTaskID = .invalid
-            }
-        }
-
-        // Defer block to end background task when function exits, only if it was started
-        defer {
-            if shouldStartBackgroundTask, backgroundTaskID != .invalid {
-                Task {
-                    UIApplication.shared.endBackgroundTask(backgroundTaskID)
-                }
-                backgroundTaskID = .invalid
-            }
+            debug(.default, "Starting background task for temp target cancel")
+            backgroundTaskID = .invalid
+            backgroundTaskID = startBackgroundTask(withName: "TempTarget Cancel")
         }
         }
 
 
         do {
         do {
-            // Get NSManagedObjectID of all active temp Targets
+            // Fetch active temp targets
             let ids = try await tempTargetsStorage.loadLatestTempTargetConfigurations(fetchLimit: 0)
             let ids = try await tempTargetsStorage.loadLatestTempTargetConfigurations(fetchLimit: 0)
-            // Fetch existing OverrideStored objects
             let results = try ids.compactMap { id in
             let results = try ids.compactMap { id in
                 try self.viewContext.existingObject(with: id) as? TempTargetStored
                 try self.viewContext.existingObject(with: id) as? TempTargetStored
             }
             }
 
 
-            // Return early if no results
-            guard !results.isEmpty else { return }
-
-            // Create TempTargetRunStored entry if needed
-            if createTempTargetRunEntry {
-                // Use the first temp target to create a new TempTargetRunStored entry
-                if let canceledTempTarget = results.first {
-                    let newTempTargetRunStored = TempTargetRunStored(context: viewContext)
-                    newTempTargetRunStored.id = UUID()
-                    newTempTargetRunStored.name = canceledTempTarget.name
-                    newTempTargetRunStored.startDate = canceledTempTarget.date ?? .distantPast
-                    newTempTargetRunStored.endDate = Date()
-                    newTempTargetRunStored
-                        .target = canceledTempTarget.target ?? 0
-                    newTempTargetRunStored.tempTarget = canceledTempTarget
-                    newTempTargetRunStored.isUploadedToNS = false
+            guard !results.isEmpty else {
+                debug(.default, "No active temp targets to cancel... returning early")
+
+                if var backgroundTaskID = backgroundTaskID {
+                    debug(.default, "Ending background task for temp target cancel")
+                    endBackgroundTaskSafely(&backgroundTaskID, taskName: "TempTarget Cancel")
                 }
                 }
+                return
+            }
+
+            // Create a new `TempTargetRunStored` entry
+            if let canceledTempTarget = results.first {
+                let newTempTargetRunStored = TempTargetRunStored(context: viewContext)
+                newTempTargetRunStored.id = UUID()
+                newTempTargetRunStored.name = canceledTempTarget.name
+                newTempTargetRunStored.startDate = canceledTempTarget.date ?? .distantPast
+                newTempTargetRunStored.endDate = Date()
+                newTempTargetRunStored.target = canceledTempTarget.target ?? 0
+                newTempTargetRunStored.tempTarget = canceledTempTarget
+                newTempTargetRunStored.isUploadedToNS = false
             }
             }
 
 
-            // Disable all override except the one with overrideID
+            // Disable all temp targets
             for tempTargetToCancel in results {
             for tempTargetToCancel in results {
-                if tempTargetToCancel.objectID != tempTargetID {
-                    tempTargetToCancel.enabled = false
-                    tempTargetToCancel.isUploadedToNS = false
-                }
+                tempTargetToCancel.enabled = false
+                tempTargetToCancel.isUploadedToNS = false
             }
             }
 
 
             if viewContext.hasChanges {
             if viewContext.hasChanges {
                 try viewContext.save()
                 try viewContext.save()
-
-                // Update State variables in OverrideView
+                debug(.default, "Waiting for notification...")
                 Foundation.NotificationCenter.default.post(name: .willUpdateTempTargetConfiguration, object: nil)
                 Foundation.NotificationCenter.default.post(name: .willUpdateTempTargetConfiguration, object: nil)
+                await awaitNotification(.didUpdateTempTargetConfiguration)
+                debug(.default, "Notification received, continuing...")
             }
             }
 
 
-            // Await the notification
-            print("Waiting for notification...")
-            await awaitNotification(.didUpdateTempTargetConfiguration)
-            print("Notification received, continuing...")
-
+            if var backgroundTaskID = backgroundTaskID {
+                debug(.default, "Ending background task for temp target cancel")
+                endBackgroundTaskSafely(&backgroundTaskID, taskName: "TempTarget Cancel")
+            }
         } catch {
         } catch {
             debugPrint(
             debugPrint(
                 "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active Temp Targets with error: \(error.localizedDescription)"
                 "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active Temp Targets with error: \(error.localizedDescription)"
             )
             )
+            if var backgroundTaskID = backgroundTaskID {
+                debug(.default, "Ending background task for temp target cancel")
+                endBackgroundTaskSafely(&backgroundTaskID, taskName: "TempTarget Cancel")
+            }
         }
         }
     }
     }
 }
 }

+ 5 - 5
Trio/Sources/Views/SettingInputSection.swift

@@ -215,15 +215,15 @@ struct SettingInputSection<VerboseHint: View>: View {
             let displayValue = units == .mmolL ? decimalValue.asMmolL : decimalValue
             let displayValue = units == .mmolL ? decimalValue.asMmolL : decimalValue
             return Text("\(displayValue.description) \(units.rawValue)")
             return Text("\(displayValue.description) \(units.rawValue)")
         case .factor:
         case .factor:
-            return Text("\(decimalValue * 100) %")
+            return Text("\(decimalValue * 100) \(String(localized: "%", comment: "Percentage symbol"))")
         case .insulinUnit:
         case .insulinUnit:
-            return Text("\(decimalValue) U")
+            return Text("\(decimalValue) \(String(localized: "U", comment: "Insulin unit abbreviation"))")
         case .gram:
         case .gram:
-            return Text("\(decimalValue) g")
+            return Text("\(decimalValue) \(String(localized: "g", comment: "Gram abbreviation"))")
         case .minute:
         case .minute:
-            return Text("\(decimalValue) min")
+            return Text("\(decimalValue) \(String(localized: "min", comment: "Minutes abbreviation"))")
         case .hour:
         case .hour:
-            return Text("\(decimalValue) hr")
+            return Text("\(decimalValue) \(String(localized: "hr", comment: "Hours abbreviation"))")
         }
         }
     }
     }
 
 

+ 3 - 1
oref0_source_version.txt

@@ -1,6 +1,8 @@
-oref0 branch: maxAbsorptionTime - git version: a542ed3
+oref0 branch: removeTDDCalc - git version: 73d6d6c
 
 
 Last commits:
 Last commits:
+73d6d6c webpack for Trio-dev
+5c6ce4b remove TDD calculation
 a542ed3 use guarded maxAbsorptionTime
 a542ed3 use guarded maxAbsorptionTime
 4c77757 Revert "reduce dynISF logging"
 4c77757 Revert "reduce dynISF logging"
 1567c76 use variable name maxMealAbsorptionTime
 1567c76 use variable name maxMealAbsorptionTime

+ 2 - 368
trio-oref/lib/determine-basal/determine-basal.js

@@ -154,9 +154,6 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_
     const isfAndCr = oref2_variables.isfAndCr;
     const isfAndCr = oref2_variables.isfAndCr;
     const isf = oref2_variables.isf;
     const isf = oref2_variables.isf;
     const cr_ = oref2_variables.cr;
     const cr_ = oref2_variables.cr;
-    const smbIsScheduledOff = oref2_variables.smbIsScheduledOff;
-    const start = oref2_variables.start;
-    var end = oref2_variables.end;
     const smbMinutes = oref2_variables.smbMinutes;
     const smbMinutes = oref2_variables.smbMinutes;
     const uamMinutes = oref2_variables.uamMinutes;
     const uamMinutes = oref2_variables.uamMinutes;
 
 
@@ -172,21 +169,10 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_
     }
     }
 
 
     // tdd past 24 hours
     // tdd past 24 hours
-    var pumpData = 0;
-    var logtdd = "";
-    var logBasal = "";
-    var logBolus = "";
-    var logTempBasal = "";
-    var dataLog = "";
     var logOutPut = "";
     var logOutPut = "";
     var tddReason = "";
     var tddReason = "";
-    var current = 0;
-    var tdd = 0;
-    var insulin = 0;
-    var tempInsulin = 0;
-    var bolusInsulin = 0;
-    var scheduledBasalInsulin = 0;
-    var quota = 0;
+    var tdd = oref2_variables.currentTDD;
+
     const weightedAverage = oref2_variables.weightedAverage;
     const weightedAverage = oref2_variables.weightedAverage;
     var overrideFactor = 1;
     var overrideFactor = 1;
     var sensitivity = profile.sens;
     var sensitivity = profile.sens;
@@ -204,55 +190,6 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_
     const weightPercentage = profile.weightPercentage;
     const weightPercentage = profile.weightPercentage;
     const average_total_data = oref2_variables.average_total_data;
     const average_total_data = oref2_variables.average_total_data;
 
 
-    function addTimeToDate(objDate, _hours) {
-        var ms = objDate.getTime();
-        var add_ms = _hours * 36e5;
-        var newDateObj = new Date(ms + add_ms);
-        return newDateObj;
-    }
-
-    function subtractTimeFromDate(date, hours_) {
-        var ms_ = date.getTime();
-        var add_ms_ = hours_ * 36e5;
-        var new_date = new Date(ms_ - add_ms_);
-        return new_date;
-    }
-
-    function accountForIncrements(insulin) {
-        // If you have not set this to.0.1 in Trio settings, this will be set to 0.05 (Omnipods) in code.
-        var minimalDose = profile.bolus_increment;
-        if (minimalDose != 0.1) {
-            minimalDose = 0.05;
-        }
-        var incrementsRaw = insulin / minimalDose;
-        if (incrementsRaw >= 1) {
-            var incrementsRounded = Math.floor(incrementsRaw);
-            return round(incrementsRounded * minimalDose, 5);
-        } else { return 0; }
-    }
-
-    function makeBaseString(base_timeStamp) {
-        function addZero(i) {
-            if (i < 10) { i = "0" + i }
-            return i;
-        }
-        let hour = addZero(base_timeStamp.getHours());
-        let minutes = addZero(base_timeStamp.getMinutes());
-        let seconds = "00";
-        let string = hour + ":" + minutes + ":" + seconds;
-        return string;
-    }
-
-    function timeDifferenceOfString(string1, string2) {
-        //Base time strings are in "00:00:00" format
-        var time1 = new Date("1/1/1999 " + string1);
-        var time2 = new Date("1/1/1999 " + string2);
-        var ms1 = time1.getTime();
-        var ms2 = time2.getTime();
-        var difference = (ms1 - ms2) / 36e5;
-        return difference;
-    }
-
     // In case the autosens.min/max limits are reversed:
     // In case the autosens.min/max limits are reversed:
     const minLimitChris = Math.min(profile.autosens_min, profile.autosens_max);
     const minLimitChris = Math.min(profile.autosens_min, profile.autosens_max);
     const maxLimitChris = Math.max(profile.autosens_min, profile.autosens_max);
     const maxLimitChris = Math.max(profile.autosens_min, profile.autosens_max);
@@ -263,306 +200,6 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_
         console.log("Dynamic ISF disabled due to current autosens settings");
         console.log("Dynamic ISF disabled due to current autosens settings");
     }
     }
 
 
-    function calcScheduledBasalInsulin(lastRealTempTime, addedLastTempTime) {
-        var totalInsulin = 0;
-        var old = addedLastTempTime;
-        var totalDuration = (lastRealTempTime - addedLastTempTime) / 36e5;
-        var basDuration = 0;
-        var totalDurationCheck = totalDuration;
-        var durationCurrentSchedule = 0;
-
-        do {
-
-            if (totalDuration > 0) {
-
-                var baseTime_ = makeBaseString(old);
-
-                //Default basalrate in case none is found...
-                var basalScheduledRate_ = basalprofile[0].rate;
-                for (let m = 0; m < basalprofile.length; m++) {
-
-                    var timeToTest = basalprofile[m].start;
-
-                    if (baseTime_ == timeToTest) {
-
-                        if (m + 1 < basalprofile.length) {
-                            let end = basalprofile[m+1].start;
-                            let start = basalprofile[m].start;
-
-                            durationCurrentSchedule = timeDifferenceOfString(end, start);
-
-                            if (totalDuration >= durationCurrentSchedule) {
-                                basDuration = durationCurrentSchedule;
-                            } else if (totalDuration < durationCurrentSchedule) {
-                                basDuration = totalDuration;
-                            }
-
-                        }
-                        else if (m + 1 == basalprofile.length) {
-                            let end = basalprofile[0].start;
-                            let start = basalprofile[m].start;
-                            // First schedule is 00:00:00. Changed places of start and end here.
-                            durationCurrentSchedule = 24 - (timeDifferenceOfString(start, end));
-
-                            if (totalDuration >= durationCurrentSchedule) {
-                                basDuration = durationCurrentSchedule;
-                            } else if (totalDuration < durationCurrentSchedule) {
-                                basDuration = totalDuration;
-                            }
-
-                        }
-                        basalScheduledRate_ = basalprofile[m].rate;
-                        totalInsulin += accountForIncrements(basalScheduledRate_ * basDuration);
-                        totalDuration -= basDuration;
-                        console.log("Dynamic ratios log: scheduled insulin added: " + accountForIncrements(basalScheduledRate_ * basDuration) + " U. Bas duration: " + basDuration.toPrecision(3) + " h. Base Rate: " + basalScheduledRate_ + " U/h" + ". Time :" + baseTime_);
-                        // Move clock to new date
-                        old = addTimeToDate(old, basDuration);
-                    }
-
-                    else if (baseTime_ > timeToTest) {
-
-                        if (m + 1 < basalprofile.length) {
-                            var timeToTest2 = basalprofile[m+1].start
-
-                            if (baseTime_ < timeToTest2) {
-
-                               //  durationCurrentSchedule = timeDifferenceOfString(end, start);
-                               durationCurrentSchedule = timeDifferenceOfString(timeToTest2, baseTime_);
-
-                                if (totalDuration >= durationCurrentSchedule) {
-                                    basDuration = durationCurrentSchedule;
-                                } else if (totalDuration < durationCurrentSchedule) {
-                                    basDuration = totalDuration;
-                                }
-
-                                basalScheduledRate_ = basalprofile[m].rate;
-                                totalInsulin += accountForIncrements(basalScheduledRate_ * basDuration);
-                                totalDuration -= basDuration;
-                                console.log("Dynamic ratios log: scheduled insulin added: " + accountForIncrements(basalScheduledRate_ * basDuration) + " U. Bas duration: " + basDuration.toPrecision(3) + " h. Base Rate: " + basalScheduledRate_ + " U/h" + ". Time :" + baseTime_);
-                                // Move clock to new date
-                                old = addTimeToDate(old, basDuration);
-                            }
-                        }
-
-                        else if (m == basalprofile.length - 1) {
-                            // let start = basalprofile[m].start;
-                            let start = baseTime_;
-                            // First schedule is 00:00:00. Changed places of start and end here.
-                            durationCurrentSchedule = timeDifferenceOfString("23:59:59", start);
-
-                            if (totalDuration >= durationCurrentSchedule) {
-                                basDuration = durationCurrentSchedule;
-                            } else if (totalDuration < durationCurrentSchedule) {
-                                basDuration = totalDuration;
-                            }
-
-                            basalScheduledRate_ = basalprofile[m].rate;
-                            totalInsulin += accountForIncrements(basalScheduledRate_ * basDuration);
-                            totalDuration -= basDuration;
-                            console.log("Dynamic ratios log: scheduled insulin added: " + accountForIncrements(basalScheduledRate_ * basDuration) + " U. Bas duration: " + basDuration.toPrecision(3) + " h. Base Rate: " + basalScheduledRate_ + " U/h" + ". Time :" + baseTime_);
-                            // Move clock to new date
-                            old = addTimeToDate(old, basDuration);
-                        }
-                    }
-                }
-            }
-            //totalDurationCheck to avoid infinite loop
-        } while (totalDuration > 0 && totalDuration < totalDurationCheck);
-
-        // amount of insulin according to pump basal rate schedules
-        return totalInsulin;
-    }
-
-    // Check that there is enough pump history data (>21 hours) for tdd calculation. Estimate the missing hours (24-pumpData) using hours with scheduled basal rates. Not perfect, but sometimes the
-    // pump history in FAX is only 22-23.5 hours, even when you've been looping with FAX for many days. This is to reduce the error from just using pump history as data source as much as possible.
-    // AT basal rates are not used for this estimation, instead the basal rates in pump settings.
-
-    // Check for empty pump history (new FAX loopers). If empty: don't use dynamic settings!
-
-    if (!pumphistory.length) {
-        console.log("Pumphistory is empty!");
-        dynISFenabled = false;
-        enableDynamicCR = false;
-    } else {
-        let phLastEntry = pumphistory.length - 1;
-        var endDate = new Date(pumphistory[phLastEntry].timestamp);
-        var startDate = new Date(pumphistory[0].timestamp);
-
-        // If latest pump event is a temp basal
-        if (pumphistory[0]._type == "TempBasalDuration") {
-            startDate = new Date();
-        }
-        pumpData = (startDate - endDate) / 36e5;
-
-        if (pumpData < 23.9 && pumpData > 21) {
-            var missingHours = 24 - pumpData;
-            // Makes new end date for a total time duration of exakt 24 hour.
-            var endDate_ = subtractTimeFromDate(endDate, missingHours);
-            // endDate - endDate_ = missingHours
-            scheduledBasalInsulin = calcScheduledBasalInsulin(endDate, endDate_);
-            dataLog = "24 hours of data is required for an accurate tdd calculation. Currently only " + pumpData.toPrecision(3) + " hours of pump history data are available. Using your pump scheduled basals to fill in the missing hours. Scheduled basals added: " + scheduledBasalInsulin.toPrecision(5) + " U. ";
-        } else if (pumpData < 21) {
-            dynISFenabled = false;
-            enableDynamicCR = false;
-        } else {  dataLog = ""; }
-    }
-
-    // Calculate tdd ----------------------------------------------------------------------
-
-    //Bolus:
-    for (let i = 0; i < pumphistory.length; i++) {
-        if (pumphistory[i]._type == "Bolus") {
-            bolusInsulin += pumphistory[i].amount;
-        }
-    }
-
-    // Temp basals:
-    for (let j = 1; j < pumphistory.length; j++) {
-        if (pumphistory[j]._type == "TempBasal" && pumphistory[j].rate > 0) {
-            current = j;
-            quota = pumphistory[j].rate;
-            var duration = pumphistory[j-1]['duration (min)'] / 60;
-            var origDur = duration;
-            var pastTime = new Date(pumphistory[j-1].timestamp);
-            var morePresentTime = new Date(pastTime);
-            var substractTimeOfRewind = 0;
-            // If temp basal hasn't yet ended, use now as end date for calculation
-            do {
-                j--;
-                if (j == 0) {
-                    morePresentTime =  new Date();
-                    break;
-                }
-                else if (pumphistory[j]._type == "TempBasal" || pumphistory[j]._type == "PumpSuspend")  {
-                    morePresentTime = new Date(pumphistory[j].timestamp);
-                    break;
-                }
-                // During the time the Medtronic pumps are rewinded and primed, this duration of suspened insulin delivery needs to be accounted for.
-                var pp = j-2;
-                if (pp >= 0) {
-                    if (pumphistory[pp]._type == "Rewind") {
-                        let rewindTimestamp = pumphistory[pp].timestamp;
-                        // There can be several Prime events
-                        while (pp - 1 >= 0) {
-                            pp -= 1;
-                            if (pumphistory[pp]._type == "Prime") {
-                                substractTimeOfRewind = (pumphistory[pp].timestamp - rewindTimestamp) / 36e5;
-                            } else { break }
-                        }
-
-                        // If Medtronic user forgets to insert infusion set
-                        if (substractTimeOfRewind >= duration) {
-                            morePresentTime = new Date(rewindTimestamp);
-                            substractTimeOfRewind = 0;
-                        }
-                    }
-                }
-            } while (j > 0);
-
-            var diff = (morePresentTime - pastTime) / 36e5;
-            if (diff < origDur) {
-                duration = diff;
-            }
-
-            insulin = quota * (duration - substractTimeOfRewind);
-            tempInsulin += accountForIncrements(insulin);
-            j = current;
-        }
-    }
-    //  Check and count for when basals are delivered with a scheduled basal rate.
-    //  1. Check for 0 temp basals with 0 min duration. This is for when ending a manual temp basal and (perhaps) continuing in open loop for a while.
-    //  2. Check for temp basals that completes. This is for when disconnected from link/iphone, or when in open loop.
-    //  3. Account for a punp suspension. This is for when pod screams or when MDT or pod is manually suspended.
-    //  4. Account for a pump resume (in case pump/cgm is disconnected before next loop).
-    //  To do: are there more circumstances when scheduled basal rates are used? Do we need to care about "Prime" and "Rewind" with MDT pumps?
-    //
-    for (let k = 0; k < pumphistory.length; k++) {
-        // Check for 0 temp basals with 0 min duration.
-        insulin = 0;
-        if (pumphistory[k]['duration (min)'] == 0 || pumphistory[k]._type == "PumpResume") {
-            let time1 = new Date(pumphistory[k].timestamp);
-            let time2 = new Date(time1);
-            let l = k;
-            do {
-                if (l > 0) {
-                    --l;
-                    if (pumphistory[l]._type == "TempBasal") {
-                        time2 = new Date(pumphistory[l].timestamp);
-                        break;
-                    }
-                }
-            } while (l > 0);
-            // duration of current scheduled basal in h
-            let basDuration = (time2 - time1) / 36e5;
-
-            if (basDuration > 0) {
-                scheduledBasalInsulin += calcScheduledBasalInsulin(time2, time1);
-            }
-        }
-    }
-
-    // Check for temp basals that completes
-    for (let n = pumphistory.length -1; n > 0; n--) {
-        if (pumphistory[n]._type == "TempBasalDuration") {
-            // duration in hours
-            let oldBasalDuration = pumphistory[n]['duration (min)'] / 60;
-            // time of old temp basal
-            let oldTime = new Date(pumphistory[n].timestamp);
-            var newTime = new Date(oldTime);
-            let o = n;
-            do {
-                --o;
-                if (o >= 0) {
-                    if (pumphistory[o]._type == "TempBasal" || pumphistory[o]._type == "PumpSuspend") {
-                        // time of next (new) temp basal or a pump suspension
-                        newTime = new Date(pumphistory[o].timestamp);
-                        break;
-                    }
-                }
-            } while (o > 0);
-
-            // When latest temp basal is index 0 in pump history
-            if (n == 0 && pumphistory[0]._type == "TempBasalDuration") {
-                newTime = new Date();
-                oldBasalDuration = pumphistory[n]['duration (min)'] / 60;
-            }
-
-            let tempBasalTimeDifference = (newTime - oldTime) / 36e5;
-            let timeOfbasal = tempBasalTimeDifference - oldBasalDuration;
-            // if duration of scheduled basal is more than 0
-            if (timeOfbasal > 0) {
-                // Timestamp after completed temp basal
-                let timeOfScheduledBasal =  addTimeToDate(oldTime, oldBasalDuration);
-                scheduledBasalInsulin += calcScheduledBasalInsulin(newTime, timeOfScheduledBasal);
-            }
-        }
-    }
-
-    tdd = bolusInsulin + tempInsulin + scheduledBasalInsulin;
-
-
-    var insulin_ = {
-        TDD: round(tdd, 5),
-        bolus: round(bolusInsulin, 5),
-        temp_basal: round(tempInsulin, 5),
-        scheduled_basal: round(scheduledBasalInsulin, 5)
-    }
-
-    if (pumpData > 21) {
-        logBolus = ". Bolus insulin: " + bolusInsulin.toPrecision(5) + " U";
-        logTempBasal = ". Temporary basal insulin: " + tempInsulin.toPrecision(5) + " U";
-        logBasal = ". Insulin with scheduled basal rate: " + scheduledBasalInsulin.toPrecision(5) + " U";
-        logtdd = " TDD past 24h is: " + tdd.toPrecision(5) + " U";
-        logOutPut = dataLog + logtdd + logBolus + logTempBasal + logBasal;
-
-        tddReason = ", TDD: " + round(tdd,2) + " U, " + round(bolusInsulin/tdd*100,0) + "% Bolus " + round((tempInsulin+scheduledBasalInsulin)/tdd*100,0) +  "% Basal";
-
-    } else { tddReason = ", TDD: Not enough pumpData (< 21h)"; }
-
-    var tdd_before = tdd;
-
-    // -------------------- END OF TDD ----------------------------------------------------
-
     // Dynamic ratios
     // Dynamic ratios
     const BG = glucose_status.glucose;
     const BG = glucose_status.glucose;
     const useDynamicCR = preferences.enableDynamicCR;
     const useDynamicCR = preferences.enableDynamicCR;
@@ -1136,8 +773,6 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_
         , 'deliverAt' : deliverAt // The time at which the microbolus should be delivered
         , 'deliverAt' : deliverAt // The time at which the microbolus should be delivered
         , 'sensitivityRatio' : sensitivityRatio
         , 'sensitivityRatio' : sensitivityRatio
         , 'CR' : round(carbRatio, 1)
         , 'CR' : round(carbRatio, 1)
-        , 'TDD': tdd_before
-        , 'insulin': insulin_
         , 'current_target': target_bg
         , 'current_target': target_bg
         , 'insulinForManualBolus': insulinForManualBolus
         , 'insulinForManualBolus': insulinForManualBolus
         , 'manualBolusErrorString': manualBolusErrorString
         , 'manualBolusErrorString': manualBolusErrorString
@@ -1553,7 +1188,6 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_
     rT.ISF=convert_bg(sens, profile);
     rT.ISF=convert_bg(sens, profile);
     rT.CR=round(carbRatio, 1);
     rT.CR=round(carbRatio, 1);
     rT.target_bg=convert_bg(target_bg, profile);
     rT.target_bg=convert_bg(target_bg, profile);
-    rT.TDD=round(tdd_before, 2);
     rT.current_target=round(target_bg, 0);
     rT.current_target=round(target_bg, 0);
 
 
     var cr_log = rT.CR;
     var cr_log = rT.CR;