فهرست منبع

Merge branch 'dev' of github.com:nightscout/Trio-dev into stats-wip

Deniz Cengiz 1 سال پیش
والد
کامیت
50ec3acf50
54فایلهای تغییر یافته به همراه2965 افزوده شده و 295 حذف شده
  1. 76 4
      Trio.xcodeproj/project.pbxproj
  2. 54 0
      Trio.xcodeproj/xcshareddata/xcschemes/Trio Tests.xcscheme
  3. 2 3
      Trio/Resources/Info.plist
  4. 24 24
      Trio/Sources/APS/Storage/CarbsStorage.swift
  5. 3 2
      Trio/Sources/APS/Storage/DeterminationStorage.swift
  6. 34 30
      Trio/Sources/APS/Storage/GlucoseStorage.swift
  7. 49 23
      Trio/Sources/APS/Storage/OverrideStorage.swift
  8. 4 2
      Trio/Sources/APS/Storage/PumpHistoryStorage.swift
  9. 43 22
      Trio/Sources/APS/Storage/TempTargetsStorage.swift
  10. 13 13
      Trio/Sources/Application/TrioApp.swift
  11. 2 2
      Trio/Sources/Assemblies/ServiceAssembly.swift
  12. 174 5
      Trio/Sources/Localizations/Main/Localizable.xcstrings
  13. 3 3
      Trio/Sources/Models/ColorSchemeOption.swift
  14. 2 2
      Trio/Sources/Models/GlucoseColorScheme.swift
  15. 9 4
      Trio/Sources/Models/GlucoseNotificationsOption.swift
  16. 32 7
      Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+Overrides.swift
  17. 5 5
      Trio/Sources/Modules/Adjustments/View/Overrides/AddOverrideForm.swift
  18. 5 5
      Trio/Sources/Modules/Adjustments/View/Overrides/EditOverrideForm.swift
  19. 5 4
      Trio/Sources/Modules/Adjustments/View/TempTargets/AddTempTargetForm.swift
  20. 1 1
      Trio/Sources/Modules/AlgorithmAdvancedSettings/View/AlgorithmAdvancedSettingsRootView.swift
  21. 3 3
      Trio/Sources/Modules/AutosensSettings/View/AutosensSettingsRootView.swift
  22. 2 4
      Trio/Sources/Modules/BasalProfileEditor/View/BasalProfileEditorRootView.swift
  23. 1 1
      Trio/Sources/Modules/BolusCalculatorConfig/View/BolusCalculatorConfigRootView.swift
  24. 4 5
      Trio/Sources/Modules/CarbRatioEditor/View/CarbRatioEditorRootView.swift
  25. 1 1
      Trio/Sources/Modules/DataTable/View/CarbEntryEditorView.swift
  26. 1 1
      Trio/Sources/Modules/DataTable/View/DataTableRootView.swift
  27. 1 1
      Trio/Sources/Modules/GeneralSettings/View/UnitsLimitsSettingsRootView.swift
  28. 2 2
      Trio/Sources/Modules/ISFEditor/View/ISFEditorRootView.swift
  29. 1 1
      Trio/Sources/Modules/LiveActivitySettings/View/LiveActivitySettingsRootView.swift
  30. 1 1
      Trio/Sources/Modules/LiveActivitySettings/View/LiveActivityWidgetConfiguration.swift
  31. 16 12
      Trio/Sources/Modules/SMBSettings/View/SMBSettingsRootView.swift
  32. 1 1
      Trio/Sources/Modules/Stat/View/ViewElements/GlucoseMetricsView.swift
  33. 9 5
      Trio/Sources/Modules/TargetBehavoir/View/TargetBehavoirRootView.swift
  34. 6 2
      Trio/Sources/Modules/Treatments/View/TreatmentsRootView.swift
  35. 1 1
      Trio/Sources/Services/LiveActivity/Data/DataManager.swift
  36. 111 3
      Trio/Sources/Services/LiveActivity/LiveActivityBridge.swift
  37. 19 8
      Trio/Sources/Services/RemoteControl/TrioRemoteControl+Meal.swift
  38. 9 0
      Trio/Sources/Shortcuts/AppShortcuts.swift
  39. 1 0
      Trio/Sources/Shortcuts/BaseIntentsRequest.swift
  40. 29 0
      Trio/Sources/Shortcuts/LiveActivity/RestartLiveActivityIntent.swift
  41. 43 0
      Trio/Sources/Shortcuts/LiveActivity/RestartLiveActivityIntentRequest.swift
  42. 5 5
      Trio/Sources/Views/SettingInputSection.swift
  43. 590 0
      TrioTests/BolusCalculatorTests/BolusCalculatorTests.swift
  44. 46 29
      TrioTests/CalibrationsTests.swift
  45. 208 0
      TrioTests/CoreDataTests/CarbsStorageTests.swift
  46. 230 0
      TrioTests/CoreDataTests/DeterminationStorageTests.swift
  47. 187 0
      TrioTests/CoreDataTests/GlucoseStorageTests.swift
  48. 224 0
      TrioTests/CoreDataTests/OverrideStorageTests.swift
  49. 288 0
      TrioTests/CoreDataTests/PumpHistoryStorageTests.swift
  50. 148 0
      TrioTests/CoreDataTests/TempTargetStorageTests.swift
  51. 44 0
      TrioTests/CoreDataTests/TestAssembly.swift
  52. 127 12
      TrioTests/FileStorageTests.swift
  53. 56 36
      TrioTests/PluginManagerTests.swift
  54. 10 0
      TrioTests/TestError.swift

+ 76 - 4
Trio.xcodeproj/project.pbxproj

@@ -208,6 +208,8 @@
 		491D6FBE2D56741C00C49F67 /* TempTargetRunStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FB92D56741C00C49F67 /* TempTargetRunStored+CoreDataClass.swift */; };
 		491D6FBF2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FBA2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift */; };
 		491D6FC02D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FBB2D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift */; };
+		49249B1C2D46E45E000F4866 /* CurrentTDDSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49249B1B2D46E45E000F4866 /* CurrentTDDSetup.swift */; };
+		49249B382D46E76A000F4866 /* TDD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49249B372D46E76A000F4866 /* TDD.swift */; };
 		49B9B57F2D5768D2009C6B59 /* AdjustmentStored+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49B9B57E2D5768D2009C6B59 /* AdjustmentStored+Helper.swift */; };
 		5075C1608E6249A51495C422 /* TargetsEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BDEA2DC60EDE0A3CA54DC73 /* TargetsEditorProvider.swift */; };
 		53F2382465BF74DB1A967C8B /* PumpConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8630D58BDAD6D9C650B9B39 /* PumpConfigProvider.swift */; };
@@ -260,7 +262,7 @@
 		6B1A8D202B14D91600E76752 /* LiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B1A8D1F2B14D91600E76752 /* LiveActivity.swift */; };
 		6B1A8D242B14D91700E76752 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6B1A8D232B14D91700E76752 /* Assets.xcassets */; };
 		6B1A8D282B14D91700E76752 /* LiveActivityExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 6B1A8D172B14D91600E76752 /* LiveActivityExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
-		6B1A8D2E2B156EEF00E76752 /* LiveActivityBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B1A8D2D2B156EEF00E76752 /* LiveActivityBridge.swift */; };
+		6B1A8D2E2B156EEF00E76752 /* LiveActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B1A8D2D2B156EEF00E76752 /* LiveActivityManager.swift */; };
 		6B1F539F9FF75646D1606066 /* SnoozeDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36A708CDB546692C2230B385 /* SnoozeDataFlow.swift */; };
 		6BCF84DD2B16843A003AD46E /* LiveActitiyAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCF84DC2B16843A003AD46E /* LiveActitiyAttributes.swift */; };
 		6BCF84DE2B16843A003AD46E /* LiveActitiyAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCF84DC2B16843A003AD46E /* LiveActitiyAttributes.swift */; };
@@ -333,6 +335,15 @@
 		BD8207C42D2B42E60023339D /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B1A8D182B14D91600E76752 /* WidgetKit.framework */; };
 		BD8207C52D2B42E60023339D /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B1A8D1A2B14D91600E76752 /* SwiftUI.framework */; };
 		BD8207CE2D2B42E70023339D /* Trio Watch Complication Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = BD8207C32D2B42E50023339D /* Trio Watch Complication Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+		BD8FC0542D66186000B95AED /* TestError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC0532D66186000B95AED /* TestError.swift */; };
+		BD8FC0572D66188700B95AED /* PumpHistoryStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC0562D66188700B95AED /* PumpHistoryStorageTests.swift */; };
+		BD8FC0592D66189700B95AED /* TestAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC0582D66189700B95AED /* TestAssembly.swift */; };
+		BD8FC05B2D6618AF00B95AED /* DeterminationStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC05A2D6618AF00B95AED /* DeterminationStorageTests.swift */; };
+		BD8FC05E2D6618CE00B95AED /* BolusCalculatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC05D2D6618CE00B95AED /* BolusCalculatorTests.swift */; };
+		BD8FC0602D6619DB00B95AED /* CarbsStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC05F2D6619DB00B95AED /* CarbsStorageTests.swift */; };
+		BD8FC0622D6619E600B95AED /* OverrideStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC0612D6619E600B95AED /* OverrideStorageTests.swift */; };
+		BD8FC0642D6619EF00B95AED /* TempTargetStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC0632D6619EF00B95AED /* TempTargetStorageTests.swift */; };
+		BD8FC0662D661A0000B95AED /* GlucoseStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC0652D661A0000B95AED /* GlucoseStorageTests.swift */; };
 		BDA25EE42D260CD500035F34 /* AppleWatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA25EE32D260CCF00035F34 /* AppleWatchManager.swift */; };
 		BDA25EE62D260D5E00035F34 /* WatchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA25EE52D260D5800035F34 /* WatchState.swift */; };
 		BDA25EFD2D261C0000035F34 /* WatchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA25EFC2D261BF200035F34 /* WatchState.swift */; };
@@ -492,6 +503,8 @@
 		DD498F2C2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
 		DD498F2D2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
 		DD4C581F2D73C43D001BFF2C /* LoopStatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4C581E2D73C43D001BFF2C /* LoopStatsView.swift */; };
+		DD4C57A82D73ADEA001BFF2C /* RestartLiveActivityIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4C57A72D73ADEA001BFF2C /* RestartLiveActivityIntent.swift */; };
+		DD4C57AA2D73B3E2001BFF2C /* RestartLiveActivityIntentRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4C57A92D73B3D9001BFF2C /* RestartLiveActivityIntentRequest.swift */; };
 		DD4FFF332D458EE600B6CFF9 /* GarminWatchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4FFF322D458EE600B6CFF9 /* GarminWatchState.swift */; };
 		DD5DC9F12CF3D97C00AB8703 /* AdjustmentsStateModel+Overrides.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5DC9F02CF3D96E00AB8703 /* AdjustmentsStateModel+Overrides.swift */; };
 		DD5DC9F32CF3D9DD00AB8703 /* AdjustmentsStateModel+TempTargets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5DC9F22CF3D9D600AB8703 /* AdjustmentsStateModel+TempTargets.swift */; };
