Kaynağa Gözat

Merge branch 'dev' of https://github.com/nightscout/Trio-dev into onboarding

polscm32 1 yıl önce
ebeveyn
işleme
be527fd290
36 değiştirilmiş dosya ile 527 ekleme ve 498 silme
  1. 31 1
      .github/workflows/build_trio.yml
  2. 1 1
      DanaKit
  3. 4 4
      Trio.xcodeproj/project.pbxproj
  4. 0 1
      Trio/Resources/json/defaults/freeaps/freeaps_settings.json
  5. 2 2
      Trio/Sources/APS/FetchGlucoseManager.swift
  6. 1 0
      Trio/Sources/Application/AppDelegate.swift
  7. 65 6
      Trio/Sources/Localizations/Main/Localizable.xcstrings
  8. 6 0
      Trio/Sources/Models/Determination.swift
  9. 0 16
      Trio/Sources/Models/TimeInRangeChartStyle.swift
  10. 0 5
      Trio/Sources/Models/TrioSettings.swift
  11. 5 1
      Trio/Sources/Modules/Adjustments/View/TempTargets/AdjustmentsRootView+TempTargets.swift
  12. 60 47
      Trio/Sources/Modules/CGMSettings/CGMSettingsStateModel.swift
  13. 5 1
      Trio/Sources/Modules/CGMSettings/View/CGMRootView.swift
  14. 4 6
      Trio/Sources/Modules/CGMSettings/View/CustomCGMOptionsView.swift
  15. 17 6
      Trio/Sources/Modules/DataTable/View/DataTableRootView.swift
  16. 79 0
      Trio/Sources/Modules/Home/HomeStateModel+CGM.swift
  17. 26 64
      Trio/Sources/Modules/Home/HomeStateModel.swift
  18. 6 6
      Trio/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift
  19. 29 37
      Trio/Sources/Modules/Settings/SettingItems.swift
  20. 5 0
      Trio/Sources/Modules/Settings/SettingsStateModel.swift
  21. 17 5
      Trio/Sources/Modules/Settings/View/SettingsRootView.swift
  22. 25 13
      Trio/Sources/Modules/Stat/StatStateModel+Setup/LoopChartSetup.swift
  23. 1 9
      Trio/Sources/Modules/Stat/StatStateModel.swift
  24. 20 12
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseMetricsView.swift
  25. 2 8
      Trio/Sources/Modules/Stat/View/ViewElements/Looping/LoopBarChartView.swift
  26. 6 1
      Trio/Sources/Modules/Stat/View/ViewElements/Looping/LoopStatsView.swift
  27. 0 58
      Trio/Sources/Modules/StatConfig/StatConfigStateModel.swift
  28. 0 113
      Trio/Sources/Modules/StatConfig/View/StatConfigRootView.swift
  29. 4 2
      Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift
  30. 16 10
      Trio/Sources/Modules/Treatments/View/ForecastChart.swift
  31. 0 3
      Trio/Sources/Modules/UserInterfaceSettings/UserInterfaceSettingsStateModel.swift
  32. 5 45
      Trio/Sources/Modules/UserInterfaceSettings/View/UserInterfaceSettingsRootView.swift
  33. 70 10
      Trio/Sources/Services/Network/Nightscout/NightscoutManager.swift
  34. 2 2
      Trio/Sources/Services/RemoteControl/TrioRemoteControl+Meal.swift
  35. 12 3
      Trio/Sources/Views/SettingInputSection.swift
  36. 1 0
      patches/save_patches_here.md

+ 31 - 1
.github/workflows/build_trio.yml

@@ -201,7 +201,7 @@ jobs:
       )
     steps:
       - name: Select Xcode version
-        run: "sudo xcode-select --switch /Applications/Xcode_16.0.app/Contents/Developer"
+        run: "sudo xcode-select --switch /Applications/Xcode_16.2.app/Contents/Developer"
 
       - name: Checkout Repo for syncing
         if: |
@@ -253,6 +253,36 @@ jobs:
           submodules: recursive
           ref: ${{ env.TARGET_BRANCH }}
 
+      # Customize Trio: Use patches or download and apply patches from GitHub
+      - name: Customize Trio
+        run: |
+
+          # Trio workspace patches
+          # -applies any patches located in the Trio/patches/ directory
+          if $(ls ./patches/* &> /dev/null); then
+          git apply ./patches/* --allow-empty -v --whitespace=fix
+          fi
+
+          # Download and apply Trio patches from GitHub:
+          # Template for customizing Trio code (as opposed to submodule code)
+          # Remove the "#" sign from the beginning of the line below to activate
+          #   and then replace the alphanumeric string with your SHA, this SHA is NOT valid
+          #curl https://github.com/nightscout/Trio/commit/d206432b024279ef710df462b20bd464cd9682d4.patch | git apply -v --whitespace=fix
+
+          # Download and apply Submodule patches from GitHub:
+          # Template for customizing submodules (you must edit the submodule name)
+          # This example is for G7SensorKit showing you can apply multiple commits, in the proper order
+          # Remove the "#" sign from the beginning of the lines below to activate
+          # This example applies 3 commits from the scan-fix folder; valid only when these are not already in Trio
+          #curl https://github.com/loopandlearn/G7SensorKit/commit/ba44beb3d1491c453f4f438443c3f8ba29146ab3.patch | git apply --directory=G7SensorKit -v --whitespace=fix
+          #curl https://github.com/loopandlearn/G7SensorKit/commit/d86ac8e9cd523d1267587dd70c96597125eef7ab.patch | git apply --directory=G7SensorKit -v --whitespace=fix
+          #curl https://github.com/loopandlearn/G7SensorKit/commit/205054e7537723c2aec58d807634b4853f687244.patch | git apply --directory=G7SensorKit -v --whitespace=fix
+
+          # Add patches for additional customization by following the templates above,
+          # and make sure to specify the submodule by setting "--directory=(submodule_name)".
+          # Several patches may be added per submodule.
+          # Adding comments (#) is strongly recommended to easily tell the individual patches apart.
+
       # Patch Fastlane Match to not print tables
       - name: Patch Match Tables
         run: |

+ 1 - 1
DanaKit

@@ -1 +1 @@
-Subproject commit b44c5df260a8b38d6fd0b5851cc3aac5da5d9d57
+Subproject commit 8628a2a67783113fedc6a5ccd4762ec59bbee4fe

+ 4 - 4
Trio.xcodeproj/project.pbxproj

@@ -204,6 +204,7 @@
 		38FEF413273B317A00574A46 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF412273B317A00574A46 /* HKUnit.swift */; };
 		3B2F77862D7E52ED005ED9FA /* TDD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B2F77852D7E52ED005ED9FA /* TDD.swift */; };
 		3B2F77882D7E5387005ED9FA /* CurrentTDDSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B2F77872D7E5387005ED9FA /* CurrentTDDSetup.swift */; };
+		3B4196E02D8C4BC00091DFF7 /* HomeStateModel+CGM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B4196DF2D8C4BBB0091DFF7 /* HomeStateModel+CGM.swift */; };
 		3BAD36B22D7CDC1A00CC298D /* MainLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAD36B12D7CDC1400CC298D /* MainLoadingView.swift */; };
 		3BAD36CC2D7D420E00CC298D /* CoreDataInitializationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAD36CB2D7D420500CC298D /* CoreDataInitializationCoordinator.swift */; };
 		45252C95D220E796FDB3B022 /* ConfigEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F8A87AA037BD079BA3528BA /* ConfigEditorDataFlow.swift */; };
@@ -458,7 +459,6 @@
 		D6D02515BBFBE64FEBE89856 /* DataTableRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 881E04BA5E0A003DE8E0A9C6 /* DataTableRootView.swift */; };
 		D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F2A13DF0EDEEEDC4106AA2A /* NightscoutConfigDataFlow.swift */; };
 		DBA5254DBB2586C98F61220C /* ISFEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9F137F126D9F8DEB799F26 /* ISFEditorProvider.swift */; };
-		DD07CA142CE80B73002D45A9 /* TimeInRangeChartStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD07CA132CE80B73002D45A9 /* TimeInRangeChartStyle.swift */; };
 		DD09D47B2C5986D1003FEA5D /* CalendarEventSettingsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD09D47A2C5986D1003FEA5D /* CalendarEventSettingsDataFlow.swift */; };
 		DD09D47D2C5986DA003FEA5D /* CalendarEventSettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD09D47C2C5986DA003FEA5D /* CalendarEventSettingsProvider.swift */; };
 		DD09D47F2C5986E5003FEA5D /* CalendarEventSettingsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD09D47E2C5986E5003FEA5D /* CalendarEventSettingsStateModel.swift */; };
@@ -935,6 +935,7 @@
 		38FEF412273B317A00574A46 /* HKUnit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HKUnit.swift; sourceTree = "<group>"; };
 		3B2F77852D7E52ED005ED9FA /* TDD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TDD.swift; sourceTree = "<group>"; };
 		3B2F77872D7E5387005ED9FA /* CurrentTDDSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentTDDSetup.swift; sourceTree = "<group>"; };
+		3B4196DF2D8C4BBB0091DFF7 /* HomeStateModel+CGM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeStateModel+CGM.swift"; sourceTree = "<group>"; };
 		3BAD36B12D7CDC1400CC298D /* MainLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainLoadingView.swift; sourceTree = "<group>"; };
 		3BAD36CB2D7D420500CC298D /* CoreDataInitializationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataInitializationCoordinator.swift; sourceTree = "<group>"; };
 		3BDEA2DC60EDE0A3CA54DC73 /* TargetsEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorProvider.swift; sourceTree = "<group>"; };
@@ -1195,7 +1196,6 @@
 		CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalStateModel.swift; sourceTree = "<group>"; };
 		D0BDC6993C1087310EDFC428 /* CarbRatioEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CarbRatioEditorRootView.swift; sourceTree = "<group>"; };
 		DC2C6489D29ECCCAD78E0721 /* GlucoseNotificationSettingsStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GlucoseNotificationSettingsStateModel.swift; sourceTree = "<group>"; };
-		DD07CA132CE80B73002D45A9 /* TimeInRangeChartStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeInRangeChartStyle.swift; sourceTree = "<group>"; };
 		DD09D47A2C5986D1003FEA5D /* CalendarEventSettingsDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarEventSettingsDataFlow.swift; sourceTree = "<group>"; };
 		DD09D47C2C5986DA003FEA5D /* CalendarEventSettingsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarEventSettingsProvider.swift; sourceTree = "<group>"; };
 		DD09D47E2C5986E5003FEA5D /* CalendarEventSettingsStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarEventSettingsStateModel.swift; sourceTree = "<group>"; };
@@ -1776,6 +1776,7 @@
 				3811DE2A25C9D49500A708ED /* HomeDataFlow.swift */,
 				3811DE2925C9D49500A708ED /* HomeProvider.swift */,
 				3811DE2825C9D49500A708ED /* HomeStateModel.swift */,
