Ver código fonte

Merge branch 'dev' into maxIOB-maxCOB

Mike Plante 1 ano atrás
pai
commit
774ab8bb2a
98 arquivos alterados com 52515 adições e 32897 exclusões
  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 timestamp: Date?
     @NSManaged var timestampEnacted: Date?
-    @NSManaged var totalDailyDose: NSDecimalNumber?
     @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)
     }
 
+    static var carbsForStats: NSPredicate {
+        let date = Date.threeMonthsAgo
+        return NSPredicate(format: "date >= %@", date as NSDate)
+    }
+
     static var carbsNotYetUploadedToNightscout: NSPredicate {
         let date = Date.oneDayAgo
         return NSPredicate(

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

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

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

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

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

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

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

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

+ 132 - 16
Trio.xcodeproj/project.pbxproj

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

Diferenças do arquivo suprimidas por serem muito extensas
+ 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,
   "showCarbsRequiredBadge" : true,
   "useFPUconversion" : true,
-  "totalInsulinDisplayType": "totalDailyDose",
   "individualAdjustmentFactor" : 0.5,
   "timeCap" : 8,
   "minuteInterval" : 30,
   "delay" : 60,
   "useAppleHealth" : false,
   "smoothGlucose" : false,
-  "hbA1cDisplayUnit" : "percent",
+  "eA1cDisplayUnit" : "percent",
   "high" : 180,
   "low" : 70,
   "hours" : 6,

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

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

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

@@ -105,53 +105,81 @@ extension PluginSource: CGMManagerDelegate {
     func recordRetractedAlert(_: LoopKit.Alert, at _: Date) {}
 
     func cgmManagerWantsDeletion(_ manager: CGMManager) {
-        dispatchPrecondition(condition: .onQueue(processQueue))
-        debug(.deviceManager, " CGM Manager with identifier \(manager.pluginIdentifier) wants deletion")
-        glucoseManager?.deleteGlucoseSource()
+        processQueue.async { [weak self] in
+            guard let self = self else { return }
+
+            dispatchPrecondition(condition: .onQueue(self.processQueue))
+
+            debug(.deviceManager, " CGM Manager with identifier \(manager.pluginIdentifier) wants deletion")
+            self.glucoseManager?.deleteGlucoseSource()
+        }
     }
 
     func cgmManager(_: CGMManager, hasNew readingResult: CGMReadingResult) {
-        dispatchPrecondition(condition: .onQueue(processQueue))
-        promise?(readCGMResult(readingResult: readingResult))
-        debug(.deviceManager, "CGM PLUGIN - Direct return done")
+        processQueue.async { [weak self] in
+            guard let self = self else { return }
+
+            dispatchPrecondition(condition: .onQueue(self.processQueue))
+
+            self.promise?(self.readCGMResult(readingResult: readingResult))
+            debug(.deviceManager, "CGM PLUGIN - Direct return done")
+        }
     }
 
     func cgmManager(_: LoopKit.CGMManager, hasNew events: [LoopKit.PersistedCgmEvent]) {
-        dispatchPrecondition(condition: .onQueue(processQueue))
-        // TODO: Events in APS ?
-        // currently only display in log the date of the event
-        events.forEach { event in
-            debug(.deviceManager, "events from CGM at \(event.date)")
-
-            if event.type == .sensorStart {
-                self.glucoseManager?.removeCalibrations()
+        processQueue.async { [weak self] in
+            guard let self = self else { return }
+
+            dispatchPrecondition(condition: .onQueue(self.processQueue))
+
+            // TODO: Events in APS ?
+            // currently only display in log the date of the event
+            events.forEach { event in
+                debug(.deviceManager, "events from CGM at \(event.date)")
+
+                if event.type == .sensorStart {
+                    self.glucoseManager?.removeCalibrations()
+                }
             }
         }
     }
 
     func startDateToFilterNewData(for _: CGMManager) -> Date? {
-        dispatchPrecondition(condition: .onQueue(processQueue))
-        return glucoseStorage.lastGlucoseDate()
+        var date: Date?
+
+        processQueue.async { [weak self] in
+            guard let self = self else { return }
+
+            dispatchPrecondition(condition: .onQueue(self.processQueue))
+
+            date = glucoseStorage.lastGlucoseDate()
+        }
+
+        return date
     }
 
     func cgmManagerDidUpdateState(_ cgmManager: CGMManager) {
-        dispatchPrecondition(condition: .onQueue(processQueue))
+        processQueue.async { [weak self] in
+            guard let self = self else { return }
 
-        guard let fetchGlucoseManager = glucoseManager else {
-            debug(
-                .deviceManager,
-                "Could not gracefully unwrap FetchGlucoseManager upon observing LoopKit's cgmManagerDidUpdateState"
+            dispatchPrecondition(condition: .onQueue(self.processQueue))
+
+            guard let fetchGlucoseManager = self.glucoseManager else {
+                debug(
+                    .deviceManager,
+                    "Could not gracefully unwrap FetchGlucoseManager upon observing LoopKit's cgmManagerDidUpdateState"
+                )
+                return
+            }
+            // Adjust app-specific NS Upload setting value when CGM setting is changed
+            fetchGlucoseManager.settingsManager.settings.uploadGlucose = cgmManager.shouldSyncToRemoteService
+
+            fetchGlucoseManager.updateGlucoseSource(
+                cgmGlucoseSourceType: fetchGlucoseManager.settingsManager.settings.cgm,
+                cgmGlucosePluginId: fetchGlucoseManager.settingsManager.settings.cgmPluginIdentifier,
+                newManager: cgmManager as? CGMManagerUI
             )
-            return
         }
-        // Adjust app-specific NS Upload setting value when CGM setting is changed
-        fetchGlucoseManager.settingsManager.settings.uploadGlucose = cgmManager.shouldSyncToRemoteService
-
-        fetchGlucoseManager.updateGlucoseSource(
-            cgmGlucoseSourceType: fetchGlucoseManager.settingsManager.settings.cgm,
-            cgmGlucosePluginId: fetchGlucoseManager.settingsManager.settings.cgmPluginIdentifier,
-            newManager: cgmManager as? CGMManagerUI
-        )
     }
 
     func credentialStoragePrefix(for _: CGMManager) -> String {
@@ -162,7 +190,11 @@ extension PluginSource: CGMManagerDelegate {
     func cgmManager(_: CGMManager, didUpdate status: CGMManagerStatus) {
         debug(.deviceManager, "CGM Manager did update state to \(status)")
 
-        processQueue.async {
+        processQueue.async { [weak self] in
+            guard let self = self else { return }
+
+            dispatchPrecondition(condition: .onQueue(self.processQueue))
+
             if self.cgmHasValidSensorSession != status.hasValidSensorSession {
                 self.cgmHasValidSensorSession = status.hasValidSensorSession
             }

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

@@ -164,13 +164,9 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         debug(.apsManager, "plugin : \(String(describing: cgmManager?.pluginIdentifier))")
 
         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 {
             cgmManager = cgmManagerFromRawValue(rawCGMManager)
             updateManagerUnits(cgmManager)
@@ -248,29 +244,22 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         var filteredByDate: [BloodGlucose] = []
         var filtered: [BloodGlucose] = []
 
-        // start background time extension
-        var backGroundFetchBGTaskID: UIBackgroundTaskIdentifier?
-        backGroundFetchBGTaskID = await UIApplication.shared.beginBackgroundTask(withName: "save BG starting") {
-            guard let bg = backGroundFetchBGTaskID else { return }
-            UIApplication.shared.endBackgroundTask(bg)
-            backGroundFetchBGTaskID = .invalid
-        }
+        // Start background task
+        var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
+        backgroundTaskID = startBackgroundTask(withName: "Glucose Store and Heartbeat Decision")
 
-        defer {
-            if let backgroundTask = backGroundFetchBGTaskID {
-                Task {
-                    await UIApplication.shared.endBackgroundTask(backgroundTask)
-                }
-                backGroundFetchBGTaskID = .invalid
-            }
+        guard newGlucose.isNotEmpty else {
+            endBackgroundTaskSafely(&backgroundTaskID, taskName: "Glucose Store and Heartbeat Decision")
+            return
         }
 
-        guard newGlucose.isNotEmpty else { return }
-
         filteredByDate = newGlucose.filter { $0.dateString > syncDate }
         filtered = glucoseStorage.filterTooFrequentGlucose(filteredByDate, at: syncDate)
 
-        guard filtered.isNotEmpty else { return }
+        guard filtered.isNotEmpty else {
+            endBackgroundTaskSafely(&backgroundTaskID, taskName: "Glucose Store and Heartbeat Decision")
+            return
+        }
         debug(.deviceManager, "New glucose found")
 
         // filter the data if it is the case
@@ -289,6 +278,8 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
 
         try await glucoseStorage.storeGlucose(filtered)
         deviceDataManager.heartbeat(date: Date())
+
+        endBackgroundTaskSafely(&backgroundTaskID, taskName: "Glucose Store and Heartbeat Decision")
     }
 
     func sourceInfo() -> [String: Any]? {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 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
     }()
 
+    static let dayFormatter: DateFormatter = {
+        let formatter = DateFormatter()
+        formatter.dateFormat = "d"
+        return formatter
+    }()
+
     static let decimalFormatterWithOneFractionDigit: NumberFormatter = {
         let formatter = NumberFormatter()
         formatter.numberStyle = .decimal
@@ -80,6 +86,14 @@ extension Formatter {
         formatter.decimalSeparator = "."
         return formatter
     }()
+
+    static let timaAgoFormatter: NumberFormatter = {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 0
+        formatter.negativePrefix = ""
+        return formatter
+    }()
 }
 
 extension JSONDecoder.DateDecodingStrategy {

Diferenças do arquivo suprimidas por serem muito extensas
+ 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 {
         switch self {
-        case .systemDefault: return "System Default"
-        case .light: return "Light"
-        case .dark: return "Dark"
+        case .systemDefault: return String(localized: "System Default")
+        case .light: return String(localized: "Light")
+        case .dark: return String(localized: "Dark")
         }
     }
 }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -42,14 +42,13 @@ struct TrioSettings: JSON, Equatable {
     var carbsRequiredThreshold: Decimal = 10
     var showCarbsRequiredBadge: Bool = true
     var useFPUconversion: Bool = true
-    var totalInsulinDisplayType: TotalInsulinDisplayType = .totalDailyDose
     var individualAdjustmentFactor: Decimal = 0.5
     var timeCap: Int = 8
     var minuteInterval: Int = 30
     var delay: Int = 60
     var useAppleHealth: Bool = false
     var smoothGlucose: Bool = false
-    var hbA1cDisplayUnit: HbA1cDisplayUnit = .percent
+    var eA1cDisplayUnit: EstimatedA1cDisplayUnit = .percent
     var high: Decimal = 180
     var low: Decimal = 70
     var hours: Int = 6
@@ -145,10 +144,6 @@ extension TrioSettings: Decodable {
             settings.useFPUconversion = useFPUconversion
         }
 
-        if let totalInsulinDisplayType = try? container.decode(TotalInsulinDisplayType.self, forKey: .totalInsulinDisplayType) {
-            settings.totalInsulinDisplayType = totalInsulinDisplayType
-        }
-
         if let individualAdjustmentFactor = try? container.decode(Decimal.self, forKey: .individualAdjustmentFactor) {
             settings.individualAdjustmentFactor = individualAdjustmentFactor
         }
@@ -275,8 +270,8 @@ extension TrioSettings: Decodable {
             settings.forecastDisplayType = forecastDisplayType
         }
 
-        if let hbA1cDisplayUnit = try? container.decode(HbA1cDisplayUnit.self, forKey: .hbA1cDisplayUnit) {
-            settings.hbA1cDisplayUnit = hbA1cDisplayUnit
+        if let eA1cDisplayUnit = try? container.decode(EstimatedA1cDisplayUnit.self, forKey: .eA1cDisplayUnit) {
+            settings.eA1cDisplayUnit = eA1cDisplayUnit
         }
 
         if let maxCarbs = try? container.decode(Decimal.self, forKey: .maxCarbs) {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -205,10 +205,7 @@ enum SettingItems {
                 "Low Threshold",
                 "High Threshold",
                 "X-Axis Interval Step",
-                "Total Insulin Display Type",
-                "Total Daily Dose (TDD)",
-                "Total Insulin in Scope (TINS)",
-                "Override HbA1c Unit",
+                "Override eA1c Unit",
                 "Standing / Laying TIR Chart",
                 "Show Carbs Required Badge",
                 "Carbs Required Threshold",

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -228,7 +228,11 @@ extension Treatments {
                             // Notes
                             HStack {
                                 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)
 
@@ -505,7 +509,7 @@ extension Treatments {
             let hasInsulin = state.amount > 0
             let hasCarbs = state.carbs > 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) {
                 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 rulerMarks: Bool = true
         @Published var forecastDisplayType: ForecastDisplayType = .cone
-        @Published var totalInsulinDisplayType: TotalInsulinDisplayType = .totalDailyDose
         @Published var showCarbsRequiredBadge: Bool = true
         @Published var carbsRequiredThreshold: Decimal = 0
         @Published var glucoseColorScheme: GlucoseColorScheme = .staticColor
-        @Published var hbA1cDisplayUnit: HbA1cDisplayUnit = .percent
+        @Published var eA1cDisplayUnit: EstimatedA1cDisplayUnit = .percent
         @Published var timeInRangeChartStyle: TimeInRangeChartStyle = .vertical
 
         var units: GlucoseUnits = .mgdL
@@ -27,8 +26,6 @@ extension UserInterfaceSettings {
 
             subscribeSetting(\.forecastDisplayType, on: $forecastDisplayType) { forecastDisplayType = $0 }
 
-            subscribeSetting(\.totalInsulinDisplayType, on: $totalInsulinDisplayType) { totalInsulinDisplayType = $0 }
-
             subscribeSetting(\.low, on: $low) { low = $0 }
 
             subscribeSetting(\.high, on: $high) { high = $0 }
@@ -42,7 +39,7 @@ extension UserInterfaceSettings {
 
             subscribeSetting(\.glucoseColorScheme, on: $glucoseColorScheme) { glucoseColorScheme = $0 }
 
-            subscribeSetting(\.hbA1cDisplayUnit, on: $hbA1cDisplayUnit) { hbA1cDisplayUnit = $0 }
+            subscribeSetting(\.eA1cDisplayUnit, on: $eA1cDisplayUnit) { eA1cDisplayUnit = $0 }
 
             subscribeSetting(\.timeInRangeChartStyle, on: $timeInRangeChartStyle) { timeInRangeChartStyle = $0 }
         }

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

@@ -376,76 +376,22 @@ extension UserInterfaceSettings {
                     }.padding(.bottom)
                 }.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(
                     header: Text("Trio Statistics"),
                     content: {
                         VStack {
                             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)
                                 }
                             }.padding(.top)
 
                             HStack(alignment: .center) {
                                 Text(
-                                    "Choose to display HbA1c in percent or mmol/mol."
+                                    "Choose to display eA1c in percent or mmol/mol."
                                 )
                                 .font(.footnote)
                                 .foregroundColor(.secondary)
@@ -453,11 +399,11 @@ extension UserInterfaceSettings {
                                 Spacer()
                                 Button(
                                     action: {
-                                        hintLabel = String(localized: "HbA1c Display Unit")
+                                        hintLabel = String(localized: "eA1c Display Unit")
                                         selectedVerboseHint =
                                             AnyView(
                                                 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()

+ 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? {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OrefDetermination.self,
@@ -33,23 +34,37 @@ extension LiveActivityManager {
             key: "deliverAt",
             ascending: false,
             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 {
-            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)
             }
 
-            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 {
         var components = URLComponents()
         components.scheme = url.scheme

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

@@ -1090,12 +1090,30 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         }
 
         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))
             }
 
             // If successful, update the isUploadedToNS property of the OverrideStored objects
-            await updateOverridesAsUploaded(overrides)
+            await updateOverridesAsUploaded(processedOverrides)
 
             debug(.nightscout, "Overrides uploaded")
         } catch {
@@ -1131,7 +1149,24 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         }
 
         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))
             }
 

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

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

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

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

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

@@ -11,18 +11,9 @@ import UIKit
     /// - Throws: An error if the restart process fails.
     /// - Returns: Void upon successful restart.
     @MainActor func performRestart() async throws {
-        var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
-
         // 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 {
             debug(.default, "Failed to start background task.")
@@ -34,10 +25,6 @@ import UIKit
         await liveActivityManager.restartActivityFromLiveActivityIntent()
 
         // 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 Foundation
 
+/// An App Intent that allows users to activate an override preset through the Shortcuts app.
 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")
 
-    // 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"))
 
+    /// The override preset to be applied.
     @Parameter(
         title: LocalizedStringResource("Override", table: "ShortcutsDetail"),
         description: LocalizedStringResource("Override choice", table: "ShortcutsDetail")
     ) var preset: OverridePreset?
 
+    /// A boolean parameter that determines whether confirmation is required before applying the override.
     @Parameter(
         title: LocalizedStringResource("Confirm Before applying", table: "ShortcutsDetail"),
         description: LocalizedStringResource("If toggled, you will need to confirm before applying", table: "ShortcutsDetail"),
         default: true
     ) var confirmBeforeApplying: Bool
 
+    /// Defines the summary format shown in the Shortcuts app when configuring this intent.
     static var parameterSummary: some ParameterSummary {
         When(\ApplyOverridePresetIntent.$confirmBeforeApplying, .equalTo, true, {
             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 {
         do {
+            // Determine which preset to apply
             let presetToApply: OverridePreset
             if let preset = preset {
                 presetToApply = preset
             } else {
+                // Request user selection if no preset is provided
                 presetToApply = try await $preset.requestDisambiguation(
                     among: await OverridePresetsIntentRequest().fetchAndProcessOverrides(),
                     dialog: IntentDialog(LocalizedStringResource("Select override", table: "ShortcutsDetail"))
@@ -44,6 +54,8 @@ struct ApplyOverridePresetIntent: AppIntent {
             }
 
             let displayName: String = presetToApply.name
+
+            // Request confirmation before applying if required
             if confirmBeforeApplying {
                 try await requestConfirmation(
                     result: .result(
@@ -55,6 +67,7 @@ struct ApplyOverridePresetIntent: AppIntent {
                 )
             }
 
+            // Apply the override and return the appropriate dialog message
             if await OverridePresetsIntentRequest().enactOverride(presetToApply) {
                 return .result(
                     dialog: IntentDialog(

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

@@ -1,19 +1,22 @@
 import AppIntents
 import Foundation
 
+/// An App Intent that allows users to cancel an active override through the Shortcuts app.
 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")
 
-    // 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"))
 
+    /// 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 {
-        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 Swinject
 
+/// Represents an override preset that can be used in the app.
 struct OverridePreset: AppEntity, Identifiable {
+    /// Default query instance for fetching override presets.
     static var defaultQuery = OverridePresetsQuery()
 
+    /// Unique identifier for the override preset.
     var id: String
+
+    /// Name of the override preset.
     var name: String
 
+    /// Provides a display representation for the override preset.
     var displayRepresentation: DisplayRepresentation {
         DisplayRepresentation(title: "\(name)")
     }
 
+    /// Representation for the entity type when displayed in UI.
     static var typeDisplayRepresentation: TypeDisplayRepresentation = "Override"
 }
 
+/// Query structure for fetching override presets in an App Intent.
 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] {
         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] {
         try await OverridePresetsIntentRequest().fetchAndProcessOverrides()
     }

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

@@ -9,6 +9,14 @@ import UIKit
         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] {
         do {
             // 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] {
         try await coredataContext.perform {
             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 {
         let fetchRequest: NSFetchRequest<OverrideStored> = OverrideStored.fetchRequest()
         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 {
-        // Start background task
+        debug(.default, "Enacting override: \(preset.name)")
+        intentSuccess = false
+
         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 {
-            // Get NSManagedObjectID of Preset
             let overrideID = try await fetchOverrideID(preset)
             guard let overrideObject = try viewContext.existingObject(with: overrideID) as? OverrideStored else {
+                endBackgroundTaskSafely(&backgroundTaskID, taskName: "Override Enact")
                 throw overridePresetsError.noTempOverrideFound
             }
 
-            // Enable Override
             overrideObject.enabled = true
             overrideObject.date = Date()
             overrideObject.isUploadedToNS = false
 
-            // Disable previous overrides if necessary
-            await disableAllActiveOverrides(except: overrideID, createOverrideRunEntry: true, shouldStartBackgroundTask: false)
-
             if viewContext.hasChanges {
+                debug(.default, "Saving changes...")
                 try viewContext.save()
+                debug(.default, "Waiting for notification...")
                 Foundation.NotificationCenter.default.post(name: .willUpdateOverrideConfiguration, object: nil)
                 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 {
             debug(
                 .default,
                 "\(DebuggingIdentifiers.failed) Failed to enact override: \(error.localizedDescription)"
             )
+            endBackgroundTaskSafely(&backgroundTaskID, taskName: "Override Enact")
             return false
         }
     }
 
+    /**
+     Cancels all active overrides asynchronously.
+     */
     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 {
             // 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
                 try self.viewContext.existingObject(with: id) as? OverrideStored
             }
 
             // 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
-            if createOverrideRunEntry, let canceledOverride = results.first {
+            if let canceledOverride = results.first {
                 let newOverrideRunStored = OverrideRunStored(context: viewContext)
                 newOverrideRunStored.id = UUID()
                 newOverrideRunStored.name = canceledOverride.name
@@ -190,30 +205,38 @@ import UIKit
                 newOverrideRunStored.isUploadedToNS = false
             }
 
-            // Disable all overrides except the one specified
+            // Disable all active overrides
             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 {
                 try viewContext.save()
-
-                // Update State variables in OverrideView
+                debug(.default, "Waiting for notification...")
                 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 {
             debugPrint(
                 "\(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 Foundation
 
+/// An App Intent that allows users to apply a temporary target preset through the Shortcuts app.
 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"
 
-    // 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")
 
+    /// The temporary target preset to be applied.
     @Parameter(title: "Preset") var preset: TempPreset?
 
+    /// A boolean parameter that determines whether confirmation is required before applying the temporary target.
     @Parameter(
         title: "Confirm Before applying",
         description: "If toggled, you will need to confirm before applying",
         default: true
     ) var confirmBeforeApplying: Bool
 
+    /// Defines the summary format shown in the Shortcuts app when configuring this intent.
     static var parameterSummary: some ParameterSummary {
         When(\ApplyTempPresetIntent.$confirmBeforeApplying, .equalTo, true, {
             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 {
-        let timeInterval = TimeInterval(decimal * 60) // seconds
+        let timeInterval = TimeInterval(decimal * 60) // Convert minutes to seconds
 
         let formatter = DateComponentsFormatter()
         formatter.allowedUnits = [.hour, .minute]
-        formatter.unitsStyle = .brief // example: 1h 10 min
+        formatter.unitsStyle = .brief // Example: "1h 10m"
 
         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 {
         do {
             let intentRequest = TempPresetsIntentRequest()
             let presetToApply: TempPreset
+
+            // Determine which preset to apply
             if let preset = preset {
                 presetToApply = preset
             } else {
+                // Request user selection if no preset is provided
                 presetToApply = try await $preset.requestDisambiguation(
                     among: intentRequest.fetchAndProcessTempTargets(),
                     dialog: "Select Temporary Target"
@@ -52,12 +67,15 @@ struct ApplyTempPresetIntent: AppIntent {
             }
 
             let displayName: String = presetToApply.name
+
+            // Request confirmation before applying if required
             if confirmBeforeApplying {
                 try await requestConfirmation(
                     result: .result(dialog: "Confirm to apply Temporary Target '\(displayName)'")
                 )
             }
 
+            // Apply the temporary target and return the appropriate dialog message
             if await intentRequest.enactTempTarget(presetToApply) {
                 return .result(
                     dialog: IntentDialog(

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

@@ -1,13 +1,18 @@
 import AppIntents
 import Foundation
 
+/// An App Intent that allows users to cancel an active temporary target through the Shortcuts app.
 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"
 
-    // 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.")
 
+    /// 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 {
         await TempPresetsIntentRequest().cancelTempTarget()
         return .result(

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

@@ -3,27 +3,50 @@ import Foundation
 import Intents
 import Swinject
 
+/// Represents a temporary target preset that can be used in the app.
 struct TempPreset: AppEntity, Identifiable {
+    /// Default query instance for fetching temporary presets.
     static var defaultQuery = TempPresetsQuery()
 
+    /// Unique identifier for the temporary preset.
     var id: UUID
+
+    /// Name of the temporary preset.
     var name: String
+
+    /// The upper target value for the preset, if applicable.
     var targetTop: Decimal?
+
+    /// The lower target value for the preset, if applicable.
     var targetBottom: Decimal?
+
+    /// The duration of the temporary preset in minutes.
     var duration: Decimal
 
+    /// Provides a display representation for the temporary preset.
     var displayRepresentation: DisplayRepresentation {
         DisplayRepresentation(title: "\(name)")
     }
 
+    /// Representation for the entity type when displayed in UI.
     static var typeDisplayRepresentation: TypeDisplayRepresentation = "Presets"
 }
 
+/// Query structure for fetching temporary target presets in an App Intent.
 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] {
         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] {
         try await TempPresetsIntentRequest().fetchAndProcessTempTargets()
     }

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

@@ -2,12 +2,21 @@ import CoreData
 import Foundation
 import UIKit
 
+/// Handles intent requests related to temporary presets, such as fetching, enacting, and canceling temp targets.
 final class TempPresetsIntentRequest: BaseIntentsRequest {
+    /// Enum representing possible errors related to temporary presets.
     enum TempPresetsError: Error {
         case noTempTargetFound
         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] {
         // Fetch all Temp Target Presets via TempTargetStorage
         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] {
         await coredataContext.perform {
             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? {
         let fetchRequest: NSFetchRequest<TempTargetStored> = TempTargetStored.fetchRequest()
         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 {
+        debug(.default, "Enacting Temp Target: \(preset.name)")
+        intentSuccess = false
+
         // Start background task
         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 {
             // Get NSManagedObjectID of Preset
             guard let tempTargetID = await fetchTempTargetID(preset),
                   let tempTargetObject = try viewContext.existingObject(with: tempTargetID) as? TempTargetStored
-            else { return false }
+            else {
+                endBackgroundTaskSafely(&backgroundTaskID, taskName: "TempTarget Enact")
+                throw TempPresetsError.noTempTargetFound
+            }
 
             // Enable TempTarget
             tempTargetObject.enabled = true
             tempTargetObject.date = Date()
             tempTargetObject.isUploadedToNS = false
 
-            // Disable previous overrides if necessary, without starting a background task
-            await disableAllActiveTempTargets(
-                except: tempTargetID,
-                createTempTargetRunEntry: true,
-                shouldStartBackgroundTask: false
-            )
-
             if viewContext.hasChanges {
+                debug(.default, "Saving changes...")
                 try viewContext.save()
-
+                debug(.default, "Waiting for notification...")
                 // Update State variables in TempTargetView
                 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,
                       let tempTargetDuration = tempTargetObject.duration else { return false }
 
@@ -148,108 +153,105 @@ final class TempPresetsIntentRequest: BaseIntentsRequest {
                     enabled: tempTargetObject.enabled,
                     halfBasalTarget: tempTargetObject.halfBasalTarget as Decimal?
                 )
-
                 // Save the temp targets to JSON so that they get used by oref
                 tempTargetsStorage.saveTempTargetsToStorage([tempTargetToStoreAsJSON])
 
                 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 {
-            // 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 {
-        await disableAllActiveTempTargets(createTempTargetRunEntry: true, shouldStartBackgroundTask: true)
+        await disableAllActiveTempTargets(shouldStartBackgroundTask: true)
         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 {
-            // 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 {
-            // Get NSManagedObjectID of all active temp Targets
+            // Fetch active temp targets
             let ids = try await tempTargetsStorage.loadLatestTempTargetConfigurations(fetchLimit: 0)
-            // Fetch existing OverrideStored objects
             let results = try ids.compactMap { id in
                 try self.viewContext.existingObject(with: id) as? TempTargetStored
             }
 
-            // 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 {
-                if tempTargetToCancel.objectID != tempTargetID {
-                    tempTargetToCancel.enabled = false
-                    tempTargetToCancel.isUploadedToNS = false
-                }
+                tempTargetToCancel.enabled = false
+                tempTargetToCancel.isUploadedToNS = false
             }
 
             if viewContext.hasChanges {
                 try viewContext.save()
-
-                // Update State variables in OverrideView
+                debug(.default, "Waiting for notification...")
                 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 {
             debugPrint(
                 "\(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
             return Text("\(displayValue.description) \(units.rawValue)")
         case .factor:
-            return Text("\(decimalValue * 100) %")
+            return Text("\(decimalValue * 100) \(String(localized: "%", comment: "Percentage symbol"))")
         case .insulinUnit:
-            return Text("\(decimalValue) U")
+            return Text("\(decimalValue) \(String(localized: "U", comment: "Insulin unit abbreviation"))")
         case .gram:
-            return Text("\(decimalValue) g")
+            return Text("\(decimalValue) \(String(localized: "g", comment: "Gram abbreviation"))")
         case .minute:
-            return Text("\(decimalValue) min")
+            return Text("\(decimalValue) \(String(localized: "min", comment: "Minutes abbreviation"))")
         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:
+73d6d6c webpack for Trio-dev
+5c6ce4b remove TDD calculation
 a542ed3 use guarded maxAbsorptionTime
 4c77757 Revert "reduce dynISF logging"
 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 isf = oref2_variables.isf;
     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 uamMinutes = oref2_variables.uamMinutes;
 
@@ -172,21 +169,10 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_
     }
 
     // tdd past 24 hours
-    var pumpData = 0;
-    var logtdd = "";
-    var logBasal = "";
-    var logBolus = "";
-    var logTempBasal = "";
-    var dataLog = "";
     var logOutPut = "";
     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;
     var overrideFactor = 1;
     var sensitivity = profile.sens;
@@ -204,55 +190,6 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_
     const weightPercentage = profile.weightPercentage;
     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:
     const minLimitChris = Math.min(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");
     }
 
-    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
     const BG = glucose_status.glucose;
     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
         , 'sensitivityRatio' : sensitivityRatio
         , 'CR' : round(carbRatio, 1)
-        , 'TDD': tdd_before
-        , 'insulin': insulin_
         , 'current_target': target_bg
         , 'insulinForManualBolus': insulinForManualBolus
         , 'manualBolusErrorString': manualBolusErrorString
@@ -1553,7 +1188,6 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_
     rT.ISF=convert_bg(sens, profile);
     rT.CR=round(carbRatio, 1);
     rT.target_bg=convert_bg(target_bg, profile);
-    rT.TDD=round(tdd_before, 2);
     rT.current_target=round(target_bg, 0);
 
     var cr_log = rT.CR;