@@ -918,6 +931,8 @@
 		491D6FBA2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetRunStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		491D6FBB2D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		491D6FBC2D56741C00C49F67 /* TempTargetStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
+		49249B1B2D46E45E000F4866 /* CurrentTDDSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentTDDSetup.swift; sourceTree = "<group>"; };
+		49249B372D46E76A000F4866 /* TDD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TDD.swift; sourceTree = "<group>"; };
 		49B9B57E2D5768D2009C6B59 /* AdjustmentStored+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentStored+Helper.swift"; sourceTree = "<group>"; };
 		4DD795BA46B193644D48138C /* TargetsEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorRootView.swift; sourceTree = "<group>"; };
 		505E09DC17A0C3D0AF4B66FE /* ISFEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorStateModel.swift; sourceTree = "<group>"; };
@@ -972,7 +987,7 @@
 		6B1A8D1F2B14D91600E76752 /* LiveActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivity.swift; sourceTree = "<group>"; };
 		6B1A8D232B14D91700E76752 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
 		6B1A8D252B14D91700E76752 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
-		6B1A8D2D2B156EEF00E76752 /* LiveActivityBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityBridge.swift; sourceTree = "<group>"; };
+		6B1A8D2D2B156EEF00E76752 /* LiveActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityManager.swift; sourceTree = "<group>"; };
 		6BCF84DC2B16843A003AD46E /* LiveActitiyAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActitiyAttributes.swift; sourceTree = "<group>"; };
 		715120D12D3C2B84005D9FB6 /* GlucoseNotificationsOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseNotificationsOption.swift; sourceTree = "<group>"; };
 		71D44AAA2CA5F5EA0036EE9E /* AlertPermissionsChecker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertPermissionsChecker.swift; sourceTree = "<group>"; };
@@ -1038,6 +1053,15 @@
 		BD7DA9AB2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigRootView.swift; sourceTree = "<group>"; };
 		BD7DB88D2D2C4A0A003D3155 /* BolusCalculationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculationManager.swift; sourceTree = "<group>"; };
 		BD8207C32D2B42E50023339D /* Trio Watch Complication Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Trio Watch Complication Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
+		BD8FC0532D66186000B95AED /* TestError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestError.swift; sourceTree = "<group>"; };
+		BD8FC0562D66188700B95AED /* PumpHistoryStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpHistoryStorageTests.swift; sourceTree = "<group>"; };
+		BD8FC0582D66189700B95AED /* TestAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAssembly.swift; sourceTree = "<group>"; };
+		BD8FC05A2D6618AF00B95AED /* DeterminationStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeterminationStorageTests.swift; sourceTree = "<group>"; };
+		BD8FC05D2D6618CE00B95AED /* BolusCalculatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorTests.swift; sourceTree = "<group>"; };
+		BD8FC05F2D6619DB00B95AED /* CarbsStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbsStorageTests.swift; sourceTree = "<group>"; };
+		BD8FC0612D6619E600B95AED /* OverrideStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideStorageTests.swift; sourceTree = "<group>"; };
+		BD8FC0632D6619EF00B95AED /* TempTargetStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetStorageTests.swift; sourceTree = "<group>"; };
+		BD8FC0652D661A0000B95AED /* GlucoseStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseStorageTests.swift; sourceTree = "<group>"; };
 		BDA25EE32D260CCF00035F34 /* AppleWatchManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleWatchManager.swift; sourceTree = "<group>"; };
 		BDA25EE52D260D5800035F34 /* WatchState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchState.swift; sourceTree = "<group>"; };
 		BDA25EFC2D261BF200035F34 /* WatchState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchState.swift; sourceTree = "<group>"; };
@@ -1203,6 +1227,8 @@
 		DD3A3CE62D29C93F00AE478E /* Helper+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Helper+Extensions.swift"; sourceTree = "<group>"; };
 		DD3A3CE82D29C97800AE478E /* Helper+ButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Helper+ButtonStyles.swift"; sourceTree = "<group>"; };
 		DD4C581E2D73C43D001BFF2C /* LoopStatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopStatsView.swift; sourceTree = "<group>"; };
+		DD4C57A72D73ADEA001BFF2C /* RestartLiveActivityIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestartLiveActivityIntent.swift; sourceTree = "<group>"; };
+		DD4C57A92D73B3D9001BFF2C /* RestartLiveActivityIntentRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestartLiveActivityIntentRequest.swift; sourceTree = "<group>"; };
 		DD4FFF322D458EE600B6CFF9 /* GarminWatchState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarminWatchState.swift; sourceTree = "<group>"; };
 		DD5DC9F02CF3D96E00AB8703 /* AdjustmentsStateModel+Overrides.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentsStateModel+Overrides.swift"; sourceTree = "<group>"; };
 		DD5DC9F22CF3D9D600AB8703 /* AdjustmentsStateModel+TempTargets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentsStateModel+TempTargets.swift"; sourceTree = "<group>"; };
@@ -2301,10 +2327,13 @@
 		38FCF3EE25E9028E0078B0D1 /* TrioTests */ = {
 			isa = PBXGroup;
 			children = (
+				BD8FC05C2D6618BE00B95AED /* BolusCalculatorTests */,
+				BD8FC0552D66187700B95AED /* CoreDataTests */,
 				38FCF3F125E9028E0078B0D1 /* Info.plist */,
 				38FCF3F825E902C20078B0D1 /* FileStorageTests.swift */,
 				CE1F6DD82BADF4620064EB8D /* PluginManagerTests.swift */,
 				CEE9A65D2BBC9F6500EB5194 /* CalibrationsTests.swift */,
+				BD8FC0532D66186000B95AED /* TestError.swift */,
 			);
 			path = TrioTests;
 			sourceTree = "<group>";
@@ -2439,7 +2468,7 @@
 		6B1A8D2C2B156EC100E76752 /* LiveActivity */ = {
 			isa = PBXGroup;
 			children = (
-				6B1A8D2D2B156EEF00E76752 /* LiveActivityBridge.swift */,
+				6B1A8D2D2B156EEF00E76752 /* LiveActivityManager.swift */,
 				6BCF84DC2B16843A003AD46E /* LiveActitiyAttributes.swift */,
 				BDF34F922C10D0E100D51995 /* LiveActivityAttributes+Helper.swift */,
 				BDF34F882C10C65E00D51995 /* Data */,
@@ -2577,6 +2606,28 @@
 			path = BolusCalculator;
 			sourceTree = "<group>";
 		};
+		BD8FC0552D66187700B95AED /* CoreDataTests */ = {
+			isa = PBXGroup;
+			children = (
+				BD8FC0562D66188700B95AED /* PumpHistoryStorageTests.swift */,
+				BD8FC0582D66189700B95AED /* TestAssembly.swift */,
+				BD8FC05A2D6618AF00B95AED /* DeterminationStorageTests.swift */,
+				BD8FC05F2D6619DB00B95AED /* CarbsStorageTests.swift */,
+				BD8FC0612D6619E600B95AED /* OverrideStorageTests.swift */,
+				BD8FC0632D6619EF00B95AED /* TempTargetStorageTests.swift */,
+				BD8FC0652D661A0000B95AED /* GlucoseStorageTests.swift */,
+			);
+			path = CoreDataTests;
+			sourceTree = "<group>";
+		};
+		BD8FC05C2D6618BE00B95AED /* BolusCalculatorTests */ = {
+			isa = PBXGroup;
+			children = (
+				BD8FC05D2D6618CE00B95AED /* BolusCalculatorTests.swift */,
+			);
+			path = BolusCalculatorTests;
+			sourceTree = "<group>";
+		};
 		BDA25F1A2D26BCE800035F34 /* Views */ = {
 			isa = PBXGroup;
 			children = (
@@ -2681,6 +2732,7 @@
 		CE7CA3422A064973004BE681 /* Shortcuts */ = {
 			isa = PBXGroup;
 			children = (
+				DD4C57A42D73ADDA001BFF2C /* LiveActivity */,
 				118DF7692C5ECBC60067FEB7 /* Override */,
 				110AEDE22C5193D100615CC9 /* Bolus */,
 				CE1856F32ADC4835007E39C7 /* Carbs */,
@@ -2949,6 +3001,15 @@
 			path = Helper;
 			sourceTree = "<group>";
 		};
+		DD4C57A42D73ADDA001BFF2C /* LiveActivity */ = {
+			isa = PBXGroup;
+			children = (
+				DD4C57A92D73B3D9001BFF2C /* RestartLiveActivityIntentRequest.swift */,
+				DD4C57A72D73ADEA001BFF2C /* RestartLiveActivityIntent.swift */,
+			);
+			path = LiveActivity;
+			sourceTree = "<group>";
+		};
 		DD5DC9EF2CF3D95400AB8703 /* AdjustmentsStateModel+Extensions */ = {
 			isa = PBXGroup;
 			children = (
@@ -3619,6 +3680,7 @@
 				38DF1786276A73D400B3528F /* TagCloudView.swift in Sources */,
 				38B4F3CD25E5031100E76A18 /* Broadcaster.swift in Sources */,
 				383420D925FFEB3F002D46C1 /* Popup.swift in Sources */,
+				DD4C57AA2D73B3E2001BFF2C /* RestartLiveActivityIntentRequest.swift in Sources */,
 				DD1745402C55BFC100211FAC /* AlgorithmAdvancedSettingsRootView.swift in Sources */,
 				58645BA52CA2D347008AFCE7 /* ForecastSetup.swift in Sources */,
 				110AEDEE2C51A0AE00615CC9 /* ShortcutsConfigStateModel.swift in Sources */,
@@ -3809,7 +3871,7 @@
 				DD940BAC2CA75889000830A5 /* DynamicGlucoseColor.swift in Sources */,
 				3883581C25EE79BB00E024B2 /* TextFieldWithToolBar.swift in Sources */,
 				58D08B302C8DEA7500AA37D3 /* ForecastView.swift in Sources */,
-				6B1A8D2E2B156EEF00E76752 /* LiveActivityBridge.swift in Sources */,
+				6B1A8D2E2B156EEF00E76752 /* LiveActivityManager.swift in Sources */,
 				581516A92BCEEDF800BF67D7 /* NSPredicates.swift in Sources */,
 				DD4FFF332D458EE600B6CFF9 /* GarminWatchState.swift in Sources */,
 				DD6B7CB22C7B6F0800B75029 /* Rounding.swift in Sources */,
@@ -3969,6 +4031,7 @@
 				389ECE052601144100D86C4F /* ConcurrentMap.swift in Sources */,
 				110AEDEC2C51A0AE00615CC9 /* ShortcutsConfigDataFlow.swift in Sources */,
 				CE7CA3562A064973004BE681 /* StateIntentRequest.swift in Sources */,
+				DD4C57A82D73ADEA001BFF2C /* RestartLiveActivityIntent.swift in Sources */,
 				19E1F7EA29D082ED005C8D20 /* IconConfigProvider.swift in Sources */,
 				DD09D4822C5986F6003FEA5D /* CalendarEventSettingsRootView.swift in Sources */,
 				CE48C86428CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift in Sources */,
@@ -4108,9 +4171,18 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				BD8FC0572D66188700B95AED /* PumpHistoryStorageTests.swift in Sources */,
+				BD8FC0642D6619EF00B95AED /* TempTargetStorageTests.swift in Sources */,
+				BD8FC0542D66186000B95AED /* TestError.swift in Sources */,
 				CEE9A65E2BBC9F6500EB5194 /* CalibrationsTests.swift in Sources */,
+				BD8FC0622D6619E600B95AED /* OverrideStorageTests.swift in Sources */,
+				BD8FC0592D66189700B95AED /* TestAssembly.swift in Sources */,
+				BD8FC0662D661A0000B95AED /* GlucoseStorageTests.swift in Sources */,
+				BD8FC05B2D6618AF00B95AED /* DeterminationStorageTests.swift in Sources */,
 				CE1F6DD92BADF4620064EB8D /* PluginManagerTests.swift in Sources */,
 				38FCF3F925E902C20078B0D1 /* FileStorageTests.swift in Sources */,
+				BD8FC0602D6619DB00B95AED /* CarbsStorageTests.swift in Sources */,
+				BD8FC05E2D6618CE00B95AED /* BolusCalculatorTests.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};

+ 54 - 0
Trio.xcodeproj/xcshareddata/xcschemes/Trio Tests.xcscheme

@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1620"
+   version = "1.7">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES"
+      buildArchitectures = "Automatic">
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      shouldAutocreateTestPlan = "YES">
+      <Testables>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "38FCF3EC25E9028E0078B0D1"
+               BuildableName = "TrioTests.xctest"
+               BlueprintName = "TrioTests"
+               ReferencedContainer = "container:Trio.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>

+ 2 - 3
Trio/Resources/Info.plist

@@ -7,7 +7,6 @@
 	<key>BGTaskSchedulerPermittedIdentifiers</key>
 	<array>
 		<string>$(PRODUCT_BUNDLE_IDENTIFIER).background-task.critical-event-log</string>
-		<string>com.trio.cleanup</string>
 	</array>
 	<key>CBBundleDisplayName</key>
 	<string>$(APP_DISPLAY_NAME)</string>
@@ -76,10 +75,10 @@
 	<string>To create events with BG reading values, so that they can be viewed on Apple Watch and CarPlay</string>
 	<key>NSCalendarsUsageDescription</key>
 	<string>Calendar is used to create a new glucose events.</string>
-	<key>NSFaceIDUsageDescription</key>
-	<string>For authorized acces to bolus</string>
 	<key>NSContactsUsageDescription</key>
 	<string>Contact is used to create a Apple Watch complication</string>
+	<key>NSFaceIDUsageDescription</key>
+	<string>For authorized acces to bolus</string>
 	<key>NSHealthShareUsageDescription</key>
 	<string>Health App is used to store blood glucose, carbs and insulin</string>
 	<key>NSHealthUpdateUsageDescription</key>

+ 24 - 24
Trio/Sources/APS/Storage/CarbsStorage.swift

@@ -13,7 +13,6 @@ protocol CarbsStorage {
     func storeCarbs(_ carbs: [CarbsEntry], areFetchedFromRemote: Bool) async throws
     func deleteCarbsEntryStored(_ treatmentObjectID: NSManagedObjectID) async
     func syncDate() -> Date
-    func recent() -> [CarbsEntry]
     func getCarbsNotYetUploadedToNightscout() async throws -> [NightscoutTreatment]
     func getFPUsNotYetUploadedToNightscout() async throws -> [NightscoutTreatment]
     func getCarbsNotYetUploadedToHealth() async throws -> [CarbsEntry]
@@ -26,15 +25,16 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var settings: SettingsManager!
 
-    let coredataContext = CoreDataStack.shared.newTaskContext()
-
     private let updateSubject = PassthroughSubject<Void, Never>()
 
     var updatePublisher: AnyPublisher<Void, Never> {
         updateSubject.eraseToAnyPublisher()
     }
 
-    init(resolver: Resolver) {
+    private let context: NSManagedObjectContext
+
+    init(resolver: Resolver, context: NSManagedObjectContext? = nil) {
+        self.context = context ?? CoreDataStack.shared.newTaskContext()
         injectServices(resolver)
     }
 
@@ -75,7 +75,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         // Fetch only the date property from Core Data
         guard let existing24hCarbEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
-            onContext: coredataContext,
+            onContext: context,
             predicate: NSPredicate.predicateForOneDayAgo,
             key: "date",
             ascending: false,
@@ -217,8 +217,8 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
     private func saveCarbsToCoreData(entries: [CarbsEntry], areFetchedFromRemote: Bool) async {
         guard let entry = entries.last else { return }
 
-        await coredataContext.perform {
-            let newItem = CarbEntryStored(context: self.coredataContext)
+        await context.perform {
+            let newItem = CarbEntryStored(context: self.context)
             newItem.date = entry.actualDate ?? entry.createdAt
             newItem.carbs = Double(truncating: NSDecimalNumber(decimal: entry.carbs))
             newItem.fat = Double(truncating: NSDecimalNumber(decimal: entry.fat ?? 0))
@@ -235,8 +235,8 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             }
 
             do {
-                guard self.coredataContext.hasChanges else { return }
-                try self.coredataContext.save()
+                guard self.context.hasChanges else { return }
+                try self.context.save()
             } catch {
                 print(error.localizedDescription)
             }
@@ -264,9 +264,9 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             // do NOT set Health and Tidepool flags to ensure they will NOT be uploaded
             return false // return false to continue
         }
-        await coredataContext.perform {
+        await context.perform {
             do {
-                try self.coredataContext.execute(batchInsert)
+                try self.context.execute(batchInsert)
                 debugPrint("Carbs Storage: \(DebuggingIdentifiers.succeeded) saved fpus to core data")
 
                 // Notify subscriber in Home State Model to update the FPU Array
@@ -281,12 +281,12 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         Date().addingTimeInterval(-1.days.timeInterval)
     }
 
-    func recent() -> [CarbsEntry] {
-        storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self)?.reversed() ?? []
-    }
-
     func deleteCarbsEntryStored(_ treatmentObjectID: NSManagedObjectID) async {
-        let taskContext = CoreDataStack.shared.newTaskContext()
+        // Use injected context if available, otherwise create new task context
+        let taskContext = context != CoreDataStack.shared.newTaskContext()
+            ? context
+            : CoreDataStack.shared.newTaskContext()
+
         taskContext.name = "deleteContext"
         taskContext.transactionAuthor = "deleteCarbs"
 
@@ -342,13 +342,13 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
     func getCarbsNotYetUploadedToNightscout() async throws -> [NightscoutTreatment] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
-            onContext: coredataContext,
+            onContext: context,
             predicate: NSPredicate.carbsNotYetUploadedToNightscout,
             key: "date",
             ascending: false
         )
 
-        return try await coredataContext.perform {
+        return try await context.perform {
             guard let carbEntries = results as? [CarbEntryStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -381,13 +381,13 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
     func getFPUsNotYetUploadedToNightscout() async throws -> [NightscoutTreatment] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
-            onContext: coredataContext,
+            onContext: context,
             predicate: NSPredicate.fpusNotYetUploadedToNightscout,
             key: "date",
             ascending: false
         )
 
-        return try await coredataContext.perform {
+        return try await context.perform {
             guard let fpuEntries = results as? [CarbEntryStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -420,13 +420,13 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
     func getCarbsNotYetUploadedToHealth() async throws -> [CarbsEntry] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
-            onContext: coredataContext,
+            onContext: context,
             predicate: NSPredicate.carbsNotYetUploadedToHealth,
             key: "date",
             ascending: false
         )
 
-        return try await coredataContext.perform {
+        return try await context.perform {
             guard let carbEntries = results as? [CarbEntryStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -451,13 +451,13 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
     func getCarbsNotYetUploadedToTidepool() async throws -> [CarbsEntry] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
-            onContext: coredataContext,
+            onContext: context,
             predicate: NSPredicate.carbsNotYetUploadedToTidepool,
             key: "date",
             ascending: false
         )
 
-        return try await coredataContext.perform {
+        return try await context.perform {
             guard let carbEntries = results as? [CarbEntryStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }

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

@@ -18,9 +18,10 @@ protocol DeterminationStorage {
 
 final class BaseDeterminationStorage: DeterminationStorage, Injectable {
     private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
-    private let context = CoreDataStack.shared.newTaskContext()
+    private let context: NSManagedObjectContext
 
-    init(resolver: Resolver) {
+    init(resolver: Resolver, context: NSManagedObjectContext? = nil) {
+        self.context = context ?? CoreDataStack.shared.newTaskContext()
         injectServices(resolver)
     }
 

+ 34 - 30
Trio/Sources/APS/Storage/GlucoseStorage.swift

@@ -33,8 +33,6 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var settingsManager: SettingsManager!
 
-    let coredataContext = CoreDataStack.shared.newTaskContext()
-
     private let updateSubject = PassthroughSubject<Void, Never>()
 
     var updatePublisher: AnyPublisher<Void, Never> {
@@ -45,7 +43,10 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
         static let filterTime: TimeInterval = 3.5 * 60
     }
 
-    init(resolver: Resolver) {
+    private let context: NSManagedObjectContext
+
+    init(resolver: Resolver, context: NSManagedObjectContext? = nil) {
+        self.context = context ?? CoreDataStack.shared.newTaskContext()
         injectServices(resolver)
     }
 
@@ -61,7 +62,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     }
 
     func storeGlucose(_ glucose: [BloodGlucose]) async throws {
-        try await coredataContext.perform {
+        try await context.perform {
             // Get new glucose values that don't exist yet
             let newGlucose = self.filterNewGlucoseValues(glucose)
             guard !newGlucose.isEmpty else { return }
@@ -93,7 +94,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
         var existingDates = Set<Date>()
         do {
-            let results = try coredataContext.fetch(fetchRequest) as? [NSDictionary]
+            let results = try context.fetch(fetchRequest) as? [NSDictionary]
             existingDates = Set(results?.compactMap({ $0["date"] as? Date }) ?? [])
         } catch {
             debugPrint("Failed to fetch existing glucose dates: \(error)")
@@ -112,12 +113,12 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
     private func storeGlucoseRegular(_ glucose: [BloodGlucose]) throws {
         for entry in glucose {
-            let glucoseEntry = GlucoseStored(context: coredataContext)
+            let glucoseEntry = GlucoseStored(context: context)
             configureGlucoseEntry(glucoseEntry, with: entry)
         }
 
-        guard coredataContext.hasChanges else { return }
-        try coredataContext.save()
+        guard context.hasChanges else { return }
+        try context.save()
     }
 
     private func storeGlucoseBatch(_ glucose: [BloodGlucose]) throws {
@@ -135,7 +136,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                 return false
             }
         )
-        try coredataContext.execute(batchInsert)
+        try context.execute(batchInsert)
         // Only send update for batch insert since regular save triggers CoreData notifications
         updateSubject.send()
     }
@@ -218,8 +219,8 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     }
 
     func addManualGlucose(glucose: Int) {
-        coredataContext.perform {
-            let newItem = GlucoseStored(context: self.coredataContext)
+        context.perform {
+            let newItem = GlucoseStored(context: self.context)
             newItem.id = UUID()
             newItem.date = Date()
             newItem.glucose = Int16(glucose)
@@ -229,8 +230,8 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             newItem.isUploadedToTidepool = false
 
             do {
-                guard self.coredataContext.hasChanges else { return }
-                try self.coredataContext.save()
+                guard self.context.hasChanges else { return }
+                try self.context.save()
 
                 // Glucose subscribers already listen to the update publisher, so call here to update glucose-related data.
                 self.updateSubject.send()
@@ -281,9 +282,9 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
         fr.fetchLimit = 1
 
         var date: Date?
-        coredataContext.performAndWait {
+        context.performAndWait {
             do {
-                let results = try self.coredataContext.fetch(fr)
+                let results = try self.context.fetch(fr)
                 date = results.first?.date
             } catch let error as NSError {
                 print("Fetch error: \(DebuggingIdentifiers.failed) \(error.localizedDescription), \(error.userInfo)")
@@ -317,7 +318,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
         let predicate = NSPredicate.predicateFor20MinAgo
         return (try CoreDataStack.shared.fetchEntities(
             ofType: GlucoseStored.self,
-            onContext: coredataContext,
+            onContext: context,
             predicate: predicate,
             key: "date",
             ascending: false,
@@ -330,13 +331,13 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     func getGlucoseNotYetUploadedToNightscout() async throws -> [BloodGlucose] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
-            onContext: coredataContext,
+            onContext: context,
             predicate: NSPredicate.glucoseNotYetUploadedToNightscout,
             key: "date",
             ascending: false
         )
 
-        return try await coredataContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [GlucoseStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -363,13 +364,13 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     func getManualGlucoseNotYetUploadedToNightscout() async throws -> [NightscoutTreatment] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
-            onContext: coredataContext,
+            onContext: context,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToNightscout,
             key: "date",
             ascending: false
         )
 
-        return try await coredataContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [GlucoseStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -419,13 +420,13 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     func getGlucoseNotYetUploadedToHealth() async throws -> [BloodGlucose] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
-            onContext: coredataContext,
+            onContext: context,
             predicate: NSPredicate.glucoseNotYetUploadedToHealth,
             key: "date",
             ascending: false
         )
 
-        return try await coredataContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [GlucoseStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -451,13 +452,13 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     func getManualGlucoseNotYetUploadedToHealth() async throws -> [BloodGlucose] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
-            onContext: coredataContext,
+            onContext: context,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToHealth,
             key: "date",
             ascending: false
         )
 
-        return try await coredataContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [GlucoseStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -483,13 +484,13 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     func getGlucoseNotYetUploadedToTidepool() async throws -> [StoredGlucoseSample] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
-            onContext: coredataContext,
+            onContext: context,
             predicate: NSPredicate.glucoseNotYetUploadedToTidepool,
             key: "date",
             ascending: false
         )
 
-        return try await coredataContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [GlucoseStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -516,13 +517,13 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     func getManualGlucoseNotYetUploadedToTidepool() async throws -> [StoredGlucoseSample] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
-            onContext: coredataContext,
+            onContext: context,
             predicate: NSPredicate.manualGlucoseNotYetUploadedToTidepool,
             key: "date",
             ascending: false
         )
 
-        return try await coredataContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [GlucoseStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -544,7 +545,10 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
     }
 
     func deleteGlucose(_ treatmentObjectID: NSManagedObjectID) async {
-        let taskContext = CoreDataStack.shared.newTaskContext()
+        // Use injected context if available, otherwise create new task context
+        let taskContext = context != CoreDataStack.shared.newTaskContext()
+            ? context
+            : CoreDataStack.shared.newTaskContext()
         taskContext.name = "deleteContext"
         taskContext.transactionAuthor = "deleteGlucose"
 
@@ -572,7 +576,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
     var alarm: GlucoseAlarm? {
         /// glucose can not be older than 20 minutes due to the predicate in the fetch request
-        coredataContext.performAndWait {
+        context.performAndWait {
             do {
                 guard let glucose = try fetchLatestGlucose() else { return nil }
 

+ 49 - 23
Trio/Sources/APS/Storage/OverrideStorage.swift

@@ -20,9 +20,10 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
     @Injected() private var settingsManager: SettingsManager!
 
     private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
-    private let backgroundContext = CoreDataStack.shared.newTaskContext()
+    private let context: NSManagedObjectContext
 
-    init(resolver: Resolver) {
+    init(resolver: Resolver, context: NSManagedObjectContext? = nil) {
+        self.context = context ?? CoreDataStack.shared.newTaskContext()
         injectServices(resolver)
     }
 
@@ -37,7 +38,7 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
     func fetchLastCreatedOverride() async throws -> [NSManagedObjectID] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: NSPredicate(
                 format: "date >= %@",
                 Date.oneDayAgo as NSDate
@@ -47,7 +48,7 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
             fetchLimit: 1
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [OverrideStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -59,14 +60,14 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
     func loadLatestOverrideConfigurations(fetchLimit: Int) async throws -> [NSManagedObjectID] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: NSPredicate.lastActiveOverride,
             key: "orderPosition",
             ascending: true,
             fetchLimit: fetchLimit
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [OverrideStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -79,13 +80,13 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
     func fetchForOverridePresets() async throws -> [NSManagedObjectID] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: NSPredicate.allOverridePresets,
             key: "orderPosition",
             ascending: true
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [OverrideStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -108,8 +109,8 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
             presetCount = presets.count
         }
 
-        try await backgroundContext.perform {
-            let newOverride = OverrideStored(context: self.backgroundContext)
+        try await context.perform {
+            let newOverride = OverrideStored(context: self.context)
 
             // override key meta data
             if !override.name.isEmpty {
@@ -157,8 +158,8 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
                 newOverride.smbIsScheduledOff = false
             }
 
-            guard self.backgroundContext.hasChanges else { return }
-            try self.backgroundContext.save()
+            guard self.context.hasChanges else { return }
+            try self.context.save()
         }
     }
 
@@ -200,22 +201,47 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
         return newOverride.objectID
     }
 
-    /// marked as MainActor to be able to publish changes from the background
     /// - Parameter: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
-    @MainActor func deleteOverridePreset(_ objectID: NSManagedObjectID) async {
-        await CoreDataStack.shared.deleteObject(identifiedBy: objectID)
+    func deleteOverridePreset(_ objectID: NSManagedObjectID) async {
+        // Use injected context if available, otherwise create new task context
+        let taskContext = context != CoreDataStack.shared.newTaskContext()
+            ? context
+            : CoreDataStack.shared.newTaskContext()
+
+        taskContext.name = "deleteContext"
+        taskContext.transactionAuthor = "deleteOverride"
+
+        await taskContext.perform {
+            do {
+                guard let override = try taskContext.existingObject(with: objectID) as? OverrideStored else {
+                    debugPrint("Override for batch delete not found. \(DebuggingIdentifiers.failed)")
+                    return
+                }
+
+                taskContext.delete(override)
+
+                guard taskContext.hasChanges else { return }
+                try taskContext.save()
+
+                debugPrint(
+                    "OverrideStorage: \(#function) \(DebuggingIdentifiers.succeeded) deleted override from core data"
+                )
+            } catch {
+                debugPrint("\(DebuggingIdentifiers.failed) Error deleting override: \(error.localizedDescription)")
+            }
+        }
     }
 
     func getOverridesNotYetUploadedToNightscout() async throws -> [NightscoutExercise] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: NSPredicate.lastActiveAdjustmentNotYetUploadedToNightscout,
             key: "date",
             ascending: false
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let fetchedOverrides = results as? [OverrideStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -237,7 +263,7 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
     func getOverrideRunsNotYetUploadedToNightscout() async throws -> [NightscoutExercise] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideRunStored.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: NSPredicate(
                 format: "startDate >= %@ AND isUploadedToNS == %@",
                 Date.oneDayAgo as NSDate,
@@ -247,7 +273,7 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
             ascending: false
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let fetchedOverrideRuns = results as? [OverrideRunStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -270,13 +296,13 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
     func getPresetOverridesForNightscout() async throws -> [NightscoutPresetOverride] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: NSPredicate.allOverridePresets,
             key: "orderPosition",
             ascending: true
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [OverrideStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -299,14 +325,14 @@ final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
     func fetchLatestActiveOverride() async throws -> NSManagedObjectID? {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: NSPredicate.lastActiveOverride,
             key: "date",
             ascending: false,
             fetchLimit: 1
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [OverrideStored],
                   let latestOverride = fetchedResults.first
             else {

+ 4 - 2
Trio/Sources/APS/Storage/PumpHistoryStorage.swift

@@ -26,13 +26,15 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
     @Injected() private var settings: SettingsManager!
 
     private let updateSubject = PassthroughSubject<Void, Never>()
-    private let context = CoreDataStack.shared.newTaskContext()
 
     var updatePublisher: AnyPublisher<Void, Never> {
         updateSubject.eraseToAnyPublisher()
     }
 
-    init(resolver: Resolver) {
+    private let context: NSManagedObjectContext
+
+    init(resolver: Resolver, context: NSManagedObjectContext? = nil) {
+        self.context = context ?? CoreDataStack.shared.newTaskContext()
         injectServices(resolver)
     }
 

+ 43 - 22
Trio/Sources/APS/Storage/TempTargetsStorage.swift

@@ -31,24 +31,26 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var settingsManager: SettingsManager!
 
-    private let backgroundContext = CoreDataStack.shared.newTaskContext()
     private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
 
-    init(resolver: Resolver) {
+    private let context: NSManagedObjectContext
+
+    init(resolver: Resolver, context: NSManagedObjectContext? = nil) {
+        self.context = context ?? CoreDataStack.shared.newTaskContext()
         injectServices(resolver)
     }
 
     func loadLatestTempTargetConfigurations(fetchLimit: Int) async throws -> [NSManagedObjectID] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetStored.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: NSPredicate.lastActiveTempTarget,
             key: "orderPosition",
             ascending: true,
             fetchLimit: fetchLimit
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [TempTargetStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -61,13 +63,13 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
     func fetchForTempTargetPresets() async throws -> [NSManagedObjectID] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetStored.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: NSPredicate.allTempTargetPresets,
             key: "orderPosition",
             ascending: true
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [TempTargetStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -81,13 +83,13 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
 
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetStored.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: scheduledTempTargets,
             key: "date",
             ascending: false
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [TempTargetStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -101,14 +103,14 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
 
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetStored.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: predicate,
             key: "date",
             ascending: false,
             fetchLimit: 1
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let fetchedResults = results as? [TempTargetStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -124,8 +126,8 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
             presetCount = presets.count
         }
 
-        try await backgroundContext.perform {
-            let newTempTarget = TempTargetStored(context: self.backgroundContext)
+        try await context.perform {
+            let newTempTarget = TempTargetStored(context: self.context)
             newTempTarget.date = tempTarget.createdAt
             newTempTarget.id = UUID()
             newTempTarget.enabled = tempTarget.enabled ?? false
@@ -149,8 +151,8 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
             }
 
             do {
-                guard self.backgroundContext.hasChanges else { return }
-                try self.backgroundContext.save()
+                guard self.context.hasChanges else { return }
+                try self.context.save()
             } catch let error as NSError {
                 debug(.default, "\(DebuggingIdentifiers.failed) Failed to save new temp target with error: \(error.userInfo)")
                 throw error
@@ -180,13 +182,13 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
     }
 
     func existsTempTarget(with date: Date) async -> Bool {
-        await backgroundContext.perform {
+        await context.perform {
             // Fetch all Temp Targets with the given date
             let fetchRequest: NSFetchRequest<TempTargetStored> = TempTargetStored.fetchRequest()
             fetchRequest.predicate = NSPredicate(format: "date == %@", date as NSDate)
 
             do {
-                let results = try self.backgroundContext.fetch(fetchRequest)
+                let results = try self.context.fetch(fetchRequest)
                 return !results.isEmpty
             } catch let error as NSError {
                 debugPrint("\(DebuggingIdentifiers.failed) Failed to check for existing Temp Target: \(error)")
@@ -223,8 +225,27 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
         return newTempTarget.objectID
     }
 
-    @MainActor func deleteTempTargetPreset(_ objectID: NSManagedObjectID) async {
-        await CoreDataStack.shared.deleteObject(identifiedBy: objectID)
+    func deleteTempTargetPreset(_ objectID: NSManagedObjectID) async {
+        let taskContext = context != CoreDataStack.shared.newTaskContext()
+            ? context
+            : CoreDataStack.shared.newTaskContext()
+
+        await taskContext.perform {
+            do {
+                let result = try taskContext.existingObject(with: objectID) as? TempTargetStored
+                guard let tempTarget = result else {
+                    debug(.default, "\(DebuggingIdentifiers.failed) Temp Target for batch delete not found.")
+                    return
+                }
+
+                taskContext.delete(tempTarget)
+
+                guard taskContext.hasChanges else { return }
+                try taskContext.save()
+            } catch {
+                debug(.default, "\(DebuggingIdentifiers.failed) Failed to delete Temp Target: \(error)")
+            }
+        }
     }
 
     func syncDate() -> Date {
@@ -252,13 +273,13 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
     func getTempTargetsNotYetUploadedToNightscout() async throws -> [NightscoutTreatment] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetStored.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: NSPredicate.lastActiveAdjustmentNotYetUploadedToNightscout,
             key: "date",
             ascending: false
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let fetchedTempTargets = results as? [TempTargetStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
@@ -289,7 +310,7 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
     func getTempTargetRunsNotYetUploadedToNightscout() async throws -> [NightscoutTreatment] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetRunStored.self,
-            onContext: backgroundContext,
+            onContext: context,
             predicate: NSPredicate(
                 format: "startDate >= %@ AND isUploadedToNS == %@",
                 Date.oneDayAgo as NSDate,
@@ -299,7 +320,7 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
             ascending: false
         )
 
-        return try await backgroundContext.perform {
+        return try await context.perform {
             guard let fetchedTempTargetRuns = results as? [TempTargetRunStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }

+ 13 - 13
Trio/Sources/Application/TrioApp.swift

@@ -58,7 +58,7 @@ import Swinject
         _ = resolver.resolve(PluginManager.self)!
         _ = resolver.resolve(AlertPermissionsChecker.self)!
         if #available(iOS 16.2, *) {
-            _ = resolver.resolve(LiveActivityBridge.self)!
+            _ = resolver.resolve(LiveActivityManager.self)!
         }
     }
 
@@ -117,12 +117,11 @@ import Swinject
                 {
                     AppVersionChecker.shared.checkAndNotifyVersionStatus(in: rootVC)
                 }
+
+                // Check if we need to perform a database cleaning
+                performCleanupIfNecessary()
             }
         }
-        .backgroundTask(.appRefresh("com.trio.cleanup")) {
-            await scheduleDatabaseCleaning()
-            await cleanupOldData()
-        }
     }
 
     func configureTabBarAppearance() {
@@ -146,14 +145,12 @@ import Swinject
         }
     }
 
-    func scheduleDatabaseCleaning() {
-        let request = BGAppRefreshTaskRequest(identifier: "com.trio.cleanup")
-        request.earliestBeginDate = .now.addingTimeInterval(7 * 24 * 60 * 60) // 7 days
-        do {
-            try BGTaskScheduler.shared.submit(request)
-            debug(.coreData, "Task for cleaning database scheduled successfully")
-        } catch {
-            debug(.coreData, "Failed to schedule tasks for cleaning database: \(error.localizedDescription)")
+    private func performCleanupIfNecessary() {
+        if let lastCleanupDate = UserDefaults.standard.object(forKey: "lastCleanupDate") as? Date {
+            let sevenDaysAgo = Date().addingTimeInterval(-7 * 24 * 60 * 60)
+            if lastCleanupDate < sevenDaysAgo {
+                cleanupOldData()
+            }
         }
     }
 
@@ -164,6 +161,9 @@ import Swinject
 
             await cleanupTokens
             try await purgeData
+
+            // Update the last cleanup date
+            UserDefaults.standard.set(Date(), forKey: "lastCleanupDate")
         }
     }
 

+ 2 - 2
Trio/Sources/Assemblies/ServiceAssembly.swift

@@ -24,8 +24,8 @@ final class ServiceAssembly: Assembly {
         container.register(ContactImageManager.self) { r in BaseContactImageManager(resolver: r) }
         container.register(AlertPermissionsChecker.self) { r in AlertPermissionsChecker(resolver: r) }
         if #available(iOS 16.2, *) {
-            container.register(LiveActivityBridge.self) { r in
-                LiveActivityBridge(resolver: r)
+            container.register(LiveActivityManager.self) { r in
+                LiveActivityManager(resolver: r)
             }
         }
     }

+ 174 - 5
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -1224,6 +1224,9 @@
         }
       }
     },
+    " %@/U" : {
+
+    },
     " + " : {
       "localizations" : {
         "ar" : {
@@ -2203,6 +2206,9 @@
         }
       }
     },
+    " needed as min. Glucose Target)!" : {
+
+    },
     " of " : {
       "comment" : "Bolus string partial message: 'x U of y U' in home view"
     },
@@ -5365,7 +5371,7 @@
       }
     },
     "%" : {
-      "extractionState" : "stale",
+      "comment" : "Percentage symbol",
       "localizations" : {
         "ar" : {
           "stringUnit" : {
@@ -6132,6 +6138,7 @@
       }
     },
     "%@ g/U" : {
+      "extractionState" : "stale",
       "localizations" : {
         "ar" : {
           "stringUnit" : {
@@ -26264,6 +26271,9 @@
         }
       }
     },
+    "All settings are at default values." : {
+
+    },
     "Allow Bolusing with Shortcuts" : {
       "localizations" : {
         "ar" : {
@@ -26479,6 +26489,35 @@
         }
       }
     },
+    "Allow SMB for 6 hrs after a carb entry." : {
+
+    },
+    "Allow SMB when a manual Temporary Target is set greater than %@ %@." : {
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "new",
+            "value" : "Allow SMB when a manual Temporary Target is set greater than %1$@ %2$@."
+          }
+        }
+      }
+    },
+    "Allow SMB when a manual Temporary Target is set under %@ %@." : {
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "new",
+            "value" : "Allow SMB when a manual Temporary Target is set under %1$@ %2$@."
+          }
+        }
+      }
+    },
+    "Allow SMB when carbs are on board." : {
+
+    },
+    "Allow SMB when glucose is above the High BG Target value." : {
+
+    },
     "Allow SMB With High Temporary Target" : {
       "localizations" : {
         "ar" : {
@@ -26692,6 +26731,12 @@
         }
       }
     },
+    "Allow SMBs at all times except when a high Temp Target is set." : {
+
+    },
+    "Allow the creation of saved, preset meals." : {
+
+    },
     "Allow to add carbs in Trio." : {
       "localizations" : {
         "ar" : {
@@ -28029,6 +28074,9 @@
     "Alter the rate of dynamic sensitivity adjustments for Sigmoid." : {
 
     },
+    "Always" : {
+      "extractionState" : "manual"
+    },
     "Always Color Glucose Value (green, yellow etc)" : {
       "comment" : "UI/UX option",
       "extractionState" : "manual",
@@ -47871,6 +47919,7 @@
 
     },
     "CR" : {
+      "comment" : "Option for Carb Ratio",
       "localizations" : {
         "ar" : {
           "stringUnit" : {
@@ -49523,6 +49572,9 @@
         }
       }
     },
+    "Dark" : {
+
+    },
     "Dark Mode" : {
       "localizations" : {
         "ar" : {
@@ -50726,6 +50778,16 @@
         }
       }
     },
+    "Decrease sensitivity when glucose is below target if a manual Temp Target < %@ %@ is set." : {
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "new",
+            "value" : "Decrease sensitivity when glucose is below target if a manual Temp Target < %1$@ %2$@ is set."
+          }
+        }
+      }
+    },
     "Decreasing this setting may result in fewer FPU entries with larger carb values." : {
       "localizations" : {
         "ar" : {
@@ -51044,6 +51106,9 @@
         }
       }
     },
+    "Default impact of carb absorption over a 5 minute interval." : {
+
+    },
     "Default is 20 minutes. How often to update and save the statistics.json and to upload last array, when enabled, to Nightscout." : {
       "comment" : "Description for update interval for statistics",
       "extractionState" : "manual",
@@ -59285,6 +59350,12 @@
         }
       }
     },
+    "Disable" : {
+      "extractionState" : "manual"
+    },
+    "Disable on Schedule" : {
+      "extractionState" : "manual"
+    },
     "Disable SMBs" : {
       "localizations" : {
         "ar" : {
@@ -59611,6 +59682,9 @@
         }
       }
     },
+    "Disables SMBs if last two glucose values differ by more than this percent." : {
+
+    },
     "Disabling this setting will still allow other commands, like Temp Targets, Add Carbs, and Start/End Overrides" : {
       "localizations" : {
         "ar" : {
@@ -62900,6 +62974,9 @@
         }
       }
     },
+    "Don't Disable" : {
+      "extractionState" : "manual"
+    },
     "Done" : {
       "comment" : "Button",
       "extractionState" : "manual",
@@ -63546,6 +63623,9 @@
         }
       }
     },
+    "Dynamic" : {
+
+    },
     "Dynamic CR adjusts your carb ratio based on your Dynamic Ratio, adapting automatically to changes in insulin sensitivity." : {
       "localizations" : {
         "ar" : {
@@ -66300,6 +66380,9 @@
         }
       }
     },
+    "Enable indefinitely or set a duration." : {
+
+    },
     "Enable Live Activity" : {
       "localizations" : {
         "ar" : {
@@ -67383,6 +67466,9 @@
         }
       }
     },
+    "Enable Unannounced Meals SMB." : {
+
+    },
     "Enable uploading of CGM readings to Nightscout." : {
 
     },
@@ -79533,7 +79619,7 @@
       }
     },
     "g" : {
-      "comment" : "The short unit display string for grams\ngram of carbs",
+      "comment" : "Gram abbreviation\nThe short unit display string for grams\ngram of carbs",
       "localizations" : {
         "ar" : {
           "stringUnit" : {
@@ -87607,6 +87693,9 @@
     "How Trio Manages Contact Images" : {
 
     },
+    "hr" : {
+      "comment" : "Hours abbreviation"
+    },
     "If \"Display IOB and COB\" is also enabled, \"IOB\" and \"COB\" will be replaced with the following emojis:" : {
       "localizations" : {
         "ar" : {
@@ -91072,6 +91161,16 @@
     "Include IOB & COB in the calendar event data." : {
 
     },
+    "Increase sensitivity when glucose is above target if a manual Temp Target > %@ %@ is set." : {
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "new",
+            "value" : "Increase sensitivity when glucose is above target if a manual Temp Target > %1$@ %2$@ is set."
+          }
+        }
+      }
+    },
     "Increase the safety threshold used to suspend insulin delivery." : {
 
     },
@@ -94474,6 +94573,7 @@
 
     },
     "ISF" : {
+      "comment" : "Option for Insulin Sensitivity Factor",
       "localizations" : {
         "ar" : {
           "stringUnit" : {
@@ -94685,6 +94785,9 @@
         }
       }
     },
+    "ISF/CR" : {
+      "comment" : "Option for both ISF and CR"
+    },
     "It allows you to refer to live information at a glance and perform quick actions in your diabetes management." : {
       "localizations" : {
         "ar" : {
@@ -97088,6 +97191,9 @@
         }
       }
     },
+    "Light" : {
+
+    },
     "Light Mode" : {
       "localizations" : {
         "ar" : {
@@ -97632,6 +97738,12 @@
     "Limits temporary basal rates to this percentage of your largest basal rate." : {
       "comment" : "Mini Hint for Max Daily Safety Multiplier"
     },
+    "Limits the size of a single Super Micro Bolus (SMB) dose." : {
+
+    },
+    "Limits the size of a single Unannounced Meal (UAM) SMB dose." : {
+
+    },
     "Lines" : {
       "localizations" : {
         "ar" : {
@@ -98386,6 +98498,9 @@
     "Live Activity Expired. Open Trio to Refresh" : {
 
     },
+    "Live Activity Personalization" : {
+
+    },
     "Local glucose source" : {
       "extractionState" : "manual",
       "localizations" : {
@@ -101451,6 +101566,12 @@
         }
       }
     },
+    "Lower limit of the Autosens Ratio." : {
+
+    },
+    "Lower target glucose when Autosens Ratio is <1." : {
+
+    },
     "m" : {
       "comment" : "abbreviation for minutes",
       "localizations" : {
@@ -105912,6 +106033,9 @@
     "Maximum Meal Absorption Time" : {
 
     },
+    "Maximum units of insulin allowed to be active." : {
+
+    },
     "Meal" : {
       "comment" : "Debug option view Meal",
       "extractionState" : "manual",
@@ -107245,7 +107369,7 @@
       }
     },
     "min" : {
-      "comment" : "Minutes ago since last loop\nShort form for minutes",
+      "comment" : "Minutes abbreviation\nMinutes ago since last loop\nShort form for minutes",
       "localizations" : {
         "ar" : {
           "stringUnit" : {
@@ -108221,6 +108345,9 @@
         }
       }
     },
+    "Minimum minutes since the last SMB or manual bolus to allow an automated SMB." : {
+
+    },
     "Minimum Safety Threshold" : {
       "localizations" : {
         "ar" : {
@@ -113279,7 +113406,7 @@
 
     },
     "None" : {
-      "comment" : "No CGM selected",
+      "comment" : "No CGM selected\nOption for no selection",
       "localizations" : {
         "ar" : {
           "stringUnit" : {
@@ -116088,6 +116215,9 @@
         }
       }
     },
+    "Note..." : {
+
+    },
     "Notes" : {
       "localizations" : {
         "ar" : {
@@ -118365,6 +118495,9 @@
         }
       }
     },
+    "Only Alarm Limits" : {
+      "extractionState" : "manual"
+    },
     "Only Alarm Limits:" : {
       "localizations" : {
         "ar" : {
@@ -122243,6 +122376,9 @@
     "Percentage of bolus suggested in bolus calculator." : {
 
     },
+    "Percentage of calculated insulin required that is given as SMB." : {
+
+    },
     "Percentage of carbs still available if no absorption is detected." : {
       "comment" : "Mini Hint for Remaining Carbs Percentage"
     },
@@ -124120,6 +124256,9 @@
         }
       }
     },
+    "Presets cannot be saved with a future date!" : {
+
+    },
     "Preview Contact Image" : {
 
     },
@@ -125896,6 +126035,9 @@
         }
       }
     },
+    "Pump rewind initiates a reset in Autosens Ratio." : {
+
+    },
     "Pump Settings" : {
       "extractionState" : "manual",
       "localizations" : {
@@ -126860,6 +127002,9 @@
         }
       }
     },
+    "Raise target glucose if when Autosens Ratio is >1." : {
+
+    },
     "Random variation added to each reading to simulate real-world sensor noise." : {
 
     },
@@ -129681,6 +129826,9 @@
         }
       }
     },
+    "Restart Live Activity" : {
+
+    },
     "Result" : {
       "comment" : "For the  Bolus View pop-up",
       "localizations" : {
@@ -132641,6 +132789,9 @@
         }
       }
     },
+    "Scales down your basal rate to 50% at this value." : {
+
+    },
     "Schedule" : {
       "localizations" : {
         "ar" : {
@@ -136712,6 +136863,9 @@
     "Set a custom time for peak insulin effect." : {
       "comment" : "Mini Hint for Insulin Peak Time"
     },
+    "Set a duration!" : {
+
+    },
     "Set Display Values" : {
 
     },
@@ -145395,6 +145549,9 @@
         }
       }
     },
+    "Static" : {
+
+    },
     "Static:" : {
       "localizations" : {
         "ar" : {
@@ -148760,6 +148917,9 @@
         }
       }
     },
+    "System Default" : {
+
+    },
     "System Default:" : {
       "localizations" : {
         "ar" : {
@@ -149634,6 +149794,9 @@
         }
       }
     },
+    "Target glucose is out of range (%@)." : {
+
+    },
     "Target presets" : {
       "comment" : "Debug option view Target presets",
       "extractionState" : "manual",
@@ -168109,6 +168272,9 @@
         }
       }
     },
+    "Trio Live Activity restarted successfully." : {
+
+    },
     "Trio Not Active" : {
       "comment" : "Trio Not Active",
       "localizations" : {
@@ -169608,7 +169774,7 @@
       }
     },
     "U" : {
-      "comment" : "Insulin unit\nThe short unit display string for international units of insulin",
+      "comment" : "Insulin unit\nInsulin unit abbreviation\nThe short unit display string for international units of insulin",
       "localizations" : {
         "ar" : {
           "stringUnit" : {
@@ -172023,6 +172189,9 @@
         }
       }
     },
+    "Upper limit of the Autosens Ratio." : {
+
+    },
     "URL" : {
       "localizations" : {
         "ar" : {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 1 - 1
Trio/Sources/Modules/AlgorithmAdvancedSettings/View/AlgorithmAdvancedSettingsRootView.swift

@@ -264,7 +264,7 @@ extension AlgorithmAdvancedSettings {
                     units: state.units,
                     type: .decimal("min5mCarbimpact"),
                     label: String(localized: "Min 5m Carb Impact", comment: "Min 5m Carb Impact"),
-                    miniHint: "Default impact of carb absorption over a 5 minute interval.",
+                    miniHint: String(localized: "Default impact of carb absorption over a 5 minute interval."),
                     verboseHint:
                     VStack(alignment: .leading, spacing: 10) {
                         Text(

+ 3 - 3
Trio/Sources/Modules/AutosensSettings/View/AutosensSettingsRootView.swift

@@ -140,7 +140,7 @@ extension AutosensSettings {
                     units: state.units,
                     type: .decimal("autosensMax"),
                     label: String(localized: "Autosens Max", comment: "Autosens Max"),
-                    miniHint: "Upper limit of the Autosens Ratio.",
+                    miniHint: String(localized: "Upper limit of the Autosens Ratio."),
                     verboseHint:
                     VStack(alignment: .leading, spacing: 10) {
                         Text("Default: 120%").bold()
@@ -171,7 +171,7 @@ extension AutosensSettings {
                     units: state.units,
                     type: .decimal("autosensMin"),
                     label: String(localized: "Autosens Min", comment: "Autosens Min"),
-                    miniHint: "Lower limit of the Autosens Ratio.",
+                    miniHint: String(localized: "Lower limit of the Autosens Ratio."),
                     verboseHint:
                     VStack(alignment: .leading, spacing: 10) {
                         Text("Default: 70%").bold()
@@ -201,7 +201,7 @@ extension AutosensSettings {
                     units: state.units,
                     type: .boolean,
                     label: String(localized: "Rewind Resets Autosens", comment: "Rewind Resets Autosens"),
-                    miniHint: "Pump rewind initiates a reset in Autosens Ratio.",
+                    miniHint: String(localized: "Pump rewind initiates a reset in Autosens Ratio."),
                     verboseHint: VStack(alignment: .leading, spacing: 5) {
                         Text("Default: ON").bold()
                         Text("Medtronic Users Only").bold()

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

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

+ 1 - 1
Trio/Sources/Modules/BolusCalculatorConfig/View/BolusCalculatorConfigRootView.swift

@@ -47,7 +47,7 @@ extension BolusCalculatorConfig {
                     units: state.units,
                     type: .boolean,
                     label: String(localized: "Display Meal Presets"),
-                    miniHint: "Allow the creation of saved, preset meals.",
+                    miniHint: String(localized: "Allow the creation of saved, preset meals."),
                     verboseHint: VStack(alignment: .leading, spacing: 10) {
                         Text("Default: ON").bold()
                         Text("Enabling this feature allows you to create and save preset meals.")

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

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

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

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

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

@@ -180,7 +180,7 @@ extension DataTable {
                 } else {
                     ContentUnavailableView(
                         "No data.",
-                        systemImage: "injection.needle"
+                        systemImage: "syringe"
                     )
                 }
             }.listRowBackground(Color.chart)

+ 1 - 1
Trio/Sources/Modules/GeneralSettings/View/UnitsLimitsSettingsRootView.swift

@@ -42,7 +42,7 @@ extension UnitsLimitsSettings {
                     units: state.units,
                     type: .decimal("maxIOB"),
                     label: String(localized: "Max IOB", comment: "Max IOB"),
-                    miniHint: "Maximum units of insulin allowed to be active.",
+                    miniHint: String(localized: "Maximum units of insulin allowed to be active."),
                     verboseHint:
                     VStack(alignment: .leading, spacing: 10) {
                         Text("Default: 0 units").bold()

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

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

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

@@ -155,7 +155,7 @@ extension LiveActivitySettings {
                 }
             }
             .listSectionSpacing(sectionSpacing)
-            .onReceive(resolver.resolve(LiveActivityBridge.self)!.$systemEnabled, perform: {
+            .onReceive(resolver.resolve(LiveActivityManager.self)!.$systemEnabled, perform: {
                 self.systemLiveActivitySetting = $0
             })
             .sheet(isPresented: $shouldDisplayHint) {

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

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

+ 16 - 12
Trio/Sources/Modules/SMBSettings/View/SMBSettingsRootView.swift

@@ -32,7 +32,7 @@ extension SMBSettings {
                     units: state.units,
                     type: .boolean,
                     label: String(localized: "Enable SMB Always", comment: "Enable SMB Always"),
-                    miniHint: "Allow SMBs at all times except when a high Temp Target is set.",
+                    miniHint: String(localized: "Allow SMBs at all times except when a high Temp Target is set."),
                     verboseHint: VStack(alignment: .leading, spacing: 10) {
                         Text("Default: OFF").bold()
                         Text(
@@ -60,7 +60,7 @@ extension SMBSettings {
                         units: state.units,
                         type: .boolean,
                         label: String(localized: "Enable SMB With COB", comment: "Enable SMB With COB"),
-                        miniHint: "Allow SMB when carbs are on board.",
+                        miniHint: String(localized: "Allow SMB when carbs are on board."),
                         verboseHint:
                         VStack(alignment: .leading, spacing: 10) {
                             Text("Default: OFF").bold()
@@ -87,7 +87,9 @@ extension SMBSettings {
                         units: state.units,
                         type: .boolean,
                         label: String(localized: "Enable SMB With Temptarget", comment: "Enable SMB With Temptarget"),
-                        miniHint: "Allow SMB when a manual Temporary Target is set under \(state.units == .mgdL ? "100" : 100.formattedAsMmolL) \(state.units.rawValue).",
+                        miniHint: String(
+                            localized: "Allow SMB when a manual Temporary Target is set under \(state.units == .mgdL ? "100" : 100.formattedAsMmolL) \(state.units.rawValue)."
+                        ),
                         verboseHint:
                         VStack(alignment: .leading, spacing: 10) {
                             Text("Default: OFF").bold()
@@ -114,7 +116,7 @@ extension SMBSettings {
                         units: state.units,
                         type: .boolean,
                         label: String(localized: "Enable SMB After Carbs", comment: "Enable SMB After Carbs"),
-                        miniHint: "Allow SMB for 6 hrs after a carb entry.",
+                        miniHint: String(localized: "Allow SMB for 6 hrs after a carb entry."),
                         verboseHint:
                         VStack(alignment: .leading, spacing: 10) {
                             Text("Default: OFF").bold()
@@ -142,7 +144,7 @@ extension SMBSettings {
                         type: .conditionalDecimal("enableSMB_high_bg_target"),
                         label: String(localized: "Enable SMB With High BG", comment: "Enable SMB With High BG"),
                         conditionalLabel: "High BG Target",
-                        miniHint: "Allow SMB when glucose is above the High BG Target value.",
+                        miniHint: String(localized: "Allow SMB when glucose is above the High BG Target value."),
                         verboseHint:
                         VStack(alignment: .leading, spacing: 10) {
                             Text("Default: OFF").bold()
@@ -178,7 +180,9 @@ extension SMBSettings {
                         "Allow SMB With High Temptarget",
                         comment: "Allow SMB With High Temptarget"
                     ),
-                    miniHint: "Allow SMB when a manual Temporary Target is set greater than \(state.units == .mgdL ? "100" : 100.formattedAsMmolL) \(state.units.rawValue).",
+                    miniHint: String(
+                        localized: "Allow SMB when a manual Temporary Target is set greater than \(state.units == .mgdL ? "100" : 100.formattedAsMmolL) \(state.units.rawValue)."
+                    ),
                     verboseHint:
                     VStack(alignment: .leading, spacing: 10) {
                         Text("Default: OFF").bold()
@@ -208,7 +212,7 @@ extension SMBSettings {
                     units: state.units,
                     type: .boolean,
                     label: String(localized: "Enable UAM", comment: "Enable UAM"),
-                    miniHint: "Enable Unannounced Meals SMB.",
+                    miniHint: String(localized: "Enable Unannounced Meals SMB."),
                     verboseHint:
                     VStack(alignment: .leading, spacing: 10) {
                         Text("Default: OFF").bold()
@@ -238,7 +242,7 @@ extension SMBSettings {
                     units: state.units,
                     type: .decimal("maxSMBBasalMinutes"),
                     label: String(localized: "Max SMB Basal Minutes", comment: "Max SMB Basal Minutes"),
-                    miniHint: "Limits the size of a single Super Micro Bolus (SMB) dose.",
+                    miniHint: String(localized: "Limits the size of a single Super Micro Bolus (SMB) dose."),
                     verboseHint: VStack(spacing: 10) {
                         VStack(alignment: .leading, spacing: 10) {
                             VStack(alignment: .leading, spacing: 1) {
@@ -284,7 +288,7 @@ extension SMBSettings {
                     units: state.units,
                     type: .decimal("maxUAMSMBBasalMinutes"),
                     label: String(localized: "Max UAM Basal Minutes", comment: "Max UAM Basal Minutes"),
-                    miniHint: "Limits the size of a single Unannounced Meal (UAM) SMB dose.",
+                    miniHint: String(localized: "Limits the size of a single Unannounced Meal (UAM) SMB dose."),
                     verboseHint: VStack(spacing: 10) {
                         VStack(alignment: .leading, spacing: 10) {
                             VStack(alignment: .leading, spacing: 1) {
@@ -329,7 +333,7 @@ extension SMBSettings {
                     units: state.units,
                     type: .decimal("maxDeltaBGthreshold"),
                     label: String(localized: "Max Delta-BG Threshold SMB", comment: "Max Delta-BG Threshold"),
-                    miniHint: "Disables SMBs if last two glucose values differ by more than this percent.",
+                    miniHint: String(localized: "Disables SMBs if last two glucose values differ by more than this percent."),
                     verboseHint:
                     VStack(alignment: .leading, spacing: 10) {
                         Text("Default: 20% increase").bold()
@@ -354,7 +358,7 @@ extension SMBSettings {
                     units: state.units,
                     type: .decimal("smbDeliveryRatio"),
                     label: String(localized: "SMB Delivery Ratio", comment: "SMB Delivery Ratio"),
-                    miniHint: "Percentage of calculated insulin required that is given as SMB.",
+                    miniHint: String(localized: "Percentage of calculated insulin required that is given as SMB."),
                     verboseHint:
                     VStack(alignment: .leading, spacing: 10) {
                         Text("Default: 50%").bold()
@@ -382,7 +386,7 @@ extension SMBSettings {
                     units: state.units,
                     type: .decimal("smbInterval"),
                     label: String(localized: "SMB Interval", comment: "SMB Interval"),
-                    miniHint: "Minimum minutes since the last SMB or manual bolus to allow an automated SMB.",
+                    miniHint: String(localized: "Minimum minutes since the last SMB or manual bolus to allow an automated SMB."),
                     verboseHint:
                     VStack(alignment: .leading, spacing: 10) {
                         Text("Default: 3 min").bold()

+ 1 - 1
Trio/Sources/Modules/Stat/View/ViewElements/GlucoseMetricsView.swift

@@ -32,7 +32,7 @@ struct GlucoseMetricsView: View {
             : glucoseStats.ngsp.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "%"
 
         let gmiString = glucoseStats.gmi.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "%"
-        
+
         // glucoseStats already parsed to units - only format decimals
         let standardDeviationString = units == .mgdL ? glucoseStats.sd.formatted(
             .number.grouping(.never).rounded().precision(.fractionLength(0))

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

@@ -40,7 +40,9 @@ extension TargetBehavoir {
                         "High Temp Target Raises Sensitivity",
                         comment: "High Temp Target Raises Sensitivity"
                     ),
-                    miniHint: "Increase sensitivity when glucose is above target if a manual Temp Target > \(state.units == .mgdL ? "100" : 100.formattedAsMmolL) \(state.units.rawValue) is set.",
+                    miniHint: String(
+                        localized: "Increase sensitivity when glucose is above target if a manual Temp Target > \(state.units == .mgdL ? "100" : 100.formattedAsMmolL) \(state.units.rawValue) is set."
+                    ),
                     verboseHint:
                     VStack(alignment: .leading, spacing: 10) {
                         Text("Default: OFF").bold()
@@ -77,7 +79,9 @@ extension TargetBehavoir {
                         "Low Temp Target Lowers Sensitivity",
                         comment: "Low Temp Target Lowers Sensitivity"
                     ),
-                    miniHint: "Decrease sensitivity when glucose is below target if a manual Temp Target < \(state.units == .mgdL ? "100" : 100.formattedAsMmolL) \(state.units.rawValue) is set.",
+                    miniHint: String(
+                        localized: "Decrease sensitivity when glucose is below target if a manual Temp Target < \(state.units == .mgdL ? "100" : 100.formattedAsMmolL) \(state.units.rawValue) is set."
+                    ),
                     verboseHint:
                     VStack(alignment: .leading, spacing: 10) {
                         Text("Default: OFF").bold()
@@ -105,7 +109,7 @@ extension TargetBehavoir {
                     units: state.units,
                     type: .boolean,
                     label: String(localized: "Sensitivity Raises Target", comment: "Sensitivity Raises Target"),
-                    miniHint: "Raise target glucose if when Autosens Ratio is >1.",
+                    miniHint: String(localized: "Raise target glucose if when Autosens Ratio is >1."),
                     verboseHint: VStack(alignment: .leading, spacing: 10) {
                         Text("Default: OFF").bold()
                         Text(
@@ -128,7 +132,7 @@ extension TargetBehavoir {
                     units: state.units,
                     type: .boolean,
                     label: String(localized: "Resistance Lowers Target", comment: "Resistance Lowers Target"),
-                    miniHint: "Lower target glucose when Autosens Ratio is <1.",
+                    miniHint: String(localized: "Lower target glucose when Autosens Ratio is <1."),
                     verboseHint: VStack(alignment: .leading, spacing: 10) {
                         Text("Default: OFF").bold()
                         Text(
@@ -151,7 +155,7 @@ extension TargetBehavoir {
                     units: state.units,
                     type: .decimal("halfBasalExerciseTarget"),
                     label: String(localized: "Half Basal Exercise Target", comment: "Half Basal Exercise Target"),
-                    miniHint: "Scales down your basal rate to 50% at this value.",
+                    miniHint: String(localized: "Scales down your basal rate to 50% at this value."),
                     verboseHint:
                     VStack(alignment: .leading, spacing: 10) {
                         Text(

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

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

+ 1 - 1
Trio/Sources/Services/LiveActivity/Data/DataManager.swift

@@ -3,7 +3,7 @@ import Foundation
 // Fetch Data for Glucose and Determination from Core Data and map them to the Structs in order to pass them thread safe to the glucoseDidUpdate/ pushUpdate function
 
 @available(iOS 16.2, *)
-extension LiveActivityBridge {
+extension LiveActivityManager {
     func fetchAndMapGlucose() async throws -> [GlucoseData] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,

+ 111 - 3
Trio/Sources/Services/LiveActivity/LiveActivityBridge.swift

@@ -9,6 +9,9 @@ import UIKit
     let activity: Activity<LiveActivityAttributes>
     let startDate: Date
 
+    /// Determines if the current activity needs to be recreated.
+    ///
+    /// - Returns: `true` if the activity is dismissed, ended, stale, or has been active for more than 60 minutes; otherwise, `false`.
     func needsRecreation() -> Bool {
         switch activity.activityState {
         case .dismissed,
@@ -24,34 +27,54 @@ import UIKit
     }
 }
 
+/// A service managing live activity updates and state management.
+///
+/// This class handles the creation, update, and termination of live activities based on various data sources
+/// (e.g. Core Data notifications, glucose updates, settings changes). It integrates with system notifications,
+/// dependency injection, and user defaults to ensure that the live activity reflects the current app state.
+///
+/// Additionally, it supports a restart functionality (via `restartActivityFromLiveActivityIntent()`)
+/// via iOS shortcuts, similar to other iOS apps like xDrip4iOS or Sweet Dreams.
 @available(iOS 16.2, *)
-final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
+final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver {
     @Injected() private var settingsManager: SettingsManager!
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var storage: FileStorage!
     @Injected() private var glucoseStorage: GlucoseStorage!
 
     private let activityAuthorizationInfo = ActivityAuthorizationInfo()
+    /// Indicates whether system live activities are enabled.
     @Published private(set) var systemEnabled: Bool
 
+    /// Returns the current Trio settings.
     private var settings: TrioSettings {
         settingsManager.settings
     }
 
+    /// Determination data used to update live activity state.
     var determination: DeterminationData?
+    /// The current active live activity.
     private var currentActivity: ActiveActivity?
+    /// The most recent glucose reading.
     private var latestGlucose: GlucoseData?
+    /// Array of glucose readings fetched from persistent storage.
     var glucoseFromPersistence: [GlucoseData]?
+    /// The current override data (if any).
     var override: OverrideData?
+    /// The widget items displayed within the live activity.
     var widgetItems: [LiveActivityAttributes.LiveActivityItem]?
 
+    /// A Core Data task context.
     let context = CoreDataStack.shared.newTaskContext()
 
-    // Queue for handling Core Data change notifications
+    /// A dispatch queue for handling Core Data change notifications.
     private let queue = DispatchQueue(label: "LiveActivityBridge.queue", qos: .userInitiated)
     private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
     private var subscriptions = Set<AnyCancellable>()
 
+    /// Initializes a new instance of `LiveActivityBridge` and sets up observers, subscribers, and notifications.
+    ///
+    /// - Parameter resolver: The dependency injection resolver.
     init(resolver: Resolver) {
         coreDataPublisher =
             changedObjectsOnManagedObjectContextDidSavePublisher()
@@ -69,6 +92,7 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
         broadcaster.register(SettingsObserver.self, observer: self)
     }
 
+    /// Sets up application notifications that trigger live activity updates when the app state changes.
     private func setupNotifications() {
         let notificationCenter = Foundation.NotificationCenter.default
         notificationCenter
@@ -91,12 +115,17 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
         )
     }
 
+    /// Called when the app settings change.
+    ///
+    /// This method triggers an update to the live activity content state based on the new settings.
+    /// - Parameter _: The updated `TrioSettings`.
     func settingsDidChange(_: TrioSettings) {
         Task {
             await updateContentState(determination)
         }
     }
 
+    /// Registers handlers for Core Data changes related to overrides, glucose readings, and determinations.
     private func registerHandler() {
         coreDataPublisher?.filteredByEntityName("OverrideStored").sink { [weak self] _ in
             guard let self = self else { return }
@@ -116,6 +145,7 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
             }.store(in: &subscriptions)
     }
 
+    /// Registers subscribers for updates from the glucose storage.
     private func registerSubscribers() {
         glucoseStorage.updatePublisher
             .receive(on: queue)
@@ -126,6 +156,7 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
             .store(in: &subscriptions)
     }
 
+    /// Fetches and maps new determination data and updates the live activity content state.
     private func cobOrIobDidUpdate() {
         Task { @MainActor in
             do {
@@ -142,6 +173,7 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
         }
     }
 
+    /// Fetches and maps override data and updates the live activity content state.
     private func overridesDidUpdate() {
         Task { @MainActor in
             do {
@@ -155,6 +187,9 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
         }
     }
 
+    /// Handles changes to the live activity order.
+    ///
+    /// Loads widget items from user defaults and triggers an update to the live activity order.
     @objc private func handleLiveActivityOrderChange() {
         Task {
             self.widgetItems = UserDefaults.standard.loadLiveActivityOrderFromUserDefaults() ?? LiveActivityAttributes
@@ -163,6 +198,9 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
         }
     }
 
+    /// Updates the live activity content state based on new determination or override data.
+    ///
+    /// - Parameter update: An object representing new `DeterminationData` or `OverrideData`.
     @MainActor private func updateContentState<T>(_ update: T) async {
         guard let latestGlucose = latestGlucose else {
             return
@@ -201,12 +239,16 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
         }
     }
 
+    /// Triggers an update of the live activity order.
+    ///
+    /// This method refreshes the activity’s content state to reflect any changes in the widget order.
     @MainActor private func updateLiveActivityOrder() async {
         Task {
             await updateContentState(determination)
         }
     }
 
+    /// Sets up the array of glucose data from persistent storage and triggers an update to the live activity.
     private func setupGlucoseArray() {
         Task { @MainActor in
             do {
@@ -218,6 +260,7 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
         }
     }
 
+    /// Monitors live activity authorization changes and updates the `systemEnabled` flag.
     private func monitorForLiveActivityAuthorizationChanges() {
         Task {
             for await activityState in activityAuthorizationInfo.activityEnablementUpdates {
@@ -230,6 +273,10 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
         }
     }
 
+    /// Forces an update to the live activity.
+    ///
+    /// If live activities are enabled and the current activity requires recreation, this method triggers a new glucose update.
+    /// Otherwise, it ends the current live activity.
     @MainActor private func forceActivityUpdate() {
         if settings.useLiveActivity {
             if currentActivity?.needsRecreation() ?? true {
@@ -242,6 +289,12 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
         }
     }
 
+    /// Pushes an update to the live activity with the specified content state.
+    ///
+    /// If an existing activity requires recreation or is outdated, this method ends it and starts a new one.
+    /// Otherwise, it updates the current live activity.
+    ///
+    /// - Parameter state: The new content state to push to the live activity.
     @MainActor private func pushUpdate(_ state: LiveActivityAttributes.ContentState) async {
         for unknownActivity in Activity<LiveActivityAttributes>.activities
             .filter({ self.currentActivity?.activity.id != $0.id })
@@ -297,20 +350,75 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
         }
     }
 
+    /// Ends the current live activity and ensures that all unknown activities are terminated.
     private func endActivity() async {
+        debug(.default, "Ending all live activities...")
+
         if let currentActivity {
+            debug(.default, "Ending current activity: \(currentActivity.activity.id)")
             await currentActivity.activity.end(nil, dismissalPolicy: .immediate)
             self.currentActivity = nil
         }
 
+        for activity in Activity<LiveActivityAttributes>.activities {
+            debug(.default, "Ending lingering activity: \(activity.id)")
+            await activity.end(nil, dismissalPolicy: .immediate)
+        }
+
         for unknownActivity in Activity<LiveActivityAttributes>.activities {
+            debug(.default, "Ending unknown activity: \(unknownActivity.id)")
             await unknownActivity.end(nil, dismissalPolicy: .immediate)
         }
+
+        debug(.default, "All live activities ended.")
+    }
+
+    /// Restarts the live activity from a Live Activity Intent.
+    ///
+    /// This method mimics xdrip’s `restartActivityFromLiveActivityIntent()` behavior by verifying that a valid content state exists,
+    /// ending the current live activity, and starting a new one using the current state.
+    @MainActor func restartActivityFromLiveActivityIntent() async {
+        guard let latestGlucose = latestGlucose,
+              let determination = determination
+        else {
+            debug(.default, "Cannot restart live activity because required persistent state is not available. Fetching data...")
+            return
+        }
+
+        guard let contentState = LiveActivityAttributes.ContentState(
+            new: latestGlucose,
+            prev: latestGlucose,
+            units: settings.units,
+            chart: glucoseFromPersistence ?? [],
+            settings: settings,
+            determination: determination,
+            override: override,
+            widgetItems: widgetItems
+        ) else {
+            debug(.default, "Cannot restart live activity because content state cannot be created")
+            return
+        }
+
+        await endActivity()
+
+        while (currentActivity != nil && currentActivity!.activity.activityState != .ended) || Activity<LiveActivityAttributes>
+            .activities.contains(where: { $0.activityState != .ended })
+        {
+            debug(.default, "Waiting for Live Activity to end...")
+            try? await Task.sleep(nanoseconds: 200_000_000) // 0.2s sleep
+        }
+
+        await pushUpdate(contentState)
+        debug(.default, "Restarted Live Activity from LiveActivityIntent (via iOS Shortcut)")
     }
 }
 
 @available(iOS 16.2, *)
-extension LiveActivityBridge {
+extension LiveActivityManager {
+    /// Updates the live activity when new glucose data is available.
+    ///
+    /// This function adjusts the live activity content based on new glucose readings and triggers an update to the live activity.
+    /// - Parameter glucose: An array of `GlucoseData` objects.
     @MainActor func glucoseDidUpdate(_ glucose: [GlucoseData]) {
         guard settings.useLiveActivity else {
             if currentActivity != nil {

+ 19 - 8
Trio/Sources/Services/RemoteControl/TrioRemoteControl+Meal.swift

@@ -41,15 +41,26 @@ extension TrioRemoteControl {
         }
 
         let pushMessageDate = Date(timeIntervalSince1970: pushMessage.timestamp)
-        let recentCarbEntries = carbsStorage.recent()
-        let carbsAfterPushMessage = recentCarbEntries.filter { $0.createdAt > pushMessageDate }
+        let taskContext = CoreDataStack.shared.newTaskContext()
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: CarbEntryStored.self,
+            onContext: taskContext,
+            predicate: NSPredicate(format: "createdAt > %@", pushMessageDate as NSDate),
+            key: "createdAt",
+            ascending: false
+        )
 
-        if !carbsAfterPushMessage.isEmpty {
-            await logError(
-                "Command rejected: newer carb entries have been logged since the command was sent.",
-                pushMessage: pushMessage
-            )
-            return
+        await taskContext.perform {
+            guard let recentCarbEntries = results as? [CarbEntryStored] else { return }
+            if !recentCarbEntries.isEmpty {
+                Task {
+                    await self.logError(
+                        "Command rejected: newer carb entries have been logged since the command was sent.",
+                        pushMessage: pushMessage
+                    )
+                    return
+                }
+            }
         }
 
         let actualDate: Date?

+ 9 - 0
Trio/Sources/Shortcuts/AppShortcuts.swift

@@ -66,5 +66,14 @@ struct AppShortcuts: AppShortcutsProvider {
             shortTitle: "Cancel Temp Target",
             systemImageName: "xmark.circle.fill"
         )
+        AppShortcut(
+            intent: RestartLiveActivityIntent(),
+            phrases: [
+                "Restart \(.applicationName) Live Activity",
+                "Restarts the Live Activity for \(.applicationName)"
+            ],
+            shortTitle: "Restart Live Activity",
+            systemImageName: "arrow.clockwise.circle.fill"
+        )
     }
 }

+ 1 - 0
Trio/Sources/Shortcuts/BaseIntentsRequest.swift

@@ -14,6 +14,7 @@ import Swinject
     @Injected() var glucoseStorage: GlucoseStorage!
     @Injected() var apsManager: APSManager!
     @Injected() var overrideStorage: OverrideStorage!
+    @Injected() var liveActivityManager: LiveActivityManager!
 
     let resolver: Resolver
 

+ 29 - 0
Trio/Sources/Shortcuts/LiveActivity/RestartLiveActivityIntent.swift

@@ -0,0 +1,29 @@
+import AppIntents
+import Foundation
+
+/// App Intent used to restart the live activity via Apple Shortcuts automation.
+/// When invoked, this intent instantiates a RestartLiveActivityIntentRequest, which has its
+/// dependencies injected via Swinject, and calls the restart functionality.
+@available(iOS 16.2, *) struct RestartLiveActivityIntent: LiveActivityIntent {
+    /// Title of the action in the Shortcuts app.
+    static var title = LocalizedStringResource("Restart Live Activity", table: "ShortcutsDetail")
+
+    /// Description of the action in the Shortcuts app.
+    static var description = IntentDescription(.init("Restarts Trio's Live Activity", table: "ShortcutsDetail"))
+
+    /// Performs the intent by triggering the live activity restart.
+    ///
+    /// This method creates an instance of RestartLiveActivityIntentRequest (which inherits from BaseIntentsRequest)
+    /// so that dependency injection provides the required services, then calls its restart functionality.
+    ///
+    /// - Returns: An intent result indicating success.
+    @MainActor func perform() async throws -> some ReturnsValue<String> {
+        let request = RestartLiveActivityIntentRequest()
+        do {
+            try await request.performRestart()
+        } catch {
+            debug(.default, "Error restarting Live Activity: \(error)")
+        }
+        return .result(value: String(localized: "Trio Live Activity restarted successfully."))
+    }
+}

+ 43 - 0
Trio/Sources/Shortcuts/LiveActivity/RestartLiveActivityIntentRequest.swift

@@ -0,0 +1,43 @@
+import AppIntents
+import Foundation
+import UIKit
+
+/// Request object that uses dependency injection to perform a live activity restart.
+/// This class inherits from BaseIntentsRequest so that its dependencies (including liveActivityManager)
+/// are automatically injected.
+@available(iOS 16.2, *) final class RestartLiveActivityIntentRequest: BaseIntentsRequest {
+    /// Triggers the live activity restart via the injected LiveActivityManager.
+    ///
+    /// - Throws: An error if the restart process fails.
+    /// - Returns: Void upon successful restart.
+    @MainActor func performRestart() async throws {
+        var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
+
+        // Start background task
+        backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "Restart Live Activity") {
+            Task { @MainActor in
+                if backgroundTaskID != .invalid {
+                    UIApplication.shared.endBackgroundTask(backgroundTaskID)
+                    backgroundTaskID = .invalid
+                    debug(.default, "Background task expired and ended.")
+                }
+            }
+        }
+
+        guard backgroundTaskID != .invalid else {
+            debug(.default, "Failed to start background task.")
+            return
+        }
+
+        debug(.default, "Background task started: \(backgroundTaskID)")
+
+        await liveActivityManager.restartActivityFromLiveActivityIntent()
+
+        // Ensure background task ends properly
+        if backgroundTaskID != .invalid {
+            UIApplication.shared.endBackgroundTask(backgroundTaskID)
+            debug(.default, "Background task ended successfully.")
+            backgroundTaskID = .invalid
+        }
+    }
+}

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

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

+ 590 - 0
TrioTests/BolusCalculatorTests/BolusCalculatorTests.swift

@@ -0,0 +1,590 @@
+import Foundation
+import Testing
+
+@testable import Trio
+
+@Suite("Bolus Calculator Tests") struct BolusCalculatorTests: Injectable {
+    @Injected() var calculator: BolusCalculationManager!
+    @Injected() var settingsManager: SettingsManager!
+    @Injected() var fileStorage: FileStorage!
+    @Injected() var apsManager: APSManager!
+    let resolver = TrioApp().resolver
+
+    init() {
+        injectServices(resolver)
+    }
+
+    @Test("Calculator is correctly initialized") func testCalculatorInitialization() {
+        #expect(calculator != nil, "BolusCalculationManager should be injected")
+        #expect(calculator is BaseBolusCalculationManager, "Calculator should be of type BaseBolusCalculationManager")
+    }
+
+    @Test("Calculate insulin for standard meal") func testStandardMealCalculation() async throws {
+        // STEP 1: Setup test scenario
+        // We need to provide a CalculationInput struct
+        let carbs: Decimal = 80
+        let currentBG: Decimal = 180 // 80 points above target, should result in 2U correction
+        let deltaBG: Decimal = 5 // Rising trend, should add small correction
+        let target: Decimal = 100
+        let isf: Decimal = 40
+        let carbRatio: Decimal = 10 // Should result in 8U for carbs
+        let iob: Decimal = 1.0 // Should subtract from final result
+        let cob: Int16 = 20
+        let useFattyMealCorrectionFactor: Bool = false
+        let useSuperBolus: Bool = false
+        let fattyMealFactor: Decimal = 0.8
+        let sweetMealFactor: Decimal = 2
+        let basal: Decimal = 1.5
+        let fraction: Decimal = 0.8
+        let maxBolus: Decimal = 10
+
+        // STEP 2: Create calculation input
+        let input = CalculationInput(
+            carbs: carbs,
+            currentBG: currentBG,
+            deltaBG: deltaBG,
+            target: target,
+            isf: isf,
+            carbRatio: carbRatio,
+            iob: iob,
+            cob: cob,
+            useFattyMealCorrectionFactor: useFattyMealCorrectionFactor,
+            fattyMealFactor: fattyMealFactor,
+            useSuperBolus: useSuperBolus,
+            sweetMealFactor: sweetMealFactor,
+            basal: basal,
+            fraction: fraction,
+            maxBolus: maxBolus
+        )
+
+        // STEP 3: Calculate insulin
+        let result = await calculator.calculateInsulin(input: input)
+
+        // STEP 4: Verify results
+        // Expected calculation breakdown:
+        // wholeCob = 80g + 20g COB = 100g
+        // wholeCobInsulin = 100g ÷ 10 g/U = 10U
+        // targetDifference = currentBG - target = 180 - 100 = 80 mg/dL
+        // targetDifferenceInsulin = 80 mg/dL ÷ 40 mg/dL/U = 2U
+        // fifteenMinutesInsulin = 5 mg/dL ÷ 40 mg/dL/U = 0.125U
+        // correctionInsulin = targetDifferenceInsulin = 2U
+        // iobInsulinReduction = 1U
+        // superBolusInsulin = 0U (disabled)
+        // no adjustment for fatty meals (disabled)
+        // wholeCalc = round(wholeCobInsulin + correctionInsulin + fifteenMinutesInsulin - iobInsulinReduction, 3) = 11.125U
+        // insulinCalculated = round(wholeCalc × fraction, 3) = 8.9U
+
+        // Calculate expected values with proper rounding using roundBolus method from the apsManager
+        let wholeCobInsulin = apsManager.roundBolus(amount: Decimal(100) / Decimal(10)) // 10U
+        let targetDifferenceInsulin = apsManager.roundBolus(amount: Decimal(80) / Decimal(40)) // 2U
+        let fifteenMinutesInsulin = apsManager.roundBolus(amount: Decimal(5) / Decimal(40)) // 0.125U
+        let wholeCalc = wholeCobInsulin + targetDifferenceInsulin + fifteenMinutesInsulin - Decimal(1) // 11.125U
+        let expectedInsulinCalculated = apsManager.roundBolus(amount: wholeCalc * fraction) // 8.9U
+
+        #expect(
+            result.insulinCalculated == expectedInsulinCalculated,
+            """
+            Incorrect insulin calculation
+            Expected: \(expectedInsulinCalculated)U
+            Actual: \(result.insulinCalculated)U
+            Components from CalculationResult:
+            - insulinCalculated: \(result.insulinCalculated)U (expected: \(expectedInsulinCalculated)U)
+            - wholeCalc: \(result.wholeCalc)U (expected: \(wholeCalc)U)
+            - correctionInsulin: \(result.correctionInsulin)U (expected: \(targetDifferenceInsulin)U)
+            - iobInsulinReduction: \(result.iobInsulinReduction)U (expected: 1U)
+            - superBolusInsulin: \(result.superBolusInsulin)U (expected: 0U)
+            - targetDifference: \(result.targetDifference) mg/dL (expected: 80 mg/dL)
+            - targetDifferenceInsulin: \(result.targetDifferenceInsulin)U (expected: \(targetDifferenceInsulin)U)
+            - fifteenMinutesInsulin: \(result.fifteenMinutesInsulin)U (expected: \(fifteenMinutesInsulin)U)
+            - wholeCob: \(result.wholeCob)g (expected: 100g)
+            - wholeCobInsulin: \(result.wholeCobInsulin)U (expected: \(wholeCobInsulin)U)
+            """
+        )
+
+        // Verify each component from CalculationResult struct with rounded values
+        #expect(
+            result.insulinCalculated == expectedInsulinCalculated,
+            "Final calculated insulin amount should be \(expectedInsulinCalculated)U"
+        )
+        #expect(result.wholeCalc == wholeCalc, "Total calculation before fraction should be \(wholeCalc)U")
+        #expect(
+            result.correctionInsulin == targetDifferenceInsulin,
+            "Insulin for BG correction should be \(targetDifferenceInsulin)U"
+        )
+        #expect(result.iobInsulinReduction == -1.0, "Absolute IOB reduction amount should be 1U, hence -1U")
+        #expect(result.superBolusInsulin == 0, "Additional insulin for super bolus should be 0U")
+        #expect(result.targetDifference == 80, "Difference from target BG should be 80 mg/dL")
+        #expect(
+            result.targetDifferenceInsulin == targetDifferenceInsulin,
+            "Insulin needed for target difference should be \(targetDifferenceInsulin)U"
+        )
+        #expect(
+            result.fifteenMinutesInsulin == fifteenMinutesInsulin,
+            "Trend-based insulin adjustment should be \(fifteenMinutesInsulin)U"
+        )
+        #expect(result.wholeCob == 100, "Total carbs (COB + new carbs) should be 100g")
+        #expect(result.wholeCobInsulin == wholeCobInsulin, "Insulin for total carbs should be \(wholeCobInsulin)U")
+    }
+
+    @Test("Calculate insulin for fatty meal") func testFattyMealCalculation() async throws {
+        // STEP 1: Setup test scenario
+        // We need to provide a CalculationInput struct
+        let carbs: Decimal = 80
+        let currentBG: Decimal = 180 // 80 points above target, should result in 2U correction
+        let deltaBG: Decimal = 5 // Rising trend, should add small correction
+        let target: Decimal = 100
+        let isf: Decimal = 40
+        let carbRatio: Decimal = 10 // Should result in 8U for carbs
+        let iob: Decimal = 1.0 // Should subtract from final result
+        let cob: Int16 = 20
+        let useFattyMealCorrectionFactor: Bool = true // now set to true
+        let useSuperBolus: Bool = false
+        let fattyMealFactor: Decimal = 0.8
+        let sweetMealFactor: Decimal = 2
+        let basal: Decimal = 1.5
+        let fraction: Decimal = 0.8
+        let maxBolus: Decimal = 10
+
+        // STEP 2: Create calculation input
+        let input = CalculationInput(
+            carbs: carbs,
+            currentBG: currentBG,
+            deltaBG: deltaBG,
+            target: target,
+            isf: isf,
+            carbRatio: carbRatio,
+            iob: iob,
+            cob: cob,
+            useFattyMealCorrectionFactor: useFattyMealCorrectionFactor,
+            fattyMealFactor: fattyMealFactor,
+            useSuperBolus: useSuperBolus,
+            sweetMealFactor: sweetMealFactor,
+            basal: basal,
+            fraction: fraction,
+            maxBolus: maxBolus
+        )
+
+        // STEP 3: Calculate insulin with fatty meal enabled
+        let fattyMealResult = await calculator.calculateInsulin(input: input)
+
+        // STEP 4: Calculate insulin with fatty meal disabled for comparison
+        let standardInput = CalculationInput(
+            carbs: carbs,
+            currentBG: currentBG,
+            deltaBG: deltaBG,
+            target: target,
+            isf: isf,
+            carbRatio: carbRatio,
+            iob: iob,
+            cob: cob,
+            useFattyMealCorrectionFactor: false, // Disabled for comparison
+            fattyMealFactor: fattyMealFactor,
+            useSuperBolus: useSuperBolus,
+            sweetMealFactor: sweetMealFactor,
+            basal: basal,
+            fraction: fraction,
+            maxBolus: maxBolus
+        )
+        let standardResult = await calculator.calculateInsulin(input: standardInput)
+
+        // STEP 5: Verify results
+        // Fatty meal should reduce the insulin amount by the fatty meal factor (0.8)
+        let expectedReduction = fattyMealFactor
+        let actualReduction = Decimal(
+            (Double(fattyMealResult.insulinCalculated) / Double(standardResult.insulinCalculated) * 10.0).rounded() / 10.0
+        )
+
+        #expect(
+            actualReduction == expectedReduction,
+            """
+            Fatty meal calculation incorrect
+            Expected reduction factor: \(expectedReduction)
+            Actual reduction factor: \(actualReduction)
+            Standard calculation: \(standardResult.insulinCalculated)U
+            Fatty meal calculation: \(fattyMealResult.insulinCalculated)U
+            """
+        )
+    }
+
+    @Test("Calculate insulin with super bolus") func testSuperBolusCalculation() async throws {
+        // STEP 1: Setup test scenario
+        // We need to provide a CalculationInput struct
+        let carbs: Decimal = 80
+        let currentBG: Decimal = 180 // 80 points above target, should result in 2U correction
+        let deltaBG: Decimal = 5 // Rising trend, should add small correction
+        let target: Decimal = 100
+        let isf: Decimal = 40
+        let carbRatio: Decimal = 10 // Should result in 8U for carbs
+        let iob: Decimal = 1.0 // Should subtract from final result
+        let cob: Int16 = 20
+        let useFattyMealCorrectionFactor: Bool = false
+        let useSuperBolus: Bool = true // Super bolus enabled
+        let fattyMealFactor: Decimal = 0.8
+        let sweetMealFactor: Decimal = 2
+        let basal: Decimal = 1.5 // Will be added to insulin calculation when super bolus is enabled
+        let fraction: Decimal = 0.8
+        let maxBolus: Decimal = 10
+
+        // STEP 2: Create calculation input with super bolus enabled
+        let input = CalculationInput(
+            carbs: carbs,
+            currentBG: currentBG,
+            deltaBG: deltaBG,
+            target: target,
+            isf: isf,
+            carbRatio: carbRatio,
+            iob: iob,
+            cob: cob,
+            useFattyMealCorrectionFactor: useFattyMealCorrectionFactor,
+            fattyMealFactor: fattyMealFactor,
+            useSuperBolus: useSuperBolus,
+            sweetMealFactor: sweetMealFactor,
+            basal: basal,
+            fraction: fraction,
+            maxBolus: maxBolus
+        )
+
+        // STEP 3: Calculate insulin with super bolus enabled
+        let superBolusResult = await calculator.calculateInsulin(input: input)
+
+        // STEP 4: Calculate insulin with super bolus disabled for comparison
+        let standardInput = CalculationInput(
+            carbs: carbs,
+            currentBG: currentBG,
+            deltaBG: deltaBG,
+            target: target,
+            isf: isf,
+            carbRatio: carbRatio,
+            iob: iob,
+            cob: cob,
+            useFattyMealCorrectionFactor: useFattyMealCorrectionFactor,
+            fattyMealFactor: fattyMealFactor,
+            useSuperBolus: false, // Disabled for comparison
+            sweetMealFactor: sweetMealFactor,
+            basal: basal,
+            fraction: fraction,
+            maxBolus: maxBolus
+        )
+        let standardResult = await calculator.calculateInsulin(input: standardInput)
+
+        // STEP 5: Verify results
+        // Super bolus should add basal rate * sweetMealFactor to the insulin calculation
+        let expectedSuperBolusInsulin = basal * sweetMealFactor
+        #expect(
+            superBolusResult.superBolusInsulin == expectedSuperBolusInsulin,
+            """
+            Super bolus insulin incorrect
+            Expected: \(expectedSuperBolusInsulin)U (basal \(basal)U × sweetMealFactor \(sweetMealFactor))
+            Actual: \(superBolusResult.superBolusInsulin)U
+            """
+        )
+
+        #expect(
+            superBolusResult.insulinCalculated > standardResult.insulinCalculated,
+            """
+            Super bolus calculation incorrect
+            Expected super bolus calculation to be higher than standard
+            Super bolus: \(superBolusResult.insulinCalculated)U
+            Standard: \(standardResult.insulinCalculated)U
+            Difference: \(superBolusResult.insulinCalculated - standardResult.insulinCalculated)U
+            """
+        )
+
+        // The difference should be the difference of super bolus (= standard dose + the basal rate * sweetMealFactor) limited by max bolus, and the standard dose.
+        let actualDifference = (superBolusResult.insulinCalculated - standardResult.insulinCalculated)
+        let expectedDifference = min(superBolusResult.insulinCalculated, maxBolus) - standardResult.insulinCalculated
+        #expect(
+            actualDifference == expectedDifference,
+            """
+            Super bolus difference incorrect
+            Expected difference: min(\(expectedSuperBolusInsulin), \(maxBolus)) U (basal \(basal)U × sweetMealFactor \(sweetMealFactor) + standard dose \(standardResult
+                .insulinCalculated)) - standard dose \(standardResult.insulinCalculated)
+            Actual difference: \(actualDifference)U
+            Standard result: \(standardResult)
+            SuperBolus result: \(superBolusResult)
+            """
+        )
+    }
+
+    @Test("Calculate insulin with zero carbs") func testZeroCarbsCalculation() async throws {
+        // Given
+        let carbs: Decimal = 0
+
+        // When
+        let result = await calculator.handleBolusCalculation(
+            carbs: carbs,
+            useFattyMealCorrection: false,
+            useSuperBolus: false
+        )
+
+        // Then
+        #expect(result.wholeCobInsulin == 0, "Zero carbs should require no insulin for carbs")
+    }
+
+    @Test("Verify settings retrieval") func testGetSettings() async throws {
+        // Given - Save original settings to restore later
+        let originalSettings = settingsManager.settings
+
+        // Setup test settings
+        let expectedUnits = GlucoseUnits.mgdL
+        let expectedFraction: Decimal = 0.7
+        let expectedFattyMealFactor: Decimal = 0.8
+        let expectedSweetMealFactor: Decimal = 2
+        let expectedMaxCarbs: Decimal = 150
+
+        // Update settings through settings manager
+        settingsManager.settings.units = expectedUnits
+        settingsManager.settings.overrideFactor = expectedFraction
+        settingsManager.settings.fattyMealFactor = expectedFattyMealFactor
+        settingsManager.settings.sweetMealFactor = expectedSweetMealFactor
+        settingsManager.settings.maxCarbs = expectedMaxCarbs
+
+        // Save settings to storage
+        fileStorage.save(settingsManager.settings, as: OpenAPS.Settings.settings)
+
+        // When
+        let (units, fraction, fattyMealFactor, sweetMealFactor, maxCarbs) = await getSettings()
+
+        // Then
+        #expect(units == expectedUnits, "Units should match settings")
+        #expect(fraction == expectedFraction, "Override factor should match settings")
+        #expect(fattyMealFactor == expectedFattyMealFactor, "Fatty meal factor should match settings")
+        #expect(sweetMealFactor == expectedSweetMealFactor, "Sweet meal factor should match settings")
+        #expect(maxCarbs == expectedMaxCarbs, "Max carbs should match settings")
+
+        // Cleanup - Restore original settings
+        settingsManager.settings = originalSettings
+        fileStorage.save(originalSettings, as: OpenAPS.Settings.settings)
+    }
+
+    @Test("Verify getCurrentSettingValue returns correct values based on time") func testGetCurrentSettingValue() async throws {
+        // STEP 1: Backup current settings
+        let originalBasalProfile = await fileStorage.retrieveAsync(OpenAPS.Settings.basalProfile, as: [BasalProfileEntry].self)
+        let originalCarbRatios = await fileStorage.retrieveAsync(OpenAPS.Settings.carbRatios, as: CarbRatios.self)
+        let originalBGTargets = await fileStorage.retrieveAsync(OpenAPS.Settings.bgTargets, as: BGTargets.self)
+        let originalISFValues = await fileStorage.retrieveAsync(
+            OpenAPS.Settings.insulinSensitivities,
+            as: InsulinSensitivities.self
+        )
+
+        // STEP 2: Setup test data with known values
+        // Note: Entries must be sorted by time for the algorithm to work correctly
+        let basalProfile = [
+            BasalProfileEntry(start: "00:00", minutes: 0, rate: 1.0), // 12:00 AM - 6:00 AM: 1.0
+            BasalProfileEntry(start: "06:00", minutes: 360, rate: 1.2), // 6:00 AM - 12:00 PM: 1.2
+            BasalProfileEntry(start: "12:00", minutes: 720, rate: 1.1), // 12:00 PM - 6:00 PM: 1.1
+            BasalProfileEntry(start: "18:00", minutes: 1080, rate: 0.9) // 6:00 PM - 12:00 AM: 0.9
+        ]
+
+        let carbRatios = CarbRatios(
+            units: .grams,
+            schedule: [
+                CarbRatioEntry(start: "00:00", offset: 0, ratio: 10), // 12:00 AM - 12:00 PM: 10
+                CarbRatioEntry(start: "12:00", offset: 720, ratio: 12) // 12:00 PM - 12:00 AM: 12
+            ]
+        )
+
+        let bgTargets = BGTargets(
+            units: .mgdL,
+            userPreferredUnits: .mgdL,
+            targets: [
+                BGTargetEntry(low: 100, high: 120, start: "00:00", offset: 0), // 12:00 AM - 8:00 AM: 100
+                BGTargetEntry(low: 90, high: 110, start: "08:00", offset: 480) // 8:00 AM - 12:00 AM: 90
+            ]
+        )
+
+        let isfValues = InsulinSensitivities(
+            units: .mgdL,
+            userPreferredUnits: .mgdL,
+            sensitivities: [
+                InsulinSensitivityEntry(sensitivity: 40, offset: 0, start: "00:00"), // 12:00 AM - 2:00 PM: 40
+                InsulinSensitivityEntry(sensitivity: 45, offset: 840, start: "14:00") // 2:00 PM - 12:00 AM: 45
+            ]
+        )
+
+        // STEP 3: Store test data
+        fileStorage.save(basalProfile, as: OpenAPS.Settings.basalProfile)
+        fileStorage.save(carbRatios, as: OpenAPS.Settings.carbRatios)
+        fileStorage.save(bgTargets, as: OpenAPS.Settings.bgTargets)
+        fileStorage.save(isfValues, as: OpenAPS.Settings.insulinSensitivities)
+
+        // STEP 4: Define test cases with specific times and expected values
+        // Format: (hour, minute, [setting type: expected value])
+        let testTimes: [(hour: Int, minute: Int, expected: [SettingType: Decimal])] = [
+            // Test midnight values (00:00)
+            (
+                hour: 0, minute: 0,
+                expected: [
+                    .basal: 1.0, // First basal rate
+                    .carbRatio: 10, // First carb ratio
+                    .bgTarget: 100, // First target
+                    .isf: 40 // First ISF
+                ]
+            ),
+            // Test mid-morning values (7:00)
+            (
+                hour: 7, minute: 0,
+                expected: [
+                    .basal: 1.2, // Second basal rate (after 6:00)
+                    .carbRatio: 10, // Still first carb ratio
+                    .bgTarget: 100, // Still first target
+                    .isf: 40 // Still first ISF
+                ]
+            ),
+            // Test afternoon values (15:00)
+            (
+                hour: 15, minute: 0,
+                expected: [
+                    .basal: 1.1, // Third basal rate (after 12:00)
+                    .carbRatio: 12, // Second carb ratio (after 12:00)
+                    .bgTarget: 90, // Second target
+                    .isf: 45 // Second ISF (after 14:00)
+                ]
+            )
+        ]
+
+        // STEP 5: Test each time point
+        for testTime in testTimes {
+            // Create a date object for the test time
+            let calendar = Calendar.current
+            var components = calendar.dateComponents([.year, .month, .day], from: Date())
+            components.hour = testTime.hour
+            components.minute = testTime.minute
+            components.second = 0
+            guard let testDate = calendar.date(from: components) else {
+                throw TestError("Failed to create test date")
+            }
+
+            // Test each setting type at this time
+            for (type, expectedValue) in testTime.expected {
+                // Get the actual value for this setting at the test time
+                let value = await getCurrentSettingValue(for: type, at: testDate)
+
+                // Compare with expected value
+                #expect(
+                    value == expectedValue,
+                    """
+                    Failed at \(testTime.hour):\(String(format: "%02d", testTime.minute))
+                    Setting: \(type)
+                    Expected: \(expectedValue)
+                    Actual: \(value)
+                    """
+                )
+            }
+        }
+
+        // STEP 6: Cleanup - Restore original settings
+        if let originalBasalProfile = originalBasalProfile {
+            fileStorage.save(originalBasalProfile, as: OpenAPS.Settings.basalProfile)
+        }
+        if let originalCarbRatios = originalCarbRatios {
+            fileStorage.save(originalCarbRatios, as: OpenAPS.Settings.carbRatios)
+        }
+        if let originalBGTargets = originalBGTargets {
+            fileStorage.save(originalBGTargets, as: OpenAPS.Settings.bgTargets)
+        }
+        if let originalISFValues = originalISFValues {
+            fileStorage.save(originalISFValues, as: OpenAPS.Settings.insulinSensitivities)
+        }
+    }
+}
+
+// Copied over from BolusCalculationManager as they are not included in the protocol definition (and I don´t want them to be included)
+
+extension BolusCalculatorTests {
+    private enum SettingType {
+        case basal
+        case carbRatio
+        case bgTarget
+        case isf
+    }
+
+    /// Retrieves current settings from the SettingsManager
+    /// - Returns: Tuple containing units, fraction, fattyMealFactor, sweetMealFactor, and maxCarbs settings
+    private func getSettings() async -> (
+        units: GlucoseUnits,
+        fraction: Decimal,
+        fattyMealFactor: Decimal,
+        sweetMealFactor: Decimal,
+        maxCarbs: Decimal
+    ) {
+        return (
+            units: settingsManager.settings.units,
+            fraction: settingsManager.settings.overrideFactor,
+            fattyMealFactor: settingsManager.settings.fattyMealFactor,
+            sweetMealFactor: settingsManager.settings.sweetMealFactor,
+            maxCarbs: settingsManager.settings.maxCarbs
+        )
+    }
+
+    /// Gets the current setting value for a specific setting type based on the time of day
+    /// - Parameter type: The type of setting to retrieve (basal, carbRatio, bgTarget, or isf)
+    /// - Returns: The current decimal value for the specified setting type
+    private func getCurrentSettingValue(for type: SettingType, at date: Date) async -> Decimal {
+        let calendar = Calendar.current
+        let midnight = calendar.startOfDay(for: date)
+        let minutesSinceMidnight = calendar.dateComponents([.minute], from: midnight, to: date).minute ?? 0
+
+        switch type {
+        case .basal:
+            let profile = await getBasalProfile()
+            return profile.last { $0.minutes <= minutesSinceMidnight }?.rate ?? 0
+
+        case .carbRatio:
+            let ratios = await getCarbRatios()
+            return ratios.schedule.last { $0.offset <= minutesSinceMidnight }?.ratio ?? 0
+
+        case .bgTarget:
+            let targets = await getBGTargets()
+            return targets.targets.last { $0.offset <= minutesSinceMidnight }?.low ?? 0
+
+        case .isf:
+            let sensitivities = await getISFValues()
+            return sensitivities.sensitivities.last { $0.offset <= minutesSinceMidnight }?.sensitivity ?? 0
+        }
+    }
+
+    /// Retrieves the pump settings from storage
+    /// - Returns: PumpSettings object containing pump configuration
+    private func getPumpSettings() async -> PumpSettings {
+        await fileStorage.retrieveAsync(OpenAPS.Settings.settings, as: PumpSettings.self)
+            ?? PumpSettings(from: OpenAPS.defaults(for: OpenAPS.Settings.settings))
+            ?? PumpSettings(insulinActionCurve: 10, maxBolus: 10, maxBasal: 2)
+    }
+
+    /// Retrieves the basal profile from storage
+    /// - Returns: Array of BasalProfileEntry objects
+    private func getBasalProfile() async -> [BasalProfileEntry] {
+        await fileStorage.retrieveAsync(OpenAPS.Settings.basalProfile, as: [BasalProfileEntry].self)
+            ?? [BasalProfileEntry](from: OpenAPS.defaults(for: OpenAPS.Settings.basalProfile))
+            ?? []
+    }
+
+    /// Retrieves carb ratios from storage
+    /// - Returns: CarbRatios object containing carb ratio schedule
+    private func getCarbRatios() async -> CarbRatios {
+        await fileStorage.retrieveAsync(OpenAPS.Settings.carbRatios, as: CarbRatios.self)
+            ?? CarbRatios(from: OpenAPS.defaults(for: OpenAPS.Settings.carbRatios))
+            ?? CarbRatios(units: .grams, schedule: [])
+    }
+
+    /// Retrieves blood glucose targets from storage
+    /// - Returns: BGTargets object containing target schedule
+    private func getBGTargets() async -> BGTargets {
+        await fileStorage.retrieveAsync(OpenAPS.Settings.bgTargets, as: BGTargets.self)
+            ?? BGTargets(from: OpenAPS.defaults(for: OpenAPS.Settings.bgTargets))
+            ?? BGTargets(units: .mgdL, userPreferredUnits: .mgdL, targets: [])
+    }
+
+    /// Retrieves insulin sensitivity factors from storage
+    /// - Returns: InsulinSensitivities object containing sensitivity schedule
+    private func getISFValues() async -> InsulinSensitivities {
+        await fileStorage.retrieveAsync(OpenAPS.Settings.insulinSensitivities, as: InsulinSensitivities.self)
+            ?? InsulinSensitivities(from: OpenAPS.defaults(for: OpenAPS.Settings.insulinSensitivities))
+            ?? InsulinSensitivities(
+                units: .mgdL,
+                userPreferredUnits: .mgdL,
+                sensitivities: []
+            )
+    }
+}

+ 46 - 29
TrioTests/CalibrationsTests.swift

@@ -1,59 +1,76 @@
+import Foundation
 import Swinject
+import Testing
+
 @testable import Trio
-import XCTest
 
-class CalibrationsTests: XCTestCase, Injectable {
+@Suite("Calibration Service Tests", .serialized) struct CalibrationTests: Injectable {
     let fileStorage = BaseFileStorage()
     @Injected() var calibrationService: CalibrationService!
     let resolver = TrioApp().resolver
 
-    override func setUp() {
+    init() {
         injectServices(resolver)
     }
 
-    func testCreateSimpleCalibration() {
-        // restore state so each test is independent
+    @Test("Can create simple calibration") func testCreateSimpleCalibration() {
+        // Given
         calibrationService.removeAllCalibrations()
-
         let calibration = Calibration(x: 100.0, y: 102.0)
-        calibrationService.addCalibration(calibration)
-
-        XCTAssertTrue(calibrationService.calibrations.isNotEmpty)
-
-        XCTAssertTrue(calibrationService.slope == 1)
 
-        XCTAssertTrue(calibrationService.intercept == 2)
+        // When
+        calibrationService.addCalibration(calibration)
 
-        XCTAssertTrue(calibrationService.calibrate(value: 104) == 106)
+        // Then
+        #expect(calibrationService.calibrations.isNotEmpty)
+        #expect(calibrationService.slope == 1)
+        #expect(calibrationService.intercept == 2)
+        #expect(calibrationService.calibrate(value: 104) == 106)
     }
 
-    func testCreateMultipleCalibration() {
-        // restore state so each test is independent
+    @Test("Can handle multiple calibrations") func testCreateMultipleCalibration() {
+        // Given
         calibrationService.removeAllCalibrations()
-
         let calibration = Calibration(x: 100.0, y: 120)
-        calibrationService.addCalibration(calibration)
-
         let calibration2 = Calibration(x: 120.0, y: 130.0)
+
+        // When
+        calibrationService.addCalibration(calibration)
         calibrationService.addCalibration(calibration2)
 
-        XCTAssertEqual(calibrationService.slope, 0.8, accuracy: 0.0001)
-        XCTAssertEqual(calibrationService.intercept, 37, accuracy: 0.0001)
-        XCTAssertEqual(calibrationService.calibrate(value: 80), 101, accuracy: 0.0001)
+        // Then
+        #expect(abs(calibrationService.slope - 0.8) < 0.0001)
+        #expect(abs(calibrationService.intercept - 37) < 0.0001)
+        #expect(abs(calibrationService.calibrate(value: 80) - 101) < 0.0001)
 
+        // When removing last
         calibrationService.removeLast()
+        #expect(calibrationService.calibrations.count == 1)
 
-        XCTAssertTrue(calibrationService.calibrations.count == 1)
-
+        // When removing all
         calibrationService.removeAllCalibrations()
-        XCTAssertTrue(calibrationService.calibrations.isEmpty)
+        #expect(calibrationService.calibrations.isEmpty)
     }
 
-    override func setUpWithError() throws {
-        // Put setup code here. This method is called before the invocation of each test method in the class.
-    }
+    @Test("Handles calibration bounds correctly") func testCalibrationBounds() {
+        // Given
+        calibrationService.removeAllCalibrations()
+
+        // When no calibrations exist
+        #expect(calibrationService.slope == 1, "Default slope should be 1")
+        #expect(calibrationService.intercept == 0, "Default intercept should be 0")
+
+        // When adding extreme values
+        let extremeCalibration1 = Calibration(x: 0.0, y: 1000.0) // Should be clamped
+        let extremeCalibration2 = Calibration(x: 1000.0, y: 0.0) // Should be clamped
+
+        calibrationService.addCalibration(extremeCalibration1)
+        calibrationService.addCalibration(extremeCalibration2)
 
-    override func tearDownWithError() throws {
-        // Put teardown code here. This method is called after the invocation of each test method in the class.
+        // Then check bounds
+        #expect(calibrationService.slope >= 0.8, "Slope should not be less than minimum")
+        #expect(calibrationService.slope <= 1.25, "Slope should not be more than maximum")
+        #expect(calibrationService.intercept >= -100, "Intercept should not be less than minimum")
+        #expect(calibrationService.intercept <= 100, "Intercept should not be more than maximum")
     }
 }

+ 208 - 0
TrioTests/CoreDataTests/CarbsStorageTests.swift

@@ -0,0 +1,208 @@
+import CoreData
+import Foundation
+import Swinject
+import Testing
+
+@testable import Trio
+
+@Suite("CarbsStorage Tests", .serialized) struct CarbsStorageTests: Injectable {
+    @Injected() var storage: CarbsStorage!
+    let resolver: Resolver
+    let coreDataStack = CoreDataStack.createForTests()
+    let testContext: NSManagedObjectContext
+
+    init() {
+        // Create test context
+        testContext = coreDataStack.newTaskContext()
+
+        // Create assembler with test assembly
+        let assembler = Assembler([
+            StorageAssembly(),
+            ServiceAssembly(),
+            APSAssembly(),
+            NetworkAssembly(),
+            UIAssembly(),
+            SecurityAssembly(),
+            TestAssembly(testContext: testContext)
+        ])
+
+        resolver = assembler.resolver
+        injectServices(resolver)
+    }
+
+    @Test("Storage is correctly initialized") func testStorageInitialization() {
+        #expect(storage != nil, "CarbsStorage should be injected")
+        #expect(storage is BaseCarbsStorage, "Storage should be of type BaseCarbsStorage")
+        #expect(storage.updatePublisher != nil, "Update publisher should be available")
+    }
+
+    @Test("Store and retrieve carbs entries") func testStoreAndRetrieveCarbs() async throws {
+        // Given
+        let testEntries = [
+            CarbsEntry(
+                id: UUID().uuidString,
+                createdAt: Date(),
+                actualDate: Date(),
+                carbs: 20,
+                fat: 0,
+                protein: 0,
+                note: "Test meal",
+                enteredBy: "Test",
+                isFPU: false,
+                fpuID: nil
+            )
+        ]
+
+        // When
+        try await storage.storeCarbs(testEntries, areFetchedFromRemote: false)
+        let recentEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: CarbEntryStored.self,
+            onContext: testContext,
+            predicate: NSPredicate(format: "TRUEPREDICATE"),
+            key: "date",
+            ascending: false
+        )
+
+        guard let recentEntries = recentEntries as? [CarbEntryStored] else {
+            throw TestError("Failed to get recent entries")
+        }
+
+        // Then
+        #expect(!recentEntries.isEmpty, "Should have stored entries")
+        #expect(recentEntries.count == 1, "Should have exactly one entry")
+        #expect(recentEntries[0].carbs == 20, "Carbs value should match")
+        #expect(recentEntries[0].fat == 0, "Fat value should match")
+        #expect(recentEntries[0].protein == 0, "Protein value should match")
+        #expect(recentEntries[0].note == "Test meal", "Note should match")
+    }
+
+    @Test("Delete carbs entry") func testDeleteCarbsEntry() async throws {
+        // Given
+        let testEntry = CarbsEntry(
+            id: UUID().uuidString,
+            createdAt: Date(),
+            actualDate: Date(),
+            carbs: 30,
+            fat: nil,
+            protein: nil,
+            note: "Delete test",
+            enteredBy: "Test",
+            isFPU: false,
+            fpuID: nil
+        )
+
+        // When
+        try await storage.storeCarbs([testEntry], areFetchedFromRemote: false)
+
+        // Get the stored entry's ObjectID
+        let storedEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: CarbEntryStored.self,
+            onContext: testContext,
+            predicate: NSPredicate(format: "carbs == 30"),
+            key: "date",
+            ascending: false
+        ) as? [CarbEntryStored]
+
+        guard let objectID = storedEntries?.first?.objectID else {
+            throw TestError("Failed to get stored entry's ObjectID")
+        }
+
+        // Delete the entry
+        await storage.deleteCarbsEntryStored(objectID)
+
+        // Then - verify deletion
+        let remainingEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: CarbEntryStored.self,
+            onContext: testContext,
+            predicate: NSPredicate(format: "carbs == 30"),
+            key: "date",
+            ascending: false
+        ) as? [CarbEntryStored]
+
+        #expect(remainingEntries?.isEmpty == true, "Should have no entries after deletion")
+    }
+
+    @Test("Get carbs not yet uploaded to Nightscout") func testGetCarbsNotYetUploadedToNightscout() async throws {
+        // Given
+        let testEntry = CarbsEntry(
+            id: UUID().uuidString,
+            createdAt: Date(),
+            actualDate: Date(),
+            carbs: 40,
+            fat: nil,
+            protein: nil,
+            note: "NS test",
+            enteredBy: "Test",
+            isFPU: false,
+            fpuID: nil
+        )
+
+        // When
+        try await storage.storeCarbs([testEntry], areFetchedFromRemote: false)
+        let notUploadedEntries = try await storage.getCarbsNotYetUploadedToNightscout()
+
+        // Then
+        #expect(!notUploadedEntries.isEmpty, "Should have entries not uploaded to NS")
+        #expect(notUploadedEntries[0].carbs == 40, "Carbs value should match")
+    }
+
+    @Test("Get FPUs not yet uploaded to Nightscout") func testGetFPUsNotYetUploadedToNightscout() async throws {
+        // Given
+        let fpuID = UUID().uuidString
+        let testEntry = CarbsEntry(
+            id: UUID().uuidString,
+            createdAt: Date(),
+            actualDate: Date(),
+            carbs: 30,
+            fat: 20,
+            protein: 10,
+            note: "FPU test",
+            enteredBy: "Test",
+            isFPU: false,
+            fpuID: fpuID
+        )
+
+        // When
+        try await storage.storeCarbs([testEntry], areFetchedFromRemote: false)
+
+        // First verify all stored entries
+        let allStoredEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: CarbEntryStored.self,
+            onContext: testContext,
+            predicate: NSPredicate(format: "fpuID == %@", fpuID),
+            key: "date",
+            ascending: true
+        ) as? [CarbEntryStored]
+
+        // Then verify the stored entries
+        #expect(allStoredEntries?.isEmpty == false, "Should have stored entries")
+        #expect(allStoredEntries?.count ?? 0 > 1, "Should have multiple entries due to FPU splitting")
+
+        // Original carb-non-fpu entry should be stored with original fat and protein values and isFPU set to false
+        let carbNonFpuEntry = allStoredEntries?.first(where: { $0.isFPU == false })
+        #expect(carbNonFpuEntry != nil, "Should have one carb non-fpu entry")
+        #expect(carbNonFpuEntry?.carbs == 30, "Original carbs should match")
+        #expect(carbNonFpuEntry?.protein == 10, "Original carbs should match")
+        #expect(carbNonFpuEntry?.fat == 20, "Original carbs should match")
+
+        // Additional carb-fpu entries should be created for fat/protein with isFPU set to true and the carbs set to the amount of each carbEquivalent
+        let carbFpuEntry = allStoredEntries?.filter { $0.isFPU == true }
+        #expect(carbFpuEntry?.isEmpty == false, "Should have additional carb-fpu entries")
+
+        // Now test the Nightscout upload function
+        let notUploadedFPUs = try await storage.getFPUsNotYetUploadedToNightscout()
+
+        // Then verify Nightscout entries
+        #expect(!notUploadedFPUs.isEmpty, "Should have FPUs not uploaded to NS")
+        let fpu = notUploadedFPUs[0]
+        #expect(fpu.carbs ?? 0 < 30, "Original carbs value should match")
+        #expect(fpu.protein == 0, "Protein value should match")
+        #expect(fpu.fat == 0, "Fat value should match")
+
+        // Verify all entries share the same fpuID
+        #expect(
+            allStoredEntries?.allSatisfy { $0.fpuID?.uuidString == fpuID } == true,
+            "All entries should share the same fpuID"
+        )
+    }
+}

+ 230 - 0
TrioTests/CoreDataTests/DeterminationStorageTests.swift

@@ -0,0 +1,230 @@
+import CoreData
+import Foundation
+import Swinject
+import Testing
+
+@testable import Trio
+
+@Suite("Determination Storage Tests", .serialized) struct DeterminationStorageTests: Injectable {
+    @Injected() var storage: DeterminationStorage!
+    let resolver: Resolver
+    let coreDataStack = CoreDataStack.createForTests()
+    let testContext: NSManagedObjectContext
+
+    init() {
+        // Create test context
+        // As we are only using this single test context to initialize our in-memory DeterminationStorage we need to perform the Unit Tests serialized
+        testContext = coreDataStack.newTaskContext()
+
+        // Create assembler with test assembly
+        let assembler = Assembler([
+            StorageAssembly(),
+            ServiceAssembly(),
+            APSAssembly(),
+            NetworkAssembly(),
+            UIAssembly(),
+            SecurityAssembly(),
+            TestAssembly(testContext: testContext) // Add our test assembly last to override Storage
+        ])
+
+        resolver = assembler.resolver
+        injectServices(resolver)
+    }
+
+    @Test("Storage is correctly initialized") func testStorageInitialization() {
+        // Verify storage exists
+        #expect(storage != nil, "DeterminationStorage should be injected")
+
+        // Verify it's the correct type
+        #expect(storage is BaseDeterminationStorage, "Storage should be of type BaseDeterminationStorage")
+    }
+
+    @Test("Test fetchLastDeterminationObjectID with different predicates") func testFetchLastDeterminationWithPredicates() async throws {
+        // Given
+        let date = Date()
+        let id = UUID()
+
+        // Create a mock determination
+        await testContext.perform {
+            let determination = OrefDetermination(context: testContext)
+            determination.id = id
+            determination.deliverAt = date
+            determination.timestamp = date
+            determination.enacted = true
+            determination.isUploadedToNS = true
+            try? testContext.save()
+        }
+
+        // Tests with predicates that we use the most for this function
+        // 1. Test within 30 minutes
+        let results = try await storage
+            .fetchLastDeterminationObjectID(predicate: NSPredicate.predicateFor30MinAgoForDetermination)
+        #expect(results.count == 1, "Should find 1 determination within 30 minutes")
+        // Get NSManagedObjectID from exactDateResults
+        try await testContext.perform {
+            do {
+                guard let results = results.first,
+                      let object = try testContext.existingObject(with: results) as? OrefDetermination
+                else {
+                    throw TestError("Failed to fetch determination")
+                }
+                #expect(object.timestamp == date, "Determination within 30 minutes should have the same timestamp as date")
+                #expect(object.deliverAt == date, "Determination within 30 minutes should have the same deliverAt as date")
+                #expect(object.enacted == true, "Determination within 30 minutes should be enacted")
+                #expect(object.isUploadedToNS == true, "Determination within 30 minutes should be uploaded to NS")
+                #expect(object.id == id, "Determination within 30 minutes should have the same id")
+            } catch {
+                throw TestError("Failed to fetch determination")
+            }
+        }
+
+        // 2. Test enacted determinations
+        let enactedPredicate = NSPredicate.enactedDetermination
+        let enactedResults = try await storage.fetchLastDeterminationObjectID(predicate: enactedPredicate)
+        #expect(enactedResults.count == 1, "Should find 1 enacted determination")
+        // Get NSManagedObjectID from enactedResults
+        try await testContext.perform {
+            do {
+                guard let results = enactedResults.first,
+                      let object = try testContext.existingObject(with: results) as? OrefDetermination
+                else {
+                    throw TestError("Failed to fetch determination")
+                }
+                #expect(object.enacted == true, "Enacted determination should be enacted")
+                #expect(object.isUploadedToNS == true, "Enacted determination should be uploaded to NS")
+                #expect(object.id == id, "Enacted determination should have the same id")
+                #expect(object.timestamp == date, "Enacted determination should have the same timestamp")
+                #expect(object.deliverAt == date, "Enacted determination should have the same deliverAt")
+
+                // Delete the determination
+                testContext.delete(object)
+                try testContext.save()
+            } catch {
+                throw TestError("Failed to fetch determination")
+            }
+        }
+    }
+
+    @Test("Test complete forecast hierarchy prefetching") func testForecastHierarchyPrefetching() async throws {
+        // Given
+        let date = Date()
+        let forecastTypes = ["iob", "cob", "zt", "uam"]
+        let expectedValuesPerForecast = 5
+
+        // STEP 1: Create test data
+        let id = try await createTestData(
+            date: date,
+            forecastTypes: forecastTypes,
+            expectedValuesPerForecast: expectedValuesPerForecast
+        )
+
+        // STEP 2: Test hierarchy fetching
+        let hierarchy = try await storage.fetchForecastHierarchy(
+            for: id,
+            in: testContext
+        )
+
+        // Test hierarchy structure
+        #expect(hierarchy.count == forecastTypes.count, "Should have correct number of forecasts")
+
+        // STEP 3: Test individual forecasts
+        for data in hierarchy {
+            let (_, forecast, values) = await storage.fetchForecastObjects(
+                for: data,
+                in: testContext
+            )
+
+            // Test basic structure
+            #expect(forecast != nil, "Forecast should exist")
+            #expect(values.count == expectedValuesPerForecast, "Should have correct number of values")
+
+            // Test forecast type and values
+            if let forecast = forecast {
+                #expect(forecastTypes.contains(forecast.type ?? ""), "Should have valid forecast type")
+
+                // Test value patterns
+                let sortedValues = values.sorted { $0.index < $1.index }
+                switch forecast.type {
+                case "iob":
+                    #expect(sortedValues.first?.value == 100, "IOB should start at 100")
+                    #expect(sortedValues.last?.value == 140, "IOB should end at 140")
+                case "cob":
+                    #expect(sortedValues.first?.value == 50, "COB should start at 50")
+                    #expect(sortedValues.last?.value == 70, "COB should end at 70")
+                case "zt":
+                    #expect(sortedValues.first?.value == 80, "ZT should start at 80")
+                    #expect(sortedValues.last?.value == 112, "ZT should end at 112")
+                case "uam":
+                    #expect(sortedValues.first?.value == 120, "UAM should start at 120")
+                    #expect(sortedValues.last?.value == 60, "UAM should end at 60")
+                default:
+                    break
+                }
+            }
+        }
+
+        // STEP 4: Test relationship integrity
+        try await testContext.perform {
+            do {
+                let determination = try testContext.existingObject(with: id) as? OrefDetermination
+                let forecasts = Array(determination?.forecasts ?? [])
+
+                #expect(forecasts.count == forecastTypes.count, "Determination should have all forecasts")
+                #expect(
+                    forecasts.allSatisfy { Array($0.forecastValues ?? []).count == expectedValuesPerForecast },
+                    "Each forecast should have correct number of values"
+                )
+            } catch {
+                throw TestError("Failed to verify relationships: \(error)")
+            }
+        }
+    }
+
+    private func createTestData(
+        date: Date,
+        forecastTypes: [String],
+        expectedValuesPerForecast: Int
+    ) async throws -> NSManagedObjectID {
+        try await testContext.perform {
+            let determination = OrefDetermination(context: testContext)
+            determination.id = UUID()
+            determination.deliverAt = date
+            determination.timestamp = date
+            determination.enacted = true
+
+            // Create all forecast types with values
+            for type in forecastTypes {
+                let forecast = Forecast(context: testContext)
+                forecast.id = UUID()
+                forecast.date = date
+                forecast.type = type
+                forecast.orefDetermination = determination
+
+                // Add test values with different patterns per type
+                for i in 0 ..< expectedValuesPerForecast {
+                    let value = ForecastValue(context: testContext)
+                    value.index = Int32(i)
+
+                    // Different value patterns for each type
+                    switch type {
+                    case "iob": value.value = Int32(100 + i * 10) // 100, 110, 120...
+                    case "cob": value.value = Int32(50 + i * 5) // 50, 55, 60...
+                    case "zt": value.value = Int32(80 + i * 8) // 80, 88, 96...
+                    case "uam": value.value = Int32(120 - i * 15) // 120, 105, 90...
+                    default: value.value = 0
+                    }
+
+                    value.forecast = forecast
+                }
+            }
+
+            do {
+                try testContext.save()
+
+                return determination.objectID
+            } catch {
+                throw TestError("Failed to create test data: \(error)")
+            }
+        }
+    }
+}

+ 187 - 0
TrioTests/CoreDataTests/GlucoseStorageTests.swift

@@ -0,0 +1,187 @@
+import CoreData
+import Foundation
+import Swinject
+import Testing
+
+@testable import Trio
+
+@Suite("GlucoseStorage Tests", .serialized) struct GlucoseStorageTests: Injectable {
+    @Injected() var storage: GlucoseStorage!
+    let resolver: Resolver
+    let coreDataStack = CoreDataStack.createForTests()
+    let testContext: NSManagedObjectContext
+
+    init() {
+        // Create test context
+        // As we are only using this single test context to initialize our in-memory DeterminationStorage we need to perform the Unit Tests serialized
+        testContext = coreDataStack.newTaskContext()
+
+        // Create assembler with test assembly
+        let assembler = Assembler([
+            StorageAssembly(),
+            ServiceAssembly(),
+            APSAssembly(),
+            NetworkAssembly(),
+            UIAssembly(),
+            SecurityAssembly(),
+            TestAssembly(testContext: testContext) // Add our test assembly last to override Storage
+        ])
+
+        resolver = assembler.resolver
+        injectServices(resolver)
+    }
+
+    @Test("Storage is correctly initialized") func testStorageInitialization() {
+        // Verify storage exists
+        #expect(storage != nil, "GlucoseStorage should be injected")
+
+        // Verify it's the correct type
+        #expect(storage is BaseGlucoseStorage, "Storage should be of type BaseGlucoseStorage")
+    }
+
+    @Test("Store and retrieve glucose entries") func testStoreAndRetrieveGlucose() async throws {
+        // Given
+        let testGlucose = [
+            BloodGlucose(direction: BloodGlucose.Direction.flat, date: 123, dateString: Date(), glucose: 126)
+        ]
+
+        // When
+        try await storage.storeGlucose(testGlucose)
+
+        // Then verify stored entries
+        let storedEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: GlucoseStored.self,
+            onContext: testContext,
+            predicate: NSPredicate(format: "glucose == 126"),
+            key: "date",
+            ascending: false
+        ) as? [GlucoseStored]
+
+        #expect(storedEntries?.isEmpty == false, "Should have stored entries")
+        #expect(storedEntries?.count == 1, "Should have exactly one entry")
+        #expect(storedEntries?[0].glucose == 126, "Glucose value should match")
+        #expect(storedEntries?[0].direction == "Flat", "Direction should match")
+    }
+
+    @Test("Delete glucose entry") func testDeleteGlucose() async throws {
+        // Given
+        let testGlucose = [
+            BloodGlucose(direction: BloodGlucose.Direction.flat, date: 123, dateString: Date(), glucose: 140)
+        ]
+        try await storage.storeGlucose(testGlucose)
+
+        // Get the stored entry's ObjectID
+        let storedEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: GlucoseStored.self,
+            onContext: testContext,
+            predicate: NSPredicate(format: "glucose == 140"),
+            key: "date",
+            ascending: false
+        ) as? [GlucoseStored]
+
+        guard let objectID = storedEntries?.first?.objectID else {
+            throw TestError("Failed to get stored entry's ObjectID")
+        }
+
+        #expect(storedEntries.isNotNilNotEmpty == true, "Should have exactly one (test) entry")
+
+        // When
+        await storage.deleteGlucose(objectID)
+
+        // Then verify deletion
+        let remainingEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: GlucoseStored.self,
+            onContext: testContext,
+            predicate: NSPredicate(format: "glucose == 140"),
+            key: "date",
+            ascending: false
+        ) as? [GlucoseStored]
+
+        #expect(remainingEntries?.isEmpty == true, "Should have no entries after deletion")
+    }
+
+    @Test("Get glucose not yet uploaded to Nightscout") func testGetGlucoseNotYetUploadedToNightscout() async throws {
+        // Given
+        let testGlucose = [
+            BloodGlucose(direction: BloodGlucose.Direction.flat, date: 123, dateString: Date(), glucose: 160)
+        ]
+        try await storage.storeGlucose(testGlucose)
+
+        // When
+        let notUploadedEntries = try await storage.getGlucoseNotYetUploadedToNightscout()
+
+        // Then
+        #expect(!notUploadedEntries.isEmpty, "Should have entries not uploaded to NS")
+        #expect(notUploadedEntries[0].glucose == 160, "Glucose value should match")
+    }
+
+    @Test("Get manual glucose not yet uploaded to Nightscout") func testGetManualGlucoseNotYetUploadedToNightscout() async throws {
+        // Given
+        storage.addManualGlucose(glucose: 180)
+
+        // When
+        let notUploadedEntries = try await storage.getManualGlucoseNotYetUploadedToNightscout()
+
+        // Then
+        #expect(!notUploadedEntries.isEmpty, "Should have manual entries not uploaded to NS")
+        let entry = notUploadedEntries[0]
+        #expect(entry.glucose == "180", "Glucose value should match")
+        #expect(entry.glucoseType == "Manual", "Type should be mbg for manual entries")
+        #expect(entry.eventType == .capillaryGlucose, "Type should be capillaryGlucose")
+    }
+
+    @Test("Test glucose alarms") func testGlucoseAlarms() async throws {
+        // Given
+        let lowGlucose = [
+            BloodGlucose(direction: BloodGlucose.Direction.flat, date: 123, dateString: Date(), glucose: 55)
+        ]
+        let highGlucose = [
+            BloodGlucose(direction: BloodGlucose.Direction.flat, date: 123, dateString: Date(), glucose: 271)
+        ]
+        let normalGlucose = [
+            BloodGlucose(direction: BloodGlucose.Direction.flat, date: 123, dateString: Date(), glucose: 100)
+        ]
+
+        // When - Test low glucose
+        try await storage.storeGlucose(lowGlucose)
+        var storedEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: GlucoseStored.self,
+            onContext: testContext,
+            predicate: NSPredicate(format: "glucose == 55"),
+            key: "date",
+            ascending: false
+        ) as? [GlucoseStored]
+
+        // Then
+        #expect(storedEntries?.first?.glucose == 55, "Low glucose value should match")
+        #expect(storage.alarm == .low, "Should trigger low glucose alarm") // default low limit is 72 mg/dL
+
+        // When - Test high glucose
+        try await storage.storeGlucose(highGlucose)
+        storedEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: GlucoseStored.self,
+            onContext: testContext,
+            predicate: NSPredicate(format: "glucose == 271"),
+            key: "date",
+            ascending: false
+        ) as? [GlucoseStored]
+
+        // Then
+        #expect(storedEntries?.first?.glucose == 271, "High glucose value should match")
+        #expect(storage.alarm == .high, "Should trigger high glucose alarm") // default high limit is 270 mg/dL
+
+        // When - Test normal glucose
+        try await storage.storeGlucose(normalGlucose)
+        storedEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: GlucoseStored.self,
+            onContext: testContext,
+            predicate: NSPredicate(format: "glucose == 100"),
+            key: "date",
+            ascending: false
+        ) as? [GlucoseStored]
+
+        // Then
+        #expect(storedEntries?.first?.glucose == 100, "Normal glucose value should match")
+        #expect(storage.alarm == nil, "Should not trigger any alarm")
+    }
+}

+ 224 - 0
TrioTests/CoreDataTests/OverrideStorageTests.swift

@@ -0,0 +1,224 @@
+import CoreData
+import Foundation
+import Swinject
+import Testing
+
+@testable import Trio
+
+@Suite("Override Storage Tests", .serialized) struct OverrideStorageTests: Injectable {
+    @Injected() var storage: OverrideStorage!
+    let resolver: Resolver
+    let coreDataStack = CoreDataStack.createForTests()
+    let testContext: NSManagedObjectContext
+
+    init() {
+        // Create test context
+        // As we are only using this single test context to initialize our in-memory DeterminationStorage we need to perform the Unit Tests serialized
+        testContext = coreDataStack.newTaskContext()
+
+        // Create assembler with test assembly
+        let assembler = Assembler([
+            StorageAssembly(),
+            ServiceAssembly(),
+            APSAssembly(),
+            NetworkAssembly(),
+            UIAssembly(),
+            SecurityAssembly(),
+            TestAssembly(testContext: testContext) // Add our test assembly last to override Storage
+        ])
+
+        resolver = assembler.resolver
+        injectServices(resolver)
+    }
+
+    @Test("Storage is correctly initialized") func testStorageInitialization() {
+        // Verify storage exists
+        #expect(storage != nil, "OverrideStorage should be injected")
+
+        // Verify it's the correct type
+        #expect(storage is BaseOverrideStorage, "Storage should be of type BaseOverrideStorage")
+    }
+
+    @Test("Store and retrieve override") func testStoreAndRetrieveOverride() async throws {
+        // Given
+        let testOverride = Override(
+            name: "Test Override",
+            enabled: false,
+            date: Date(),
+            duration: 120,
+            indefinite: false,
+            percentage: 130,
+            smbIsOff: true,
+            isPreset: false,
+            id: UUID().uuidString,
+            overrideTarget: true,
+            target: 110,
+            advancedSettings: false,
+            isfAndCr: false,
+            isf: false,
+            cr: false,
+            smbIsScheduledOff: false,
+            start: 1,
+            end: 2,
+            smbMinutes: 100,
+            uamMinutes: 120
+        )
+
+        // When
+        try await storage.storeOverride(override: testOverride)
+
+        // Then verify stored entries
+        let storedEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: OverrideStored.self,
+            onContext: testContext,
+            predicate: NSPredicate(format: "name == %@", "Test Override"),
+            key: "date",
+            ascending: false
+        ) as? [OverrideStored]
+
+        #expect(storedEntries?.isEmpty == false, "Should have stored entries")
+        #expect(storedEntries?.count == 1, "Should have exactly one entry")
+        let storedOverride = storedEntries?.first
+        #expect(storedOverride?.name == "Test Override", "Name should match")
+        #expect(storedOverride?.percentage == 130, "Percentage should match")
+        #expect(storedOverride?.target?.decimalValue == 110, "Target should match")
+        #expect(storedOverride?.isPreset == false, "isPreset should match")
+    }
+
+    @Test("Store and retrieve override preset") func testStoreAndRetrieveOverridePreset() async throws {
+        // Given
+        let testPreset = Override(
+            name: "Test Preset",
+            enabled: false,
+            date: Date(),
+            duration: 0,
+            indefinite: true,
+            percentage: 120,
+            smbIsOff: true,
+            isPreset: true,
+            id: UUID().uuidString,
+            overrideTarget: true,
+            target: 110,
+            advancedSettings: false,
+            isfAndCr: false,
+            isf: false,
+            cr: false,
+            smbIsScheduledOff: false,
+            start: 1,
+            end: 2,
+            smbMinutes: 100,
+            uamMinutes: 120
+        )
+
+        // When
+        try await storage.storeOverride(override: testPreset)
+        let presetIDs = try await storage.fetchForOverridePresets()
+
+        // Then
+        #expect(!presetIDs.isEmpty, "Should have stored preset")
+
+        let storedPresets = try await testContext.perform {
+            try presetIDs.map { try testContext.existingObject(with: $0) as! OverrideStored }
+        }
+
+        #expect(storedPresets.count >= 1, "Should have at least one preset")
+        let storedPreset = storedPresets.first { $0.name == "Test Preset" }
+        #expect(storedPreset != nil, "Should find the test preset")
+        #expect(storedPreset?.isPreset == true, "Should be marked as preset")
+        #expect(storedPreset?.indefinite == true, "Should be indefinite")
+        #expect(storedPreset?.percentage == 120, "Percentage should match")
+    }
+
+    @Test("Delete override preset") func testDeleteOverridePreset() async throws {
+        // Given
+        let testPreset = Override(
+            name: "Delete Test",
+            enabled: false,
+            date: Date(),
+            duration: 0,
+            indefinite: true,
+            percentage: 120,
+            smbIsOff: true,
+            isPreset: true,
+            id: UUID().uuidString,
+            overrideTarget: true,
+            target: 110,
+            advancedSettings: false,
+            isfAndCr: false,
+            isf: false,
+            cr: false,
+            smbIsScheduledOff: false,
+            start: 1,
+            end: 2,
+            smbMinutes: 100,
+            uamMinutes: 120
+        )
+
+        // Store the preset
+        try await storage.storeOverride(override: testPreset)
+
+        // Get the stored preset's ObjectID
+        let storedEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: OverrideStored.self,
+            onContext: testContext,
+            predicate: NSPredicate(format: "name == %@", "Delete Test"),
+            key: "date",
+            ascending: false
+        ) as? [OverrideStored]
+
+        guard let objectID = storedEntries?.first?.objectID else {
+            throw TestError("Failed to get stored preset's ObjectID")
+        }
+
+        // When
+        await storage.deleteOverridePreset(objectID)
+
+        // Then verify deletion
+        let remainingEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: OverrideStored.self,
+            onContext: testContext,
+            predicate: NSPredicate(format: "name == %@", "Delete Test"),
+            key: "date",
+            ascending: false
+        ) as? [OverrideStored]
+
+        #expect(remainingEntries?.isEmpty == true, "Should have no entries after deletion")
+    }
+
+    @Test("Get overrides not yet uploaded to Nightscout") func testGetOverridesNotYetUploadedToNightscout() async throws {
+        // Given
+        let testOverride = Override(
+            name: "NS Test",
+            enabled: true, // getOverridesNotYetUploadedToNightscout() fetches only active overrides
+            date: Date(),
+            duration: 90,
+            indefinite: false,
+            percentage: 120,
+            smbIsOff: true,
+            isPreset: true,
+            id: UUID().uuidString,
+            overrideTarget: true,
+            target: 110,
+            advancedSettings: false,
+            isfAndCr: false,
+            isf: false,
+            cr: false,
+            smbIsScheduledOff: false,
+            start: 1,
+            end: 2,
+            smbMinutes: 100,
+            uamMinutes: 120
+        )
+
+        // When
+        try await storage.storeOverride(override: testOverride)
+
+        let notUploadedOverrides = try await storage.getOverridesNotYetUploadedToNightscout()
+
+        // Then
+        #expect(!notUploadedOverrides.isEmpty == true, "Should have overrides not uploaded to NS")
+        #expect(notUploadedOverrides[0].notes == "NS Test", "Override name should match")
+        #expect(notUploadedOverrides[0].duration == 90, "Duration should match")
+        #expect(notUploadedOverrides[0].eventType == .nsExercise, "Event type should be exercise")
+    }
+}

+ 288 - 0
TrioTests/CoreDataTests/PumpHistoryStorageTests.swift

@@ -0,0 +1,288 @@
+import CoreData
+import Foundation
+import Swinject
+import Testing
+
+@testable import LoopKit
+@testable import Trio
+
+@Suite("PumpHistoryStorage Tests", .serialized) struct PumpHistoryStorageTests: Injectable {
+    @Injected() var storage: PumpHistoryStorage!
+    let resolver: Resolver
+    let coreDataStack = CoreDataStack.createForTests()
+    let testContext: NSManagedObjectContext
+    typealias PumpEvent = PumpEventStored.EventType
+
+    init() {
+        // Create test context
+        testContext = coreDataStack.newTaskContext()
+
+        // Create assembler with test assembly
+        let assembler = Assembler([
+            StorageAssembly(),
+            ServiceAssembly(),
+            APSAssembly(),
+            NetworkAssembly(),
+            UIAssembly(),
+            SecurityAssembly(),
+            TestAssembly(testContext: testContext) // Add our test assembly last to override PumpHistoryStorage
+        ])
+
+        resolver = assembler.resolver
+        injectServices(resolver)
+    }
+
+    @Test("Storage is correctly initialized") func testStorageInitialization() {
+        // Verify storage exists
+        #expect(storage != nil, "PumpHistoryStorage should be injected")
+
+        // Verify it's the correct type
+        #expect(
+            storage is BasePumpHistoryStorage, "Storage should be of type BasePumpHistoryStorage"
+        )
+
+        // Verify we can access the update publisher
+        #expect(storage.updatePublisher != nil, "Update publisher should be available")
+    }
+
+    @Test("Test read and delete using generic CoreDataStack functions") func testFetchAndDeletePumpEvents() async throws {
+        // Given
+        let date = Date()
+
+        // Insert mock entry
+        let events: [LoopKit.NewPumpEvent] = [
+            LoopKit.NewPumpEvent(
+                date: date,
+                dose: LoopKit.DoseEntry(
+                    type: .bolus,
+                    startDate: date,
+                    value: 0.5,
+                    unit: .units,
+                    deliveredUnits: nil,
+                    description: nil,
+                    syncIdentifier: nil,
+                    scheduledBasalRate: nil,
+                    insulinType: .lyumjev,
+                    automatic: false,
+                    manuallyEntered: false,
+                    isMutable: false
+                ),
+                raw: Data(),
+                title: "Test Bolus for Fetch",
+                type: .bolus
+            )
+        ]
+
+        // Store test event
+        try await storage.storePumpEvents(events)
+
+        // When - Fetch events with our generic fetch function
+        let fetchedEvents = try await coreDataStack.fetchEntitiesAsync(
+            ofType: PumpEventStored.self,
+            onContext: testContext,
+            predicate: NSPredicate(
+                format: "type == %@ AND timestamp == %@",
+                PumpEvent.bolus.rawValue,
+                date as NSDate
+            ),
+            key: "timestamp",
+            ascending: false
+        )
+
+        guard let fetchedEvents = fetchedEvents as? [PumpEventStored] else { return }
+
+        // Then
+        #expect(fetchedEvents.count == 1, "Should have found exactly one event")
+        let fetchedEvent = fetchedEvents.first
+        #expect(fetchedEvent?.type == PumpEvent.bolus.rawValue, "Should be a bolus event")
+        #expect(fetchedEvent?.bolus?.amount as? Decimal == 0.5, "Bolus amount should be 0.5")
+        #expect(
+            abs(fetchedEvent?.timestamp?.timeIntervalSince(date) ?? 1) < 1,
+            "Timestamp should match"
+        )
+
+        // When - Delete event
+        if let fetchedEvent = fetchedEvent {
+            await coreDataStack.deleteObject(identifiedBy: fetchedEvent.objectID)
+        }
+
+        // Then - Verify deletion
+        let eventsAfterDeletion = try await coreDataStack.fetchEntitiesAsync(
+            ofType: PumpEventStored.self,
+            onContext: testContext,
+            predicate: NSPredicate(
+                format: "type == %@ AND timestamp == %@",
+                PumpEvent.bolus.rawValue,
+                date as NSDate
+            ),
+            key: "timestamp",
+            ascending: false
+        )
+
+        guard let eventsAfterDeletion = eventsAfterDeletion as? [PumpEventStored] else { return }
+
+        #expect(eventsAfterDeletion.isEmpty, "Should have no events after deletion")
+    }
+
+    @Test("Test store function in PumpHistoryStorage") func testStorePumpEvents() async throws {
+        // Given
+        let date = Date()
+        let tenMinAgo = date.addingTimeInterval(-10.minutes.timeInterval)
+        let halfHourInFuture = date.addingTimeInterval(30.minutes.timeInterval)
+
+        // Get initial entries to compare to final entries later
+        let initialEntries = try await testContext.perform {
+            try testContext.fetch(PumpEventStored.fetchRequest())
+        }
+
+        // Create 2 test events, 1 bolus + 1 temp basal event
+        let events: [LoopKit.NewPumpEvent] = [
+            // SMB
+            LoopKit.NewPumpEvent(
+                date: tenMinAgo,
+                dose: LoopKit.DoseEntry(
+                    type: .bolus,
+                    startDate: tenMinAgo,
+                    value: 0.4,
+                    unit: .units,
+                    deliveredUnits: nil,
+                    description: nil,
+                    syncIdentifier: nil,
+                    scheduledBasalRate: nil,
+                    insulinType: .lyumjev,
+                    automatic: true,
+                    manuallyEntered: false,
+                    isMutable: false
+                ),
+                raw: Data(),
+                title: "Test Bolus",
+                type: .bolus
+            ),
+            // Temp Basal event
+            LoopKit.NewPumpEvent(
+                date: date,
+                dose: LoopKit.DoseEntry(
+                    type: .tempBasal,
+                    startDate: date,
+                    endDate: halfHourInFuture,
+                    value: 1.2,
+                    unit: .unitsPerHour,
+                    deliveredUnits: nil,
+                    description: nil,
+                    syncIdentifier: nil,
+                    scheduledBasalRate: nil,
+                    insulinType: .lyumjev,
+                    automatic: true,
+                    manuallyEntered: false,
+                    isMutable: true
+                ),
+                raw: Data(),
+                title: "Test Temp Basal",
+                type: .tempBasal
+            )
+        ]
+
+        // When
+        // Store in our in-memory PumphistoryStorage
+        try await storage.storePumpEvents(events)
+
+        // Then
+        // Fetch all events after storing
+        let finalEntries = try await testContext.perform {
+            try testContext.fetch(PumpEventStored.fetchRequest())
+        }
+
+        // Verify there were no initial entries
+        #expect(initialEntries.isEmpty, "There should be no initial entries")
+
+        // Verify count increased by 2
+        #expect(finalEntries.count == initialEntries.count + 2, "Should have added 2 new events")
+
+        // Verify bolus event
+        let bolusEvent = finalEntries.first {
+            $0.type == PumpEvent.bolus.rawValue &&
+                abs($0.timestamp?.timeIntervalSince(tenMinAgo) ?? 1) < 1
+        }
+        #expect(bolusEvent != nil, "Should have found bolus event")
+        #expect(bolusEvent?.bolus?.amount as? Decimal == 0.4, "Bolus amount should be 0.4")
+        #expect(bolusEvent?.isUploadedToNS == false, "Should not be uploaded to NS")
+        #expect(bolusEvent?.isUploadedToHealth == false, "Should not be uploaded to Health")
+        #expect(bolusEvent?.isUploadedToTidepool == false, "Should not be uploaded to Tidepool")
+        #expect(bolusEvent?.bolus?.isSMB == true, "Should be a SMB")
+        #expect(bolusEvent?.bolus?.isExternal == false, "Should not be external insulin")
+
+        // Verify temp basal event
+        let tempBasalEvent = finalEntries.first {
+            $0.type == PumpEvent.tempBasal.rawValue &&
+                abs($0.timestamp?.timeIntervalSince(date) ?? 1) < 1
+        }
+        #expect(tempBasalEvent != nil, "Should have found temp basal event")
+        #expect(tempBasalEvent?.tempBasal?.rate as? Decimal == 1.2, "Temp basal rate should be 1.2")
+        #expect(tempBasalEvent?.tempBasal?.duration == 30, "Temp basal duration should be 30 minutes")
+        #expect(tempBasalEvent?.isUploadedToNS == false, "Should not be uploaded to NS")
+        #expect(tempBasalEvent?.isUploadedToHealth == false, "Should not be uploaded to Health")
+        #expect(bolusEvent?.isUploadedToTidepool == false, "Should not be uploaded to Tidepool")
+    }
+
+    @Test("Test store function for manual boluses") func testStorePumpEventsWithManualBoluses() async throws {
+        // Given
+        let date = Date()
+
+        // Insert mock entry
+        let events: [LoopKit.NewPumpEvent] = [
+            LoopKit.NewPumpEvent(
+                date: date,
+                dose: LoopKit.DoseEntry(
+                    type: .bolus,
+                    startDate: date,
+                    value: 4,
+                    unit: .units,
+                    deliveredUnits: nil,
+                    description: nil,
+                    syncIdentifier: nil,
+                    scheduledBasalRate: nil,
+                    insulinType: .lyumjev,
+                    automatic: false,
+                    manuallyEntered: false,
+                    isMutable: false
+                ),
+                raw: Data(),
+                title: "Test Bolus",
+                type: .bolus
+            )
+        ]
+
+        // Store test event and wait for storage to complete the task
+        try await storage.storePumpEvents(events)
+
+        // When - Fetch events with our generic fetch function
+        let fetchedEvents = try await coreDataStack.fetchEntitiesAsync(
+            ofType: PumpEventStored.self,
+            onContext: testContext,
+            predicate: NSPredicate(
+                format: "type == %@ AND timestamp == %@",
+                PumpEvent.bolus.rawValue,
+                date as NSDate
+            ),
+            key: "timestamp",
+            ascending: false
+        )
+
+        guard let fetchedEvents = fetchedEvents as? [PumpEventStored] else { return }
+
+        // Then
+        #expect(fetchedEvents.count == 1, "Should have found exactly one event")
+        let fetchedEvent = fetchedEvents.first
+        #expect(fetchedEvent?.type == PumpEvent.bolus.rawValue, "Should be a bolus event")
+        #expect(fetchedEvent?.bolus?.amount as? Decimal == 4, "Bolus amount should be 4 U")
+        #expect(
+            abs(fetchedEvent?.timestamp?.timeIntervalSince(date) ?? 1) < 1,
+            "Timestamp should match"
+        )
+        #expect(fetchedEvent?.bolus?.isSMB == false, "Should not be a SMB")
+        #expect(fetchedEvent?.bolus?.isExternal == false, "Should not be external Insulin")
+        #expect(fetchedEvent?.isUploadedToNS == false, "Should not be uploaded to NS")
+        #expect(fetchedEvent?.isUploadedToHealth == false, "Should not be uploaded to Health")
+        #expect(fetchedEvent?.isUploadedToTidepool == false, "Should not be uploaded to Tidepool")
+    }
+}

+ 148 - 0
TrioTests/CoreDataTests/TempTargetStorageTests.swift

@@ -0,0 +1,148 @@
+import CoreData
+import Foundation
+import Swinject
+import Testing
+
+@testable import Trio
+
+@Suite("TempTargetStorage Tests", .serialized) struct TempTargetsStorageTests: Injectable {
+    @Injected() var storage: TempTargetsStorage!
+    let resolver: Resolver
+    let coreDataStack = CoreDataStack.createForTests()
+    let testContext: NSManagedObjectContext
+
+    init() {
+        // Create test context
+        testContext = coreDataStack.newTaskContext()
+
+        // Create assembler with test assembly
+        let assembler = Assembler([
+            StorageAssembly(),
+            ServiceAssembly(),
+            APSAssembly(),
+            NetworkAssembly(),
+            UIAssembly(),
+            SecurityAssembly(),
+            TestAssembly(testContext: testContext) // Add our test assembly last to override TempTargetStorage
+        ])
+
+        resolver = assembler.resolver
+        injectServices(resolver)
+    }
+
+    @Test("Storage is correctly initialized") func testStorageInitialization() {
+        // Verify storage exists
+        #expect(storage != nil, "TempTargetsStorage should be injected")
+
+        // Verify it's the correct type
+        #expect(
+            storage is BaseTempTargetsStorage, "Storage should be of type BaseTempTargetsStorage"
+        )
+    }
+
+    @Test("Store and retrieve temp target") func testStoreAndRetrieveTempTarget() async throws {
+        // Given
+        let testTarget = TempTarget(
+            name: "Test Target",
+            createdAt: Date(),
+            targetTop: 120,
+            targetBottom: 120,
+            duration: 60,
+            enteredBy: "Test",
+            reason: "Testing",
+            isPreset: false,
+            halfBasalTarget: 160
+        )
+
+        // When
+        try await storage.storeTempTarget(tempTarget: testTarget)
+
+        // Then verify stored entries
+        let storedEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: TempTargetStored.self,
+            onContext: testContext,
+            predicate: NSPredicate(format: "name == %@", "Test Target"),
+            key: "date",
+            ascending: false
+        ) as? [TempTargetStored]
+
+        #expect(storedEntries?.isEmpty == false, "Should have stored entries")
+        #expect(storedEntries?.count == 1, "Should have exactly one entry")
+        let storedTarget = storedEntries?.first
+        #expect(storedTarget?.name == "Test Target", "Name should match")
+        #expect(storedTarget?.target == 120, "Target should match")
+        #expect(storedTarget?.duration == 60, "Duration should match")
+    }
+
+    @Test("Delete temp target Preset") func testDeleteTempTarget() async throws {
+        // Given
+        let testTarget = TempTarget(
+            name: "Delete Test",
+            createdAt: Date(),
+            targetTop: 120,
+            targetBottom: 120,
+            duration: 60,
+            enteredBy: "Test",
+            reason: "Testing deletion of a preset",
+            isPreset: true,
+            halfBasalTarget: 160
+        )
+        // Store the target
+        try await storage.storeTempTarget(tempTarget: testTarget)
+
+        // Get the stored target's ObjectID
+        let storedEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: TempTargetStored.self,
+            onContext: testContext,
+            predicate: NSPredicate(format: "name == %@", "Delete Test"),
+            key: "date",
+            ascending: false
+        ) as? [TempTargetStored]
+
+        guard let objectID = storedEntries?.first?.objectID else {
+            throw TestError("Failed to get stored target's ObjectID")
+        }
+
+        // When
+        await storage.deleteTempTargetPreset(objectID)
+
+        // Then verify deletion
+        let remainingEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: TempTargetStored.self,
+            onContext: testContext,
+            predicate: NSPredicate(format: "name == %@", "Delete Test"),
+            key: "date",
+            ascending: false
+        ) as? [TempTargetStored]
+
+        #expect(remainingEntries?.isEmpty == true, "Should have no entries after deletion")
+    }
+
+    @Test("Get temp targets not yet uploaded to Nightscout") func testGetTempTargetsNotYetUploadedToNightscout() async throws {
+        // Given
+        let testTarget = TempTarget(
+            name: "NS Test",
+            createdAt: Date(),
+            targetTop: 120,
+            targetBottom: 120,
+            duration: 45,
+            enteredBy: "Test",
+            reason: "Testing NS Upload",
+            isPreset: true,
+            enabled: true,
+            halfBasalTarget: 160
+        )
+
+        // When
+        try await storage.storeTempTarget(tempTarget: testTarget)
+        let notUploadedTargets = try await storage.getTempTargetsNotYetUploadedToNightscout()
+
+        // Then
+        #expect(!notUploadedTargets.isEmpty, "Should have targets not uploaded to NS")
+        let target = notUploadedTargets[0]
+        #expect(target.eventType == .nsTempTarget, "Event type should be NS temp target")
+        #expect(target.duration == 45, "Duration should match")
+        #expect(target.targetTop == 120, "Target top should match target")
+        #expect(target.targetBottom == 120, "Target bottom should match target")
+    }
+}

+ 44 - 0
TrioTests/CoreDataTests/TestAssembly.swift

@@ -0,0 +1,44 @@
+import CoreData
+import Foundation
+import Swinject
+@testable import Trio
+
+class TestAssembly: Assembly {
+    private let testContext: NSManagedObjectContext
+
+    init(testContext: NSManagedObjectContext) {
+        self.testContext = testContext
+    }
+
+    func assemble(container: Container) {
+        // Override PumpHistoryStorage registration for tests
+        container.register(PumpHistoryStorage.self) { r in
+            BasePumpHistoryStorage(resolver: r, context: self.testContext)
+        }.inObjectScope(.container)
+
+        // Override DeterminationStorage registration for tests
+        container.register(DeterminationStorage.self) { r in
+            BaseDeterminationStorage(resolver: r, context: self.testContext)
+        }.inObjectScope(.container)
+
+        // Override CarbsStorage registration for tests
+        container.register(CarbsStorage.self) { r in
+            BaseCarbsStorage(resolver: r, context: self.testContext)
+        }.inObjectScope(.container)
+
+        // Override GlucoseStorage registration for tests
+        container.register(GlucoseStorage.self) { r in
+            BaseGlucoseStorage(resolver: r, context: self.testContext)
+        }.inObjectScope(.container)
+
+        // Override TempTargetStorage registration for tests
+        container.register(TempTargetsStorage.self) { r in
+            BaseTempTargetsStorage(resolver: r, context: self.testContext)
+        }.inObjectScope(.container)
+
+        // Override OverrideStorage registration for tests
+        container.register(OverrideStorage.self) { r in
+            BaseOverrideStorage(resolver: r, context: self.testContext)
+        }.inObjectScope(.container)
+    }
+}

+ 127 - 12
TrioTests/FileStorageTests.swift

@@ -1,26 +1,141 @@
+import Foundation
+import Testing
+
 @testable import Trio
-import XCTest
 
-class FileStorageTests: XCTestCase {
-    let fileStorage = BaseFileStorage()
+@Suite("File Storage Tests", .serialized) struct FileStorageTests {
+    let storage = BaseFileStorage()
 
     struct DummyObject: JSON, Equatable {
         let id: String
         let value: Decimal
     }
 
-    func testFileStorageTrio() {
-        let dummyObject = DummyObject(id: "21342Z", value: 78.2)
-        fileStorage.save(dummyObject, as: "dummyObject")
-        let dummyObjectRetrieve = fileStorage.retrieve("dummyObject", as: DummyObject.self)
-        XCTAssertTrue(dummyObject == dummyObjectRetrieve)
+    @Test("Can save and retrieve object") func testSaveAndRetrieve() {
+        // Given
+        let dummy = DummyObject(id: "123", value: 78.2)
+
+        // When
+        storage.save(dummy, as: "dummy")
+        let retrieved = storage.retrieve("dummy", as: DummyObject.self)
+
+        // Then
+        #expect(retrieved == dummy)
+    }
+
+    @Test("Can save and retrieve async") func testAsyncSaveAndRetrieve() async {
+        // Given
+        let dummy = DummyObject(id: "123", value: 78.2)
+
+        // When
+        await storage.saveAsync(dummy, as: "dummy_async")
+        let retrieved = await storage.retrieveAsync("dummy_async", as: DummyObject.self)
+
+        // Then
+        #expect(retrieved == dummy)
+    }
+
+    @Test("Can append single value") func testAppendSingleValue() {
+        // Given
+        let dummy1 = DummyObject(id: "1", value: 10.0)
+        let dummy2 = DummyObject(id: "2", value: 20.0)
+
+        // When
+        storage.save([dummy1], as: "dummies")
+        storage.append(dummy2, to: "dummies")
+
+        // Then
+        let retrieved = storage.retrieve("dummies", as: [DummyObject].self)
+        #expect(retrieved?.count == 2)
+        #expect(retrieved?.contains(dummy1) == true)
+        #expect(retrieved?.contains(dummy2) == true)
+    }
+
+    @Test("Can append multiple values") func testAppendMultipleValues() {
+        // Given
+        let dummy1 = DummyObject(id: "1", value: 10.0)
+        let newDummies = [
+            DummyObject(id: "2", value: 20.0),
+            DummyObject(id: "3", value: 30.0)
+        ]
+
+        // When
+        storage.save([dummy1], as: "dummies_multiple")
+        storage.append(newDummies, to: "dummies_multiple")
+
+        // Then
+        let retrieved = storage.retrieve("dummies_multiple", as: [DummyObject].self)
+        #expect(retrieved?.count == 3)
+    }
+
+    @Test("Can append unique values by key path") func testAppendUniqueByKeyPath() {
+        // Given
+        let dummy1 = DummyObject(id: "1", value: 10.0)
+        let dummy2 = DummyObject(id: "1", value: 20.0) // Same id
+
+        // When
+        storage.save([dummy1], as: "unique_dummies")
+        storage.append(dummy2, to: "unique_dummies", uniqBy: \.id)
+
+        // Then
+        let retrieved = storage.retrieve("unique_dummies", as: [DummyObject].self)
+        #expect(retrieved?.count == 1, "Should not append duplicate id")
+    }
+
+    @Test("Can remove file") func testRemoveFile() {
+        // Given
+        let dummy = DummyObject(id: "123", value: 78.2)
+        storage.save(dummy, as: "to_delete")
+
+        // When
+        storage.remove("to_delete")
+
+        // Then
+        let retrieved = storage.retrieve("to_delete", as: DummyObject.self)
+        #expect(retrieved == nil)
+    }
+
+    @Test("Can rename file") func testRenameFile() {
+        // Given
+        let dummy = DummyObject(id: "123", value: 78.2)
+        storage.save(dummy, as: "old_name")
+
+        // When
+        storage.rename("old_name", to: "new_name")
+
+        // Then
+        let oldRetrieved = storage.retrieve("old_name", as: DummyObject.self)
+        let newRetrieved = storage.retrieve("new_name", as: DummyObject.self)
+
+        #expect(newRetrieved == dummy)
     }
 
-    override func setUpWithError() throws {
-        // Put setup code here. This method is called before the invocation of each test method in the class.
+    @Test("Can execute transaction") func testTransaction() {
+        // Given
+        let dummy = DummyObject(id: "123", value: 78.2)
+
+        // When
+        storage.transaction { storage in
+            storage.save(dummy, as: "transaction_test")
+        }
+
+        // Then
+        let retrieved = storage.retrieve("transaction_test", as: DummyObject.self)
+        #expect(retrieved == dummy)
     }
 
-    override func tearDownWithError() throws {
-        // Put teardown code here. This method is called after the invocation of each test method in the class.
+    @Test("Can parse mmol/L settings to mg/dL") func testParseSettingsToMgdL() {
+        // Given
+        var preferences = Preferences()
+        preferences.threshold_setting = 5.5 // mmol/L
+        storage.save(preferences, as: OpenAPS.Settings.preferences)
+
+        // When
+        let wasParsed = storage.parseOnFileSettingsToMgdL()
+
+        // Then
+        #expect(wasParsed == true)
+        let parsed = storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self)
+        #expect(parsed?.threshold_setting == 100) // default mg/dL value
     }
 }

+ 56 - 36
TrioTests/PluginManagerTests.swift

@@ -1,71 +1,91 @@
+import Foundation
 import Swinject
+import Testing
 @testable import Trio
-import XCTest
 
-class PluginManagerTests: XCTestCase, Injectable {
+@Suite("Plugin Manager Tests", .serialized) struct PluginManagerTests: Injectable {
     let fileStorage = BaseFileStorage()
     @Injected() var pluginManager: PluginManager!
     let resolver = TrioApp().resolver
 
-    override func setUp() {
+    init() {
         injectServices(resolver)
     }
 
-    func testCGMManagerLoad() {
+    @Test("Can load CGM managers") func testCGMManagerLoad() {
+        // Given
         let cgmLoopManagers = pluginManager.availableCGMManagers
-        XCTAssertNotNil(cgmLoopManagers)
-        XCTAssertTrue(!cgmLoopManagers.isEmpty)
+
+        // Then
+        #expect(!cgmLoopManagers.isEmpty, "Should have available CGM managers")
+
+        // When loading valid CGM manager
         if let cgmLoop = cgmLoopManagers.first {
             let cgmLoopManager = pluginManager.getCGMManagerTypeByIdentifier(cgmLoop.identifier)
-            XCTAssertNotNil(cgmLoopManager)
-        } else {
-            XCTFail("Not found CGM loop manager")
+            #expect(cgmLoopManager != nil, "Should load valid CGM manager")
         }
-        /// try to load a Pump manager with a CGM identifier
+
+        // When trying to load CGM manager with pump identifier
         if let cgmLoop = cgmLoopManagers.last {
-            let cgmLoopManager = pluginManager.getPumpManagerTypeByIdentifier(cgmLoop.identifier)
-            XCTAssertNil(cgmLoopManager)
-        } else {
-            XCTFail("Not found CGM loop manager")
+            let invalidManager = pluginManager.getPumpManagerTypeByIdentifier(cgmLoop.identifier)
+            #expect(invalidManager == nil, "Should not load CGM manager with pump identifier")
         }
     }
 
-    func testPumpManagerLoad() {
+    @Test("Can load pump managers") func testPumpManagerLoad() {
+        // Given
         let pumpLoopManagers = pluginManager.availablePumpManagers
-        XCTAssertNotNil(pumpLoopManagers)
-        XCTAssertTrue(!pumpLoopManagers.isEmpty)
+
+        // Then
+        #expect(!pumpLoopManagers.isEmpty, "Should have available pump managers")
+
+        // When loading valid pump manager
         if let pumpLoop = pumpLoopManagers.first {
             let pumpLoopManager = pluginManager.getPumpManagerTypeByIdentifier(pumpLoop.identifier)
-            XCTAssertNotNil(pumpLoopManager)
-        } else {
-            XCTFail("Not found pump loop manager")
+            #expect(pumpLoopManager != nil, "Should load valid pump manager")
         }
-        /// try to load a CGM manager with a pump identifier
+
+        // When trying to load pump manager with CGM identifier
         if let pumpLoop = pumpLoopManagers.last {
-            let pumpLoopManager = pluginManager.getCGMManagerTypeByIdentifier(pumpLoop.identifier)
-            XCTAssertNil(pumpLoopManager)
-        } else {
-            XCTFail("Not found pump loop manager")
+            let invalidManager = pluginManager.getCGMManagerTypeByIdentifier(pumpLoop.identifier)
+            #expect(invalidManager == nil, "Should not load pump manager with CGM identifier")
         }
     }
 
-    func testServiceManagerLoad() {
+    @Test("Can load service managers") func testServiceManagerLoad() {
+        // Given
         let serviceManagers = pluginManager.availableServices
-        XCTAssertNotNil(serviceManagers)
-        XCTAssertTrue(!serviceManagers.isEmpty)
+
+        // Then
+        #expect(!serviceManagers.isEmpty, "Should have available services")
+
+        // When
         if let serviceLoop = serviceManagers.first {
             let serviceManager = pluginManager.getServiceTypeByIdentifier(serviceLoop.identifier)
-            XCTAssertNotNil(serviceManager)
-        } else {
-            XCTFail("Not found Service loop manager")
+            #expect(serviceManager != nil, "Should load valid service manager")
         }
     }
 
-    override func setUpWithError() throws {
-        // Put setup code here. This method is called before the invocation of each test method in the class.
-    }
+    @Test("Available managers have valid descriptors") func testManagerDescriptors() {
+        // Given/When
+        let pumpManagers = pluginManager.availablePumpManagers
+        let cgmManagers = pluginManager.availableCGMManagers
+        let serviceManagers = pluginManager.availableServices
+
+        // Then
+        for manager in pumpManagers {
+            #expect(!manager.identifier.isEmpty, "Pump manager should have non-empty identifier")
+            #expect(!manager.localizedTitle.isEmpty, "Pump manager should have non-empty title")
+        }
 
-    override func tearDownWithError() throws {
-        // Put teardown code here. This method is called after the invocation of each test method in the class.
+        for manager in cgmManagers {
+            #expect(!manager.identifier.isEmpty, "CGM manager should have non-empty identifier")
+            #expect(!manager.localizedTitle.isEmpty, "CGM manager should have non-empty title")
+        }
+
+        for manager in serviceManagers {
+            #expect(!manager.identifier.isEmpty, "Service should have non-empty identifier")
+            #expect(!manager.localizedTitle.isEmpty, "Service should have non-empty title")
+        }
     }
 }

+ 10 - 0
TrioTests/TestError.swift

@@ -0,0 +1,10 @@
+import Foundation
+
+// Custom error type for test failures
+struct TestError: Error {
+    let message: String
+
+    init(_ message: String) {
+        self.message = message
+    }
+}