+				3B4196DF2D8C4BBB0091DFF7 /* HomeStateModel+CGM.swift */,
 				58645B972CA2D16A008AFCE7 /* HomeStateModel+Setup */,
 				3811DE2C25C9D49500A708ED /* View */,
 			);
@@ -2128,7 +2129,6 @@
 				BD54A95A2D28087700F9C1EE /* OverridePresetWatch.swift */,
 				BDA25EFC2D261BF200035F34 /* WatchState.swift */,
 				715120D12D3C2B84005D9FB6 /* GlucoseNotificationsOption.swift */,
-				DD07CA132CE80B73002D45A9 /* TimeInRangeChartStyle.swift */,
 				DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */,
 				DD6D67E32C9C253500660C9B /* ColorSchemeOption.swift */,
 				388E5A5F25B6F2310019842D /* Autosens.swift */,
@@ -3774,7 +3774,6 @@
 				382C134B25F14E3700715CE1 /* BGTargets.swift in Sources */,
 				38AEE75725F0F18E0013F05B /* CarbsStorage.swift in Sources */,
 				38B4F3CA25E502E200E76A18 /* SwiftNotificationCenter.swift in Sources */,
-				DD07CA142CE80B73002D45A9 /* TimeInRangeChartStyle.swift in Sources */,
 				38AEE75225F022080013F05B /* SettingsManager.swift in Sources */,
 				3894873A2614928B004DF424 /* DispatchTimer.swift in Sources */,
 				3895E4C625B9E00D00214B37 /* Preferences.swift in Sources */,
@@ -3947,6 +3946,7 @@
 				CE7950242997D81700FA576E /* CGMSettingsView.swift in Sources */,
 				58237D9E2BCF0A6B00A47A79 /* PopupView.swift in Sources */,
 				BD793CB02CE7C61500D669AC /* OverrideRunStored+helper.swift in Sources */,
+				3B4196E02D8C4BC00091DFF7 /* HomeStateModel+CGM.swift in Sources */,
 				38D0B3D925EC07C400CB6E88 /* CarbsEntry.swift in Sources */,
 				DD32CF9C2CC82499003686D6 /* TrioRemoteControl+TempTarget.swift in Sources */,
 				BD249D882D42FC0000412DEB /* BolusStatsView.swift in Sources */,

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

@@ -33,7 +33,6 @@
   "glucoseColorScheme" : "staticColor",
   "xGridLines" : true,
   "yGridLines" : true,
-  "timeInRangeChartStyle" : "vertical",
   "rulerMarks" : true,
   "forecastDisplayType": "cone",
   "maxCarbs": 250,

+ 2 - 2
Trio/Sources/APS/FetchGlucoseManager.swift

@@ -122,12 +122,12 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     @MainActor func deleteGlucoseSource() async {
         cgmManager = nil
         glucoseSource = nil
+        settingsManager.settings.cgm = cgmDefaultModel.type
+        settingsManager.settings.cgmPluginIdentifier = cgmDefaultModel.id
         updateGlucoseSource(
             cgmGlucoseSourceType: cgmDefaultModel.type,
             cgmGlucosePluginId: cgmDefaultModel.id
         )
-        settingsManager.settings.cgm = cgmDefaultModel.type
-        settingsManager.settings.cgmPluginIdentifier = cgmDefaultModel.id
     }
 
     func saveConfigManager() {

+ 1 - 0
Trio/Sources/Application/AppDelegate.swift

@@ -31,6 +31,7 @@ class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject, UNUserNoti
                         .default,
                         "\(DebuggingIdentifiers.failed) failed to handle remote notification with error: \(error.localizedDescription)"
                     )
+                    completionHandler(.failed)
                 }
             }
         } catch {

+ 65 - 6
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -2193,6 +2193,9 @@
         }
       }
     },
+    " SMB" : {
+      "comment" : "Super Micro Bolus indicator in delete alert"
+    },
     " SMBs are disabled either by schedule or during the entire duration." : {
       "comment" : "Alert string. Keep spaces.",
       "extractionState" : "manual",
@@ -18814,6 +18817,7 @@
     },
     "Activate Dynamic Carb Ratio (CR)" : {
       "comment" : "Enable Dyn CR",
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -19215,6 +19219,7 @@
     },
     "Activate Dynamic Sensitivity (ISF)" : {
       "comment" : "Enable Dyn ISF",
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -19731,6 +19736,9 @@
         }
       }
     },
+    "Add a CGM and pump to enable automated insuin delivery" : {
+
+    },
     "Add a Garmin Device to Trio." : {
       "localizations" : {
         "bg" : {
@@ -25653,6 +25661,7 @@
     },
     "Adjustment Factor" : {
       "comment" : "Headline \"Adjustment Factor\"",
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -26784,6 +26793,7 @@
       }
     },
     "Algorithm" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -27299,6 +27309,9 @@
         }
       }
     },
+    "All FPUs and the carbs of the meal will be deleted." : {
+      "comment" : "Alert message for meal deletion"
+    },
     "All FPUs of the meal will be deleted." : {
       "extractionState" : "manual",
       "localizations" : {
@@ -27826,6 +27839,7 @@
       }
     },
     "Allow Fetching From Nightscout" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -28438,6 +28452,7 @@
       }
     },
     "Allow SMB With High Temporary Target" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -48352,6 +48367,7 @@
       }
     },
     "Choose the orientation of the Time in Range Chart." : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -48451,7 +48467,7 @@
         }
       }
     },
-    "Choose to display eA1c in percent or mmol/mol." : {
+    "Choose to display eA1c and GMI in percent or mmol/mol." : {
 
     },
     "Choose to display HbA1c in percent or mmol/mol." : {
@@ -48767,7 +48783,7 @@
         }
       }
     },
-    "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)." : {
+    "Choose which format you'd prefer the eA1c (estimated A1c) and GMI (Glucose Management Index) value in the statistics view as a percentage (Example: eA1c: 6.5%) or mmol/mol (Example: eA1c: 48 mmol/mol)." : {
 
     },
     "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)." : {
@@ -48872,6 +48888,7 @@
       }
     },
     "Choose which style for the time in range chart you'd prefer: a standing, i.e., vertical, bar chart or a laying, i.e., horizontal, line chart." : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -53784,6 +53801,7 @@
       }
     },
     "Create Calendar Events" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -55738,6 +55756,7 @@
 
     },
     "Dark Mode" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -55838,6 +55857,7 @@
       }
     },
     "Dark Scheme" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -62851,6 +62871,9 @@
         }
       }
     },
+    "Delete Carbs Equivalents?" : {
+      "comment" : "Alert title for deleting carb equivalents"
+    },
     "Delete Carbs?" : {
       "comment" : "Delete carbs from data table and Nightscout",
       "extractionState" : "manual",
@@ -63906,6 +63929,9 @@
         }
       }
     },
+    "Delete the Temp Target Preset \"%@\"?" : {
+      "comment" : "Delete confirmation title for temporary target presets"
+    },
     "Delivery limits" : {
       "extractionState" : "manual",
       "localizations" : {
@@ -65446,6 +65472,7 @@
       }
     },
     "DIA" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -66683,6 +66710,7 @@
       }
     },
     "Display and Allow Fat and Protein Entries" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -67820,6 +67848,7 @@
       }
     },
     "Display on Watch" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -71103,7 +71132,7 @@
     "eA1c" : {
 
     },
-    "eA1c Display Unit" : {
+    "eA1c/GMI Display Unit" : {
 
     },
     "Edit" : {
@@ -72868,6 +72897,7 @@
       }
     },
     "Enable Fatty Meal Factor" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -74182,6 +74212,7 @@
       }
     },
     "Enable SMB With Temporary Target" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -74491,6 +74522,7 @@
       }
     },
     "Enable Super Bolus" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -83369,6 +83401,7 @@
       }
     },
     "Fat and Protein Factor" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -84074,6 +84107,7 @@
     },
     "Fatty Meal Factor" : {
       "comment" : "For the  Bolus View pop-up",
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -84274,6 +84308,7 @@
       }
     },
     "Features" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -84474,6 +84509,7 @@
       }
     },
     "Fetch and Remote Control" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -87321,6 +87357,7 @@
       }
     },
     "FPU" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -93708,6 +93745,7 @@
       }
     },
     "High BG Target" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -94328,6 +94366,7 @@
     },
     "High Temptarget Raises Sensitivity" : {
       "comment" : "Headline \"High Temptarget Raises Sensitivity\"",
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -95146,6 +95185,7 @@
       }
     },
     "Horizontal" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -106140,6 +106180,7 @@
 
     },
     "Light Mode" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -106240,6 +106281,7 @@
       }
     },
     "Light Scheme" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -110771,6 +110813,7 @@
     },
     "Low Temptarget Lowers Sensitivity" : {
       "comment" : "Headline ”Low Temptarget Lowers Sensitivity\"",
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -115070,6 +115113,7 @@
     },
     "Max UAM SMB Basal Minutes" : {
       "comment" : "Headline \"Max UAM SMB Basal Minutes\"",
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -115671,6 +115715,7 @@
       }
     },
     "Maximum Duration (hours)" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -117941,6 +117986,7 @@
     },
     "Min 5m Carbimpact" : {
       "comment" : "Headline \"Min 5m Carbimpact\"",
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -121247,6 +121293,7 @@
       }
     },
     "Nightscout Fetch & Remote Control" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -121548,6 +121595,7 @@
       }
     },
     "Nightscout Upload" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -130800,9 +130848,6 @@
     "Override choice" : {
 
     },
-    "Override eA1c Unit" : {
-
-    },
     "Override HbA1c Unit" : {
       "extractionState" : "stale",
       "localizations" : {
@@ -141194,6 +141239,7 @@
     },
     "Remaining Carbs Fraction" : {
       "comment" : "Headline \"Remaining Carbs Fraction\"",
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -149960,6 +150006,7 @@
       }
     },
     "Set low and high glucose values for the main screen glucose graph and statistics." : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -150059,6 +150106,9 @@
         }
       }
     },
+    "Set low and high glucose values for the main screen, watch app and live activity glucose graph." : {
+
+    },
     "Set Rate" : {
       "localizations" : {
         "bg" : {
@@ -153331,6 +153381,7 @@
       }
     },
     "Show Protein and Fat" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -157698,6 +157749,7 @@
       }
     },
     "Spread Interval (minutes)" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -157899,6 +157951,7 @@
       }
     },
     "Standing / Laying TIR Chart" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -161566,6 +161619,7 @@
       }
     },
     "Super Bolus Factor" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -169720,6 +169774,7 @@
       }
     },
     "Therapy" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -177080,6 +177135,7 @@
       }
     },
     "Time in Range Chart Style" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -189538,6 +189594,7 @@
       }
     },
     "Vertical" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -191067,6 +191124,7 @@
       }
     },
     "Watch Complication" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -194957,6 +195015,7 @@
       }
     },
     "X-Axis Interval Step" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {

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

@@ -19,6 +19,11 @@ struct Determination: JSON, Equatable {
     let reservoir: Decimal?
     var isf: Decimal?
     var timestamp: Date?
+
+    /// `tdd` (Total Daily Dose) is included so it can be part of the
+    /// enacted and suggested devicestatus data that gets uploaded to Nightscout.
+    var tdd: Decimal?
+
     var current_target: Decimal?
     let insulinForManualBolus: Decimal?
     let manualBolusErrorString: Decimal?
@@ -59,6 +64,7 @@ extension Determination {
         case timestamp
         case isf = "ISF"
         case current_target
+        case tdd = "TDD"
         case insulinForManualBolus
         case manualBolusErrorString
         case minDelta

+ 0 - 16
Trio/Sources/Models/TimeInRangeChartStyle.swift

@@ -1,16 +0,0 @@
-import Foundation
-
-enum TimeInRangeChartStyle: String, JSON, CaseIterable, Identifiable, Codable, Hashable {
-    var id: String { rawValue }
-    case vertical
-    case horizontal
-
-    var displayName: String {
-        switch self {
-        case .vertical:
-            return String(localized: "Vertical", comment: "")
-        case .horizontal:
-            return String(localized: "Horizontal", comment: "")
-        }
-    }
-}

+ 0 - 5
Trio/Sources/Models/TrioSettings.swift

@@ -54,7 +54,6 @@ struct TrioSettings: JSON, Equatable {
     var glucoseColorScheme: GlucoseColorScheme = .staticColor
     var xGridLines: Bool = true
     var yGridLines: Bool = true
-    var timeInRangeChartStyle: TimeInRangeChartStyle = .vertical
     var rulerMarks: Bool = true
     var forecastDisplayType: ForecastDisplayType = .cone
     var maxCarbs: Decimal = 250
@@ -252,10 +251,6 @@ extension TrioSettings: Decodable {
             settings.yGridLines = yGridLines
         }
 
-        if let timeInRangeChartStyle = try? container.decode(TimeInRangeChartStyle.self, forKey: .timeInRangeChartStyle) {
-            settings.timeInRangeChartStyle = timeInRangeChartStyle
-        }
-
         if let rulerMarks = try? container.decode(Bool.self, forKey: .rulerMarks) {
             settings.rulerMarks = rulerMarks
         }

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

@@ -96,7 +96,11 @@ extension Adjustments.RootView {
     }
 
     private var deleteConfirmationTitle: String {
-        "Delete the Temp Target Preset \"\(selectedTempTarget?.name ?? "")\"?"
+        let presetName = selectedTempTarget?.name ?? ""
+        return String(
+            localized: "Delete the Temp Target Preset \"\(presetName)\"?",
+            comment: "Delete confirmation title for temporary target presets"
+        )
     }
 
     private func deleteConfirmationButtons() -> some View {

+ 60 - 47
Trio/Sources/Modules/CGMSettings/CGMSettingsStateModel.swift

@@ -4,6 +4,9 @@ import G7SensorKit
 import LoopKitUI
 import SwiftUI
 
+/// For a full description of the events that can happen for the CGM lifecycle, see comment at the top
+/// of HomeStateModel+CGM since these are the same events
+
 struct CGMModel: Identifiable, Hashable {
     var id: String
     var type: CGMType
@@ -23,18 +26,6 @@ let cgmDefaultModel = CGMModel(
     subtitle: CGMType.none.subtitle
 )
 
-struct OtherCGMSourceCompletionNotifying: CompletionNotifying {
-    var completionDelegate: (any LoopKitUI.CompletionDelegate)?
-}
-
-class CGMSetupCompletionNotifying: CompletionNotifying {
-    var completionDelegate: (any LoopKitUI.CompletionDelegate)?
-}
-
-class CGMDeletionCompletionNotifying: CompletionNotifying {
-    var completionDelegate: (any LoopKitUI.CompletionDelegate)?
-}
-
 extension CGMSettings {
     final class StateModel: BaseStateModel<Provider> {
         // Singleton implementation
@@ -49,7 +40,7 @@ extension CGMSettings {
 
         @Injected() var fetchGlucoseManager: FetchGlucoseManager!
         @Injected() var pluginCGMManager: PluginManager!
-        @Injected() private var broadcaster: Broadcaster!
+        @Injected() var broadcaster: Broadcaster!
         @Injected() var nightscoutManager: NightscoutManager!
 
         @Published var units: GlucoseUnits = .mgdL
@@ -60,8 +51,11 @@ extension CGMSettings {
         @Published var listOfCGM: [CGMModel] = []
         @Published var url: URL?
 
+        var shouldRunDeleteOnSettingsChange = true
+
         override func subscribe() {
             units = settingsManager.settings.units
+            broadcaster.register(SettingsObserver.self, observer: self)
 
             // collect the list of CGM available with plugins and CGMType defined manually
             listOfCGM = (
@@ -122,28 +116,36 @@ extension CGMSettings {
             subscribeSetting(\.smoothGlucose, on: $smoothGlucose, initial: { smoothGlucose = $0 })
         }
 
+        // this function will get called for all CGM types (plugin and non plugin)
         func addCGM(cgm: CGMModel) {
             cgmCurrent = cgm
-            switch cgmCurrent.type {
+            switch cgm.type {
             case .plugin:
                 shouldDisplayCGMSetupSheet.toggle()
             default:
-                fetchGlucoseManager.cgmGlucoseSourceType = cgmCurrent.type
-                completionNotifyingDidComplete(OtherCGMSourceCompletionNotifying())
+                // non plugin CGM types should be considered onboarded right away
+                shouldDisplayCGMSetupSheet = true
+                settingsManager.settings.cgm = cgmCurrent.type
+                settingsManager.settings.cgmPluginIdentifier = ""
+                fetchGlucoseManager.updateGlucoseSource(cgmGlucoseSourceType: cgmCurrent.type, cgmGlucosePluginId: cgmCurrent.id)
+                broadcaster.notify(GlucoseObserver.self, on: .main) {
+                    $0.glucoseDidUpdate([])
+                }
             }
         }
 
+        // Note: This function does _not_ get called for plugin CGMs
+        // instead, they will get cgmManagerWantsDeletion events which
+        // are handled by PluginSource
         func deleteCGM() {
-            fetchGlucoseManager.performOnCGMManagerQueue {
-                // Call plugin functionality on the manager queue (or at least attempt to)
-                Task {
-                    await self.fetchGlucoseManager?.deleteGlucoseSource()
-                }
+            Task {
+                await self.fetchGlucoseManager?.deleteGlucoseSource()
 
-                // UI updates go back to Main
-                DispatchQueue.main.async {
+                await MainActor.run {
                     self.shouldDisplayCGMSetupSheet = false
-                    self.completionNotifyingDidComplete(CGMDeletionCompletionNotifying())
+                    broadcaster.notify(GlucoseObserver.self, on: .main) {
+                        $0.glucoseDidUpdate([])
+                    }
                 }
             }
         }
@@ -152,40 +154,36 @@ extension CGMSettings {
 
 extension CGMSettings.StateModel: CompletionDelegate {
     func completionNotifyingDidComplete(_: CompletionNotifying) {
-        // if CGM was deleted
-        if fetchGlucoseManager.cgmGlucoseSourceType == .none {
-            cgmCurrent = cgmDefaultModel
-            settingsManager.settings.cgm = cgmDefaultModel.type
-            settingsManager.settings.cgmPluginIdentifier = cgmDefaultModel.id
-            Task {
-                await fetchGlucoseManager.deleteGlucoseSource()
-            }
-            shouldDisplayCGMSetupSheet = false
-        } else {
-            settingsManager.settings.cgm = cgmCurrent.type
-            settingsManager.settings.cgmPluginIdentifier = cgmCurrent.id
-            fetchGlucoseManager.updateGlucoseSource(cgmGlucoseSourceType: cgmCurrent.type, cgmGlucosePluginId: cgmCurrent.id)
-            shouldDisplayCGMSetupSheet = cgmCurrent.type == .simulator || cgmCurrent.type == .nightscout || cgmCurrent
-                .type == .xdrip || cgmCurrent.type == .enlite
-        }
-
-        // update glucose source if required
-        DispatchQueue.main.async {
-            self.broadcaster.notify(GlucoseObserver.self, on: .main) {
-                $0.glucoseDidUpdate([])
+        Task {
+            // this sleep is because this event and cgmManagerWantsDeletion
+            // are called in parallel.
+            try await Task.sleep(for: .seconds(0.2))
+            await MainActor.run {
+                if fetchGlucoseManager.cgmGlucoseSourceType == .none {
+                    cgmCurrent = cgmDefaultModel
+                }
             }
         }
+        shouldDisplayCGMSetupSheet = false
     }
 }
 
 extension CGMSettings.StateModel: CGMManagerOnboardingDelegate {
     func cgmManagerOnboarding(didCreateCGMManager manager: LoopKitUI.CGMManagerUI) {
-        // update the glucose source
+        // cgmCurrent should have been set in addCGM
+        debug(.service, "didCreateCGMManager called \(cgmCurrent)")
+        settingsManager.settings.cgm = cgmCurrent.type
+        settingsManager.settings.cgmPluginIdentifier = cgmCurrent.id
         fetchGlucoseManager.updateGlucoseSource(
             cgmGlucoseSourceType: cgmCurrent.type,
             cgmGlucosePluginId: cgmCurrent.id,
             newManager: manager
         )
+        DispatchQueue.main.async {
+            self.broadcaster.notify(GlucoseObserver.self, on: .main) {
+                $0.glucoseDidUpdate([])
+            }
+        }
     }
 
     func cgmManagerOnboarding(didOnboardCGMManager _: LoopKitUI.CGMManagerUI) {
@@ -193,8 +191,23 @@ extension CGMSettings.StateModel: CGMManagerOnboardingDelegate {
     }
 }
 
-extension CGMSettings.StateModel {
+extension CGMSettings.StateModel: SettingsObserver {
     func settingsDidChange(_: TrioSettings) {
         units = settingsManager.settings.units
+        // Deletes are handled differently for plugins vs non plugins
+        // but both will call deleteGlucoseSource on the fetchGlucoseManager
+        // so we listen for changes to the cgm setting and update our internal
+        // state accordingly
+        if settingsManager.settings.cgm == .none, shouldRunDeleteOnSettingsChange {
+            shouldRunDeleteOnSettingsChange = false
+            cgmCurrent = cgmDefaultModel
+            DispatchQueue.main.async {
+                self.broadcaster.notify(GlucoseObserver.self, on: .main) {
+                    $0.glucoseDidUpdate([])
+                }
+            }
+        } else {
+            shouldRunDeleteOnSettingsChange = true
+        }
     }
 }

+ 5 - 1
Trio/Sources/Modules/CGMSettings/View/CGMRootView.swift

@@ -161,7 +161,11 @@ extension CGMSettings {
                                 completionDelegate: state,
                                 setupDelegate: state,
                                 pluginCGMManager: self.state.pluginCGMManager
-                            )
+                            ).onDisappear {
+                                if state.fetchGlucoseManager.cgmGlucoseSourceType == .none {
+                                    state.cgmCurrent = cgmDefaultModel
+                                }
+                            }
                         }
                     }
                 }

+ 4 - 6
Trio/Sources/Modules/CGMSettings/View/CustomCGMOptionsView.swift

@@ -140,12 +140,10 @@ extension CGMSettings {
                                 .padding(.vertical)
                         }
 
-                        if state.url == nil {
-                            NavigationLink(
-                                destination: NightscoutConfig.RootView(resolver: resolver, displayClose: false),
-                                label: { Text("Configure Nightscout").foregroundStyle(Color.accentColor) }
-                            )
-                        }
+                        NavigationLink(
+                            destination: NightscoutConfig.RootView(resolver: resolver, displayClose: false),
+                            label: { Text("Configure Nightscout").foregroundStyle(Color.accentColor) }
+                        )
                     }
                 ).listRowBackground(Color.chart)
 

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

@@ -367,7 +367,7 @@ extension DataTable {
                                 action: {
                                     alertGlucoseToDelete = glucose
 
-                                    alertTitle = "Delete Glucose?"
+                                    alertTitle = String(localized: "Delete Glucose?", comment: "Alert title for deleting glucose")
                                     alertMessage = Formatter.dateFormatter
                                         .string(from: glucose.date ?? Date()) + ", " +
                                         (Formatter.decimalFormatterWithTwoFractionDigits.string(for: glucose.glucose) ?? "0")
@@ -526,7 +526,7 @@ extension DataTable {
                         role: .none,
                         action: {
                             alertTreatmentToDelete = item
-                            alertTitle = "Delete Insulin?"
+                            alertTitle = String(localized: "Delete Insulin?", comment: "Alert title for deleting insulin")
                             alertMessage = Formatter.dateFormatter
                                 .string(from: item.timestamp ?? Date()) + ", " +
                                 (Formatter.decimalFormatterWithTwoFractionDigits.string(from: item.bolus?.amount ?? 0) ?? "0") +
@@ -534,7 +534,11 @@ extension DataTable {
 
                             if let bolus = item.bolus {
                                 // Add text snippet, so that alert message is more descriptive for SMBs
-                                alertMessage += bolus.isSMB ? " SMB" : ""
+                                alertMessage += bolus.isSMB ? String(
+                                    localized: " SMB",
+                                    comment: "Super Micro Bolus indicator in delete alert"
+                                )
+                                    : ""
                             }
 
                             isRemoveHistoryItemAlertPresented = true
@@ -603,7 +607,7 @@ extension DataTable {
 
                         // meal is carb-only
                         if meal.fpuID == nil {
-                            alertTitle = "Delete Carbs?"
+                            alertTitle = String(localized: "Delete Carbs?", comment: "Alert title for deleting carbs")
                             alertMessage = Formatter.dateFormatter
                                 .string(from: meal.date ?? Date()) + ", " +
                                 (Formatter.decimalFormatterWithTwoFractionDigits.string(for: meal.carbs) ?? "0") +
@@ -611,8 +615,15 @@ extension DataTable {
                         }
                         // meal is complex-meal or fpu-only
                         else {
-                            alertTitle = meal.isFPU ? "Delete Carbs Equivalents?" : "Delete Carbs?"
-                            alertMessage = "All FPUs and the carbs of the meal will be deleted."
+                            alertTitle = meal.isFPU ? String(
+                                localized: "Delete Carbs Equivalents?",
+                                comment: "Alert title for deleting carb equivalents"
+                            )
+                                : String(localized: "Delete Carbs?", comment: "Alert title for deleting carbs")
+                            alertMessage = String(
+                                localized: "All FPUs and the carbs of the meal will be deleted.",
+                                comment: "Alert message for meal deletion"
+                            )
                         }
 
                         isRemoveHistoryItemAlertPresented = true

+ 79 - 0
Trio/Sources/Modules/Home/HomeStateModel+CGM.swift

@@ -0,0 +1,79 @@
+import LoopKitUI
+
+/// Notes on the CGM lifecycle:
+/// There are two classes of CGM devices: plugins and non-plugins. Plugins are implemented using
+/// LoopKit APIs and include most hardware CGMs like Dexcom G6, G7, Libre, and so on. Non-plugins
+/// drivers are implemented directly in Trio, and include the CGM Simulator and Nightscout CGM. For
+/// these different CGMs, there are a few different events, handled in different places, that happen to
+/// signify a change in the CGM lifecycle.
+///
+/// Both:
+/// - addCGM function invocation: Called by the UI in response to a user clicking the "add CGM" button
+///
+/// Non-plugins only:
+/// - deleteCGM function invocation: Called by the CGM View in response to a user clicking the "delete CGM" button
+///
+/// Plugins only:
+/// - completionNotifyingDidComplete: Called by the CGM driver to signify that Trio should close its UIViewController
+/// - cgmManagerOnboarding didCreateCGMManager: Called by the CGM driver after adding a new CGM
+/// - cgmManagerWantsDeletion: Called by the CGM driver when the user asks to delete a CGM
+/// There are no ordering constraints between completionNotifyingDidComplete and the other two
+/// Plugin events (it's up to the implementation of each individual driver). For example, the G7 driver invokes
+/// cgmManagerWantsDeletion on the delegate's queue while calling completionNotifyingDidComplete in parallel
+/// on the main queue.
+///
+/// In additinon to having different events for different types of CGMs, the handling of these events is spread out
+/// across various state managers, like HomeStateModel, CGMSettingsStateModel, and PluginSource.
+///
+/// There is CGM state in the HomeStateModel and CGMSettingsStateModel, FetchGlucoseManager, and
+/// SettingsManger
+///
+/// The flow for adding a CGM:
+/// - Non-plugin: addCGM (considered onboarded at this point)
+/// - Plugin: addCGM -> cgmManagerOnboarding (after success)
+///
+/// For deleting a CGM:
+/// - Non-plugin: deleteCGM (in HomeStateModel and CGMSettingsStateModel)
+/// - Plugin: cgmManagerWantsDeletion (in PluginSource)
+/// Then, both non-plugin and plugin:  set settings.cgm (in FetchGlucoseManager) ->
+///     settingsDidChange (in HomeStateModel and CGMSettingsStateModel)
+
+extension Home.StateModel: CompletionDelegate {
+    /// This completion handler is called by both the CGM and the pump
+    func completionNotifyingDidComplete(_ notifying: CompletionNotifying) {
+        debug(.service, "Completion fired by: \(type(of: notifying))")
+        Task {
+            // this sleep is because this event and cgmManagerWantsDeletion
+            // are called in parallel.
+            try await Task.sleep(for: .seconds(0.2))
+            await MainActor.run {
+                if fetchGlucoseManager.cgmGlucoseSourceType == .none {
+                    cgmCurrent = cgmDefaultModel
+                }
+            }
+        }
+        shouldDisplayCGMSetupSheet = false
+        shouldDisplayPumpSetupSheet = false
+    }
+}
+
+extension Home.StateModel: CGMManagerOnboardingDelegate {
+    func cgmManagerOnboarding(didCreateCGMManager manager: LoopKitUI.CGMManagerUI) {
+        settingsManager.settings.cgm = cgmCurrent.type
+        settingsManager.settings.cgmPluginIdentifier = cgmCurrent.id
+        fetchGlucoseManager.updateGlucoseSource(
+            cgmGlucoseSourceType: cgmCurrent.type,
+            cgmGlucosePluginId: cgmCurrent.id,
+            newManager: manager
+        )
+        DispatchQueue.main.async {
+            self.broadcaster.notify(GlucoseObserver.self, on: .main) {
+                $0.glucoseDidUpdate([])
+            }
+        }
+    }
+
+    func cgmManagerOnboarding(didOnboardCGMManager _: LoopKitUI.CGMManagerUI) {
+        // nothing to do
+    }
+}

+ 26 - 64
Trio/Sources/Modules/Home/HomeStateModel.swift

@@ -103,6 +103,7 @@ extension Home {
         var cgmAvailable: Bool = false
         var listOfCGM: [CGMModel] = []
         var cgmCurrent = cgmDefaultModel
+        var shouldRunDeleteOnSettingsChange = true
 
         var showCarbsRequiredBadge: Bool = true
         private(set) var setupPumpType: PumpConfig.PumpType = .minimed
@@ -455,8 +456,13 @@ extension Home {
             case .plugin:
                 shouldDisplayCGMSetupSheet = true
             default:
-                fetchGlucoseManager.cgmGlucoseSourceType = cgmCurrent.type
-                completionNotifyingDidComplete(CGMSetupCompletionNotifying())
+                shouldDisplayCGMSetupSheet = true
+                settingsManager.settings.cgm = cgmCurrent.type
+                settingsManager.settings.cgmPluginIdentifier = ""
+                fetchGlucoseManager.updateGlucoseSource(cgmGlucoseSourceType: cgmCurrent.type, cgmGlucosePluginId: cgmCurrent.id)
+                broadcaster.notify(GlucoseObserver.self, on: .main) {
+                    $0.glucoseDidUpdate([])
+                }
             }
         }
 
@@ -465,12 +471,14 @@ extension Home {
                 // Call plugin functionality on the manager queue (or at least attempt to)
                 Task {
                     await self.fetchGlucoseManager?.deleteGlucoseSource()
-                }
 
-                // UI updates go back to Main
-                DispatchQueue.main.async {
-                    self.shouldDisplayCGMSetupSheet = false
-                    self.completionNotifyingDidComplete(CGMDeletionCompletionNotifying())
+                    // UI updates go back to Main
+                    await MainActor.run {
+                        self.shouldDisplayCGMSetupSheet = false
+                        self.broadcaster.notify(GlucoseObserver.self, on: .main) {
+                            $0.glucoseDidUpdate([])
+                        }
+                    }
                 }
             }
         }
@@ -643,6 +651,17 @@ extension Home.StateModel:
         Task {
             await setupCGMSettings()
         }
+        if settingsManager.settings.cgm == .none, shouldRunDeleteOnSettingsChange {
+            shouldRunDeleteOnSettingsChange = false
+            cgmCurrent = cgmDefaultModel
+            DispatchQueue.main.async {
+                self.broadcaster.notify(GlucoseObserver.self, on: .main) {
+                    $0.glucoseDidUpdate([])
+                }
+            }
+        } else {
+            shouldRunDeleteOnSettingsChange = true
+        }
     }
 
     func preferencesDidChange(_: Preferences) {
@@ -685,48 +704,6 @@ extension Home.StateModel:
     }
 }
 
-extension Home.StateModel: CompletionDelegate {
-    func completionNotifyingDidComplete(_ notifying: CompletionNotifying) {
-        debug(.service, "Completion fired by: \(type(of: notifying))")
-        shouldDisplayCGMSetupSheet = false
-
-        if notifying is CGMSetupCompletionNotifying || notifying is CGMDeletionCompletionNotifying ||
-            notifying is CGMManagerSettingsNavigationViewController || notifying is any SetupTableViewControllerDelegate ||
-            notifying is any CGMManagerOnboarding
-        {
-            if fetchGlucoseManager.cgmGlucoseSourceType == .none {
-                debug(.service, "CGMDeletionCompletionNotifying: CGM Deletion Completed")
-
-                cgmCurrent = cgmDefaultModel
-                settingsManager.settings.cgm = cgmDefaultModel.type
-                settingsManager.settings.cgmPluginIdentifier = cgmDefaultModel.id
-                Task {
-                    await fetchGlucoseManager.deleteGlucoseSource()
-                }
-            } else {
-                debug(.service, "CGMSetupCompletionNotifying: CGM Setup Completed")
-
-                settingsManager.settings.cgm = cgmCurrent.type
-                settingsManager.settings.cgmPluginIdentifier = cgmCurrent.id
-                fetchGlucoseManager.updateGlucoseSource(cgmGlucoseSourceType: cgmCurrent.type, cgmGlucosePluginId: cgmCurrent.id)
-
-                shouldDisplayCGMSetupSheet = cgmCurrent.type == .simulator || cgmCurrent.type == .nightscout || cgmCurrent
-                    .type == .xdrip || cgmCurrent.type == .enlite
-            }
-
-            // update glucose source if required
-            DispatchQueue.main.async {
-                self.broadcaster.notify(GlucoseObserver.self, on: .main) {
-                    $0.glucoseDidUpdate([])
-                }
-            }
-        } else {
-            // pump related handling
-            shouldDisplayPumpSetupSheet = false // hides sheet
-        }
-    }
-}
-
 extension Home.StateModel: PumpManagerOnboardingDelegate {
     func pumpManagerOnboarding(didCreatePumpManager pumpManager: PumpManagerUI) {
         provider.apsManager.pumpManager = pumpManager
@@ -743,18 +720,3 @@ extension Home.StateModel: PumpManagerOnboardingDelegate {
         // nothing to do
     }
 }
-
-extension Home.StateModel: CGMManagerOnboardingDelegate {
-    func cgmManagerOnboarding(didCreateCGMManager manager: LoopKitUI.CGMManagerUI) {
-        // update the glucose source
-        fetchGlucoseManager.updateGlucoseSource(
-            cgmGlucoseSourceType: cgmCurrent.type,
-            cgmGlucosePluginId: cgmCurrent.id,
-            newManager: manager
-        )
-    }
-
-    func cgmManagerOnboarding(didOnboardCGMManager _: LoopKitUI.CGMManagerUI) {
-        // nothing to do
-    }
-}

+ 6 - 6
Trio/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift

@@ -123,6 +123,9 @@ extension NightscoutConfig {
                                         }
                                     }
                                 ).buttonStyle(BorderlessButtonStyle())
+                                    .alert(isPresented: $isImportAlertPresented) {
+                                        importAlert ?? Alert(title: Text("Unknown Error"))
+                                    }
                             }.padding(.top)
                         }.padding(.vertical)
                     }.listRowBackground(Color.chart)
@@ -177,6 +180,9 @@ extension NightscoutConfig {
                                             }
                                         }
                                     ).buttonStyle(BorderlessButtonStyle())
+                                        .alert(isPresented: $isBackfillAlertPresented) {
+                                            backfillAlert ?? Alert(title: Text("Unknown Error"))
+                                        }
                                 }.padding(.top)
                             }.padding(.vertical)
                         }
@@ -206,12 +212,6 @@ extension NightscoutConfig {
             }
             .navigationBarTitle("Nightscout")
             .navigationBarTitleDisplayMode(.automatic)
-            .alert(isPresented: $isImportAlertPresented) {
-                importAlert ?? Alert(title: Text("Unknown Error"))
-            }
-            .alert(isPresented: $isBackfillAlertPresented) {
-                backfillAlert ?? Alert(title: Text("Unknown Error"))
-            }
             .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
             .onAppear(perform: configureView)
         }

+ 29 - 37
Trio/Sources/Modules/Settings/SettingItems.swift

@@ -4,16 +4,16 @@ import SwiftUI
 
 struct SettingItem: Identifiable {
     let id = UUID()
-    let title: LocalizedStringKey
+    let title: String
     let view: Screen
-    let searchContents: [LocalizedStringKey]?
-    let path: [LocalizedStringKey]?
+    let searchContents: [String]?
+    let path: [String]?
 
     init(
-        title: LocalizedStringKey,
+        title: String,
         view: Screen,
-        searchContents: [LocalizedStringKey]? = nil,
-        path: [LocalizedStringKey]? = nil
+        searchContents: [String]? = nil,
+        path: [String]? = nil
     ) {
         self.title = title
         self.view = view
@@ -25,7 +25,7 @@ struct SettingItem: Identifiable {
 struct FilteredSettingItem: Identifiable {
     let id = UUID()
     let settingItem: SettingItem
-    let matchedContent: LocalizedStringKey
+    let matchedContent: String
 }
 
 enum SettingItems {
@@ -302,19 +302,23 @@ enum SettingItems {
     static func filteredItems(searchText: String) -> [FilteredSettingItem] {
         allItems.flatMap { item in
             var results = [FilteredSettingItem]()
-            let searchTextToLower = searchText.lowercased()
+            let searchLower = searchText.lowercased()
 
-            if item.title.stringValue.localizedCaseInsensitiveContains(searchTextToLower) ||
-                item.title.englishValue.localizedCaseInsensitiveContains(searchTextToLower)
+            let titleLocalized = item.title.localized
+            let titleEnglish = item.title.englishLocalized
+
+            if titleLocalized.localizedCaseInsensitiveContains(searchLower) ||
+                titleEnglish.localizedCaseInsensitiveContains(searchLower)
             {
                 results.append(FilteredSettingItem(settingItem: item, matchedContent: item.title))
             }
 
-            if let matchedContents = item.searchContents?.filter({
-                $0.stringValue.localizedCaseInsensitiveContains(searchTextToLower) ||
-                    $0.englishValue.localizedCaseInsensitiveContains(searchTextToLower)
-            }) {
-                results.append(contentsOf: matchedContents.map { FilteredSettingItem(settingItem: item, matchedContent: $0) })
+            if let contents = item.searchContents {
+                let matched = contents.filter {
+                    $0.localized.localizedCaseInsensitiveContains(searchLower) ||
+                        $0.englishLocalized.localizedCaseInsensitiveContains(searchLower)
+                }
+                results.append(contentsOf: matched.map { FilteredSettingItem(settingItem: item, matchedContent: $0) })
             }
 
             return results
@@ -322,29 +326,17 @@ enum SettingItems {
     }
 }
 
-extension LocalizedStringKey {
-    var stringValue: String {
-        let mirror = Mirror(reflecting: self)
-        let children = mirror.children
-        if let label = children.first(where: { $0.label == "key" })?.value as? String {
-            return String(localized: "\(label)", comment: "")
-        } else {
-            return ""
+extension String {
+    func localizedString(locale: Locale = .current) -> String {
+        if locale.identifier == "en",
+           let path = Bundle.main.path(forResource: "en", ofType: "lproj"),
+           let bundle = Bundle(path: path)
+        {
+            return NSLocalizedString(self, bundle: bundle, comment: "")
         }
+        return NSLocalizedString(self, comment: "")
     }
 
-    var englishValue: String {
-        let mirror = Mirror(reflecting: self)
-        let children = mirror.children
-
-        if let key = children.first(where: { $0.label == "key" })?.value as? String {
-            if let path = Bundle.main.path(forResource: "en", ofType: "lproj"),
-               let bundle = Bundle(path: path)
-            {
-                return bundle.localizedString(forKey: key, value: nil, table: nil)
-            }
-        }
-
-        return ""
-    }
+    var localized: String { localizedString() }
+    var englishLocalized: String { localizedString(locale: Locale(identifier: "en")) }
 }

+ 5 - 0
Trio/Sources/Modules/Settings/SettingsStateModel.swift

@@ -74,6 +74,11 @@ extension Settings {
 //            let storageURL = localDocuments.appendingPathComponent("PumpManagerState" + ".plist")
 //            try? FileManager.default.removeItem(at: storageURL)
 //        }
+        func hasCgmAndPump() -> Bool {
+            let hasCgm = fetchCgmManager.cgmGlucoseSourceType != .none
+            let hasPump = provider.deviceManager.pumpManager != nil
+            return hasCgm && hasPump
+        }
     }
 }
 

+ 17 - 5
Trio/Sources/Modules/Settings/View/SettingsRootView.swift

@@ -29,6 +29,7 @@ extension Settings {
             isUpdateAvailable: false,
             isBlacklisted: false
         )
+        @State private var closedLoopDisabled = true
 
         @Environment(\.colorScheme) var colorScheme
         @EnvironmentObject var appIcons: Icons
@@ -113,6 +114,11 @@ extension Settings {
                         }
                     ).listRowBackground(Color.chart)
 
+                    let miniHintText = closedLoopDisabled ?
+                        String(localized: "Add a CGM and pump to enable automated insuin delivery") :
+                        String(localized: "Enable automated insulin delivery.")
+                    let miniHintTextColorForDisabled: Color = colorScheme == .dark ? .orange : .accentColor
+                    let miniHintTextColor: Color = closedLoopDisabled ? miniHintTextColorForDisabled : .secondary
                     SettingInputSection(
                         decimalValue: $decimalPlaceholder,
                         booleanValue: $state.closedLoop,
@@ -127,7 +133,7 @@ extension Settings {
                         units: state.units,
                         type: .boolean,
                         label: String(localized: "Closed Loop"),
-                        miniHint: String(localized: "Enable automated insulin delivery."),
+                        miniHint: miniHintText,
                         verboseHint: VStack(alignment: .leading, spacing: 10) {
                             Text(
                                 "Running Trio in closed loop mode requires an active CGM sensor session and a connected pump. This enables automated insulin delivery."
@@ -136,14 +142,19 @@ extension Settings {
                                 "Before enabling, dial in your settings (basal / insulin sensitivity / carb ratio), and familiarize yourself with the app."
                             )
                         },
-                        headerText: String(localized: "Automated Insulin Delivery")
+                        headerText: String(localized: "Automated Insulin Delivery"),
+                        isToggleDisabled: closedLoopDisabled,
+                        miniHintColor: miniHintTextColor
                     )
+                    .onAppear {
+                        closedLoopDisabled = !state.hasCgmAndPump()
+                    }
 
                     Section(
                         header: Text("Trio Configuration"),
                         content: {
                             ForEach(SettingItems.trioConfig) { item in
-                                Text(item.title).navigationLink(to: item.view, from: self)
+                                Text(LocalizedStringKey(item.title)).navigationLink(to: item.view, from: self)
                             }
                         }
                     )
@@ -239,12 +250,13 @@ extension Settings {
                             if filteredItems.isNotEmpty {
                                 ForEach(filteredItems) { filteredItem in
                                     VStack(alignment: .leading) {
-                                        Text(filteredItem.matchedContent).bold()
+                                        Text(filteredItem.matchedContent.localized).bold()
                                         if let path = filteredItem.settingItem.path {
-                                            Text(path.map(\.stringValue).joined(separator: " > "))
+                                            Text(path.map(\.localized).joined(separator: " > "))
                                                 .font(.caption)
                                                 .foregroundColor(.secondary)
                                         }
+
                                     }.navigationLink(to: filteredItem.settingItem.view, from: self)
                                 }
                             } else {

+ 25 - 13
Trio/Sources/Modules/Stat/StatStateModel+Setup/LoopChartSetup.swift

@@ -23,6 +23,16 @@ struct LoopStatsByPeriod: Identifiable {
     var id: Date { period }
 }
 
+struct LoopStatsProcessedData: Identifiable {
+    var id = UUID()
+    let category: LoopStatsDataType
+    let count: Int
+    let percentage: Double
+    let medianDuration: Double
+    let medianInterval: Double
+    let totalDays: Int
+}
+
 enum LoopStatsDataType: String {
     case successfulLoop
     case glucoseCount
@@ -142,7 +152,7 @@ extension Stat.StateModel {
         failedLoopIds: [NSManagedObjectID],
         interval: StatsTimeIntervalWithToday
     ) async throws
-        -> [(category: LoopStatsDataType, count: Int, percentage: Double, medianDuration: Double, medianInterval: Double)]
+        -> [LoopStatsProcessedData]
     {
         // Calculate the date range for glucose readings
         let now = Date()
@@ -197,19 +207,21 @@ extension Stat.StateModel {
             let glucosePercentage = (averageGlucosePerDay / maxLoopsPerDay) * 100
 
             return [
-                (
-                    LoopStatsDataType.successfulLoop,
-                    Int(round(averageLoopsPerDay)),
-                    loopPercentage,
-                    medianDuration,
-                    medianInterval
+                LoopStatsProcessedData(
+                    category: LoopStatsDataType.successfulLoop,
+                    count: Int(round(averageLoopsPerDay)),
+                    percentage: loopPercentage,
+                    medianDuration: medianDuration,
+                    medianInterval: medianInterval,
+                    totalDays: numberOfDays
                 ),
-                (
-                    LoopStatsDataType.glucoseCount,
-                    Int(round(averageGlucosePerDay)),
-                    glucosePercentage,
-                    medianDuration,
-                    medianInterval
+                LoopStatsProcessedData(
+                    category: LoopStatsDataType.glucoseCount,
+                    count: Int(round(averageGlucosePerDay)),
+                    percentage: glucosePercentage,
+                    medianDuration: medianDuration,
+                    medianInterval: medianInterval,
+                    totalDays: numberOfDays
                 )
             ]
         }

+ 1 - 9
Trio/Sources/Modules/Stat/StatStateModel.swift

@@ -10,18 +10,11 @@ extension Stat {
         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 loopStats: [LoopStatsProcessedData] = []
         var groupedLoopStats: [LoopStatsByPeriod] = []
         var bolusStats: [BolusStats] = []
         var hourlyStats: [HourlyStats] = []
@@ -91,7 +84,6 @@ extension Stat {
             setupMealStats()
             units = settingsManager.settings.units
             eA1cDisplayUnit = settingsManager.settings.eA1cDisplayUnit
-            timeInRangeChartStyle = settingsManager.settings.timeInRangeChartStyle
             useFPUconversion = settingsManager.settings.useFPUconversion
         }
 

+ 20 - 12
Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseMetricsView.swift

@@ -27,11 +27,13 @@ struct GlucoseMetricsView: View {
         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 eA1cString = preferredUnit == .mgdL
+            ? (glucoseStats.ngsp.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "%") : glucoseStats
+            .ifcc.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))
 
-        let gmiString = glucoseStats.gmi.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "%"
+        let gmiString = preferredUnit == .mgdL ?
+            (glucoseStats.gmiPercentage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "%") :
+            glucoseStats.gmiMmolMol.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))
 
         // glucoseStats already parsed to units - only format decimals
         let standardDeviationString = units == .mgdL ? glucoseStats.sd.formatted(
@@ -40,7 +42,7 @@ struct GlucoseMetricsView: View {
             .number.grouping(.never).rounded().precision(.fractionLength(1))
         )
         let coefficientOfVariationString = glucoseStats.cv
-            .formatted(.number.grouping(.never).rounded().precision(.fractionLength(0)))
+            .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "%"
         let daysTrackedString = totalDays.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))
 
         VStack(alignment: .leading) {
@@ -60,14 +62,14 @@ struct GlucoseMetricsView: View {
 
     /// Computes various statistical metrics from stored glucose readings, including:
     /// - Estimated A1c in NGSP (%) and IFCC (mmol/mol)
-    /// - Glucose Management Index (GMI)
+    /// - Glucose Management Index (GMI) in both mmol/mol and percentage
     /// - 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,
+        ifcc: Double, ngsp: Double, gmiMmolMol: Double, gmiPercentage: Double, average: Double,
         median: Double, sd: Double, cv: Double, readingsPerDay: Double
     ) {
         // Determine the date range of the glucose data
@@ -84,7 +86,7 @@ struct GlucoseMetricsView: View {
 
         // 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)
+            return (ifcc: 0, ngsp: 0, gmiMmolMol: 0, gmiPercentage: 0, average: 0, median: 0, sd: 0, cv: 0, readingsPerDay: 0)
         }
 
         let sumOfReadings = glucoseValues.reduce(0, +)
@@ -96,7 +98,8 @@ struct GlucoseMetricsView: View {
         // 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)
+        var gmiValuePercentage = 0.0 // GMI (%)
+        var gmiValueMmolMol = 0.0 // GMI (mmol/mol)
 
         if totalDays > 0 {
             // **eA1c NGSP Calculation** (CGM-based)
@@ -107,9 +110,13 @@ struct GlucoseMetricsView: View {
             // eA1c IFCC (mmol/mol) = 10.929 * (eA1c NGSP - 2.152)
             eA1cIFCC = 10.929 * (eA1cNGSP - 2.152)
 
-            // **Glucose Management Index (GMI)**
+            // **Glucose Management Index (GMI) in %**
             // GMI = 3.31 + (0.02392 × Average Glucose mg/dL)
-            gmiValue = 3.31 + (0.02392 * meanGlucose)
+            gmiValuePercentage = 3.31 + (0.02392 * meanGlucose)
+
+            // **Glucose Management Index (GMI) in mmol/mol**
+            // GMI mmol/mol = (GMI % - 2.15) * 10.929
+            gmiValueMmolMol = (gmiValuePercentage - 2.152) * 10.929
         }
 
         // Compute Standard Deviation (SD) and Coefficient of Variation (CV)
@@ -123,7 +130,8 @@ struct GlucoseMetricsView: View {
         return (
             ifcc: eA1cIFCC, // eA1c in IFCC (mmol/mol)
             ngsp: eA1cNGSP, // eA1c in NGSP (%)
-            gmi: gmiValue, // Glucose Management Index
+            gmiMmolMol: gmiValueMmolMol, // GMI in mmol/mol
+            gmiPercentage: gmiValuePercentage, // GMI in %
             average: Double(units == .mgdL ? Decimal(meanGlucose) : meanGlucose.asMmolL),
             median: Double(units == .mgdL ? Decimal(medianGlucose) : medianGlucose.asMmolL),
             sd: Double(units == .mgdL ? Decimal(standardDeviation) : standardDeviation.asMmolL),

+ 2 - 8
Trio/Sources/Modules/Stat/View/ViewElements/Looping/LoopBarChartView.swift

@@ -4,7 +4,7 @@ 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)]
+    let statsData: [LoopStatsProcessedData]
 
     var body: some View {
         VStack(spacing: 20) {
@@ -50,13 +50,7 @@ struct LoopBarChartView: View {
         }
     }
 
-    private func annotationText(for data: (
-        category: LoopStatsDataType,
-        count: Int,
-        percentage: Double,
-        medianDuration: Double,
-        medianInterval: Double
-    )) -> String {
+    private func annotationText(for data: LoopStatsProcessedData) -> String {
         if data.category == .successfulLoop {
             switch selectedInterval {
             case .day,

+ 6 - 1
Trio/Sources/Modules/Stat/View/ViewElements/Looping/LoopStatsView.swift

@@ -4,7 +4,7 @@ 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)]
+    let statsData: [LoopStatsProcessedData]
 
     /// The main body of the `LoopStatsView`, displaying loop statistics.
     var body: some View {
@@ -32,6 +32,11 @@ struct LoopStatsView: View {
                     value: (successfulStats.percentage / 100)
                         .formatted(.percent.grouping(.never).rounded().precision(.fractionLength(1)))
                 )
+                Spacer()
+                StatChartUtils.statView(
+                    title: String(localized: "Days"),
+                    value: successfulStats.totalDays.description
+                )
             }
             .padding()
         }

+ 0 - 58
Trio/Sources/Modules/StatConfig/StatConfigStateModel.swift

@@ -1,58 +0,0 @@
-import SwiftUI
-
-extension StatConfig {
-    final class StateModel: BaseStateModel<Provider> {
-        @Published var overrideHbA1cUnit = false
-
-        @Published var skipBolusScreenAfterCarbs: Bool = false
-        @Published var useFPUconversion: Bool = true
-        @Published var tins: Bool = false
-        @Published var historyLayout: HistoryLayout = .twoTabs
-        @Published var lockScreenView: LockScreenView = .simple
-        @Published var low: Decimal = 70
-        @Published var high: Decimal = 180
-        @Published var hours: Decimal = 6
-        @Published var dynamicGlucoseColor = false
-        @Published var xGridLines = false
-        @Published var yGridLines: Bool = false
-        @Published var oneDimensionalGraph = false
-        @Published var rulerMarks: Bool = true
-        @Published var displayForecastsAsLines: Bool = false
-
-        var units: GlucoseUnits = .mgdL
-
-        override func subscribe() {
-            let units = settingsManager.settings.units
-            self.units = units
-
-            subscribeSetting(\.overrideHbA1cUnit, on: $overrideHbA1cUnit) { overrideHbA1cUnit = $0 }
-            subscribeSetting(\.dynamicGlucoseColor, on: $dynamicGlucoseColor) { dynamicGlucoseColor = $0 }
-            subscribeSetting(\.xGridLines, on: $xGridLines) { xGridLines = $0 }
-            subscribeSetting(\.yGridLines, on: $yGridLines) { yGridLines = $0 }
-            subscribeSetting(\.rulerMarks, on: $rulerMarks) { rulerMarks = $0 }
-            subscribeSetting(\.displayForecastsAsLines, on: $displayForecastsAsLines) { displayForecastsAsLines = $0 }
-            subscribeSetting(\.useFPUconversion, on: $useFPUconversion) { useFPUconversion = $0 }
-            subscribeSetting(\.tins, on: $tins) { tins = $0 }
-            subscribeSetting(\.skipBolusScreenAfterCarbs, on: $skipBolusScreenAfterCarbs) { skipBolusScreenAfterCarbs = $0 }
-            subscribeSetting(\.oneDimensionalGraph, on: $oneDimensionalGraph) { oneDimensionalGraph = $0 }
-            subscribeSetting(\.historyLayout, on: $historyLayout) { historyLayout = $0 }
-            subscribeSetting(\.lockScreenView, on: $lockScreenView) { lockScreenView = $0 }
-
-            subscribeSetting(\.low, on: $low, initial: {
-                let value = max(min($0, 90), 40)
-                low = units == .mmolL ? value.asMmolL : value
-            }, map: {
-                guard units == .mmolL else { return $0 }
-                return $0.asMgdL
-            })
-
-            subscribeSetting(\.high, on: $high, initial: {
-                let value = max(min($0, 270), 110)
-                high = units == .mmolL ? value.asMmolL : value
-            }, map: {
-                guard units == .mmolL else { return $0 }
-                return $0.asMgdL
-            })
-        }
-    }
-}

+ 0 - 113
Trio/Sources/Modules/StatConfig/View/StatConfigRootView.swift

@@ -1,113 +0,0 @@
-import SwiftUI
-import Swinject
-
-extension StatConfig {
-    struct RootView: BaseView {
-        let resolver: Resolver
-        @StateObject var state = StateModel()
-
-        @Environment(\.colorScheme) var colorScheme
-        var color: LinearGradient {
-            colorScheme == .dark ? LinearGradient(
-                gradient: Gradient(colors: [
-                    Color.bgDarkBlue,
-                    Color.bgDarkerDarkBlue
-                ]),
-                startPoint: .top,
-                endPoint: .bottom
-            )
-                :
-                LinearGradient(
-                    gradient: Gradient(colors: [Color.gray.opacity(0.1)]),
-                    startPoint: .top,
-                    endPoint: .bottom
-                )
-        }
-
-        private var glucoseFormatter: NumberFormatter {
-            let formatter = NumberFormatter()
-            formatter.numberStyle = .decimal
-            formatter.maximumFractionDigits = 0
-            if state.units == .mmolL {
-                formatter.maximumFractionDigits = 1
-            }
-            formatter.roundingMode = .halfUp
-            return formatter
-        }
-
-        private var carbsFormatter: NumberFormatter {
-            let formatter = NumberFormatter()
-            formatter.numberStyle = .decimal
-            formatter.maximumFractionDigits = 0
-            return formatter
-        }
-
-        var body: some View {
-            Form {
-                Section {
-                    Toggle("Use Dynamic BG Color", isOn: $state.dynamicGlucoseColor)
-                    Toggle("Display Chart X - Grid lines", isOn: $state.xGridLines)
-                    Toggle("Display Chart Y - Grid lines", isOn: $state.yGridLines)
-                    Toggle("Display Chart Threshold lines for Low and High", isOn: $state.rulerMarks)
-                    Toggle("Standing / Laying TIR Chart", isOn: $state.oneDimensionalGraph)
-                    Toggle("Enable total insulin in scope", isOn: $state.tins)
-                    HStack {
-                        Text("Hours X-Axis (6 default)")
-                        Spacer()
-                        TextFieldWithToolBar(text: $state.hours, placeholder: "6", numberFormatter: carbsFormatter)
-                        Text("hours").foregroundColor(.secondary)
-                    }
-                    Toggle("Show Forecasts as Lines", isOn: $state.displayForecastsAsLines)
-                } header: { Text("Home Chart settings ") }
-
-                Section {
-                    HStack {
-                        Text("Low")
-                        Spacer()
-                        TextFieldWithToolBar(text: $state.low, placeholder: "0", numberFormatter: glucoseFormatter)
-                        Text(state.units.rawValue).foregroundColor(.secondary)
-                    }
-                    HStack {
-                        Text("High")
-                        Spacer()
-                        TextFieldWithToolBar(text: $state.high, placeholder: "0", numberFormatter: glucoseFormatter)
-                        Text(state.units.rawValue).foregroundColor(.secondary)
-                    }
-                    Toggle("Override HbA1c Unit", isOn: $state.overrideHbA1cUnit)
-
-                } header: { Text("Statistics settings ") }
-
-                Section {
-                    Toggle("Skip Bolus screen after carbs", isOn: $state.skipBolusScreenAfterCarbs)
-                    Toggle("Display and allow Fat and Protein entries", isOn: $state.useFPUconversion)
-                } header: { Text("Add Meal View settings ") }
-
-                Section {
-                    Picker(
-                        selection: $state.historyLayout,
-                        label: Text("History Layout")
-                    ) {
-                        ForEach(HistoryLayout.allCases) { selection in
-                            Text(selection.displayName).tag(selection)
-                        }
-                    }
-                } header: { Text("History Settings") }
-
-                Section {
-                    Picker(
-                        selection: $state.lockScreenView,
-                        label: Text("Lock screen widget")
-                    ) {
-                        ForEach(LockScreenView.allCases) { selection in
-                            Text(selection.displayName).tag(selection)
-                        }
-                    }
-                } header: { Text("Lock screen widget") }
-            }
-            .scrollContentBackground(.hidden).background(color)
-            .onAppear(perform: configureView)
-            .navigationBarTitle("UI/UX")
-            .navigationBarTitleDisplayMode(.automatic)
-        }
-    }
-}

+ 4 - 2
Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift

@@ -157,8 +157,10 @@ extension Treatments {
 
         deinit {
             // Unregister from broadcaster
-            broadcaster.unregister(DeterminationObserver.self, observer: self)
-            broadcaster.unregister(BolusFailureObserver.self, observer: self)
+            if let broadcaster = broadcaster {
+                broadcaster.unregister(DeterminationObserver.self, observer: self)
+                broadcaster.unregister(BolusFailureObserver.self, observer: self)
+            }
 
             // Cancel Combine subscriptions
             unsubscribe()

+ 16 - 10
Trio/Sources/Modules/Treatments/View/ForecastChart.swift

@@ -83,16 +83,9 @@ struct ForecastChart: View {
                 Image(systemName: "arrow.right.circle")
 
                 if let simulatedDetermination = state.simulatedDetermination, let eventualBG = simulatedDetermination.eventualBG {
-                    HStack {
-                        Text(
-                            state.units == .mgdL ? Decimal(eventualBG).description : eventualBG.formattedAsMmolL
-                        )
-                        .font(.footnote)
-                        .foregroundStyle(.primary)
-                        Text("\(state.units.rawValue)")
-                            .font(.footnote)
-                            .foregroundStyle(.secondary)
-                    }
+                    eventualGlucoseBadge(for: eventualBG)
+                } else if let lastDetermination = state.determination.first, let eventualBG = lastDetermination.eventualBG {
+                    eventualGlucoseBadge(for: Int(truncating: eventualBG))
                 } else {
                     Text("---")
                         .font(.footnote)
@@ -112,6 +105,19 @@ struct ForecastChart: View {
         }
     }
 
+    @ViewBuilder private func eventualGlucoseBadge(for eventualBG: Int) -> some View {
+        HStack {
+            Text(
+                state.units == .mgdL ? Decimal(eventualBG).description : eventualBG.formattedAsMmolL
+            )
+            .font(.footnote)
+            .foregroundStyle(.primary)
+            Text("\(state.units.rawValue)")
+                .font(.footnote)
+                .foregroundStyle(.secondary)
+        }
+    }
+
     private var forecastChart: some View {
         Chart {
             drawGlucose()

+ 0 - 3
Trio/Sources/Modules/UserInterfaceSettings/UserInterfaceSettingsStateModel.swift

@@ -12,7 +12,6 @@ extension UserInterfaceSettings {
         @Published var carbsRequiredThreshold: Decimal = 0
         @Published var glucoseColorScheme: GlucoseColorScheme = .staticColor
         @Published var eA1cDisplayUnit: EstimatedA1cDisplayUnit = .percent
-        @Published var timeInRangeChartStyle: TimeInRangeChartStyle = .vertical
 
         var units: GlucoseUnits = .mgdL
 
@@ -40,8 +39,6 @@ extension UserInterfaceSettings {
             subscribeSetting(\.glucoseColorScheme, on: $glucoseColorScheme) { glucoseColorScheme = $0 }
 
             subscribeSetting(\.eA1cDisplayUnit, on: $eA1cDisplayUnit) { eA1cDisplayUnit = $0 }
-
-            subscribeSetting(\.timeInRangeChartStyle, on: $timeInRangeChartStyle) { timeInRangeChartStyle = $0 }
         }
     }
 }

+ 5 - 45
Trio/Sources/Modules/UserInterfaceSettings/View/UserInterfaceSettingsRootView.swift

@@ -286,7 +286,7 @@ extension UserInterfaceSettings {
 
                             HStack(alignment: .center) {
                                 Text(
-                                    "Set low and high glucose values for the main screen glucose graph and statistics."
+                                    "Set low and high glucose values for the main screen, watch app and live activity glucose graph."
                                 )
                                 .lineLimit(nil)
                                 .font(.footnote)
@@ -382,7 +382,7 @@ extension UserInterfaceSettings {
                         VStack {
                             Picker(
                                 selection: $state.eA1cDisplayUnit,
-                                label: Text("eA1c Display Unit")
+                                label: Text("eA1c/GMI Display Unit")
                             ) {
                                 ForEach(EstimatedA1cDisplayUnit.allCases) { selection in
                                     Text(selection.displayName).tag(selection)
@@ -391,7 +391,7 @@ extension UserInterfaceSettings {
 
                             HStack(alignment: .center) {
                                 Text(
-                                    "Choose to display eA1c in percent or mmol/mol."
+                                    "Choose to display eA1c and GMI in percent or mmol/mol."
                                 )
                                 .font(.footnote)
                                 .foregroundColor(.secondary)
@@ -399,11 +399,11 @@ extension UserInterfaceSettings {
                                 Spacer()
                                 Button(
                                     action: {
-                                        hintLabel = String(localized: "eA1c Display Unit")
+                                        hintLabel = String(localized: "eA1c/GMI Display Unit")
                                         selectedVerboseHint =
                                             AnyView(
                                                 Text(
-                                                    "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)."
+                                                    "Choose which format you'd prefer the eA1c (estimated A1c) and GMI (Glucose Management Index) value in the statistics view as a percentage (Example: eA1c: 6.5%) or mmol/mol (Example: eA1c: 48 mmol/mol)."
                                                 )
                                             )
                                         shouldDisplayHint.toggle()
@@ -419,46 +419,6 @@ extension UserInterfaceSettings {
                     }
                 ).listRowBackground(Color.chart)
 
-                Section {
-                    VStack(alignment: .leading) {
-                        Picker(
-                            selection: $state.timeInRangeChartStyle,
-                            label: Text("Time in Range Chart Style").multilineTextAlignment(.leading)
-                        ) {
-                            ForEach(TimeInRangeChartStyle.allCases) { selection in
-                                Text(selection.displayName).tag(selection)
-                            }
-                        }.padding(.top)
-
-                        HStack(alignment: .center) {
-                            Text(
-                                "Choose the orientation of the Time in Range Chart."
-                            )
-                            .font(.footnote)
-                            .foregroundColor(.secondary)
-                            .lineLimit(nil)
-                            Spacer()
-                            Button(
-                                action: {
-                                    hintLabel = String(localized: "Time in Range Chart Style")
-                                    selectedVerboseHint =
-                                        AnyView(
-                                            Text(
-                                                "Choose which style for the time in range chart you'd prefer: a standing, i.e., vertical, bar chart or a laying, i.e., horizontal, line chart."
-                                            )
-                                        )
-                                    shouldDisplayHint.toggle()
-                                },
-                                label: {
-                                    HStack {
-                                        Image(systemName: "questionmark.circle")
-                                    }
-                                }
-                            ).buttonStyle(BorderlessButtonStyle())
-                        }.padding(.top)
-                    }.padding(.bottom)
-                }.listRowBackground(Color.chart)
-
                 SettingInputSection(
                     decimalValue: $state.carbsRequiredThreshold,
                     booleanValue: $state.showCarbsRequiredBadge,

+ 70 - 10
Trio/Sources/Services/Network/Nightscout/NightscoutManager.swift

@@ -504,6 +504,19 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             return
         }
 
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: TDDStored.self,
+            onContext: backgroundContext,
+            predicate: NSPredicate.predicateFor30MinAgo,
+            key: "date",
+            ascending: false,
+            fetchLimit: 1
+        )
+
+        let tdd: Decimal? = await backgroundContext.perform {
+            (results as? [TDDStored])?.first?.total as? Decimal
+        }
+
         // Suggested / Enacted
         async let enactedDeterminationID = determinationStorage
             .fetchLastDeterminationObjectID(predicate: NSPredicate.enactedDeterminationsNotYetUploadedToNightscout)
@@ -543,6 +556,10 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
                 suggestion.minPredBG = suggestion.minPredBG?.asMmolL
                 suggestion.threshold = suggestion.threshold?.asMmolL
             }
+
+            suggestion.reason = injectTDD(into: suggestion.reason, tdd: tdd)
+            suggestion.tdd = tdd
+
             // Check whether the last suggestion that was uploaded is the same that is fetched again when we are attempting to upload the enacted determination
             // Apparently we are too fast; so the flag update is not fast enough to have the predicate filter last suggestion out
             // If this check is truthy, set suggestion to nil so it's not uploaded again
@@ -553,17 +570,20 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             }
         }
 
-        if let fetchedEnacted = fetchedEnactedDetermination, settingsManager.settings.units == .mmolL {
-            var modifiedFetchedEnactedDetermination = fetchedEnactedDetermination
-            modifiedFetchedEnactedDetermination?
-                .reason = parseReasonGlucoseValuesToMmolL(fetchedEnacted.reason)
-            // TODO: verify that these parsings are needed for 3rd party apps, e.g., LoopFollow
-            modifiedFetchedEnactedDetermination?.current_target = fetchedEnacted.current_target?.asMmolL
-            modifiedFetchedEnactedDetermination?.minGuardBG = fetchedEnacted.minGuardBG?.asMmolL
-            modifiedFetchedEnactedDetermination?.minPredBG = fetchedEnacted.minPredBG?.asMmolL
-            modifiedFetchedEnactedDetermination?.threshold = fetchedEnacted.threshold?.asMmolL
+        if var enacted = fetchedEnactedDetermination {
+            if settingsManager.settings.units == .mmolL {
+                enacted.reason = parseReasonGlucoseValuesToMmolL(enacted.reason)
+                // TODO: verify that these parsings are needed for 3rd party apps, e.g., LoopFollow
+                enacted.current_target = enacted.current_target?.asMmolL
+                enacted.minGuardBG = enacted.minGuardBG?.asMmolL
+                enacted.minPredBG = enacted.minPredBG?.asMmolL
+                enacted.threshold = enacted.threshold?.asMmolL
+            }
 
-            fetchedEnactedDetermination = modifiedFetchedEnactedDetermination
+            enacted.reason = injectTDD(into: enacted.reason, tdd: tdd)
+            enacted.tdd = tdd
+
+            fetchedEnactedDetermination = enacted
         }
 
         // Gather all relevant data for OpenAPS Status
@@ -1453,3 +1473,43 @@ extension BaseNightscoutManager {
         return updatedReason
     }
 }
+
+extension BaseNightscoutManager {
+    /// Injects TDD into the provided `reason` string if TDD is available.
+    ///
+    /// - Parameters:
+    ///   - reason: The raw reason string (e.g., "minPredBG=5.2, IOBpredBG=102").
+    ///   - tdd: The total daily dose of insulin.
+    /// - Returns: A modified reason string that includes "TDD: x U" appended
+    ///   after the last matched prediction term, or at the end if no match is found.
+    func injectTDD(into reason: String, tdd: Decimal?) -> String {
+        guard let tdd = tdd else { return reason }
+
+        let tddString = ", TDD: \(tdd) U"
+
+        // Regex that matches any of the keywords followed by an optional colon, whitespace, then a number.
+        let pattern = "(minPredBG|minGuardBG|IOBpredBG|COBpredBG|UAMpredBG):?\\s*(-?\\d+(?:\\.\\d+)?)"
+        guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
+            return reason + tddString
+        }
+
+        // Split the reason at the first semicolon (if present)
+        let components = reason.split(separator: ";", maxSplits: 1, omittingEmptySubsequences: false)
+        let mainPart = String(components[0])
+        let tailPart = components.count > 1 ? ";" + components[1] : ""
+
+        // Search only in the main part for the keywords
+        let nsRange = NSRange(mainPart.startIndex ..< mainPart.endIndex, in: mainPart)
+        let matches = regex.matches(in: mainPart, options: [], range: nsRange)
+
+        // If found, insert TDD after the last occurrence in the main part.
+        if let lastMatch = matches.last, let matchRange = Range(lastMatch.range, in: mainPart) {
+            var modifiedMainPart = mainPart
+            modifiedMainPart.insert(contentsOf: tddString, at: matchRange.upperBound)
+            return modifiedMainPart + tailPart
+        }
+
+        // If no match is found, append TDD at the end of the original reason string.
+        return reason + tddString
+    }
+}

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

@@ -45,8 +45,8 @@ extension TrioRemoteControl {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
             onContext: taskContext,
-            predicate: NSPredicate(format: "createdAt > %@", pushMessageDate as NSDate),
-            key: "createdAt",
+            predicate: NSPredicate(format: "date > %@", pushMessageDate as NSDate),
+            key: "date",
             ascending: false
         )
 

+ 12 - 3
Trio/Sources/Views/SettingInputSection.swift

@@ -33,6 +33,8 @@ struct SettingInputSection<VerboseHint: View>: View {
     var verboseHint: VerboseHint
     var headerText: String?
     var footerText: String?
+    var isToggleDisabled: Bool = false
+    var miniHintColor: Color = .secondary
 
     @ObservedObject private var pickerSettingsProvider = PickerSettingsProvider.shared
     @State private var displayPicker: Bool = false
@@ -55,6 +57,7 @@ struct SettingInputSection<VerboseHint: View>: View {
 
                     case .boolean:
                         toggleView(label: label, isOn: $booleanValue)
+                            .disabled(isToggleDisabled)
 
                     case let .conditionalDecimal(key):
                         VStack {
@@ -73,7 +76,8 @@ struct SettingInputSection<VerboseHint: View>: View {
                     hintSection(
                         miniHint: miniHint,
                         shouldDisplayHint: $shouldDisplayHint,
-                        verboseHint: verboseHint
+                        verboseHint: verboseHint,
+                        miniHintColor: miniHintColor
                     )
                 }
             },
@@ -235,11 +239,16 @@ struct SettingInputSection<VerboseHint: View>: View {
         }.padding(.top)
     }
 
-    private func hintSection(miniHint: String, shouldDisplayHint: Binding<Bool>, verboseHint: VerboseHint) -> some View {
+    private func hintSection(
+        miniHint: String,
+        shouldDisplayHint: Binding<Bool>,
+        verboseHint: VerboseHint,
+        miniHintColor: Color = .secondary
+    ) -> some View {
         HStack(alignment: .center) {
             Text(miniHint)
                 .font(.footnote)
-                .foregroundColor(.secondary)
+                .foregroundColor(miniHintColor)
                 .lineLimit(nil)
             Spacer()
             Button(action: {

+ 1 - 0
patches/save_patches_here.md

@@ -0,0 +1 @@
+Trio workspace patches can be saved in this directory (Trio/patches/)