Przeglądaj źródła

Merge branch 'core-data-sync-trio' of https://github.com/nightscout/Trio-dev into tdd

polscm32 aka Marvout 1 rok temu
rodzic
commit
61fba2500c
79 zmienionych plików z 1912 dodań i 1123 usunięć
  1. 1 1
      DanaKit
  2. 24 0
      FreeAPS.xcodeproj/project.pbxproj
  3. 1 1
      FreeAPS/Resources/javascript/bundle/autosens.js
  4. 1 1
      FreeAPS/Resources/javascript/bundle/autotune-prep.js
  5. 1 1
      FreeAPS/Resources/javascript/bundle/determine-basal.js
  6. 1 1
      FreeAPS/Resources/javascript/bundle/iob.js
  7. 1 1
      FreeAPS/Resources/javascript/bundle/meal.js
  8. 1 1
      FreeAPS/Resources/javascript/bundle/profile.js
  9. 13 11
      FreeAPS/Sources/APS/APSManager.swift
  10. 1 1
      FreeAPS/Sources/APS/DeviceDataManager.swift
  11. 2 1
      FreeAPS/Sources/APS/FetchGlucoseManager.swift
  12. 38 41
      FreeAPS/Sources/APS/Storage/CarbsStorage.swift
  13. 2 1
      FreeAPS/Sources/APS/Storage/GlucoseStorage.swift
  14. 1 0
      FreeAPS/Sources/Application/FreeAPSApp.swift
  15. 2 23
      FreeAPS/Sources/Helpers/MainChartHelper.swift
  16. 4 4
      FreeAPS/Sources/Models/DecimalPickerSettings.swift
  17. 6 3
      FreeAPS/Sources/Models/FreeAPSSettings.swift
  18. 22 0
      FreeAPS/Sources/Models/GlucoseNotificationsOption.swift
  19. 11 8
      FreeAPS/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+Overrides.swift
  20. 1 1
      FreeAPS/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+TempTargets.swift
  21. 1 1
      FreeAPS/Sources/Modules/Adjustments/AdjustmentsStateModel.swift
  22. 42 477
      FreeAPS/Sources/Modules/Adjustments/View/AdjustmentsRootView.swift
  23. 2 2
      FreeAPS/Sources/Modules/Adjustments/View/Overrides/AddOverrideForm.swift
  24. 256 0
      FreeAPS/Sources/Modules/Adjustments/View/Overrides/AdjustmentsRootView+Overrides.swift
  25. 1 1
      FreeAPS/Sources/Modules/Adjustments/View/Overrides/EditOverrideForm.swift
  26. 231 0
      FreeAPS/Sources/Modules/Adjustments/View/TempTargets/AdjustmentsRootView+TempTargets.swift
  27. 7 0
      FreeAPS/Sources/Modules/BasalProfileEditor/BasalProfileEditorStateModel.swift
  28. 11 9
      FreeAPS/Sources/Modules/BasalProfileEditor/View/BasalProfileEditorRootView.swift
  29. 1 1
      FreeAPS/Sources/Modules/BolusCalculatorConfig/View/BolusCalculatorConfigRootView.swift
  30. 3 1
      FreeAPS/Sources/Modules/CarbRatioEditor/View/CarbRatioEditorRootView.swift
  31. 337 33
      FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift
  32. 229 0
      FreeAPS/Sources/Modules/DataTable/View/CarbEntryEditorView.swift
  33. 29 18
      FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift
  34. 2 2
      FreeAPS/Sources/Modules/GlucoseNotificationSettings/GlucoseNotificationSettingsStateModel.swift
  35. 111 64
      FreeAPS/Sources/Modules/GlucoseNotificationSettings/View/GlucoseNotificationSettingsRootView.swift
  36. 0 2
      FreeAPS/Sources/Modules/Home/HomeDataFlow.swift
  37. 0 10
      FreeAPS/Sources/Modules/Home/HomeProvider.swift
  38. 95 0
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/GlucoseTargetSetup.swift
  39. 18 0
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/StartEndMarkerSetup.swift
  40. 6 14
      FreeAPS/Sources/Modules/Home/HomeStateModel.swift
  41. 6 6
      FreeAPS/Sources/Modules/Home/View/Chart/ChartElements/BasalChart.swift
  42. 1 1
      FreeAPS/Sources/Modules/Home/View/Chart/ChartElements/CarbView.swift
  43. 1 1
      FreeAPS/Sources/Modules/Home/View/Chart/ChartElements/CobIobChart.swift
  44. 2 2
      FreeAPS/Sources/Modules/Home/View/Chart/ChartElements/DummyCharts.swift
  45. 4 109
      FreeAPS/Sources/Modules/Home/View/Chart/ChartElements/GlucoseTargetsView.swift
  46. 9 15
      FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift
  47. 13 11
      FreeAPS/Sources/Modules/Home/View/Header/LoopView.swift
  48. 41 19
      FreeAPS/Sources/Modules/Home/View/Header/PumpView.swift
  49. 107 51
      FreeAPS/Sources/Modules/Home/View/HomeRootView.swift
  50. 8 1
      FreeAPS/Sources/Modules/ISFEditor/View/ISFEditorRootView.swift
  51. 5 6
      FreeAPS/Sources/Modules/MealSettings/MealSettingsStateModel.swift
  52. 4 4
      FreeAPS/Sources/Modules/MealSettings/View/MealSettingsRootView.swift
  53. 7 1
      FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift
  54. 12 6
      FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutConnectView.swift
  55. 36 39
      FreeAPS/Sources/Modules/PumpConfig/View/PumpConfigRootView.swift
  56. 6 1
      FreeAPS/Sources/Modules/PumpConfig/View/PumpSetupView.swift
  57. 1 1
      FreeAPS/Sources/Modules/SMBSettings/View/SMBSettingsRootView.swift
  58. 2 2
      FreeAPS/Sources/Modules/Settings/SettingItems.swift
  59. 2 7
      FreeAPS/Sources/Modules/Stat/View/StatsView.swift
  60. 7 0
      FreeAPS/Sources/Modules/TargetsEditor/TargetsEditorStateModel.swift
  61. 22 22
      FreeAPS/Sources/Modules/TargetsEditor/View/TargetsEditorRootView.swift
  62. 2 1
      FreeAPS/Sources/Modules/Treatments/TreatmentsStateModel.swift
  63. 1 1
      FreeAPS/Sources/Modules/Treatments/View/MealPreset/MealPresetView.swift
  64. 19 17
      FreeAPS/Sources/Modules/Treatments/View/TreatmentsRootView.swift
  65. 1 1
      FreeAPS/Sources/Modules/UserInterfaceSettings/View/UserInterfaceSettingsRootView.swift
  66. 14 16
      FreeAPS/Sources/Modules/WatchConfig/View/WatchConfigGarminView.swift
  67. 5 1
      FreeAPS/Sources/Router/Router.swift
  68. 19 18
      FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift
  69. 8 3
      FreeAPS/Sources/Services/Network/Nightscout/NightscoutManager.swift
  70. 1 1
      FreeAPS/Sources/Services/RemoteControl/TrioRemoteControl+Meal.swift
  71. 7 11
      FreeAPS/Sources/Services/UserNotifications/UserNotificationsManager.swift
  72. 1 1
      Model/Classes+Properties/OpenAPS_Battery+CoreDataProperties.swift
  73. 2 2
      Model/Helper/CarbEntryStored+helper.swift
  74. 5 0
      Model/Helper/NSPredicates.swift
  75. 1 1
      Model/TrioCoreDataPersistentContainer.xcdatamodeld/TrioCoreDataPersistentContainer.xcdatamodel/contents
  76. 1 1
      OmniBLE
  77. 1 1
      OmniKit
  78. 11 4
      oref0_source_version.txt
  79. 1 2
      trio-oref/lib/determine-basal/determine-basal.js

+ 1 - 1
DanaKit

@@ -1 +1 @@
-Subproject commit b07f236677b205d31d7ecf6144970348e8d5a3fe
+Subproject commit df0081b6db9e70818638954941916206ecbf6ece

+ 24 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -297,6 +297,7 @@
 		6EADD581738D64431902AC0A /* (null) in Sources */ = {isa = PBXBuildFile; };
 		6FFAE524D1D9C262F2407CAE /* SnoozeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CAE81192B118804DCD23034 /* SnoozeProvider.swift */; };
 		711C0CB42CAABE788916BC9D /* ManualTempBasalDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96653287EDB276A111288305 /* ManualTempBasalDataFlow.swift */; };
+		715120D22D3C2BB4005D9FB6 /* GlucoseNotificationsOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 715120D12D3C2B84005D9FB6 /* GlucoseNotificationsOption.swift */; };
 		71D44AAB2CA5F5EA0036EE9E /* AlertPermissionsChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D44AAA2CA5F5EA0036EE9E /* AlertPermissionsChecker.swift */; };
 		72F1BD388F42FCA6C52E4500 /* ConfigEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44080E4709E3AE4B73054563 /* ConfigEditorProvider.swift */; };
 		7BCFACB97C821041BA43A114 /* ManualTempBasalRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */; };
@@ -322,6 +323,8 @@
 		BD4D738D2D15A4080052227B /* TDDStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4D738B2D15A4080052227B /* TDDStored+CoreDataClass.swift */; };
 		BD4D738E2D15A4080052227B /* TDDStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4D738C2D15A4080052227B /* TDDStored+CoreDataProperties.swift */; };
 		BD4D73A22D15A42A0052227B /* TDDStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4D73A12D15A4220052227B /* TDDStorage.swift */; };
+		BD4E1A7A2D3681B700D21626 /* GlucoseTargetSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4E1A792D3681AD00D21626 /* GlucoseTargetSetup.swift */; };
+		BD4E1A7C2D3686D900D21626 /* StartEndMarkerSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4E1A7B2D3686D400D21626 /* StartEndMarkerSetup.swift */; };
 		BD4ED4FD2CF9D5E8000EDC9C /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4ED4FC2CF9D5E8000EDC9C /* AppState.swift */; };
 		BD6EB2D62C7D049B0086BBB6 /* LiveActivityWidgetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD6EB2D52C7D049B0086BBB6 /* LiveActivityWidgetConfiguration.swift */; };
 		BD793CB02CE7C61500D669AC /* OverrideRunStored+helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD793CAF2CE7C60E00D669AC /* OverrideRunStored+helper.swift */; };
@@ -331,6 +334,7 @@
 		BD7DA9A92AE06E9200601B20 /* BolusCalculatorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */; };
 		BD7DA9AC2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9AB2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift */; };
 		BDA6CC882CAF219B00F942F9 /* TempTargetSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA6CC872CAF219800F942F9 /* TempTargetSetup.swift */; };
+		BDA7593E2D37CFC400E649A4 /* CarbEntryEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA7593D2D37CFC000E649A4 /* CarbEntryEditorView.swift */; };
 		BDB3C1192C03DD1000CEEAA1 /* UserDefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */; };
 		BDB899882C564509006F3298 /* ForecastChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB899872C564509006F3298 /* ForecastChart.swift */; };
 		BDB8998A2C565D0C006F3298 /* CarbsGlucose+helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB899892C565D0B006F3298 /* CarbsGlucose+helper.swift */; };
@@ -487,6 +491,8 @@
 		DDA6E3202D258E0500C2988C /* OverrideHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E31F2D258E0500C2988C /* OverrideHelpView.swift */; };
 		DDA6E3222D25901100C2988C /* TempTargetHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E3212D25901100C2988C /* TempTargetHelpView.swift */; };
 		DDA6E3572D25988500C2988C /* ContactImageHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E3562D25988500C2988C /* ContactImageHelpView.swift */; };
+		DDAA29832D2D1D93006546A1 /* AdjustmentsRootView+Overrides.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAA29822D2D1D7B006546A1 /* AdjustmentsRootView+Overrides.swift */; };
+		DDAA29852D2D1D9E006546A1 /* AdjustmentsRootView+TempTargets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAA29842D2D1D98006546A1 /* AdjustmentsRootView+TempTargets.swift */; };
 		DDB37CC52D05048F00D99BF4 /* ContactImageStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB37CC42D05048F00D99BF4 /* ContactImageStorage.swift */; };
 		DDB37CC72D05127500D99BF4 /* FontExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB37CC62D05127500D99BF4 /* FontExtensions.swift */; };
 		DDCEBF5B2CC1B76400DF4C36 /* LiveActivity+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCEBF5A2CC1B76400DF4C36 /* LiveActivity+Helper.swift */; };
@@ -998,6 +1004,7 @@
 		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>"; };
 		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>"; };
 		79BDA519C9B890FD9A5DFCF3 /* ISFEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorDataFlow.swift; sourceTree = "<group>"; };
 		7E22146D3DF4853786C78132 /* CarbRatioEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CarbRatioEditorDataFlow.swift; sourceTree = "<group>"; };
@@ -1026,6 +1033,8 @@
 		BD4D738B2D15A4080052227B /* TDDStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TDDStored+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; };
 		BD4D738C2D15A4080052227B /* TDDStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TDDStored+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; };
 		BD4D73A12D15A4220052227B /* TDDStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TDDStorage.swift; sourceTree = "<group>"; };
+		BD4E1A792D3681AD00D21626 /* GlucoseTargetSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseTargetSetup.swift; sourceTree = "<group>"; };
+		BD4E1A7B2D3686D400D21626 /* StartEndMarkerSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartEndMarkerSetup.swift; sourceTree = "<group>"; };
 		BD4ED4FC2CF9D5E8000EDC9C /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
 		BD6EB2D52C7D049B0086BBB6 /* LiveActivityWidgetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityWidgetConfiguration.swift; sourceTree = "<group>"; };
 		BD793CAF2CE7C60E00D669AC /* OverrideRunStored+helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OverrideRunStored+helper.swift"; sourceTree = "<group>"; };
@@ -1035,6 +1044,7 @@
 		BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorStateModel.swift; sourceTree = "<group>"; };
 		BD7DA9AB2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigRootView.swift; sourceTree = "<group>"; };
 		BDA6CC872CAF219800F942F9 /* TempTargetSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetSetup.swift; sourceTree = "<group>"; };
+		BDA7593D2D37CFC000E649A4 /* CarbEntryEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbEntryEditorView.swift; sourceTree = "<group>"; };
 		BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsExtension.swift; sourceTree = "<group>"; };
 		BDB899872C564509006F3298 /* ForecastChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastChart.swift; sourceTree = "<group>"; };
 		BDB899892C565D0B006F3298 /* CarbsGlucose+helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CarbsGlucose+helper.swift"; sourceTree = "<group>"; };
@@ -1193,6 +1203,8 @@
 		DDA6E31F2D258E0500C2988C /* OverrideHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideHelpView.swift; sourceTree = "<group>"; };
 		DDA6E3212D25901100C2988C /* TempTargetHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetHelpView.swift; sourceTree = "<group>"; };
 		DDA6E3562D25988500C2988C /* ContactImageHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactImageHelpView.swift; sourceTree = "<group>"; };
+		DDAA29822D2D1D7B006546A1 /* AdjustmentsRootView+Overrides.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentsRootView+Overrides.swift"; sourceTree = "<group>"; };
+		DDAA29842D2D1D98006546A1 /* AdjustmentsRootView+TempTargets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentsRootView+TempTargets.swift"; sourceTree = "<group>"; };
 		DDB37CC22D05044D00D99BF4 /* ContactTrickEntryStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContactTrickEntryStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		DDB37CC32D05044D00D99BF4 /* ContactTrickEntryStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContactTrickEntryStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDB37CC42D05048F00D99BF4 /* ContactImageStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactImageStorage.swift; sourceTree = "<group>"; };
@@ -1359,6 +1371,7 @@
 		0EE66DD474AFFD4FD787D5B9 /* View */ = {
 			isa = PBXGroup;
 			children = (
+				BDA7593D2D37CFC000E649A4 /* CarbEntryEditorView.swift */,
 				881E04BA5E0A003DE8E0A9C6 /* DataTableRootView.swift */,
 			);
 			path = View;
@@ -1999,6 +2012,7 @@
 		388E5A5925B6F0250019842D /* Models */ = {
 			isa = PBXGroup;
 			children = (
+				715120D12D3C2B84005D9FB6 /* GlucoseNotificationsOption.swift */,
 				DD07CA132CE80B73002D45A9 /* TimeInRangeChartStyle.swift */,
 				DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */,
 				DD6D67E32C9C253500660C9B /* ColorSchemeOption.swift */,
@@ -2359,6 +2373,8 @@
 		58645B972CA2D16A008AFCE7 /* HomeStateModel+Setup */ = {
 			isa = PBXGroup;
 			children = (
+				BD4E1A7B2D3686D400D21626 /* StartEndMarkerSetup.swift */,
+				BD4E1A792D3681AD00D21626 /* GlucoseTargetSetup.swift */,
 				BDA6CC872CAF219800F942F9 /* TempTargetSetup.swift */,
 				58645B982CA2D1A4008AFCE7 /* GlucoseSetup.swift */,
 				58645B9A2CA2D24F008AFCE7 /* CarbSetup.swift */,
@@ -2474,6 +2490,7 @@
 		BD793CAD2CE7660C00D669AC /* Overrides */ = {
 			isa = PBXGroup;
 			children = (
+				DDAA29822D2D1D7B006546A1 /* AdjustmentsRootView+Overrides.swift */,
 				DDA6E31F2D258E0500C2988C /* OverrideHelpView.swift */,
 				DDD1631B2C4C697400CD525A /* AddOverrideForm.swift */,
 				DDD163192C4C695E00CD525A /* EditOverrideForm.swift */,
@@ -2487,6 +2504,7 @@
 				DDA6E3212D25901100C2988C /* TempTargetHelpView.swift */,
 				58A3D5392C96D4DE003F90FC /* AddTempTargetForm.swift */,
 				5825A1BD2C97335C0046467E /* EditTempTargetForm.swift */,
+				DDAA29842D2D1D98006546A1 /* AdjustmentsRootView+TempTargets.swift */,
 			);
 			path = TempTargets;
 			sourceTree = "<group>";
@@ -3537,6 +3555,7 @@
 				3811DEE825CA063400A708ED /* Injected.swift in Sources */,
 				DD1745152C54388A00211FAC /* TherapySettingsView.swift in Sources */,
 				585E2CAE2BE7BF46006ECF1A /* PumpEvent+helper.swift in Sources */,
+				DDAA29832D2D1D93006546A1 /* AdjustmentsRootView+Overrides.swift in Sources */,
 				DDD1631F2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodeld in Sources */,
 				DD1745482C55C61D00211FAC /* AutosensSettingsStateModel.swift in Sources */,
 				DD1745462C55C61500211FAC /* AutosensSettingsProvider.swift in Sources */,
@@ -3623,6 +3642,7 @@
 				190EBCC829FF13AA00BA767D /* UserInterfaceSettingsStateModel.swift in Sources */,
 				DDF847EA2C5DABAC0049BB3B /* WatchConfigGarminView.swift in Sources */,
 				38BF021F25E7F0DE00579895 /* DeviceDataManager.swift in Sources */,
+				BD4E1A7A2D3681B700D21626 /* GlucoseTargetSetup.swift in Sources */,
 				BDB3C1192C03DD1000CEEAA1 /* UserDefaultsExtension.swift in Sources */,
 				38A504A425DD9C4000C5B9E8 /* UserDefaultsExtensions.swift in Sources */,
 				38FE826A25CC82DB001FF17A /* NetworkService.swift in Sources */,
@@ -3785,6 +3805,7 @@
 				CE7CA34F2A064973004BE681 /* BaseIntentsRequest.swift in Sources */,
 				E592A3772CEEC038009A472C /* ContactImageStateModel.swift in Sources */,
 				E592A3782CEEC038009A472C /* ContactImageDataFlow.swift in Sources */,
+				DDAA29852D2D1D9E006546A1 /* AdjustmentsRootView+TempTargets.swift in Sources */,
 				E592A3792CEEC038009A472C /* ContactImageRootView.swift in Sources */,
 				BDC531182D1062F200088832 /* ContactImageState.swift in Sources */,
 				E592A37A2CEEC038009A472C /* ContactImageProvider.swift in Sources */,
@@ -3799,6 +3820,7 @@
 				DD32CF9E2CC824C5003686D6 /* TrioRemoteControl+Override.swift in Sources */,
 				23888883D4EA091C88480FF2 /* TreatmentsProvider.swift in Sources */,
 				38E98A2D25F52DC400C0CED0 /* NSLocking+Extensions.swift in Sources */,
+				715120D22D3C2BB4005D9FB6 /* GlucoseNotificationsOption.swift in Sources */,
 				BDBAACFA2C2D439700370AAE /* OverrideData.swift in Sources */,
 				DD9ECB682CA99F4500AA7C45 /* TrioRemoteControl.swift in Sources */,
 				38569353270B5E350002C50D /* CGMRootView.swift in Sources */,
@@ -3807,6 +3829,7 @@
 				19D4E4EB29FC6A9F00351451 /* Charts.swift in Sources */,
 				BDC531162D10629000088832 /* ContactPicture.swift in Sources */,
 				FEFFA7A22929FE49007B8193 /* UIDevice+Extensions.swift in Sources */,
+				BD4E1A7C2D3686D900D21626 /* StartEndMarkerSetup.swift in Sources */,
 				F90692D3274B9A130037068D /* AppleHealthKitRootView.swift in Sources */,
 				BDF34F852C10C62E00D51995 /* GlucoseData.swift in Sources */,
 				19E1F7EC29D082FE005C8D20 /* IconConfigStateModel.swift in Sources */,
@@ -3818,6 +3841,7 @@
 				BD1661312B82ADAB00256551 /* CustomProgressView.swift in Sources */,
 				C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */,
 				38E4453B274E411700EC9A94 /* Disk+VolumeInformation.swift in Sources */,
+				BDA7593E2D37CFC400E649A4 /* CarbEntryEditorView.swift in Sources */,
 				118DF76A2C5ECBC60067FEB7 /* ApplyOverridePresetIntent.swift in Sources */,
 				58645B992CA2D1A4008AFCE7 /* GlucoseSetup.swift in Sources */,
 				7BCFACB97C821041BA43A114 /* ManualTempBasalRootView.swift in Sources */,

Plik diff jest za duży
+ 1 - 1
FreeAPS/Resources/javascript/bundle/autosens.js


Plik diff jest za duży
+ 1 - 1
FreeAPS/Resources/javascript/bundle/autotune-prep.js


Plik diff jest za duży
+ 1 - 1
FreeAPS/Resources/javascript/bundle/determine-basal.js


Plik diff jest za duży
+ 1 - 1
FreeAPS/Resources/javascript/bundle/iob.js


Plik diff jest za duży
+ 1 - 1
FreeAPS/Resources/javascript/bundle/meal.js


Plik diff jest za duży
+ 1 - 1
FreeAPS/Resources/javascript/bundle/profile.js


+ 13 - 11
FreeAPS/Sources/APS/APSManager.swift

@@ -1192,16 +1192,18 @@ final class BaseAPSManager: APSManager, Injectable {
             let hbA1cDisplayUnit = self.settingsManager.settings.hbA1cDisplayUnit
 
             let hbs = Durations(
-                day: ((units == .mmolL && hbA1cDisplayUnit == .mmolMol) || (units == .mgdL && hbA1cDisplayUnit == .percent)) ?
-                    self.roundDecimal(Decimal(oneDayGlucose.ifcc), 1) : self.roundDecimal(Decimal(oneDayGlucose.ngsp), 1),
-                week: ((units == .mmolL && hbA1cDisplayUnit == .mmolMol) || (units == .mgdL && hbA1cDisplayUnit == .percent)) ?
-                    self.roundDecimal(Decimal(sevenDaysGlucose.ifcc), 1) : self
-                    .roundDecimal(Decimal(sevenDaysGlucose.ngsp), 1),
-                month: ((units == .mmolL && hbA1cDisplayUnit == .mmolMol) || (units == .mgdL && hbA1cDisplayUnit == .percent)) ?
-                    self.roundDecimal(Decimal(thirtyDaysGlucose.ifcc), 1) : self
-                    .roundDecimal(Decimal(thirtyDaysGlucose.ngsp), 1),
-                total: ((units == .mmolL && hbA1cDisplayUnit == .mmolMol) || (units == .mgdL && hbA1cDisplayUnit == .percent)) ?
-                    self.roundDecimal(Decimal(totalDaysGlucose.ifcc), 1) : self.roundDecimal(Decimal(totalDaysGlucose.ngsp), 1)
+                day: hbA1cDisplayUnit == .mmolMol ?
+                    self.roundDecimal(Decimal(oneDayGlucose.ifcc), 1) :
+                    self.roundDecimal(Decimal(oneDayGlucose.ngsp), 1),
+                week: hbA1cDisplayUnit == .mmolMol ?
+                    self.roundDecimal(Decimal(sevenDaysGlucose.ifcc), 1) :
+                    self.roundDecimal(Decimal(sevenDaysGlucose.ngsp), 1),
+                month: hbA1cDisplayUnit == .mmolMol ?
+                    self.roundDecimal(Decimal(thirtyDaysGlucose.ifcc), 1) :
+                    self.roundDecimal(Decimal(thirtyDaysGlucose.ngsp), 1),
+                total: hbA1cDisplayUnit == .mmolMol ?
+                    self.roundDecimal(Decimal(totalDaysGlucose.ifcc), 1) :
+                    self.roundDecimal(Decimal(totalDaysGlucose.ngsp), 1)
             )
 
             var oneDay_: (TIR: Double, hypos: Double, hypers: Double, normal_: Double) = (0.0, 0.0, 0.0, 0.0)
@@ -1411,7 +1413,7 @@ extension BaseAPSManager: PumpManagerStatusObserver {
                 }
 
                 batteryToStore.date = Date()
-                batteryToStore.percent = Int16(percent)
+                batteryToStore.percent = Double(percent)
                 batteryToStore.voltage = nil
                 batteryToStore.status = percent > 10 ? "normal" : "low"
                 batteryToStore.display = status.pumpBatteryChargeRemaining != nil

+ 1 - 1
FreeAPS/Sources/APS/DeviceDataManager.swift

@@ -132,7 +132,7 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
                             let saveBatteryToCoreData = OpenAPS_Battery(context: self.privateContext)
                             saveBatteryToCoreData.id = UUID()
                             saveBatteryToCoreData.date = Date()
-                            saveBatteryToCoreData.percent = Int16(batteryPercent)
+                            saveBatteryToCoreData.percent = Double(batteryPercent)
                             saveBatteryToCoreData.voltage = nil
                             saveBatteryToCoreData.status = batteryPercent >= 10 ? BatteryState.normal.rawValue : BatteryState
                                 .low.rawValue

+ 2 - 1
FreeAPS/Sources/APS/FetchGlucoseManager.swift

@@ -213,7 +213,8 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
                     unfiltered: Decimal(result.glucose),
                     filtered: Decimal(result.glucose),
                     noise: nil,
-                    glucose: Int(result.glucose)
+                    glucose: Int(result.glucose),
+                    type: "sgv"
                 )
             }
         }

+ 38 - 41
FreeAPS/Sources/APS/Storage/CarbsStorage.swift

@@ -11,12 +11,11 @@ protocol CarbsObserver {
 protocol CarbsStorage {
     var updatePublisher: AnyPublisher<Void, Never> { get }
     func storeCarbs(_ carbs: [CarbsEntry], areFetchedFromRemote: Bool) async
-    func deleteCarbs(_ treatmentObjectID: NSManagedObjectID) async
+    func deleteCarbsEntryStored(_ treatmentObjectID: NSManagedObjectID) async
     func syncDate() -> Date
     func recent() -> [CarbsEntry]
     func getCarbsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
     func getFPUsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
-    func deleteCarbs(at uniqueID: String, fpuID: String, complex: Bool)
     func getCarbsNotYetUploadedToHealth() async -> [CarbsEntry]
     func getCarbsNotYetUploadedToTidepool() async -> [CarbsEntry]
 }
@@ -46,8 +45,29 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             entriesToStore = await filterRemoteEntries(entries: entriesToStore)
         }
 
-        await saveCarbsToCoreData(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
+        // Check for FPU-only entries (fat/protein without carbs)
+        let fpuOnlyEntries = entriesToStore.filter { entry in
+            entry.carbs == 0 && (entry.fat ?? 0 > 0 || entry.protein ?? 0 > 0)
+        }
+
+        // Create additional Carb (non-FPU) entries with fat/protein amounts and carbs == 0
+        for entry in fpuOnlyEntries {
+            let additionalEntry = CarbsEntry(
+                id: entry.id,
+                createdAt: entry.createdAt,
+                actualDate: entry.actualDate,
+                carbs: Decimal(0),
+                fat: entry.fat,
+                protein: entry.protein,
+                note: entry.note,
+                enteredBy: entry.enteredBy,
+                isFPU: false, // it should be a Carb entry
+                fpuID: entry.fpuID
+            )
+            entriesToStore.append(additionalEntry)
+        }
 
+        await saveCarbsToCoreData(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
         await saveCarbEquivalents(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
     }
 
@@ -195,7 +215,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
     }
 
     private func saveCarbsToCoreData(entries: [CarbsEntry], areFetchedFromRemote: Bool) async {
-        guard let entry = entries.last, entry.carbs != 0 else { return }
+        guard let entry = entries.last else { return }
 
         await coredataContext.perform {
             let newItem = CarbEntryStored(context: self.coredataContext)
@@ -265,22 +285,26 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self)?.reversed() ?? []
     }
 
-    func deleteCarbs(_ treatmentObjectID: NSManagedObjectID) async {
+    func deleteCarbsEntryStored(_ treatmentObjectID: NSManagedObjectID) async {
         let taskContext = CoreDataStack.shared.newTaskContext()
         taskContext.name = "deleteContext"
         taskContext.transactionAuthor = "deleteCarbs"
 
-        var carbEntry: CarbEntryStored?
+        var carbEntryFromCoreData: CarbEntryStored?
 
         await taskContext.perform {
             do {
-                carbEntry = try taskContext.existingObject(with: treatmentObjectID) as? CarbEntryStored
-                guard let carbEntry = carbEntry else {
+                carbEntryFromCoreData = try taskContext.existingObject(with: treatmentObjectID) as? CarbEntryStored
+                guard let carbEntry = carbEntryFromCoreData else {
                     debugPrint("Carb entry for batch delete not found. \(DebuggingIdentifiers.failed)")
                     return
                 }
 
-                if carbEntry.isFPU, let fpuID = carbEntry.fpuID {
+                // entry has fpuID
+                // case 1: carb equivalent entry
+                // case 2: "parent" entry, but containing fat and/or protein, and possibly carbs
+                // => use fpuID ID to delete all corresponding entries via batch delete
+                if let fpuID = carbEntry.fpuID {
                     // fetch request for all carb entries with the same id
                     let fetchRequest: NSFetchRequest<NSFetchRequestResult> = CarbEntryStored.fetchRequest()
                     fetchRequest.predicate = NSPredicate(format: "fpuID == %@", fpuID as CVarArg)
@@ -295,14 +319,17 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
 
                     // Notifiy subscribers of the batch delete
                     self.updateSubject.send(())
-                } else {
+                }
+                // entry has no fpuID
+                // => it's a carb-only entry. use its ID to for deletion
+                else {
                     taskContext.delete(carbEntry)
 
                     guard taskContext.hasChanges else { return }
                     try taskContext.save()
 
                     debugPrint(
-                        "Data Table State: \(#function) \(DebuggingIdentifiers.succeeded) deleted carb entry from core data"
+                        "CarbsStorage: \(#function) \(DebuggingIdentifiers.succeeded) deleted carb entry from core data"
                     )
                 }
 
@@ -312,36 +339,6 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         }
     }
 
-    func deleteCarbs(at uniqueID: String, fpuID: String, complex: Bool) {
-        processQueue.sync {
-            var allValues = storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self) ?? []
-
-            if fpuID != "" {
-                if allValues.firstIndex(where: { $0.fpuID == fpuID }) == nil {
-                    debug(.default, "Didn't find any carb equivalents to delete. ID to search for: " + fpuID.description)
-                } else {
-                    allValues.removeAll(where: { $0.fpuID == fpuID })
-                    storage.save(allValues, as: OpenAPS.Monitor.carbHistory)
-                    broadcaster.notify(CarbsObserver.self, on: processQueue) {
-                        $0.carbsDidUpdate(allValues)
-                    }
-                }
-            }
-
-            if fpuID == "" || complex {
-                if allValues.firstIndex(where: { $0.id == uniqueID }) == nil {
-                    debug(.default, "Didn't find any carb entries to delete. ID to search for: " + uniqueID.description)
-                } else {
-                    allValues.removeAll(where: { $0.id == uniqueID })
-                    storage.save(allValues, as: OpenAPS.Monitor.carbHistory)
-                    broadcaster.notify(CarbsObserver.self, on: processQueue) {
-                        $0.carbsDidUpdate(allValues)
-                    }
-                }
-            }
-        }
-    }
-
     func getCarbsNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,

+ 2 - 1
FreeAPS/Sources/APS/Storage/GlucoseStorage.swift

@@ -297,7 +297,8 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                     unfiltered: Decimal(result.glucose),
                     filtered: Decimal(result.glucose),
                     noise: nil,
-                    glucose: Int(result.glucose)
+                    glucose: Int(result.glucose),
+                    type: "sgv"
                 )
             }
         }

+ 1 - 0
FreeAPS/Sources/Application/FreeAPSApp.swift

@@ -49,6 +49,7 @@ import Swinject
         _ = resolver.resolve(CalendarManager.self)!
         _ = resolver.resolve(UserNotificationsManager.self)!
         _ = resolver.resolve(WatchManager.self)!
+        _ = resolver.resolve(ContactImageManager.self)!
         _ = resolver.resolve(HealthKitManager.self)!
         _ = resolver.resolve(BluetoothStateManager.self)!
         _ = resolver.resolve(PluginManager.self)!

+ 2 - 23
FreeAPS/Sources/Helpers/MainChartHelper.swift

@@ -109,7 +109,7 @@ extension MainChartView {
         RuleMark(
             x: .value(
                 "",
-                startMarker,
+                state.startMarker,
                 unit: .second
             )
         ).foregroundStyle(Color.clear)
@@ -119,7 +119,7 @@ extension MainChartView {
         RuleMark(
             x: .value(
                 "",
-                endMarker,
+                state.endMarker,
                 unit: .second
             )
         ).foregroundStyle(Color.clear)
@@ -181,29 +181,8 @@ extension MainChartView {
             }
         }
     }
-}
-
-// MARK: - Calculations and formatting
 
-extension MainChartView {
     func fullWidth(viewWidth: CGFloat) -> CGFloat {
         viewWidth * CGFloat(hours) / CGFloat(min(max(screenHours, 2), 24))
     }
-
-    // Update start and  end marker to fix scroll update problem with x axis
-    func updateStartEndMarkers() {
-        startMarker = Date(timeIntervalSince1970: TimeInterval(NSDate().timeIntervalSince1970 - 86400))
-
-        let threeHourSinceNow = Date(timeIntervalSinceNow: TimeInterval(hours: 3))
-
-        // min is 1.5h -> (1.5*1h = 1.5*(5*12*60))
-        let dynamicFutureDateForCone = Date(timeIntervalSinceNow: TimeInterval(
-            Int(1.5) * 5 * state
-                .minCount * 60
-        ))
-
-        endMarker = state
-            .forecastDisplayType == .lines ? threeHourSinceNow : dynamicFutureDateForCone <= threeHourSinceNow ?
-            dynamicFutureDateForCone.addingTimeInterval(TimeInterval(minutes: 30)) : threeHourSinceNow
-    }
 }

+ 4 - 4
FreeAPS/Sources/Models/DecimalPickerSettings.swift

@@ -40,7 +40,7 @@ struct DecimalPickerSettings {
         value: 0.5,
         step: 0.05,
         min: 0.1,
-        max: 2,
+        max: 1.2,
         type: PickerSetting.PickerSettingType.factor
     )
     var high = PickerSetting(value: 180, step: 1, min: 100, max: 500, type: PickerSetting.PickerSettingType.glucose)
@@ -130,13 +130,13 @@ struct DecimalPickerSettings {
     )
     var threshold_setting = PickerSetting(value: 60, step: 1, min: 60, max: 120, type: PickerSetting.PickerSettingType.glucose)
     var updateInterval = PickerSetting(value: 20, step: 5, min: 1, max: 60, type: PickerSetting.PickerSettingType.minute)
-    var delay = PickerSetting(value: 60, step: 15, min: 30, max: 120, type: PickerSetting.PickerSettingType.minute)
-    var minuteInterval = PickerSetting(value: 20, step: 5, min: 5, max: 60, type: PickerSetting.PickerSettingType.minute)
+    var delay = PickerSetting(value: 60, step: 10, min: 60, max: 120, type: PickerSetting.PickerSettingType.minute)
+    var minuteInterval = PickerSetting(value: 30, step: 5, min: 10, max: 60, type: PickerSetting.PickerSettingType.minute)
     var timeCap = PickerSetting(value: 8, step: 1, min: 5, max: 12, type: PickerSetting.PickerSettingType.hour)
     var hours = PickerSetting(value: 6, step: 0.5, min: 2, max: 24, type: PickerSetting.PickerSettingType.hour)
     var dia = PickerSetting(value: 10, step: 0.5, min: 5, max: 10, type: PickerSetting.PickerSettingType.hour)
     var maxBolus = PickerSetting(value: 10, step: 0.5, min: 1, max: 30, type: PickerSetting.PickerSettingType.insulinUnit)
-    var maxBasal = PickerSetting(value: 10, step: 0.5, min: 1, max: 30, type: PickerSetting.PickerSettingType.insulinUnit)
+    var maxBasal = PickerSetting(value: 10, step: 0.5, min: 0.5, max: 30, type: PickerSetting.PickerSettingType.insulinUnit)
 }
 
 struct PickerSetting {

+ 6 - 3
FreeAPS/Sources/Models/FreeAPSSettings.swift

@@ -35,7 +35,7 @@ struct FreeAPSSettings: JSON, Equatable {
     var notificationsCgm: Bool = true
     var notificationsCarb: Bool = true
     var notificationsAlgorithm: Bool = true
-    var glucoseNotificationsAlways: Bool = false
+    var glucoseNotificationsOption: GlucoseNotificationsOption = .onlyAlarmLimits
     var useAlarmSound: Bool = false
     var addSourceInfoToGlucoseNotifications: Bool = false
     var lowGlucose: Decimal = 72
@@ -213,8 +213,11 @@ extension FreeAPSSettings: Decodable {
             settings.notificationsAlgorithm = notificationsAlgorithm
         }
 
-        if let glucoseNotificationsAlways = try? container.decode(Bool.self, forKey: .glucoseNotificationsAlways) {
-            settings.glucoseNotificationsAlways = glucoseNotificationsAlways
+        if let glucoseNotificationsOption = try? container.decode(
+            GlucoseNotificationsOption.self,
+            forKey: .glucoseNotificationsOption
+        ) {
+            settings.glucoseNotificationsOption = glucoseNotificationsOption
         }
 
         if let useAlarmSound = try? container.decode(Bool.self, forKey: .useAlarmSound) {

+ 22 - 0
FreeAPS/Sources/Models/GlucoseNotificationsOption.swift

@@ -0,0 +1,22 @@
+//
+//  GlucoseNotificationOption.swift
+//  FreeAPS
+//
+//  Created by Kimberlie Skandis on 1/18/25.
+//
+import Foundation
+
+public enum GlucoseNotificationsOption: String, JSON, CaseIterable, Identifiable, Codable, Hashable {
+    public var id: String { rawValue }
+    case disabled
+    case alwaysEveryCGM
+    case onlyAlarmLimits
+
+    var displayName: String {
+        switch self {
+        case .disabled: return "Disabled"
+        case .alwaysEveryCGM: return "Always"
+        case .onlyAlarmLimits: return "Only Alarm Limits"
+        }
+    }
+}

+ 11 - 8
FreeAPS/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+Overrides.swift

@@ -12,7 +12,7 @@ extension Adjustments.StateModel {
             overrideToEnact?.enabled = true
             overrideToEnact?.date = Date()
             overrideToEnact?.isUploadedToNS = false
-            isEnabled = true
+            isOverrideEnabled = true
 
             await disableAllActiveOverrides(except: id, createOverrideRunEntry: currentActiveOverride != nil)
             await resetStateVariables()
@@ -187,11 +187,14 @@ extension Adjustments.StateModel {
     /// Then unpack it on the view context and update the State variables which can be used on in the View for some Logic
     /// This also needs to be called when we cancel an Override via the Home View to update the State of the Button for this case
     func updateLatestOverrideConfiguration() {
-        Task {
-            let id = await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 1)
-            async let updateState: () = updateLatestOverrideConfigurationOfState(from: id)
-            async let setOverride: () = setCurrentOverride(from: id)
-            _ = await (updateState, setOverride)
+        Task { [weak self] in
+            guard let self = self else { return }
+
+            let id = await self.overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 1)
+
+            // execute sequentially instead of concurrently
+            await self.updateLatestOverrideConfigurationOfState(from: id)
+            await self.setCurrentOverride(from: id)
         }
     }
 
@@ -201,8 +204,8 @@ extension Adjustments.StateModel {
             let result = try IDs.compactMap { id in
                 try viewContext.existingObject(with: id) as? OverrideStored
             }
-            isEnabled = result.first?.enabled ?? false
-            if !isEnabled {
+            isOverrideEnabled = result.first?.enabled ?? false
+            if !isOverrideEnabled {
                 await resetStateVariables()
             }
         } catch {

+ 1 - 1
FreeAPS/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+TempTargets.swift

@@ -25,7 +25,7 @@ extension Adjustments.StateModel {
                 try viewContext.existingObject(with: id) as? TempTargetStored
             }
             isTempTargetEnabled = result.first?.enabled ?? false
-            if !isEnabled {
+            if !isOverrideEnabled {
                 await resetTempTargetState()
             }
         } catch {

+ 1 - 1
FreeAPS/Sources/Modules/Adjustments/AdjustmentsStateModel.swift

@@ -16,7 +16,7 @@ extension Adjustments {
         // MARK: - Override and Temp Target Properties
 
         var overridePercentage: Double = 100
-        var isEnabled = false
+        var isOverrideEnabled = false
         var indefinite = true
         var overrideDuration: Decimal = 0
         var target: Decimal = 0

+ 42 - 477
FreeAPS/Sources/Modules/Adjustments/View/AdjustmentsRootView.swift

@@ -6,25 +6,34 @@ extension Adjustments {
     struct RootView: BaseView {
         let resolver: Resolver
         @State var state = StateModel()
-        @State private var isEditing = false
-        @State private var showOverrideCreationSheet = false
-        @State private var showTempTargetCreationSheet = false
-        @State private var showingDetail = false
-        @State private var showCheckmark: Bool = false
-        @State private var selectedPresetID: String?
-        @State private var selectedTempTargetPresetID: String?
-        @State private var selectedOverride: OverrideStored?
-        @State private var selectedTempTarget: TempTargetStored?
-        @State private var isConfirmDeletePresented = false
-        @State private var isPromptPresented = false
-        @State private var isRemoveAlertPresented = false
-        @State private var removeAlert: Alert?
-        @State private var isEditingTT = false
+        @State var isEditing = false
+        @State var showOverrideCreationSheet = false
+        @State var showTempTargetCreationSheet = false
+        @State var showingDetail = false
+        @State var showOverrideCheckmark: Bool = false
+        @State var showTempTargetCheckmark: Bool = false
+        @State var selectedOverridePresetID: String?
+        @State var selectedTempTargetPresetID: String?
+        @State var selectedOverride: OverrideStored?
+        @State var selectedTempTarget: TempTargetStored?
+        @State var isConfirmDeletePresented = false
+        @State var isPromptPresented = false
+        @State var isRemoveAlertPresented = false
+        @State var removeAlert: Alert?
+        @State var isEditingTT = false
+
+        private var shouldDisplayStickyOverrideStopButton: Bool {
+            state.isOverrideEnabled && state.activeOverrideName.isNotEmpty
+        }
+
+        private var shouldDisplayStickyTempTargetStopButton: Bool {
+            state.isTempTargetEnabled && state.activeTempTargetName.isNotEmpty
+        }
 
         @Environment(\.colorScheme) var colorScheme
         @Environment(AppState.self) var appState
 
-        private func formattedGlucose(glucose: Decimal) -> String {
+        func formattedGlucose(glucose: Decimal) -> String {
             let formattedValue: String
             if state.units == .mgdL {
                 formattedValue = Formatter.glucoseFormatter(for: state.units)
@@ -55,7 +64,18 @@ extension Adjustments {
                     .background(appState.trioBackgroundColor(for: colorScheme))
                 }
                 .listSectionSpacing(10)
-                .safeAreaInset(edge: .bottom, spacing: 30) { stickyStopButton }
+                .safeAreaInset(
+                    edge: .bottom,
+                    spacing: shouldDisplayStickyOverrideStopButton || shouldDisplayStickyTempTargetStopButton ? 30 : 0
+                ) {
+                    if shouldDisplayStickyOverrideStopButton, state.selectedTab == .overrides {
+                        stickyStopOverrideButton
+                    } else if shouldDisplayStickyTempTargetStopButton, state.selectedTab == .tempTargets {
+                        stickyStopTempTargetButton
+                    } else {
+                        EmptyView()
+                    }
+                }
                 .scrollContentBackground(.hidden)
                 .background(appState.trioBackgroundColor(for: colorScheme))
                 .onAppear(perform: configureView)
@@ -126,32 +146,7 @@ extension Adjustments {
             }).background(appState.trioBackgroundColor(for: colorScheme))
         }
 
-        @ViewBuilder func overrides() -> some View {
-            if state.isEnabled, state.activeOverrideName.isNotEmpty {
-                currentActiveAdjustment
-            }
-            if state.overridePresets.isNotEmpty {
-                overridePresets
-            } else {
-                defaultText
-            }
-        }
-
-        @ViewBuilder func tempTargets() -> some View {
-            if state.isTempTargetEnabled, state.activeTempTargetName.isNotEmpty {
-                currentActiveAdjustment
-            }
-            if state.scheduledTempTargets.isNotEmpty {
-                scheduledTempTargets
-            }
-            if state.tempTargetPresets.isNotEmpty {
-                tempTargetPresets
-            } else {
-                defaultText
-            }
-        }
-
-        private var defaultText: some View {
+        var defaultText: some View {
             switch state.selectedTab {
             case .overrides:
                 Section {} header: {
@@ -170,193 +165,7 @@ extension Adjustments {
             }
         }
 
-        private var overridePresets: some View {
-            Section {
-                ForEach(state.overridePresets) { preset in
-                    overridesView(for: preset)
-                        .swipeActions(edge: .trailing, allowsFullSwipe: true) {
-                            Button(role: .none) {
-                                selectedOverride = preset
-                                isConfirmDeletePresented = true
-                            } label: {
-                                Label("Delete", systemImage: "trash")
-                                    .tint(.red)
-                            }
-                            Button(action: {
-                                // Set the selected Override to the chosen Preset and pass it to the Edit Sheet
-                                selectedOverride = preset
-                                state.showOverrideEditSheet = true
-                            }, label: {
-                                Label("Edit", systemImage: "pencil")
-                                    .tint(.blue)
-                            })
-                        }
-                }
-                .onMove(perform: state.reorderOverride)
-                .confirmationDialog(
-                    "Delete the Override Preset \"\(selectedOverride?.name ?? "")\"?",
-                    isPresented: $isConfirmDeletePresented,
-                    titleVisibility: .visible
-                ) {
-                    if let itemToDelete = selectedOverride {
-                        Button(
-                            state.currentActiveOverride == selectedOverride ? "Stop and Delete" : "Delete",
-                            role: .destructive
-                        ) {
-                            if state.currentActiveOverride == selectedOverride {
-                                Task {
-                                    // Save cancelled Override in OverrideRunStored Entity
-                                    // Cancel ALL active Override
-                                    await state.disableAllActiveOverrides(createOverrideRunEntry: true)
-                                }
-                            }
-                            // Perform the delete action
-                            Task {
-                                await state.invokeOverridePresetDeletion(itemToDelete.objectID)
-                            }
-                            // Reset the selected item after deletion
-                            selectedOverride = nil
-                        }
-                    }
-                    Button("Cancel", role: .cancel) {
-                        // Dismiss the dialog without action
-                        selectedOverride = nil
-                    }
-                } message: {
-                    if state.currentActiveOverride == selectedOverride {
-                        Text(
-                            state
-                                .currentActiveOverride == selectedOverride ?
-                                "This override preset is currently running. Deleting will stop it." : ""
-                        )
-                    }
-                }
-                .listRowBackground(Color.chart)
-            } header: {
-                Text("Override Presets")
-            } footer: {
-                HStack {
-                    Image(systemName: "hand.draw.fill").foregroundStyle(.primary)
-                    Text("Swipe left to edit or delete an override preset. Hold, drag and drop to reorder a preset.")
-                }
-            }
-        }
-
-        private var scheduledTempTargets: some View {
-            Section {
-                ForEach(state.scheduledTempTargets) { tempTarget in
-                    tempTargetView(for: tempTarget)
-                        .swipeActions(edge: .trailing, allowsFullSwipe: true) {
-                            swipeActions(for: tempTarget)
-                        }
-                }
-                .listRowBackground(Color.chart)
-            } header: {
-                Text("Scheduled Temp Targets")
-            }
-        }
-
-        private var tempTargetPresets: some View {
-            Section {
-                ForEach(state.tempTargetPresets) { preset in
-                    tempTargetView(for: preset, showCheckmark: showCheckmark) {
-                        enactTempTargetPreset(preset)
-                    }
-                    .swipeActions(edge: .trailing, allowsFullSwipe: true) {
-                        swipeActions(for: preset)
-                    }
-                }
-                .onMove(perform: state.reorderTempTargets)
-                .confirmationDialog(
-                    deleteConfirmationTitle,
-                    isPresented: $isConfirmDeletePresented,
-                    titleVisibility: .visible
-                ) {
-                    deleteConfirmationButtons()
-                } message: {
-                    deleteConfirmationMessage
-                }
-                .listRowBackground(Color.chart)
-            } header: {
-                Text("Temporary Target Presets")
-            } footer: {
-                HStack {
-                    Image(systemName: "hand.draw.fill").foregroundStyle(.primary)
-                    Text("Swipe left to edit or delete a temporary target preset. Hold, drag and drop to reorder a preset.")
-                }
-            }
-        }
-
-        private func enactTempTargetPreset(_ preset: TempTargetStored) {
-            Task {
-                let objectID = preset.objectID
-                await state.enactTempTargetPreset(withID: objectID)
-                selectedTempTargetPresetID = preset.id?.uuidString
-                showCheckmark.toggle()
-
-                DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
-                    showCheckmark = false
-                }
-            }
-        }
-
-        private func swipeActions(for tempTarget: TempTargetStored) -> some View {
-            Group {
-                Button {
-                    Task {
-                        selectedTempTarget = tempTarget
-                        isConfirmDeletePresented = true
-                    }
-                } label: {
-                    Label("Delete", systemImage: "trash")
-                        .tint(.red)
-                }
-                Button(action: {
-                    selectedTempTarget = tempTarget
-                    state.showTempTargetEditSheet = true
-                }, label: {
-                    Label("Edit", systemImage: "pencil")
-                        .tint(.blue)
-                })
-            }
-        }
-
-        private var deleteConfirmationTitle: String {
-            "Delete the Temp Target Preset \"\(selectedTempTarget?.name ?? "")\"?"
-        }
-
-        private func deleteConfirmationButtons() -> some View {
-            Group {
-                if let itemToDelete = selectedTempTarget {
-                    Button(
-                        state.currentActiveTempTarget == selectedTempTarget ? "Stop and Delete" : "Delete",
-                        role: .destructive
-                    ) {
-                        if state.currentActiveTempTarget == selectedTempTarget {
-                            Task {
-                                await state.disableAllActiveTempTargets(createTempTargetRunEntry: true)
-                            }
-                        }
-                        Task {
-                            await state.invokeTempTargetPresetDeletion(itemToDelete.objectID)
-                        }
-                        selectedTempTarget = nil
-                    }
-                }
-                Button("Cancel", role: .cancel) {
-                    selectedTempTarget = nil
-                }
-            }
-        }
-
-        private var deleteConfirmationMessage: Text? {
-            if state.currentActiveTempTarget == selectedTempTarget {
-                return Text("This Temp Target preset is currently running. Deleting will stop it.")
-            }
-            return nil
-        }
-
-        private var currentActiveAdjustment: some View {
+        var currentActiveAdjustment: some View {
             switch state.selectedTab {
             case .overrides:
                 Section {
@@ -411,60 +220,7 @@ extension Adjustments {
             }
         }
 
-        var stickyStopButton: some View {
-            ZStack {
-                Rectangle()
-                    .frame(width: UIScreen.main.bounds.width, height: 65)
-                    .foregroundStyle(colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white)
-                    .background(.thinMaterial)
-                    .opacity(0.8)
-                    .clipShape(Rectangle())
-
-                Group {
-                    switch state.selectedTab {
-                    case .overrides:
-                        Button(action: {
-                            Task {
-                                // Save cancelled Override in OverrideRunStored Entity
-                                // Cancel ALL active Override
-                                await state.disableAllActiveOverrides(createOverrideRunEntry: true)
-                            }
-                        }, label: {
-                            Text("Stop Override")
-                                .frame(maxWidth: .infinity, maxHeight: .infinity)
-                                .padding(10)
-                        })
-                            .frame(width: UIScreen.main.bounds.width * 0.9, height: 40, alignment: .center)
-                            .disabled(!state.isEnabled)
-                            .background(!state.isEnabled ? Color(.systemGray4) : Color(.systemRed))
-                            .tint(.white)
-                            .clipShape(RoundedRectangle(cornerRadius: 8))
-                    case .tempTargets:
-                        Button(action: {
-                            Task {
-                                // Save cancelled Temp Targets in TempTargetRunStored Entity
-                                // Cancel ALL active Temp Targets
-                                await state.disableAllActiveTempTargets(createTempTargetRunEntry: true)
-                                // Update View
-                                state.updateLatestTempTargetConfiguration()
-                            }
-                        }, label: {
-                            Text("Stop Temp Target")
-                                .frame(maxWidth: .infinity, maxHeight: .infinity)
-                                .padding(10)
-                        })
-                            .frame(width: UIScreen.main.bounds.width * 0.9, height: 40, alignment: .center)
-                            .disabled(!state.isTempTargetEnabled)
-                            .background(!state.isTempTargetEnabled ? Color(.systemGray4) : Color(.systemRed))
-                            .tint(.white)
-                            .clipShape(RoundedRectangle(cornerRadius: 8))
-                    }
-                }
-                .padding(5)
-            }
-        }
-
-        private var cancelAdjustmentButton: some View {
+        var cancelAdjustmentButton: some View {
             switch state.selectedTab {
             case .overrides:
                 Button(action: {
@@ -478,8 +234,8 @@ extension Adjustments {
 
                 })
                     .frame(maxWidth: .infinity, alignment: .center)
-                    .disabled(!state.isEnabled)
-                    .listRowBackground(!state.isEnabled ? Color(.systemGray4) : Color(.systemRed))
+                    .disabled(!state.isOverrideEnabled)
+                    .listRowBackground(!state.isOverrideEnabled ? Color(.systemGray4) : Color(.systemRed))
                     .tint(.white)
             case .tempTargets:
                 Button(action: {
@@ -502,75 +258,7 @@ extension Adjustments {
             }
         }
 
-        private func tempTargetView(
-            for tempTarget: TempTargetStored,
-            showCheckmark: Bool = false,
-            onTap: (() -> Void)? = nil
-        ) -> some View {
-            let target = tempTarget.target ?? 100
-            let tempTargetValue = Decimal(target as! Double.RawValue)
-            let isSelected = tempTarget.id?.uuidString == selectedPresetID
-            let tempTargetHalfBasal = Decimal(
-                tempTarget.halfBasalTarget as? Double
-                    .RawValue ?? Double(state.settingHalfBasalTarget)
-            )
-            let percentage = Int(
-                state.computeAdjustedPercentage(usingHBT: tempTargetHalfBasal, usingTarget: tempTargetValue)
-            )
-            let remainingTime = tempTarget.date?.timeIntervalSinceNow ?? 0
-
-            return ZStack(alignment: .trailing) {
-                HStack {
-                    VStack(alignment: .leading) {
-                        HStack {
-                            Text(tempTarget.name ?? "")
-                            Spacer()
-                            if remainingTime > 0 {
-                                Text("Starts in \(formattedTimeRemaining(remainingTime))")
-                                    .foregroundColor(colorScheme == .dark ? .orange : .accentColor)
-                            }
-                        }
-                        HStack(spacing: 2) {
-                            Text(formattedGlucose(glucose: target as Decimal))
-                                .foregroundColor(.secondary)
-                                .font(.caption)
-                            Text("for")
-                                .foregroundColor(.secondary)
-                                .font(.caption)
-                            Text("\(Formatter.integerFormatter.string(from: (tempTarget.duration ?? 0) as NSNumber)!)")
-                                .foregroundColor(.secondary)
-                                .font(.caption)
-                            Text("min")
-                                .foregroundColor(.secondary)
-                                .font(.caption)
-                            if state.isAdjustSensEnabled(usingTarget: tempTargetValue) {
-                                Text(", \(percentage)%")
-                                    .foregroundColor(.secondary)
-                                    .font(.caption)
-                            }
-                            Spacer()
-                        }
-                        .padding(.top, 2)
-                    }
-                    .contentShape(Rectangle())
-                    .onTapGesture {
-                        onTap?()
-                    }
-                }
-                if showCheckmark && isSelected {
-                    Image(systemName: "checkmark.circle.fill")
-                        .imageScale(.large)
-                        .fontWeight(.bold)
-                        .foregroundStyle(Color.green)
-                } else if onTap != nil {
-                    Image(systemName: "line.3.horizontal")
-                        .imageScale(.medium)
-                        .foregroundStyle(.secondary)
-                }
-            }
-        }
-
-        private func formattedTimeRemaining(_ timeInterval: TimeInterval) -> String {
+        func formattedTimeRemaining(_ timeInterval: TimeInterval) -> String {
             let totalSeconds = Int(timeInterval)
             let hours = totalSeconds / 3600
             let minutes = (totalSeconds % 3600) / 60
@@ -584,128 +272,5 @@ extension Adjustments {
                 return "<1m"
             }
         }
-
-        private var overrideLabelDivider: some View {
-            Divider()
-                .frame(width: 1, height: 20)
-        }
-
-        @ViewBuilder private func overridesView(for preset: OverrideStored) -> some View {
-            let isSelected = preset.id == selectedPresetID
-            let name = preset.name ?? ""
-            let indefinite = preset.indefinite
-            let duration = preset.duration?.decimalValue ?? Decimal(0)
-            let percentage = preset.percentage
-            let smbMinutes = preset.smbMinutes?.decimalValue ?? Decimal(0)
-            let uamMinutes = preset.uamMinutes?.decimalValue ?? Decimal(0)
-
-            let target: String = {
-                guard let targetValue = preset.target, targetValue != 0 else { return "" }
-                return state.units == .mgdL ? targetValue.description : targetValue.decimalValue.formattedAsMmolL
-            }()
-
-            let targetString = target.isEmpty ? "" : "\(target) \(state.units.rawValue)"
-
-            let durationString = indefinite ? "" : "\(state.formatHrMin(Int(duration)))"
-
-            let scheduledSMBString: String = {
-                guard preset.smbIsScheduledOff, preset.start != preset.end else { return "" }
-                return " \(formatTimeRange(start: preset.start?.stringValue, end: preset.end?.stringValue))"
-            }()
-
-            let smbString: String = {
-                guard preset.smbIsOff || preset.smbIsScheduledOff else { return "" }
-                return "SMBs Off\(scheduledSMBString)"
-            }()
-
-            let maxSmbMinsString: String = {
-                guard smbMinutes != 0, preset.advancedSettings, !preset.smbIsOff,
-                      smbMinutes != state.defaultSmbMinutes else { return "" }
-                return "\(smbMinutes.formatted()) min SMB"
-            }()
-
-            let maxUamMinsString: String = {
-                guard uamMinutes != 0, preset.advancedSettings, !preset.smbIsOff,
-                      uamMinutes != state.defaultUamMinutes else { return "" }
-                return "\(uamMinutes.formatted()) min UAM"
-            }()
-
-            let isfAndCrString: String = {
-                switch (preset.isfAndCr, preset.isf, preset.cr) {
-                case (_, true, true),
-                     (true, _, _):
-                    return " ISF/CR"
-                case (false, true, false):
-                    return " ISF"
-                case (false, false, true):
-                    return " CR"
-                default:
-                    return ""
-                }
-            }()
-
-            let percentageString = percentage != 100 ? "\(Int(percentage))%\(isfAndCrString)" : ""
-
-            // Combine all labels into a single array, filtering out empty strings
-            let labels: [String] = [
-                durationString,
-                percentageString,
-                targetString,
-                smbString,
-                maxSmbMinsString,
-                maxUamMinsString
-            ].filter { !$0.isEmpty }
-
-            if !name.isEmpty {
-                ZStack(alignment: .trailing) {
-                    HStack {
-                        VStack {
-                            HStack {
-                                Text(name)
-                                Spacer()
-                            }
-                            HStack(spacing: 5) {
-                                ForEach(labels, id: \.self) { label in
-                                    Text(label)
-                                    if label != labels.last { // Add divider between labels
-                                        overrideLabelDivider
-                                    }
-                                }
-                                Spacer()
-                            }
-                            .padding(.top, 2)
-                            .foregroundColor(.secondary)
-                            .font(.caption)
-                        }
-                        .contentShape(Rectangle())
-                        .onTapGesture {
-                            Task {
-                                let objectID = preset.objectID
-                                await state.enactOverridePreset(withID: objectID)
-                                state.hideModal()
-                                showCheckmark.toggle()
-                                selectedPresetID = preset.id
-
-                                // Deactivate checkmark after 3 seconds
-                                DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
-                                    showCheckmark = false
-                                }
-                            }
-                        }
-                    }
-                    // show checkmark to indicate if the preset was actually pressed
-                    if showCheckmark && isSelected {
-                        Image(systemName: "checkmark.circle.fill")
-                            .imageScale(.large)
-                            .fontWeight(.bold)
-                            .foregroundStyle(Color.green)
-                    } else {
-                        Image(systemName: "line.3.horizontal")
-                            .imageScale(.medium)
-                            .foregroundStyle(.secondary)
-                    }
-                }
-            }
-        }
     }
 }

+ 2 - 2
FreeAPS/Sources/Modules/Adjustments/View/Overrides/AddOverrideForm.swift

@@ -74,7 +74,7 @@ struct AddOverrideForm: View {
             Section(footer: state.percentageDescription(state.overridePercentage)) {
                 // Percentage Picker
                 HStack {
-                    Text("Change Basal Rate by")
+                    Text("Basal Rate Adjustment")
                     Spacer()
                     Text("\(state.overridePercentage.formatted(.number)) %")
                         .foregroundColor(!displayPickerPercentage ? .primary : .accentColor)
@@ -398,7 +398,7 @@ struct AddOverrideForm: View {
                     Button(action: {
                         Task {
                             if state.indefinite { state.overrideDuration = 0 }
-                            state.isEnabled.toggle()
+                            state.isOverrideEnabled.toggle()
                             await state.saveCustomOverride()
                             await state.resetStateVariables()
                             dismiss()

+ 256 - 0
FreeAPS/Sources/Modules/Adjustments/View/Overrides/AdjustmentsRootView+Overrides.swift

@@ -0,0 +1,256 @@
+import CoreData
+import SwiftUI
+
+extension Adjustments.RootView {
+    @ViewBuilder func overrides() -> some View {
+        if state.isOverrideEnabled, state.activeOverrideName.isNotEmpty {
+            currentActiveAdjustment
+        }
+        if state.overridePresets.isNotEmpty {
+            overridePresets
+        } else {
+            defaultText
+        }
+    }
+
+    var overridePresets: some View {
+        Section {
+            ForEach(state.overridePresets) { preset in
+                overridesView(for: preset, showCheckMark: showOverrideCheckmark) {
+                    enactOverridePreset(preset)
+                }
+                .swipeActions(edge: .trailing, allowsFullSwipe: true) {
+                    swipeActionsForOverrides(for: preset)
+                }
+            }
+            .onMove(perform: state.reorderOverride)
+            .confirmationDialog(
+                "Delete the Override Preset \"\(selectedOverride?.name ?? "")\"?",
+                isPresented: $isConfirmDeletePresented,
+                titleVisibility: .visible
+            ) {
+                if let itemToDelete = selectedOverride {
+                    Button(
+                        state.currentActiveOverride == selectedOverride ? "Stop and Delete" : "Delete",
+                        role: .destructive
+                    ) {
+                        if state.currentActiveOverride == selectedOverride {
+                            Task {
+                                // Save cancelled Override in OverrideRunStored Entity
+                                // Cancel ALL active Override
+                                await state.disableAllActiveOverrides(createOverrideRunEntry: true)
+                            }
+                        }
+                        // Perform the delete action
+                        Task {
+                            await state.invokeOverridePresetDeletion(itemToDelete.objectID)
+                        }
+                        // Reset the selected item after deletion
+                        selectedOverride = nil
+                    }
+                }
+                Button("Cancel", role: .cancel) {
+                    // Dismiss the dialog without action
+                    selectedOverride = nil
+                }
+            } message: {
+                if state.currentActiveOverride == selectedOverride {
+                    Text(
+                        state
+                            .currentActiveOverride == selectedOverride ?
+                            "This override preset is currently running. Deleting will stop it." : ""
+                    )
+                }
+            }
+            .listRowBackground(Color.chart)
+        } header: {
+            Text("Override Presets")
+        } footer: {
+            HStack {
+                Image(systemName: "hand.draw.fill").foregroundStyle(.primary)
+                Text("Swipe left to edit or delete an override preset. Hold, drag and drop to reorder a preset.")
+            }
+        }
+    }
+
+    func enactOverridePreset(_ preset: OverrideStored) {
+        Task {
+            let objectID = preset.objectID
+            await state.enactOverridePreset(withID: objectID)
+            state.hideModal()
+            selectedOverridePresetID = preset.id
+            showOverrideCheckmark = true
+
+            // Deactivate checkmark after 3 seconds
+            DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
+                showOverrideCheckmark = false
+            }
+        }
+    }
+
+    func swipeActionsForOverrides(for preset: OverrideStored) -> some View {
+        Group {
+            Button(role: .none) {
+                selectedOverride = preset
+                isConfirmDeletePresented = true
+            } label: {
+                Label("Delete", systemImage: "trash.fill")
+                    .tint(.red)
+            }
+            Button(action: {
+                // Set the selected Override to the chosen Preset and pass it to the Edit Sheet
+                selectedOverride = preset
+                state.showOverrideEditSheet = true
+            }, label: {
+                Label("Edit", systemImage: "pencil")
+                    .tint(.blue)
+            })
+        }
+    }
+
+    var overrideLabelDivider: some View {
+        Divider()
+            .frame(width: 1, height: 20)
+    }
+
+    var stickyStopOverrideButton: some View {
+        ZStack {
+            Rectangle()
+                .frame(width: UIScreen.main.bounds.width, height: 65)
+                .foregroundStyle(colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white)
+                .background(.thinMaterial)
+                .opacity(0.8)
+                .clipShape(Rectangle())
+
+            Button(action: {
+                Task {
+                    // Save cancelled Override in OverrideRunStored Entity
+                    // Cancel ALL active Override
+                    await state.disableAllActiveOverrides(createOverrideRunEntry: true)
+                }
+            }, label: {
+                Text("Stop Override")
+                    .frame(maxWidth: .infinity, maxHeight: .infinity)
+                    .padding(10)
+            })
+                .frame(width: UIScreen.main.bounds.width * 0.9, height: 40, alignment: .center)
+                .disabled(!state.isOverrideEnabled)
+                .background(!state.isOverrideEnabled ? Color(.systemGray4) : Color(.systemRed))
+                .tint(.white)
+                .clipShape(RoundedRectangle(cornerRadius: 8))
+
+                .padding(5)
+        }
+    }
+
+    @ViewBuilder func overridesView(
+        for preset: OverrideStored,
+        showCheckMark _: Bool = false,
+        onTap: (() -> Void)? = nil
+    ) -> some View {
+        let isSelected = preset.id == selectedOverridePresetID
+        let name = preset.name ?? ""
+        let indefinite = preset.indefinite
+        let duration = preset.duration?.decimalValue ?? Decimal(0)
+        let percentage = preset.percentage
+        let smbMinutes = preset.smbMinutes?.decimalValue ?? Decimal(0)
+        let uamMinutes = preset.uamMinutes?.decimalValue ?? Decimal(0)
+
+        let target: String = {
+            guard let targetValue = preset.target, targetValue != 0 else { return "" }
+            return state.units == .mgdL ? targetValue.description : targetValue.decimalValue.formattedAsMmolL
+        }()
+
+        let targetString = target.isEmpty ? "" : "\(target) \(state.units.rawValue)"
+
+        let durationString = indefinite ? "" : "\(state.formatHrMin(Int(duration)))"
+
+        let scheduledSMBString: String = {
+            guard preset.smbIsScheduledOff, preset.start != preset.end else { return "" }
+            return " \(formatTimeRange(start: preset.start?.stringValue, end: preset.end?.stringValue))"
+        }()
+
+        let smbString: String = {
+            guard preset.smbIsOff || preset.smbIsScheduledOff else { return "" }
+            return "SMBs Off\(scheduledSMBString)"
+        }()
+
+        let maxSmbMinsString: String = {
+            guard smbMinutes != 0, preset.advancedSettings, !preset.smbIsOff,
+                  smbMinutes != state.defaultSmbMinutes else { return "" }
+            return "\(smbMinutes.formatted()) min SMB"
+        }()
+
+        let maxUamMinsString: String = {
+            guard uamMinutes != 0, preset.advancedSettings, !preset.smbIsOff,
+                  uamMinutes != state.defaultUamMinutes else { return "" }
+            return "\(uamMinutes.formatted()) min UAM"
+        }()
+
+        let isfAndCrString: String = {
+            switch (preset.isfAndCr, preset.isf, preset.cr) {
+            case (_, true, true),
+                 (true, _, _):
+                return " ISF/CR"
+            case (false, true, false):
+                return " ISF"
+            case (false, false, true):
+                return " CR"
+            default:
+                return ""
+            }
+        }()
+
+        let percentageString = percentage != 100 ? "\(Int(percentage))%\(isfAndCrString)" : ""
+
+        // Combine all labels into a single array, filtering out empty strings
+        let labels: [String] = [
+            durationString,
+            percentageString,
+            targetString,
+            smbString,
+            maxSmbMinsString,
+            maxUamMinsString
+        ].filter { !$0.isEmpty }
+
+        if !name.isEmpty {
+            ZStack(alignment: .trailing) {
+                HStack {
+                    VStack {
+                        HStack {
+                            Text(name)
+                            Spacer()
+                        }
+                        HStack(spacing: 5) {
+                            ForEach(labels, id: \.self) { label in
+                                Text(label)
+                                if label != labels.last { // Add divider between labels
+                                    overrideLabelDivider
+                                }
+                            }
+                            Spacer()
+                        }
+                        .padding(.top, 2)
+                        .foregroundColor(.secondary)
+                        .font(.caption)
+                    }
+                    .contentShape(Rectangle())
+                    .onTapGesture {
+                        onTap?()
+                    }
+                }
+                // show checkmark to indicate if the preset was actually pressed
+                if showOverrideCheckmark && isSelected {
+                    Image(systemName: "checkmark.circle.fill")
+                        .imageScale(.large)
+                        .fontWeight(.bold)
+                        .foregroundStyle(Color.green)
+                } else {
+                    Image(systemName: "line.3.horizontal")
+                        .imageScale(.medium)
+                        .foregroundStyle(.secondary)
+                }
+            }
+        }
+    }
+}

+ 1 - 1
FreeAPS/Sources/Modules/Adjustments/View/Overrides/EditOverrideForm.swift

@@ -140,7 +140,7 @@ struct EditOverrideForm: View {
             // Percentage Picker
             Section(footer: state.percentageDescription(percentage)) {
                 HStack {
-                    Text("Change Basal Rate by")
+                    Text("Basal Rate Adjustment")
                     Spacer()
                     Text("\(percentage.formatted(.number)) %")
                         .foregroundColor(!displayPickerPercentage ? .primary : .accentColor)

+ 231 - 0
FreeAPS/Sources/Modules/Adjustments/View/TempTargets/AdjustmentsRootView+TempTargets.swift

@@ -0,0 +1,231 @@
+import CoreData
+import SwiftUI
+
+extension Adjustments.RootView {
+    @ViewBuilder func tempTargets() -> some View {
+        if state.isTempTargetEnabled, state.activeTempTargetName.isNotEmpty {
+            currentActiveAdjustment
+        }
+        if state.scheduledTempTargets.isNotEmpty {
+            scheduledTempTargets
+        }
+        if state.tempTargetPresets.isNotEmpty {
+            tempTargetPresets
+        } else {
+            defaultText
+        }
+    }
+
+    private var scheduledTempTargets: some View {
+        Section {
+            ForEach(state.scheduledTempTargets) { tempTarget in
+                tempTargetView(for: tempTarget)
+                    .swipeActions(edge: .trailing, allowsFullSwipe: true) {
+                        swipeActionsForTempTargets(for: tempTarget)
+                    }
+            }
+            .listRowBackground(Color.chart)
+        } header: {
+            Text("Scheduled Temp Targets")
+        }
+    }
+
+    private var tempTargetPresets: some View {
+        Section {
+            ForEach(state.tempTargetPresets) { preset in
+                tempTargetView(for: preset, showCheckmark: showTempTargetCheckmark) {
+                    enactTempTargetPreset(preset)
+                }
+                .swipeActions(edge: .trailing, allowsFullSwipe: true) {
+                    swipeActionsForTempTargets(for: preset)
+                }
+            }
+            .onMove(perform: state.reorderTempTargets)
+            .confirmationDialog(
+                deleteConfirmationTitle,
+                isPresented: $isConfirmDeletePresented,
+                titleVisibility: .visible
+            ) {
+                deleteConfirmationButtons()
+            } message: {
+                deleteConfirmationMessage
+            }
+            .listRowBackground(Color.chart)
+        } header: {
+            Text("Temporary Target Presets")
+        } footer: {
+            HStack {
+                Image(systemName: "hand.draw.fill").foregroundStyle(.primary)
+                Text("Swipe left to edit or delete a temporary target preset. Hold, drag and drop to reorder a preset.")
+            }
+        }
+    }
+
+    private func enactTempTargetPreset(_ preset: TempTargetStored) {
+        Task {
+            let objectID = preset.objectID
+            await state.enactTempTargetPreset(withID: objectID)
+            selectedTempTargetPresetID = preset.id?.uuidString
+            showTempTargetCheckmark = true
+
+            DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
+                showTempTargetCheckmark = false
+            }
+        }
+    }
+
+    private func swipeActionsForTempTargets(for tempTarget: TempTargetStored) -> some View {
+        Group {
+            Button {
+                Task {
+                    selectedTempTarget = tempTarget
+                    isConfirmDeletePresented = true
+                }
+            } label: {
+                Label("Delete", systemImage: "trash.fill")
+                    .tint(.red)
+            }
+            Button(action: {
+                selectedTempTarget = tempTarget
+                state.showTempTargetEditSheet = true
+            }, label: {
+                Label("Edit", systemImage: "pencil")
+                    .tint(.blue)
+            })
+        }
+    }
+
+    private var deleteConfirmationTitle: String {
+        "Delete the Temp Target Preset \"\(selectedTempTarget?.name ?? "")\"?"
+    }
+
+    private func deleteConfirmationButtons() -> some View {
+        Group {
+            if let itemToDelete = selectedTempTarget {
+                Button(
+                    state.currentActiveTempTarget == selectedTempTarget ? "Stop and Delete" : "Delete",
+                    role: .destructive
+                ) {
+                    if state.currentActiveTempTarget == selectedTempTarget {
+                        Task {
+                            await state.disableAllActiveTempTargets(createTempTargetRunEntry: true)
+                        }
+                    }
+                    Task {
+                        await state.invokeTempTargetPresetDeletion(itemToDelete.objectID)
+                    }
+                    selectedTempTarget = nil
+                }
+            }
+            Button("Cancel", role: .cancel) {
+                selectedTempTarget = nil
+            }
+        }
+    }
+
+    private var deleteConfirmationMessage: Text? {
+        if state.currentActiveTempTarget == selectedTempTarget {
+            return Text("This Temp Target preset is currently running. Deleting will stop it.")
+        }
+        return nil
+    }
+
+    var stickyStopTempTargetButton: some View {
+        ZStack {
+            Rectangle()
+                .frame(width: UIScreen.main.bounds.width, height: 65)
+                .foregroundStyle(colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white)
+                .background(.thinMaterial)
+                .opacity(0.8)
+                .clipShape(Rectangle())
+
+            Button(action: {
+                Task {
+                    // Save cancelled Temp Targets in TempTargetRunStored Entity
+                    // Cancel ALL active Temp Targets
+                    await state.disableAllActiveTempTargets(createTempTargetRunEntry: true)
+                    // Update View
+                    state.updateLatestTempTargetConfiguration()
+                }
+            }, label: {
+                Text("Stop Temp Target")
+                    .frame(maxWidth: .infinity, maxHeight: .infinity)
+                    .padding(10)
+            })
+                .frame(width: UIScreen.main.bounds.width * 0.9, height: 40, alignment: .center)
+                .disabled(!state.isTempTargetEnabled)
+                .background(!state.isTempTargetEnabled ? Color(.systemGray4) : Color(.systemRed))
+                .tint(.white)
+                .clipShape(RoundedRectangle(cornerRadius: 8))
+                .padding(5)
+        }
+    }
+
+    private func tempTargetView(
+        for tempTarget: TempTargetStored,
+        showCheckmark: Bool = false,
+        onTap: (() -> Void)? = nil
+    ) -> some View {
+        let target = tempTarget.target ?? 100
+        let tempTargetValue = Decimal(target as! Double.RawValue)
+        let isSelected = tempTarget.id?.uuidString == selectedTempTargetPresetID
+        let tempTargetHalfBasal = Decimal(
+            tempTarget.halfBasalTarget as? Double
+                .RawValue ?? Double(state.settingHalfBasalTarget)
+        )
+        let percentage = Int(
+            state.computeAdjustedPercentage(usingHBT: tempTargetHalfBasal, usingTarget: tempTargetValue)
+        )
+        let remainingTime = tempTarget.date?.timeIntervalSinceNow ?? 0
+
+        return ZStack(alignment: .trailing) {
+            HStack {
+                VStack(alignment: .leading) {
+                    HStack {
+                        Text(tempTarget.name ?? "")
+                        Spacer()
+                        if remainingTime > 0 {
+                            Text("Starts in \(formattedTimeRemaining(remainingTime))")
+                                .foregroundColor(colorScheme == .dark ? .orange : .accentColor)
+                        }
+                    }
+                    HStack(spacing: 2) {
+                        Text(formattedGlucose(glucose: target as Decimal))
+                            .foregroundColor(.secondary)
+                            .font(.caption)
+                        Text("for")
+                            .foregroundColor(.secondary)
+                            .font(.caption)
+                        Text("\(Formatter.integerFormatter.string(from: (tempTarget.duration ?? 0) as NSNumber)!)")
+                            .foregroundColor(.secondary)
+                            .font(.caption)
+                        Text("min")
+                            .foregroundColor(.secondary)
+                            .font(.caption)
+                        if state.isAdjustSensEnabled(usingTarget: tempTargetValue) {
+                            Text(", \(percentage)%")
+                                .foregroundColor(.secondary)
+                                .font(.caption)
+                        }
+                        Spacer()
+                    }
+                    .padding(.top, 2)
+                }
+                .contentShape(Rectangle())
+                .onTapGesture {
+                    onTap?()
+                }
+            }
+            if showCheckmark && isSelected {
+                Image(systemName: "checkmark.circle.fill")
+                    .imageScale(.large)
+                    .fontWeight(.bold)
+                    .foregroundStyle(Color.green)
+            } else if onTap != nil {
+                Image(systemName: "line.3.horizontal")
+                    .imageScale(.medium)
+                    .foregroundStyle(.secondary)
+            }
+        }
+    }
+}

+ 7 - 0
FreeAPS/Sources/Modules/BasalProfileEditor/BasalProfileEditorStateModel.swift

@@ -4,6 +4,7 @@ import SwiftUI
 extension BasalProfileEditor {
     @Observable final class StateModel: BaseStateModel<Provider> {
         @ObservationIgnored @Injected() private var nightscout: NightscoutManager!
+        @ObservationIgnored @Injected() private var broadcaster: Broadcaster!
 
         var syncInProgress: Bool = false
         var initialItems: [Item] = []
@@ -105,6 +106,12 @@ extension BasalProfileEditor {
                     print("We were successful")
                 }
                 .store(in: &lifetime)
+
+            DispatchQueue.main.async {
+                self.broadcaster.notify(BasalProfileObserver.self, on: .main) {
+                    $0.basalProfileDidChange(profile)
+                }
+            }
         }
 
         @MainActor func validate() {

+ 11 - 9
FreeAPS/Sources/Modules/BasalProfileEditor/View/BasalProfileEditorRootView.swift

@@ -84,23 +84,25 @@ extension BasalProfileEditor {
 
                 Group {
                     HStack {
-                        Button {
+                        Button(action: {
                             let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
                             impactHeavy.impactOccurred()
                             state.save()
-                        } label: {
+                        }, label: {
                             HStack {
                                 if state.syncInProgress {
                                     ProgressView().padding(.trailing, 10)
                                 }
                                 Text(state.syncInProgress ? "Saving..." : "Save")
-                            }.padding(10)
-                        }
-                        .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
-                        .disabled(shouldDisableButton)
-                        .background(shouldDisableButton ? Color(.systemGray4) : Color(.systemBlue))
-                        .tint(.white)
-                        .clipShape(RoundedRectangle(cornerRadius: 8))
+                            }
+                            .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
+                            .padding(10)
+                        })
+                            .frame(width: UIScreen.main.bounds.width * 0.9, height: 40, alignment: .center)
+                            .disabled(shouldDisableButton)
+                            .background(shouldDisableButton ? Color(.systemGray4) : Color(.systemBlue))
+                            .tint(.white)
+                            .clipShape(RoundedRectangle(cornerRadius: 8))
                     }
                 }.padding(5)
             }

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

@@ -138,7 +138,7 @@ extension BolusCalculatorConfig {
                     verboseHint:
                     VStack(alignment: .leading, spacing: 10) {
                         Text("Default: OFF").bold()
-                        Text("Default Percent: 200%").bold()
+                        Text("Default Percent: 100%").bold()
                         Text("Do not enable this feature until you have optimized your CR (carb ratio) setting.").bold()
                         Text(
                             "Enabling this setting adds a \"Super Bolus\" option to the bolus calculator. Once this feature is enabled, a percentage setting will appear for you to select."

+ 3 - 1
FreeAPS/Sources/Modules/CarbRatioEditor/View/CarbRatioEditorRootView.swift

@@ -53,7 +53,9 @@ extension CarbRatioEditor {
                                         ProgressView().padding(.trailing, 10)
                                     }
                                     Text(state.shouldDisplaySaving ? "Saving..." : "Save")
-                                }.padding(10)
+                                }
+                                .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
+                                .padding(10)
                             }
                         }
                         .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)

+ 337 - 33
FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -18,8 +18,6 @@ extension DataTable {
 
         var mode: Mode = .treatments
         var treatments: [Treatment] = []
-        var glucose: [Glucose] = []
-        var meals: [Treatment] = []
         var manualGlucose: Decimal = 0
         var waitForSuggestion: Bool = false
 
@@ -28,18 +26,24 @@ extension DataTable {
 
         var units: GlucoseUnits = .mgdL
 
+        var carbEntryToEdit: CarbEntryStored?
+        var showCarbEntryEditor = false
+
         override func subscribe() {
             units = settingsManager.settings.units
             broadcaster.register(DeterminationObserver.self, observer: self)
             broadcaster.register(SettingsObserver.self, observer: self)
         }
 
+        /// Checks if the glucose data is fresh based on the given date
+        /// - Parameter glucoseDate: The date to check
+        /// - Returns: Boolean indicating if the data is fresh
         func isGlucoseDataFresh(_ glucoseDate: Date?) -> Bool {
             glucoseStorage.isGlucoseDataFresh(glucoseDate)
         }
 
-        // Glucose deletion from history and from remote services
-        /// -**Parameter**: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
+        /// Initiates the glucose deletion process asynchronously
+        /// - Parameter treatmentObjectID: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
         func invokeGlucoseDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
             Task {
                 await deleteGlucose(treatmentObjectID)
@@ -99,9 +103,9 @@ extension DataTable {
 
         // Carb and FPU deletion from history
         /// - **Parameter**: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
-        func invokeCarbDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
+        func invokeCarbDeletionTask(_ treatmentObjectID: NSManagedObjectID, isFpuOrComplexMeal: Bool = false) {
             Task {
-                await deleteCarbs(treatmentObjectID)
+                await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: isFpuOrComplexMeal)
 
                 await MainActor.run {
                     carbEntryDeleted = true
@@ -110,34 +114,58 @@ extension DataTable {
             }
         }
 
-        func deleteCarbs(_ treatmentObjectID: NSManagedObjectID) async {
-            // Delete from Apple Health/Tidepool
-            await deleteCarbsFromServices(treatmentObjectID)
+        func deleteCarbs(_ treatmentObjectID: NSManagedObjectID, isFpuOrComplexMeal: Bool = false) async {
+            // Delete from Nightscout/Apple Health/Tidepool
+            await deleteFromServices(treatmentObjectID, isFPUDeletion: isFpuOrComplexMeal)
 
             // Delete carbs from Core Data
-            await carbsStorage.deleteCarbs(treatmentObjectID)
+            await carbsStorage.deleteCarbsEntryStored(treatmentObjectID)
 
             // Perform a determine basal sync to update cob
             await apsManager.determineBasalSync()
         }
 
-        func deleteCarbsFromServices(_ treatmentObjectID: NSManagedObjectID) async {
+        /// Deletes carb and FPU entries from all connected services (Nightscout, HealthKit, Tidepool)
+        /// - Parameters:
+        ///   - treatmentObjectID: The Core Data object ID of the entry to delete
+        ///   - isFPUDeletion: Flag indicating if this is a FPU deletion that requires special handling
+        ///     - If true: Will first fetch the corresponding carb entry and then delete both FPU and carb entries
+        ///     - If false: Will delete the entry directly as a standard carb deletion
+        /// - Note: This function handles three scenarios:
+        ///   1. Standard carb deletion (isFPUDeletion = false)
+        ///   2. FPU-only deletion (isFPUDeletion = true)
+        ///   3. Combined carb+FPU deletion (isFPUDeletion = true)
+        func deleteFromServices(_ treatmentObjectID: NSManagedObjectID, isFPUDeletion: Bool = false) async {
             let taskContext = CoreDataStack.shared.newTaskContext()
             taskContext.name = "deleteContext"
             taskContext.transactionAuthor = "deleteCarbsFromServices"
 
             var carbEntry: CarbEntryStored?
+            var objectIDToDelete = treatmentObjectID
+
+            // For FPU deletions, first get the corresponding carb entry
+            if isFPUDeletion {
+                guard let correspondingEntry: (
+                    entryValues: (carbs: Decimal, fat: Decimal, protein: Decimal, note: String, date: Date)?,
+                    entryID: NSManagedObjectID?
+                ) = await handleFPUEntry(treatmentObjectID),
+                    let nsManagedObjectID = correspondingEntry.entryID
+                else { return }
+
+                objectIDToDelete = nsManagedObjectID
+            }
 
-            // Delete carbs or FPUs from Nightscout
+            // Delete entries from all services
             await taskContext.perform {
                 do {
-                    carbEntry = try taskContext.existingObject(with: treatmentObjectID) as? CarbEntryStored
+                    carbEntry = try taskContext.existingObject(with: objectIDToDelete) as? CarbEntryStored
                     guard let carbEntry = carbEntry else {
                         debugPrint("Carb entry for deletion not found. \(DebuggingIdentifiers.failed)")
                         return
                     }
 
-                    if carbEntry.isFPU, let fpuID = carbEntry.fpuID {
+                    // Delete FPU related entries if they exist
+                    if let fpuID = carbEntry.fpuID {
                         // Delete Fat and Protein entries from Nightscout
                         self.provider.deleteCarbsFromNightscout(withID: fpuID.uuidString)
 
@@ -152,29 +180,26 @@ extension DataTable {
                                 self.provider.deleteMealDataFromHealth(byID: fpuID.uuidString, sampleType: validSampleType)
                             }
                         }
-                    } else {
-                        // Delete carbs from Nightscout
-                        if let id = carbEntry.id, let entryDate = carbEntry.date {
-                            self.provider.deleteCarbsFromNightscout(withID: id.uuidString)
-
-                            // Delete carbs from Apple Health
-                            if let sampleType = AppleHealthConfig.healthCarbObject {
-                                self.provider.deleteMealDataFromHealth(byID: id.uuidString, sampleType: sampleType)
-                            }
+                    }
+
+                    // Delete carb entries if they exist
+                    if let id = carbEntry.id, let entryDate = carbEntry.date {
+                        self.provider.deleteCarbsFromNightscout(withID: id.uuidString)
 
-                            self.provider.deleteCarbsFromTidepool(
-                                withSyncId: id,
-                                carbs: Decimal(carbEntry.carbs),
-                                at: entryDate,
-                                enteredBy: CarbsEntry.local
-                            )
+                        // Delete carbs from Apple Health
+                        if let sampleType = AppleHealthConfig.healthCarbObject {
+                            self.provider.deleteMealDataFromHealth(byID: id.uuidString, sampleType: sampleType)
                         }
-                    }
 
+                        self.provider.deleteCarbsFromTidepool(
+                            withSyncId: id,
+                            carbs: Decimal(carbEntry.carbs),
+                            at: entryDate,
+                            enteredBy: CarbsEntry.local
+                        )
+                    }
                 } catch {
-                    debugPrint(
-                        "\(DebuggingIdentifiers.failed) Error deleting carb entry from remote service(s) (Nightscout, Apple Health, Tidepool) with error: \(error.localizedDescription)"
-                    )
+                    debugPrint("\(DebuggingIdentifiers.failed) Error deleting entries: \(error.localizedDescription)")
                 }
             }
         }
@@ -246,6 +271,285 @@ extension DataTable {
                 }
             }
         }
+
+        // MARK: - Entry Management
+
+        /// Updates a carb/FPU entry with new values and handles the necessary cleanup and recreation of FPU entries
+        /// - Parameters:
+        ///   - treatmentObjectID: The ID of the entry to update
+        ///   - newCarbs: The new carbs value
+        ///   - newFat: The new fat value
+        ///   - newProtein: The new protein value
+        ///   - newNote: The new note text
+        ///   - newDate: The new date for the entry
+        func updateEntry(
+            _ treatmentObjectID: NSManagedObjectID,
+            newCarbs: Decimal,
+            newFat: Decimal,
+            newProtein: Decimal,
+            newNote: String,
+            newDate: Date
+        ) {
+            Task {
+                // Get original date from entry to re-create the entry later with the updated values and the same date
+                guard let originalEntry = await getOriginalEntryValues(treatmentObjectID) else { return }
+
+                // Deletion logic for carb and FPU entries
+                await deleteOldEntries(
+                    treatmentObjectID,
+                    originalEntry: originalEntry,
+                    newCarbs: newCarbs,
+                    newFat: newFat,
+                    newProtein: newProtein,
+                    newNote: newNote
+                )
+
+                await createNewEntries(
+                    originalDate: newDate,
+                    newCarbs: newCarbs,
+                    newFat: newFat,
+                    newProtein: newProtein,
+                    newNote: newNote
+                )
+
+                await syncWithServices()
+            }
+        }
+
+        private func createNewEntries(
+            originalDate: Date,
+            newCarbs: Decimal,
+            newFat: Decimal,
+            newProtein: Decimal,
+            newNote: String
+        ) async {
+            let newEntry = CarbsEntry(
+                id: UUID().uuidString,
+                createdAt: Date(),
+                actualDate: originalDate,
+                carbs: newCarbs,
+                fat: newFat,
+                protein: newProtein,
+                note: newNote,
+                enteredBy: CarbsEntry.local,
+                isFPU: false,
+                fpuID: newFat > 0 || newProtein > 0 ? UUID().uuidString : nil
+            )
+
+            // Handles internally whether to create fake carbs or not based on whether fat > 0 or protein > 0
+            await carbsStorage.storeCarbs([newEntry], areFetchedFromRemote: false)
+        }
+
+        /// Deletes the old carb/ FPU entries and creates new ones with updated values
+        /// - Parameters:
+        ///   - treatmentObjectID: The ID of the entry to delete
+        ///   - originalDate: The original date to preserve
+        ///   - newCarbs: The new carbs value
+        ///   - newFat: The new fat value
+        ///   - newProtein: The new protein value
+        ///   - newNote: The new note text
+        private func deleteOldEntries(
+            _ treatmentObjectID: NSManagedObjectID,
+            originalEntry: (
+                entryValues: (date: Date, carbs: Double, fat: Double, protein: Double)?,
+                entryId: NSManagedObjectID
+            ),
+            newCarbs _: Decimal,
+            newFat _: Decimal,
+            newProtein _: Decimal,
+            newNote _: String
+        ) async {
+            if ((originalEntry.entryValues?.carbs ?? 0) == 0 && (originalEntry.entryValues?.fat ?? 0) > 0) ||
+                ((originalEntry.entryValues?.carbs ?? 0) == 0 && (originalEntry.entryValues?.protein ?? 0) > 0)
+            {
+                // Delete the zero-carb-entry and all its carb equivalents connected by the same fpuID from remote services and Core Data
+                // Use fpuID
+                await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: true)
+            } else if ((originalEntry.entryValues?.carbs ?? 0) > 0 && (originalEntry.entryValues?.fat ?? 0) > 0) ||
+                ((originalEntry.entryValues?.carbs ?? 0) > 0 && (originalEntry.entryValues?.protein ?? 0) > 0)
+            {
+                // Delete carb entry and carb equivalents that are all connected by the same fpuID from remote services and Core Data
+                // Use fpuID
+                await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: true)
+
+            } else {
+                // Delete just the carb entry since there are no carb equivalents
+                // Use NSManagedObjectID
+                await deleteCarbs(treatmentObjectID)
+            }
+        }
+
+        /// Retrieves the original entry values
+        /// - Parameter objectID: The ID of the entry
+        /// - Returns: A tuple of the old entry values and its original date and the objectID or nil
+        private func getOriginalEntryValues(_ objectID: NSManagedObjectID) async
+            -> (entryValues: (date: Date, carbs: Double, fat: Double, protein: Double)?, entryId: NSManagedObjectID)?
+        {
+            let context = CoreDataStack.shared.newTaskContext()
+            context.name = "updateContext"
+            context.transactionAuthor = "updateEntry"
+
+            return await context.perform {
+                do {
+                    guard let entry = try context.existingObject(with: objectID) as? CarbEntryStored, let entryDate = entry.date
+                    else { return nil }
+
+                    return (
+                        entryValues: (date: entryDate, carbs: entry.carbs, fat: entry.fat, protein: entry.protein),
+                        entryId: entry.objectID
+                    )
+                } catch let error as NSError {
+                    debugPrint("\(DebuggingIdentifiers.failed) Failed to get original date with error: \(error.userInfo)")
+                    return nil
+                }
+            }
+        }
+
+        /// Synchronizes the FPU/ Carb entry with all remote services in parallel
+        private func syncWithServices() async {
+            async let nightscoutUpload: () = provider.nightscoutManager.uploadCarbs()
+            async let healthKitUpload: () = provider.healthkitManager.uploadCarbs()
+            async let tidepoolUpload: () = provider.tidepoolManager.uploadCarbs()
+
+            _ = await [nightscoutUpload, healthKitUpload, tidepoolUpload]
+        }
+
+        // MARK: - Entry Loading
+
+        /// Loads the values of a carb or FPU entry from Core Data
+        /// - Parameter objectID: The ID of the entry to load
+        /// - Returns: A tuple containing the entry's values, or nil if not found
+        func loadEntryValues(from objectID: NSManagedObjectID) async
+            -> (carbs: Decimal, fat: Decimal, protein: Decimal, note: String, date: Date)?
+        {
+            let context = CoreDataStack.shared.persistentContainer.viewContext
+
+            return await context.perform {
+                do {
+                    guard let entry = try context.existingObject(with: objectID) as? CarbEntryStored,
+                          let entryDate = entry.date
+                    else { return nil }
+
+                    return (
+                        carbs: Decimal(entry.carbs),
+                        fat: Decimal(entry.fat),
+                        protein: Decimal(entry.protein),
+                        note: entry.note ?? "",
+                        date: entryDate
+                    )
+                } catch {
+                    debugPrint("\(DebuggingIdentifiers.failed) Failed to load entry: \(error.localizedDescription)")
+                    return nil
+                }
+            }
+        }
+
+        // MARK: - FPU Entry Handling
+
+        /// Handles the loading of FPU entries based on their type
+        /// If the user taps on an FPU entry in the DataTable list, there are two cases:
+        /// - the User has entered this FPU entry WITH carbs
+        /// - the User has entered this FPU entry WITHOUT carbs
+        /// In the first case, we simply need to load the corresponding carb entry. For this case THIS is the entry we want to edit.
+        /// In the second case, we need to load the zero-carb entry that actually holds the FPU values (and the carbs). For this case THIS is the entry we want to edit.
+        /// - Parameter objectID: The ID of the FPU entry
+        /// - Returns: A tuple containing the entry values and ID, or nil if not found
+        func handleFPUEntry(_ objectID: NSManagedObjectID) async
+            -> (
+                entryValues: (carbs: Decimal, fat: Decimal, protein: Decimal, note: String, date: Date)?,
+                entryID: NSManagedObjectID?
+            )?
+        {
+            // Case 1: FPU entry WITH carbs
+            if let correspondingCarbEntryID = await getCorrespondingCarbEntry(objectID) {
+                if let values = await loadEntryValues(from: correspondingCarbEntryID) {
+                    return (values, correspondingCarbEntryID)
+                }
+            }
+            // Case 2: FPU entry WITHOUT carbs
+            else if let originalEntryID = await getZeroCarbNonFPUEntry(objectID) {
+                if let values = await loadEntryValues(from: originalEntryID) {
+                    return (values, originalEntryID)
+                }
+            }
+            return nil
+        }
+
+        /// Retrieves the original zero-carb non-FPU entry for a given FPU entry.
+        /// This is used when the user has entered a FPU entry WITHOUT carbs.
+        /// - Parameter treatmentObjectID: The ID of the FPU entry
+        /// - Returns: The ID of the original entry, or nil if not found
+        func getZeroCarbNonFPUEntry(_ treatmentObjectID: NSManagedObjectID) async -> NSManagedObjectID? {
+            let context = CoreDataStack.shared.newTaskContext()
+            context.name = "fpuContext"
+
+            return await context.perform {
+                do {
+                    // Get the fpuID from the selected entry
+                    guard let selectedEntry = try context.existingObject(with: treatmentObjectID) as? CarbEntryStored,
+                          let fpuID = selectedEntry.fpuID
+                    else { return nil }
+
+                    // Fetch the original zero-carb entry (non-FPU) with the same fpuID
+                    let last24Hours = Date().addingTimeInterval(-60 * 60 * 24)
+                    let request = CarbEntryStored.fetchRequest()
+                    request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
+                        NSPredicate(format: "date >= %@", last24Hours as NSDate),
+                        NSPredicate(format: "fpuID == %@", fpuID as CVarArg),
+                        NSPredicate(format: "isFPU == NO"),
+                        NSPredicate(format: "carbs == 0")
+                    ])
+                    request.fetchLimit = 1
+
+                    let originalEntry = try context.fetch(request).first
+                    debugPrint("FPU fetch result: \(originalEntry != nil ? "Entry found" : "No entry found")")
+                    return originalEntry?.objectID
+
+                } catch let error as NSError {
+                    debugPrint("\(DebuggingIdentifiers.failed) Failed to fetch original FPU entry: \(error.userInfo)")
+                    return nil
+                }
+            }
+        }
+
+        /// Retrieves the corresponding carb entry for a given FPU entry.
+        /// This is used when the user has entered a carb entry WITH FPUs all at once.
+        /// - Parameter treatmentObjectID: The ID of the FPU entry
+        /// - Returns: The ID of the corresponding carb entry, or nil if not found
+        func getCorrespondingCarbEntry(_ treatmentObjectID: NSManagedObjectID) async -> NSManagedObjectID? {
+            let context = CoreDataStack.shared.newTaskContext()
+            context.name = "carbContext"
+
+            return await context.perform {
+                do {
+                    // Get the fpuID from the selected entry
+                    guard let selectedEntry = try context.existingObject(with: treatmentObjectID) as? CarbEntryStored,
+                          let fpuID = selectedEntry.fpuID
+                    else { return nil }
+
+                    // Fetch the corresponding carb entry with the same fpuID
+                    let last24Hours = Date().addingTimeInterval(-24.hours.timeInterval)
+                    let request = CarbEntryStored.fetchRequest()
+                    request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
+                        NSPredicate(format: "date >= %@", last24Hours as NSDate),
+                        NSPredicate(format: "fpuID == %@", fpuID as CVarArg),
+                        NSPredicate(format: "isFPU == NO"),
+                        NSPredicate(format: "(carbs > 0) OR (fat > 0) OR (protein > 0)")
+                    ])
+                    request.fetchLimit = 1
+
+                    let correspondingCarbEntry = try context.fetch(request).first
+                    debugPrint(
+                        "Corresponding carb entry fetch result: \(correspondingCarbEntry != nil ? "Entry found" : "No entry found")"
+                    )
+                    return correspondingCarbEntry?.objectID
+
+                } catch let error as NSError {
+                    debugPrint("\(DebuggingIdentifiers.failed) Failed to fetch corresponding carb entry: \(error.userInfo)")
+                    return nil
+                }
+            }
+        }
     }
 }
 

+ 229 - 0
FreeAPS/Sources/Modules/DataTable/View/CarbEntryEditorView.swift

@@ -0,0 +1,229 @@
+//
+//  CarbEntryEditorView.swift
+//  FreeAPS
+//
+//  Created by Marvin Polscheit on 15.01.25.
+//
+import CoreData
+import SwiftUI
+
+struct CarbEntryEditorView: View {
+    @Environment(\.dismiss) private var dismiss
+    @Environment(\.colorScheme) var colorScheme
+    @Environment(AppState.self) var appState
+
+    var state: DataTable.StateModel
+    let carbEntry: CarbEntryStored
+
+    /*
+     This is the objectID of the entry that the user is editing. It is NOT always the `carbEntry: CarbEntryStored` that we pass to the `CarbEntryEditorView`.
+     We need this because FPUs and carbs are treated completely different and that complicates the update process.
+     */
+    @State private var entryToEdit: NSManagedObjectID?
+
+    @State private var editedCarbs: Decimal
+    @State private var editedFat: Decimal
+    @State private var editedProtein: Decimal
+    @State private var editedNote: String
+    @State private var isFPU: Bool
+    @State private var editedDate: Date
+
+    init(state: DataTable.StateModel, carbEntry: CarbEntryStored) {
+        self.state = state
+        self.carbEntry = carbEntry
+        _editedCarbs = State(initialValue: 0) // gets updated in the task block
+        _editedFat = State(initialValue: 0) // gets updated in the task block
+        _editedProtein = State(initialValue: 0) // gets updated in the task block
+        _editedNote = State(initialValue: carbEntry.note ?? "")
+        _isFPU = State(initialValue: carbEntry.isFPU)
+        _entryToEdit = State(initialValue: nil)
+        _editedDate = State(initialValue: Date())
+    }
+
+    private var carbLimitExceeded: Bool {
+        editedCarbs > state.settingsManager.settings.maxCarbs
+    }
+
+    private var fatLimitExceeded: Bool {
+        editedFat > state.settingsManager.settings.maxFat
+    }
+
+    private var proteinLimitExceeded: Bool {
+        editedProtein > state.settingsManager.settings.maxProtein
+    }
+
+    private var limitExceeded: Bool {
+        carbLimitExceeded || fatLimitExceeded || proteinLimitExceeded
+    }
+
+    private var isButtonDisabled: Bool {
+        editedCarbs == 0 && editedFat == 0 && editedProtein == 0
+    }
+
+    private var buttonLabel: some View {
+        if carbLimitExceeded {
+            return Text("Max Carbs of \(state.settingsManager.settings.maxCarbs.description) g Exceeded")
+        } else if fatLimitExceeded {
+            return Text("Max Fat of \(state.settingsManager.settings.maxFat.description) g Exceeded")
+        } else if proteinLimitExceeded {
+            return Text("Max Protein of \(state.settingsManager.settings.maxProtein.description) g Exceeded")
+        }
+
+        return Text("Save and Update")
+    }
+
+    private var buttonBackgroundColor: Color {
+        var treatmentButtonBackground = Color(.systemBlue)
+        if limitExceeded {
+            treatmentButtonBackground = Color(.systemRed)
+        } else if isButtonDisabled {
+            treatmentButtonBackground = Color(.systemGray)
+        }
+
+        return treatmentButtonBackground
+    }
+
+    var stickyButton: some View {
+        ZStack {
+            Rectangle()
+                .frame(width: UIScreen.main.bounds.width, height: 65)
+                .foregroundStyle(colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white)
+                .background(.thinMaterial)
+                .opacity(0.8)
+                .clipShape(Rectangle())
+
+            Button(
+                action: {
+                    guard let entryToEdit = entryToEdit else { return }
+
+                    state.updateEntry(
+                        entryToEdit,
+                        newCarbs: editedCarbs,
+                        newFat: editedFat,
+                        newProtein: editedProtein,
+                        newNote: editedNote,
+                        newDate: editedDate
+                    )
+                    dismiss()
+                }, label: {
+                    buttonLabel
+                        .font(.headline)
+                        .frame(maxWidth: .infinity, maxHeight: .infinity)
+                        .padding(10)
+                }
+            )
+            .frame(width: UIScreen.main.bounds.width * 0.9, height: 40, alignment: .center)
+            .disabled(isButtonDisabled)
+            .background(buttonBackgroundColor)
+            .tint(.white)
+            .clipShape(RoundedRectangle(cornerRadius: 8))
+        }
+    }
+
+    var body: some View {
+        NavigationView {
+            Form {
+                Section {
+                    HStack {
+                        Text("Carbs")
+                        TextFieldWithToolBar(
+                            text: $editedCarbs,
+                            placeholder: "0",
+                            keyboardType: .numberPad,
+                            numberFormatter: Formatter.decimalFormatterWithOneFractionDigit
+                        )
+                        Text("g").foregroundStyle(.secondary)
+                    }
+
+                    if state.settingsManager.settings.useFPUconversion {
+                        HStack {
+                            Text("Protein")
+                            TextFieldWithToolBar(
+                                text: $editedProtein,
+                                placeholder: "0",
+                                keyboardType: .numberPad,
+                                numberFormatter: Formatter.decimalFormatterWithOneFractionDigit
+                            )
+                            Text("g").foregroundStyle(.secondary)
+                        }
+
+                        HStack {
+                            Text("Fat")
+                            TextFieldWithToolBar(
+                                text: $editedFat,
+                                placeholder: "0",
+                                keyboardType: .numberPad,
+                                numberFormatter: Formatter.decimalFormatterWithOneFractionDigit
+                            )
+                            Text("g").foregroundStyle(.secondary)
+                        }
+                    }
+
+                    HStack {
+                        Image(systemName: "square.and.pencil")
+                        TextFieldWithToolBarString(text: $editedNote, placeholder: "Note...", maxLength: 25)
+                    }
+                }.listRowBackground(Color.chart)
+
+                Section {
+                    DatePicker(
+                        "Time",
+                        selection: $editedDate,
+                        displayedComponents: [.date, .hourAndMinute]
+                    )
+                }
+            }
+            .safeAreaInset(
+                edge: .bottom,
+                spacing: 30
+            ) {
+                stickyButton
+            }
+            .scrollContentBackground(.hidden)
+            .background(appState.trioBackgroundColor(for: colorScheme))
+            .navigationTitle("Edit Meal")
+            .navigationBarTitleDisplayMode(.inline)
+            .toolbar {
+                ToolbarItem(placement: .topBarLeading) {
+                    Button("Cancel") {
+                        dismiss()
+                    }
+                }
+            }
+        }
+        .task {
+            /*
+             User taps on a FPU entry in the DataTable list. There are two cases:
+             - the User has entered this FPU entry WITH carbs
+             - the User has entered this FPU entry WITHOUT carbs
+             In the first case, we simply need to load the corresponding carb entry. For this case THIS is the entry we want to edit.
+             In the second case, we need to load the zero-carb entry that actualy holds the FPU values (and the carbs). For this case THIS is the entry we want to edit.
+             */
+            if carbEntry.isFPU {
+                if let result = await state.handleFPUEntry(carbEntry.objectID) {
+                    editedCarbs = result.entryValues?.carbs ?? 0
+                    editedFat = result.entryValues?.fat ?? 0
+                    editedProtein = result.entryValues?.protein ?? 0
+                    editedNote = result.entryValues?.note ?? ""
+                    entryToEdit = result.entryID
+                    editedDate = result.entryValues?.date ?? Date()
+                }
+                /*
+                 User taps on a carb entry in the DataTable list. There are again two cases which don't need explicit handling:
+                 - the User has only entered carbs
+                 - the User has entered carbs with FPU
+                 In both cases, we need to simply load the carb entry that holds all the necessary values for us. This is the entry we want to edit.
+                 */
+            } else {
+                if let values = await state.loadEntryValues(from: carbEntry.objectID) {
+                    editedCarbs = values.carbs
+                    editedFat = values.fat
+                    editedProtein = values.protein
+                    editedNote = values.note
+                    editedDate = values.date
+                    entryToEdit = carbEntry.objectID
+                }
+            }
+        }
+    }
+}

+ 29 - 18
FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift

@@ -40,7 +40,7 @@ extension DataTable {
         @FetchRequest(
             entity: CarbEntryStored.entity(),
             sortDescriptors: [NSSortDescriptor(keyPath: \CarbEntryStored.date, ascending: false)],
-            predicate: NSPredicate.predicateForOneDayAgo,
+            predicate: NSPredicate.carbsHistory,
             animation: .bouncy
         ) var carbEntryStored: FetchedResults<CarbEntryStored>
 
@@ -110,18 +110,6 @@ extension DataTable {
                 .navigationTitle("History")
                 .navigationBarTitleDisplayMode(.large)
                 .toolbar {
-                    ToolbarItem(placement: .topBarLeading, content: {
-                        Button(
-                            action: { state.showModal(for: .statistics) },
-                            label: {
-                                HStack {
-                                    Text("Statistics")
-                                }
-                            }
-                        )
-                    })
-                }
-                .toolbar {
                     ToolbarItem(placement: .topBarTrailing, content: {
                         addButton({
                             showManualGlucose = true
@@ -132,6 +120,11 @@ extension DataTable {
                 .sheet(isPresented: $showManualGlucose) {
                     addGlucoseView()
                 }
+                .sheet(isPresented: $state.showCarbEntryEditor) {
+                    if let carbEntry = state.carbEntryToEdit {
+                        CarbEntryEditorView(state: state, carbEntry: carbEntry)
+                    }
+                }
         }
 
         @ViewBuilder func addButton(_ action: @escaping () -> Void) -> some View {
@@ -596,20 +589,35 @@ extension DataTable {
                     action: {
                         alertCarbEntryToDelete = meal
 
-                        if !meal.isFPU {
+                        // meal is carb-only
+                        if meal.fpuID == nil {
                             alertTitle = "Delete Carbs?"
                             alertMessage = Formatter.dateFormatter
                                 .string(from: meal.date ?? Date()) + ", " +
                                 (Formatter.decimalFormatterWithTwoFractionDigits.string(for: meal.carbs) ?? "0") +
                                 NSLocalizedString(" g", comment: "gram of carbs")
-                        } else {
-                            alertTitle = "Delete Carb Equivalents?"
-                            alertMessage = "All FPUs of the meal will be deleted."
+                        }
+                        // meal is complex-meal or fpu-only
+                        else {
+                            alertTitle = meal.isFPU ? "Delete Carbs Equivalents?" : "Delete Carbs?"
+                            alertMessage = "All FPUs and the carbs of the meal will be deleted."
                         }
 
                         isRemoveHistoryItemAlertPresented = true
                     }
                 ).tint(.red)
+
+                Button(
+                    "Edit",
+                    systemImage: "pencil",
+                    role: .none,
+                    action: {
+                        state.carbEntryToEdit = meal
+                        state.showCarbEntryEditor = true
+                    }
+                )
+                .tint(!state.settingsManager.settings.useFPUconversion && meal.isFPU ? Color(.systemGray4) : Color.blue)
+                .disabled(!state.settingsManager.settings.useFPUconversion && meal.isFPU)
             }
             .alert(
                 Text(NSLocalizedString(alertTitle, comment: "")),
@@ -623,7 +631,10 @@ extension DataTable {
                     }
                     let treatmentObjectID = carbEntryToDelete.objectID
 
-                    state.invokeCarbDeletionTask(treatmentObjectID)
+                    state.invokeCarbDeletionTask(
+                        treatmentObjectID,
+                        isFpuOrComplexMeal: carbEntryToDelete.isFPU || carbEntryToDelete.fat > 0 || carbEntryToDelete.protein > 0
+                    )
                 }
             } message: {
                 Text("\n" + NSLocalizedString(alertMessage, comment: ""))

+ 2 - 2
FreeAPS/Sources/Modules/GlucoseNotificationSettings/GlucoseNotificationSettingsStateModel.swift

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

+ 111 - 64
FreeAPS/Sources/Modules/GlucoseNotificationSettings/View/GlucoseNotificationSettingsRootView.swift

@@ -42,6 +42,28 @@ extension GlucoseNotificationSettings {
             List {
                 SettingInputSection(
                     decimalValue: $decimalPlaceholder,
+                    booleanValue: $state.useAlarmSound,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    selectedVerboseHint: Binding(
+                        get: { selectedVerboseHint },
+                        set: {
+                            selectedVerboseHint = $0.map { AnyView($0) }
+                            hintLabel = "Play Alarm Sound"
+                        }
+                    ),
+                    units: state.units,
+                    type: .boolean,
+                    label: "Play Alarm Sound",
+                    miniHint: "Alarm with every Trio notification.",
+                    verboseHint: VStack(alignment: .leading, spacing: 10) {
+                        Text("Default: OFF").bold()
+                        Text(
+                            "This will cause a sound to be triggered by Trio notifications for Carbs Required, and Glucose Low/High Alarms."
+                        )
+                    }
+                )
+                SettingInputSection(
+                    decimalValue: $decimalPlaceholder,
                     booleanValue: $state.notificationsPump,
                     shouldDisplayHint: $shouldDisplayHint,
                     selectedVerboseHint: Binding(
@@ -161,75 +183,94 @@ extension GlucoseNotificationSettings {
                     miniHint: "Show your current glucose on Trio app icon.",
                     verboseHint: VStack(alignment: .leading, spacing: 10) {
                         Text("Default: OFF").bold()
-                        Text("This will add your current glucose on the top right of your Trio icon as a red notification badge.")
+                        Text(
+                            "This will add your current glucose on the top right of your Trio icon as a red notification badge. Changing setting takes effect on next Glucose reading."
+                        )
                     },
                     headerText: "Various Glucose Notifications"
                 )
 
-                SettingInputSection(
-                    decimalValue: $decimalPlaceholder,
-                    booleanValue: $state.glucoseNotificationsAlways,
-                    shouldDisplayHint: $shouldDisplayHint,
-                    selectedVerboseHint: Binding(
-                        get: { selectedVerboseHint },
-                        set: {
-                            selectedVerboseHint = $0.map { AnyView($0) }
-                            hintLabel = "Always Notify Glucose"
-                        }
-                    ),
-                    units: state.units,
-                    type: .boolean,
-                    label: "Always Notify Glucose",
-                    miniHint: "Trigger a notification every time your glucose is updated.",
-                    verboseHint: VStack(alignment: .leading, spacing: 10) {
-                        Text("Default: OFF").bold()
-                        Text("A notification will be triggered every time your glucose is updated in Trio.")
-                    }
-                )
+                Section {
+                    VStack {
+                        Picker(
+                            selection: $state.glucoseNotificationsOption,
+                            label: Text("Glucose Notifications")
+                        ) {
+                            ForEach(GlucoseNotificationsOption.allCases) { selection in
+                                Text(selection.displayName).tag(selection)
+                            }
+                        }.padding(.top)
 
-                SettingInputSection(
-                    decimalValue: $decimalPlaceholder,
-                    booleanValue: $state.useAlarmSound,
-                    shouldDisplayHint: $shouldDisplayHint,
-                    selectedVerboseHint: Binding(
-                        get: { selectedVerboseHint },
-                        set: {
-                            selectedVerboseHint = $0.map { AnyView($0) }
-                            hintLabel = "Play Alarm Sound"
-                        }
-                    ),
-                    units: state.units,
-                    type: .boolean,
-                    label: "Play Alarm Sound",
-                    miniHint: "Alarm with every Trio notification.",
-                    verboseHint: VStack(alignment: .leading, spacing: 10) {
-                        Text("Default: OFF").bold()
-                        Text("This will cause a sound to be triggered by every Trio notification.")
-                    }
-                )
+                        HStack(alignment: .center) {
+                            Text(
+                                "Choose glucose notifications option. See hint for more details."
+                            )
+                            .font(.footnote)
+                            .foregroundColor(.secondary)
+                            .lineLimit(nil)
+                            Spacer()
+                            Button(
+                                action: {
+                                    hintLabel = "Glucose Notifications"
+                                    selectedVerboseHint =
+                                        AnyView(
+                                            VStack(alignment: .leading, spacing: 10) {
+                                                Text(
+                                                    "Set the Glucose Notifications Option. Descriptions for each option found below."
+                                                )
+                                                VStack(alignment: .leading, spacing: 5) {
+                                                    Text("Disabled:").bold()
+                                                    Text("No Glucose Notificatitons will be triggered.")
+                                                }
+                                                VStack(alignment: .leading, spacing: 5) {
+                                                    Text("Always:").bold()
+                                                    Text(
+                                                        "A notification will be triggered every time your glucose is updated in Trio."
+                                                    )
+                                                }
+                                                VStack(alignment: .leading, spacing: 5) {
+                                                    Text("Only Alarm Limits:").bold()
+                                                    Text(
+                                                        "A notification will be triggered only when glucose levels are below the LOW limit or above the HIGH limit, as specified in Glucose Alarm Limits below."
+                                                    )
+                                                }
+                                            }
+                                        )
+                                    shouldDisplayHint.toggle()
+                                },
+                                label: {
+                                    HStack {
+                                        Image(systemName: "questionmark.circle")
+                                    }
+                                }
+                            ).buttonStyle(BorderlessButtonStyle())
+                        }.padding(.top)
+                    }.padding(.bottom)
+                }.listRowBackground(Color.chart)
 
-                SettingInputSection(
-                    decimalValue: $decimalPlaceholder,
-                    booleanValue: $state.addSourceInfoToGlucoseNotifications,
-                    shouldDisplayHint: $shouldDisplayHint,
-                    selectedVerboseHint: Binding(
-                        get: { selectedVerboseHint },
-                        set: {
-                            selectedVerboseHint = $0.map { AnyView($0) }
-                            hintLabel = "Add Glucose Source to Alarm"
+                if state.glucoseNotificationsOption != GlucoseNotificationsOption.disabled {
+                    self.lowAndHighGlucoseAlertSection
+                    SettingInputSection(
+                        decimalValue: $decimalPlaceholder,
+                        booleanValue: $state.addSourceInfoToGlucoseNotifications,
+                        shouldDisplayHint: $shouldDisplayHint,
+                        selectedVerboseHint: Binding(
+                            get: { selectedVerboseHint },
+                            set: {
+                                selectedVerboseHint = $0.map { AnyView($0) }
+                                hintLabel = "Add Glucose Source to Alarm"
+                            }
+                        ),
+                        units: state.units,
+                        type: .boolean,
+                        label: "Add Glucose Source to Alarm",
+                        miniHint: "Source of the glucose reading will be added to the notification.",
+                        verboseHint: VStack(alignment: .leading, spacing: 10) {
+                            Text("Default: OFF").bold()
+                            Text("The source of the glucose reading will be added to the notification.")
                         }
-                    ),
-                    units: state.units,
-                    type: .boolean,
-                    label: "Add Glucose Source to Alarm",
-                    miniHint: "Source of the glucose reading will be added to the notification.",
-                    verboseHint: VStack(alignment: .leading, spacing: 10) {
-                        Text("Default: OFF").bold()
-                        Text("The source of the glucose reading will be added to the notification.")
-                    }
-                )
-
-                self.lowAndHighGlucoseAlertSection
+                    )
+                }
             }
             .listSectionSpacing(sectionSpacing)
             .sheet(isPresented: $shouldDisplayHint) {
@@ -336,8 +377,14 @@ extension GlucoseNotificationSettings {
                                 hintLabel = "Low and High Glucose Alarm Limits"
                                 selectedVerboseHint =
                                     AnyView(VStack(alignment: .leading, spacing: 10) {
-                                        Text("Low Default: 70 mg/dL").bold()
-                                        Text("High Default: 180 mg/dL").bold()
+                                        let low: Decimal = 70
+                                        let high: Decimal = 180
+                                        let labelLow = (state.units == .mgdL ? low.description : low.formattedAsMmolL) + " " +
+                                            state.units.rawValue
+                                        let labelHigh = (state.units == .mgdL ? high.description : high.formattedAsMmolL) + " " +
+                                            state.units.rawValue
+                                        Text("Low Default: " + labelLow).bold()
+                                        Text("High Default: " + labelHigh).bold()
                                         VStack(alignment: .leading, spacing: 10) {
                                             Text(
                                                 "These two settings determine the range outside of which you will be notified via push notifications."

+ 0 - 2
FreeAPS/Sources/Modules/Home/HomeDataFlow.swift

@@ -9,8 +9,6 @@ protocol HomeProvider: Provider {
     func heartbeatNow()
     func pumpSettings() async -> PumpSettings
     func getBasalProfile() async -> [BasalProfileEntry]
-    func tempTargets(hours: Int) -> [TempTarget]
     func pumpReservoir() async -> Decimal?
-    func tempTarget() -> TempTarget?
     func getBGTargets() async -> BGTargets
 }

+ 0 - 10
FreeAPS/Sources/Modules/Home/HomeProvider.swift

@@ -16,16 +16,6 @@ extension Home {
             apsManager.heartbeat(date: Date())
         }
 
-        func tempTargets(hours: Int) -> [TempTarget] {
-            tempTargetsStorage.recent().filter {
-                $0.createdAt.addingTimeInterval(hours.hours.timeInterval) > Date()
-            }
-        }
-
-        func tempTarget() -> TempTarget? {
-            tempTargetsStorage.current()
-        }
-
         func pumpSettings() async -> PumpSettings {
             await storage.retrieveAsync(OpenAPS.Settings.settings, as: PumpSettings.self)
                 ?? PumpSettings(from: OpenAPS.defaults(for: OpenAPS.Settings.settings))

+ 95 - 0
FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/GlucoseTargetSetup.swift

@@ -0,0 +1,95 @@
+import Foundation
+
+extension Home.StateModel {
+    /**
+     Processes raw glucose target data into a list of target profiles for visualization.
+
+     - Parameters:
+        - rawTargets: The raw glucose target data containing offset and glucose values.
+        - startMarker: The reference date to start the target profiles from.
+     - Returns: An array of `TargetProfile` objects, each representing a glucose target range starting from the day of the startMarker and ending two days later.
+
+     The function:
+     - Converts glucose targets into profiles covering three consecutive days (day of startMarker, day after startMarker and day after that).
+     - Calculates start and end times for each target based on the offsets provided.
+     - Handles conversions between mg/dL and mmol/L as per user settings.
+     - Ensures targets span across midnight to avoid data cutoff.
+     */
+    func processFetchedTargets(_ rawTargets: BGTargets, startMarker: Date) -> [TargetProfile] {
+        var targetProfiles: [TargetProfile] = []
+
+        // Ensure there are targets to process
+        guard !rawTargets.targets.isEmpty else {
+            debugPrint("\(DebuggingIdentifiers.failed) Warning: No targets to process in rawTargets.")
+            return []
+        }
+
+        let targets = rawTargets.targets
+
+        // Base date is the start of the day for the startMarker
+        let baseDate = Calendar.current.startOfDay(for: startMarker)
+
+        // Process each target three times
+        for index in 0 ..< (targets.count * 3) {
+            // Calculate the day offset (0 for today, 1 for tomorrow, 2 for day after)
+            let dayOffset = index / targets.count
+            let targetIndex = index % targets.count
+
+            // Validate target index to ensure safety
+            guard targetIndex < targets.count else {
+                debugPrint("\(DebuggingIdentifiers.failed) Error: Invalid target index \(targetIndex).")
+                continue
+            }
+
+            // Fetch the target for the current iteration
+            let target = targets[targetIndex]
+
+            // Calculate the time offset for the current day
+            let dayTimeOffset = TimeInterval(dayOffset * 24 * 60 * 60)
+
+            // Calculate the start time for the current target
+            let startTime = baseDate
+                .addingTimeInterval(dayTimeOffset)
+                .addingTimeInterval(TimeInterval(target.offset * 60))
+
+            // Calculate the end time for the current target
+            let endTime: Date = {
+                if targetIndex + 1 < targets.count {
+                    // End time is the start time of the next target within the same day
+                    return baseDate
+                        .addingTimeInterval(dayTimeOffset)
+                        .addingTimeInterval(TimeInterval(targets[targetIndex + 1].offset * 60))
+                } else {
+                    // End time is the end of the day (midnight of the next day)
+                    return baseDate.addingTimeInterval(dayTimeOffset + 24 * 60 * 60)
+                }
+            }()
+
+            // Convert glucose value based on user unit preference (mg/dL or mmol/L)
+            let targetValue = units == .mgdL ? target.low : target.low.asMmolL
+
+            // Append the processed target profile to the list
+            targetProfiles.append(
+                TargetProfile(
+                    value: targetValue,
+                    startTime: startTime.timeIntervalSinceReferenceDate,
+                    endTime: endTime.timeIntervalSinceReferenceDate
+                )
+            )
+        }
+      
+        return targetProfiles
+    }
+}
+
+struct TargetProfile: Hashable {
+    let value: Decimal
+    let startTime: TimeInterval
+    let endTime: TimeInterval
+}
+
+private extension Date {
+    var startOfDay: Date {
+        Calendar.current.startOfDay(for: self)
+    }
+}

+ 18 - 0
FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/StartEndMarkerSetup.swift

@@ -0,0 +1,18 @@
+import Foundation
+
+extension Home.StateModel {
+    // Update start and  end marker to fix scroll update problem with x axis
+    func updateStartEndMarkers() {
+        startMarker = Date(timeIntervalSince1970: TimeInterval(NSDate().timeIntervalSince1970 - 86400))
+
+        let threeHourSinceNow = Date(timeIntervalSinceNow: TimeInterval(hours: 3))
+
+        // min is 1.5h -> (1.5*1h = 1.5*(5*12*60))
+        let dynamicFutureDateForCone = Date(timeIntervalSinceNow: TimeInterval(
+            Int(1.5) * 5 * minCount * 60
+        ))
+
+        endMarker = forecastDisplayType == .lines ? threeHourSinceNow : dynamicFutureDateForCone <= threeHourSinceNow ?
+            dynamicFutureDateForCone.addingTimeInterval(TimeInterval(minutes: 30)) : threeHourSinceNow
+    }
+}

+ 6 - 14
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -19,6 +19,8 @@ extension Home {
         @ObservationIgnored @Injected() var overrideStorage: OverrideStorage!
         private let timer = DispatchTimer(timeInterval: 5)
         private(set) var filteredHours = 24
+        var startMarker = Date(timeIntervalSinceNow: TimeInterval(hours: -24))
+        var endMarker = Date(timeIntervalSinceNow: TimeInterval(hours: 3))
         var manualGlucose: [BloodGlucose] = []
         var uploadStats = false
         var recentGlucose: BloodGlucose?
@@ -26,7 +28,7 @@ extension Home {
         var basalProfile: [BasalProfileEntry] = []
         var bgTargets = BGTargets(from: OpenAPS.defaults(for: OpenAPS.Settings.bgTargets))
             ?? BGTargets(units: .mgdL, userPreferredUnits: .mgdL, targets: [])
-        var tempTargets: [TempTarget] = []
+        var targetProfiles: [TargetProfile] = []
         var timerDate = Date()
         var closedLoop = false
         var pumpSuspended = false
@@ -37,7 +39,6 @@ extension Home {
         var reservoir: Decimal?
         var pumpName = ""
         var pumpExpiresAtDate: Date?
-        var tempTarget: TempTarget?
         var highTTraisesSens: Bool = false
         var lowTTlowersSens: Bool = false
         var isExerciseModeActive: Bool = false
@@ -267,7 +268,6 @@ extension Home {
         }
 
         private func registerObservers() {
-            broadcaster.register(GlucoseObserver.self, observer: self)
             broadcaster.register(DeterminationObserver.self, observer: self)
             broadcaster.register(SettingsObserver.self, observer: self)
             broadcaster.register(PreferencesObserver.self, observer: self)
@@ -479,8 +479,10 @@ extension Home {
 
         private func setupGlucoseTargets() async {
             let bgTargets = await provider.getBGTargets()
+            let targetProfiles = processFetchedTargets(bgTargets, startMarker: startMarker)
             await MainActor.run {
                 self.bgTargets = bgTargets
+                self.targetProfiles = targetProfiles
             }
         }
 
@@ -554,7 +556,6 @@ extension Home {
 }
 
 extension Home.StateModel:
-    GlucoseObserver,
     DeterminationObserver,
     SettingsObserver,
     PreferencesObserver,
@@ -565,11 +566,6 @@ extension Home.StateModel:
     PumpTimeZoneObserver,
     PumpDeactivatedObserver
 {
-    // TODO: still needed?
-    func glucoseDidUpdate(_: [BloodGlucose]) {
-//        setupGlucose()
-    }
-
     func determinationDidUpdate(_: Determination) {
         waitForSuggestion = false
     }
@@ -584,6 +580,7 @@ extension Home.StateModel:
         highGlucose = settingsManager.settings.high
         Task {
             await getCurrentGlucoseTarget()
+            await setupGlucoseTargets()
         }
         hbA1cDisplayUnit = settingsManager.settings.hbA1cDisplayUnit
         glucoseColorScheme = settingsManager.settings.glucoseColorScheme
@@ -606,11 +603,6 @@ extension Home.StateModel:
         lowTTlowersSens = settingsManager.preferences.lowTemptargetLowersSensitivity
     }
 
-    // TODO: is this ever really triggered? react to MOC changes?
-    func pumpHistoryDidUpdate(_: [PumpHistoryEvent]) {
-        displayPumpStatusHighlightMessage()
-    }
-
     func pumpSettingsDidChange(_: PumpSettings) {
         Task {
             await setupPumpSettings()

+ 6 - 6
FreeAPS/Sources/Modules/Home/View/Chart/ChartElements/BasalChart.swift

@@ -34,7 +34,7 @@ extension MainChartView {
             }
             .frame(minHeight: geo.size.height * 0.05)
             .frame(width: fullWidth(viewWidth: screenSize.width))
-            .chartXScale(domain: startMarker ... endMarker)
+            .chartXScale(domain: state.startMarker ... state.endMarker)
             .chartXAxis { basalChartXAxis }
             .chartXAxis(.hidden)
             .chartYAxis(.hidden)
@@ -96,7 +96,7 @@ extension MainChartView {
                 series: .value("profile", "profile")
             ).lineStyle(.init(lineWidth: 2, dash: [2, 4])).foregroundStyle(Color.insulin)
             LineMark(
-                x: .value("End Date", profile.endDate ?? endMarker),
+                x: .value("End Date", profile.endDate ?? state.endMarker),
                 y: .value("Amount", profile.amount),
                 series: .value("profile", "profile")
             ).lineStyle(.init(lineWidth: 2.5, dash: [2, 4])).foregroundStyle(Color.insulin)
@@ -204,7 +204,7 @@ extension MainChartView {
 
             async let getRegularBasalPoints = findRegularBasalPoints(
                 timeBegin: dayAgoTime,
-                timeEnd: endMarker.timeIntervalSince1970
+                timeEnd: state.endMarker.timeIntervalSince1970
             )
 
             var regularPoints = await getRegularBasalPoints
@@ -224,8 +224,8 @@ extension MainChartView {
                     BasalProfile(
                         amount: single.amount,
                         isOverwritten: single.isOverwritten,
-                        startDate: startMarker,
-                        endDate: endMarker
+                        startDate: state.startMarker,
+                        endDate: state.endMarker
                     )
                 )
             }
@@ -248,7 +248,7 @@ extension MainChartView {
                             amount: lastItem.amount,
                             isOverwritten: lastItem.isOverwritten,
                             startDate: lastItem.startDate,
-                            endDate: endMarker
+                            endDate: state.endMarker
                         )
                     )
                 }

+ 1 - 1
FreeAPS/Sources/Modules/Home/View/Chart/ChartElements/CarbView.swift

@@ -50,7 +50,7 @@ struct CarbView: ChartContent {
         ForEach(fpuData, id: \.id) { fpu in
             let fpuAmount = fpu.carbs
             let size = (MainChartHelper.Config.fpuSize + CGFloat(fpuAmount) * MainChartHelper.Config.carbsScale) * 1.8
-            let yPosition = minValue
+            let yPosition = minValue // value is parsed to mmol/L when passed into struct based on user settings
 
             PointMark(
                 x: .value("Time", fpu.date ?? Date(), unit: .second),

+ 1 - 1
FreeAPS/Sources/Modules/Home/View/Chart/ChartElements/CobIobChart.swift

@@ -46,7 +46,7 @@ extension MainChartView {
         .chartLegend(.hidden)
         .frame(minHeight: geo.size.height * 0.12)
         .frame(width: fullWidth(viewWidth: screenSize.width))
-        .chartXScale(domain: startMarker ... endMarker)
+        .chartXScale(domain: state.startMarker ... state.endMarker)
         .chartXSelection(value: $selection)
         .chartXAxis { basalChartXAxis }
         .chartYAxis { cobIobChartYAxis }

+ 2 - 2
FreeAPS/Sources/Modules/Home/View/Chart/ChartElements/DummyCharts.swift

@@ -43,7 +43,7 @@ extension MainChartView {
         )
         .frame(width: screenSize.width - 10)
         .chartXAxis { mainChartXAxis }
-        .chartXScale(domain: startMarker ... endMarker)
+        .chartXScale(domain: state.startMarker ... state.endMarker)
         .chartXAxis(.hidden)
         .chartYAxis { mainChartYAxis }
         .chartYScale(
@@ -69,7 +69,7 @@ extension MainChartView {
             .id("DummyCobChart")
             .frame(minHeight: geo.size.height * 0.12)
             .frame(width: screenSize.width - 10)
-            .chartXScale(domain: startMarker ... endMarker)
+            .chartXScale(domain: state.startMarker ... state.endMarker)
             .chartXAxis { basalChartXAxis }
             .chartXAxis(.hidden)
             .chartYAxis { cobIobChartYAxis }

+ 4 - 109
FreeAPS/Sources/Modules/Home/View/Chart/ChartElements/GlucoseTargetsView.swift

@@ -3,30 +3,20 @@ import Foundation
 import SwiftUI
 
 struct GlucoseTargetsView: ChartContent {
-    let startMarker: Date
-    let units: GlucoseUnits
-    let bgTargets: BGTargets
+    let targetProfiles: [TargetProfile]
 
     var body: some ChartContent {
-        drawGlucoseTargets()
+        drawGlucoseTargets(for: targetProfiles)
     }
 
     /**
      Draws glucose target ranges on the chart
 
      - Returns: A ChartContent containing line marks representing target glucose ranges
-
-     The function:
-     - Creates target profiles for two consecutive days
-     - Converts values between mg/dL and mmol/L based on user settings
-     - Draws green lines to visualize the target ranges
      */
-    private func drawGlucoseTargets() -> some ChartContent {
-        // Array to store target profiles for visualization
-        let targetProfiles: [TargetProfile] = processFetchedTargets(bgTargets)
-
+    private func drawGlucoseTargets(for targetProfiles: [TargetProfile]) -> some ChartContent {
         // Draw target lines for each profile
-        return ForEach(targetProfiles, id: \.self) { profile in
+        ForEach(targetProfiles, id: \.self) { profile in
             LineMark(
                 x: .value("Time", Date(timeIntervalSinceReferenceDate: profile.startTime)),
                 y: .value("Target", profile.value)
@@ -42,99 +32,4 @@ struct GlucoseTargetsView: ChartContent {
             .foregroundStyle(Color.green.gradient)
         }
     }
-
-    /**
-     Processes raw glucose target data into a list of target profiles for visualization.
-
-     - Parameter rawTargets: The raw glucose target data containing offset and glucose values.
-     - Returns: An array of `TargetProfile` objects, each representing a glucose target range for today and tomorrow.
-
-     The function:
-     - Converts glucose targets into profiles covering two consecutive days (today and tomorrow).
-     - Calculates start and end times for each target based on the offsets provided.
-     - Handles conversions between mg/dL and mmol/L as per user settings.
-     - Ensures targets span across midnight to avoid data cutoff.
-
-     Example:
-     For a target at offset 0 (midnight) with low glucose value 70 mg/dL, the function generates two profiles:
-     - One for today from midnight to the next target offset or end of the day.
-     - Another for tomorrow covering the same time range.
-     */
-    private func processFetchedTargets(_ rawTargets: BGTargets) -> [TargetProfile] {
-        var targetProfiles: [TargetProfile] = []
-
-        // Ensure there are targets to process
-        guard !rawTargets.targets.isEmpty else {
-            print("Warning: No targets to process in rawTargets.")
-            return []
-        }
-
-        let targets = rawTargets.targets
-
-        // Base date is the start of the day for the startMarker
-        let baseDate = Calendar.current.startOfDay(for: startMarker)
-
-        // Process each target twice: once for today and once for tomorrow
-        for index in 0 ..< (targets.count * 2) {
-            // Calculate the day offset (0 for today, 1 for tomorrow)
-            let dayOffset = index / targets.count
-            let targetIndex = index % targets.count
-
-            // Validate target index to ensure safety
-            guard targetIndex < targets.count else {
-                print("Error: Invalid target index \(targetIndex).")
-                continue
-            }
-
-            // Fetch the target for the current iteration
-            let target = targets[targetIndex]
-
-            // Calculate the time offset for the current day
-            let dayTimeOffset = TimeInterval(dayOffset * 24 * 60 * 60)
-
-            // Calculate the start time for the current target
-            let startTime = baseDate
-                .addingTimeInterval(dayTimeOffset)
-                .addingTimeInterval(TimeInterval(target.offset * 60))
-
-            // Calculate the end time for the current target
-            let endTime: Date = {
-                if targetIndex + 1 < targets.count {
-                    // End time is the start time of the next target within the same day
-                    return baseDate
-                        .addingTimeInterval(dayTimeOffset)
-                        .addingTimeInterval(TimeInterval(targets[targetIndex + 1].offset * 60))
-                } else {
-                    // End time is the end of the day (midnight of the next day)
-                    return baseDate.addingTimeInterval(dayTimeOffset + 24 * 60 * 60)
-                }
-            }()
-
-            // Convert glucose value based on user unit preference (mg/dL or mmol/L)
-            let targetValue = units == .mgdL ? target.low : target.low.asMmolL
-
-            // Append the processed target profile to the list
-            targetProfiles.append(
-                TargetProfile(
-                    value: targetValue,
-                    startTime: startTime.timeIntervalSinceReferenceDate,
-                    endTime: endTime.timeIntervalSinceReferenceDate
-                )
-            )
-        }
-
-        return targetProfiles
-    }
-}
-
-struct TargetProfile: Hashable {
-    let value: Decimal
-    let startTime: TimeInterval
-    let endTime: TimeInterval
-}
-
-private extension Date {
-    var startOfDay: Date {
-        Calendar.current.startOfDay(for: self)
-    }
 }

+ 9 - 15
FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift

@@ -10,7 +10,6 @@ struct MainChartView: View {
     var safeAreaSize: CGFloat
     var units: GlucoseUnits
     var hours: Int
-    var tempTargets: [TempTarget]
     var highGlucose: Decimal
     var lowGlucose: Decimal
     var currentGlucoseTarget: Decimal
@@ -23,10 +22,6 @@ struct MainChartView: View {
 
     @State var basalProfiles: [BasalProfile] = []
     @State var preparedTempBasals: [(start: Date, end: Date, rate: Double)] = []
-    @State var startMarker =
-        Date(timeIntervalSinceNow: TimeInterval(hours: -24))
-    @State var endMarker = Date(timeIntervalSinceNow: TimeInterval(hours: 3))
-
     @State var selection: Date? = nil
 
     @State var mainChartHasInitialized = false
@@ -88,7 +83,7 @@ struct MainChartView: View {
                         }
                         .onChange(of: state.glucoseFromPersistence.last?.glucose) {
                             scroller.scrollTo("MainChart", anchor: .trailing)
-                            updateStartEndMarkers()
+                            state.updateStartEndMarkers()
                         }
                         .onChange(of: state.enactedAndNonEnactedDeterminations.first?.deliverAt) {
                             scroller.scrollTo("MainChart", anchor: .trailing)
@@ -100,7 +95,7 @@ struct MainChartView: View {
                         .onAppear {
                             if !mainChartHasInitialized {
                                 scroller.scrollTo("MainChart", anchor: .trailing)
-                                updateStartEndMarkers()
+                                state.updateStartEndMarkers()
                                 calculateTempBasalsInBackground()
                                 mainChartHasInitialized = true
                             }
@@ -122,6 +117,10 @@ extension MainChartView {
                 drawEndRuleMark()
                 drawCurrentTimeMarker()
 
+                GlucoseTargetsView(
+                    targetProfiles: state.targetProfiles
+                )
+
                 OverrideView(
                     state: state,
                     overrides: state.overrides,
@@ -137,12 +136,6 @@ extension MainChartView {
                     viewContext: context
                 )
 
-                GlucoseTargetsView(
-                    startMarker: startMarker,
-                    units: state.units,
-                    bgTargets: state.bgTargets
-                )
-
                 GlucoseChartView(
                     glucoseData: state.glucoseFromPersistence,
                     units: state.units,
@@ -164,7 +157,8 @@ extension MainChartView {
                     units: state.units,
                     carbData: state.carbsFromPersistence,
                     fpuData: state.fpusFromPersistence,
-                    minValue: state.minYAxisValue
+                    minValue: units == .mgdL ? state.minYAxisValue : state.minYAxisValue
+                        .asMmolL
                 )
 
                 ForecastView(
@@ -198,7 +192,7 @@ extension MainChartView {
                 minHeight: geo.size.height * (0.28 - safeAreaSize)
             )
             .frame(width: fullWidth(viewWidth: screenSize.width))
-            .chartXScale(domain: startMarker ... endMarker)
+            .chartXScale(domain: state.startMarker ... state.endMarker)
             .chartXAxis { mainChartXAxis }
             .chartYAxis { mainChartYAxis }
             .chartYAxis(.hidden)

+ 13 - 11
FreeAPS/Sources/Modules/Home/View/Header/LoopView.swift

@@ -4,6 +4,8 @@ import SwiftUI
 import UIKit
 
 struct LoopView: View {
+    @Environment(\.colorScheme) var colorScheme
+
     private enum Config {
         static let lag: TimeInterval = 30
     }
@@ -19,10 +21,19 @@ struct LoopView: View {
     private let rect = CGRect(x: 0, y: 0, width: 18, height: 18)
 
     var body: some View {
+        loopStatusWithMinutes
+            .padding(.vertical, 5)
+            .padding(.horizontal, 10)
+            .overlay(
+                Capsule()
+                    .stroke(color.opacity(0.4), lineWidth: 2)
+            )
+    }
+
+    private var loopStatusWithMinutes: some View {
         HStack(alignment: .center) {
             ZStack {
-                Image(systemName: "circle")
-                    .mask(mask(in: rect).fill(style: FillStyle(eoFill: true)))
+                Image(systemName: (!closedLoop || manualTempBasal) ? "circle.and.line.horizontal" : "circle")
                 if isLooping {
                     ProgressView()
                 }
@@ -41,7 +52,6 @@ struct LoopView: View {
                 Text("--")
             }
         }
-        .strikethrough(!closedLoop || manualTempBasal, pattern: .solid, color: color)
         .font(.callout).fontWeight(.bold).fontDesign(.rounded)
         .foregroundColor(color)
     }
@@ -80,14 +90,6 @@ struct LoopView: View {
             return .loopRed
         }
     }
-
-    func mask(in rect: CGRect) -> Path {
-        var path = Rectangle().path(in: rect)
-        if !closedLoop || manualTempBasal {
-            path.addPath(Rectangle().path(in: CGRect(x: rect.minX, y: rect.midY - 4, width: rect.width, height: 8)))
-        }
-        return path
-    }
 }
 
 extension View {

+ 41 - 19
FreeAPS/Sources/Modules/Home/View/Header/PumpView.swift

@@ -45,24 +45,44 @@ struct PumpView: View {
                     HStack {
                         Image(systemName: "cross.vial.fill")
                             .font(.callout)
-                            .foregroundColor(reservoirColor)
+
                         if reservoir == 0xDEAD_BEEF {
                             Text("50+ " + NSLocalizedString("U", comment: "Insulin unit"))
-                                .font(.callout).fontWeight(.bold).fontDesign(.rounded)
+                                .font(.callout)
+                                .fontWeight(.bold)
+                                .fontDesign(.rounded)
                         } else {
                             Text(
                                 Formatter.integerFormatter
                                     .string(from: reservoir as NSNumber)! + NSLocalizedString(" U", comment: "Insulin unit")
                             )
-                            .font(.callout).fontWeight(.bold).fontDesign(.rounded)
+                            .font(.callout)
+                            .fontWeight(.bold)
+                            .fontDesign(.rounded)
                         }
                     }
+                    .padding(.vertical, 5)
+                    .padding(.horizontal, 10)
+                    .foregroundStyle(reservoirColor)
+                    .overlay(
+                        Capsule()
+                            .stroke(reservoirColor.opacity(0.4), lineWidth: 2)
+                    )
 
                     if let timeZone = timeZone, timeZone.secondsFromGMT() != TimeZone.current.secondsFromGMT() {
-                        Image(systemName: "clock.badge.exclamationmark.fill")
-                            .font(.callout)
-                            .symbolRenderingMode(.palette)
-                            .foregroundStyle(.red, Color(.warning))
+                        HStack {
+                            Image(systemName: "clock.badge.exclamationmark.fill")
+                                .font(.callout)
+                                .symbolRenderingMode(.palette)
+                                .foregroundStyle(.red, Color(.warning))
+
+                            Text("Timezone")
+                                .font(.callout)
+                                .fontWeight(.bold)
+                                .fontDesign(.rounded)
+                                .foregroundStyle(.red)
+                        }
+                        .padding(.leading, 12)
                     }
                 }
 
@@ -70,8 +90,8 @@ struct PumpView: View {
                     HStack {
                         Image(systemName: "battery.100")
                             .font(.callout)
-                            .foregroundColor(batteryColor)
-                        Text("\(Int(battery.first?.percent ?? 100)) %")
+                            .foregroundStyle(batteryColor)
+                        Text("\(Formatter.integerFormatter.string(for: battery.first?.percent ?? 100) ?? "100") %")
                             .font(.callout).fontWeight(.bold).fontDesign(.rounded)
                     }
                 }
@@ -80,13 +100,15 @@ struct PumpView: View {
                     HStack {
                         Image(systemName: "stopwatch.fill")
                             .font(.callout)
-                            .foregroundColor(timerColor)
+                            .foregroundStyle(timerColor)
 
                         Text(remainingTimeString(time: date.timeIntervalSince(timerDate)))
                             .font(!(date.timeIntervalSince(timerDate) > 0) ? .subheadline : .callout)
                             .fontWeight(.bold)
                             .fontDesign(.rounded)
                     }
+                    // aligns the stopwatch icon exactly with the first pixel of the reservoir icon
+                    .padding(.leading, 12)
                 }
             }
         }
@@ -123,11 +145,11 @@ struct PumpView: View {
 
         switch battery.percent {
         case ...10:
-            return .red
+            return Color.loopRed
         case ...20:
-            return .yellow
+            return Color.orange
         default:
-            return .green
+            return Color.loopGreen
         }
     }
 
@@ -138,11 +160,11 @@ struct PumpView: View {
 
         switch reservoir {
         case ...10:
-            return .red
+            return Color.loopRed
         case ...30:
-            return .yellow
+            return Color.orange
         default:
-            return .blue
+            return Color.insulin
         }
     }
 
@@ -155,11 +177,11 @@ struct PumpView: View {
 
         switch time {
         case ...8.hours.timeInterval:
-            return .red
+            return Color.loopRed
         case ...1.days.timeInterval:
-            return .yellow
+            return Color.orange
         default:
-            return .green
+            return Color.loopGreen
         }
     }
 }

+ 107 - 51
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -5,11 +5,9 @@ import SwiftUI
 import Swinject
 
 struct TimePicker: Identifiable {
-    let label: String
-    let number: String
     var active: Bool
     let hours: Int16
-    var id: String { label }
+    var id: String { hours.description }
 }
 
 extension Home {
@@ -36,15 +34,12 @@ extension Home {
         @State var showPumpSelection: Bool = false
         @State var notificationsDisabled = false
         @State var timeButtons: [TimePicker] = [
-            TimePicker(label: "2 hours", number: "2", active: false, hours: 2),
-            TimePicker(label: "4 hours", number: "4", active: false, hours: 4),
-            TimePicker(label: "6 hours", number: "6", active: false, hours: 6),
-            TimePicker(label: "12 hours", number: "12", active: false, hours: 12),
-            TimePicker(label: "24 hours", number: "24", active: false, hours: 24)
+            TimePicker(active: false, hours: 4),
+            TimePicker(active: false, hours: 6),
+            TimePicker(active: false, hours: 12),
+            TimePicker(active: false, hours: 24)
         ]
 
-        let buttonFont = Font.custom("TimeButtonFont", size: 14)
-
         @FetchRequest(fetchRequest: OverrideStored.fetch(
             NSPredicate.lastActiveOverride,
             ascending: false,
@@ -116,7 +111,8 @@ extension Home {
                 timeZone: state.timeZone,
                 pumpStatusHighlightMessage: state.pumpStatusHighlightMessage,
                 battery: state.batteryFromPersistence
-            ).onTapGesture {
+            )
+            .onTapGesture {
                 if state.pumpDisplayState == nil {
                     // shows user confirmation dialog with pump model choices, then proceeds to setup
                     showPumpSelection.toggle()
@@ -245,45 +241,73 @@ extension Home {
             return components.isEmpty ? nil : components.joined(separator: ", ")
         }
 
-        var timeInterval: some View {
-            HStack(alignment: .center) {
+        var timeIntervalButtons: some View {
+            let buttonColor = (colorScheme == .dark ? Color.white : Color.black).opacity(0.8)
+
+            return HStack(alignment: .center) {
                 ForEach(timeButtons) { button in
-                    Text(button.active ? NSLocalizedString(button.label, comment: "") : button.number).onTapGesture {
+                    Button(action: {
                         state.hours = button.hours
+                    }) {
+                        Group {
+                            if button.active {
+                                Text(
+                                    NSLocalizedString(button.hours.description, comment: "") + " " +
+                                        NSLocalizedString("h", comment: "h")
+                                )
+                            } else {
+                                Text(NSLocalizedString(button.hours.description, comment: ""))
+                            }
+                        }
+                        .font(.footnote)
+                        .fontWeight(button.active ? .semibold : .regular)
+                        .padding(.vertical, 5)
+                        .padding(.horizontal, 10)
+                        .foregroundColor(
+                            button
+                                .active ? (colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white) : buttonColor
+                        )
+                        .background(button.active ? buttonColor.opacity(colorScheme == .dark ? 1 : 0.8) : Color.clear)
+                        .clipShape(Capsule())
+                        .overlay(
+                            Capsule()
+                                .stroke(button.active ? buttonColor.opacity(0.4) : Color.clear, lineWidth: 2)
+                        )
                     }
-                    .foregroundStyle(button.active ? (colorScheme == .dark ? Color.white : Color.black).opacity(0.9) : .secondary)
-                    .frame(maxHeight: 30).padding(.horizontal, 8)
-                    .background(
-                        button.active ?
-                            // RGB(30, 60, 95)
-                            (
-                                colorScheme == .dark ? Color(red: 0.1176470588, green: 0.2352941176, blue: 0.3725490196) :
-                                    Color.white
-                            ) :
-                            Color
-                            .clear
-                    )
-                    .cornerRadius(20)
                 }
-                Button(action: {
-                    state.isLegendPresented.toggle()
-                }) {
-                    Image(systemName: "info")
-                        .foregroundColor(colorScheme == .dark ? Color.white : Color.black).opacity(0.9)
-                        .frame(width: 20, height: 20)
-                        .background(
-                            colorScheme == .dark ? Color(red: 0.1176470588, green: 0.2352941176, blue: 0.3725490196) :
-                                Color.white
-                        )
-                        .clipShape(Circle())
+            }
+        }
+
+        var statsIconString: String {
+            if #available(iOS 18, *) {
+                return "chart.line.text.clipboard"
+            } else {
+                return "list.clipboard"
+            }
+        }
+
+        @ViewBuilder private func tappableButton(
+            buttonColor: Color,
+            label: String,
+            iconString: String,
+            action: @escaping () -> Void
+        ) -> some View {
+            Button(action: {
+                action()
+            }) {
+                HStack {
+                    Image(systemName: iconString)
+                    Text(label)
                 }
-                .padding([.top, .bottom])
+                .font(.footnote)
+                .padding(.vertical, 5)
+                .padding(.horizontal, 10)
+                .foregroundStyle(buttonColor)
+                .overlay(
+                    Capsule()
+                        .stroke(buttonColor.opacity(0.4), lineWidth: 2)
+                )
             }
-            .shadow(
-                color: Color.black.opacity(colorScheme == .dark ? 0.75 : 0.33),
-                radius: colorScheme == .dark ? 5 : 3
-            )
-            .font(buttonFont)
         }
 
         @ViewBuilder func mainChart(geo: GeometryProxy) -> some View {
@@ -293,7 +317,6 @@ extension Home {
                     safeAreaSize: notificationsDisabled == true ? safeAreaSize : 0,
                     units: state.units,
                     hours: state.filteredHours,
-                    tempTargets: state.tempTargets,
                     highGlucose: state.highGlucose,
                     lowGlucose: state.lowGlucose,
                     currentGlucoseTarget: state.currentGlucoseTarget,
@@ -324,9 +347,11 @@ extension Home {
                     lastLoopDate: state.lastLoopDate,
                     manualTempBasal: state.manualTempBasal,
                     determination: state.determinationsFromPersistence
-                ).onTapGesture {
+                )
+                .onTapGesture {
                     state.isLoopStatusPresented = true
-                }.onLongPressGesture {
+                }
+                .onLongPressGesture {
                     let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
                     impactHeavy.impactOccurred()
                     state.runLoop()
@@ -347,6 +372,8 @@ extension Home {
                             )!
                         ).font(.callout).fontWeight(.bold).fontDesign(.rounded)
                     }
+                    // aligns the evBG icon exactly with the first pixel of loop status icon
+                    .padding(.leading, 12)
                 } else {
                     HStack {
                         Image(systemName: "arrow.right.circle")
@@ -402,8 +429,17 @@ extension Home {
                         Image(systemName: "drop.circle")
                             .font(.callout)
                             .foregroundColor(.insulinTintColor)
-                        Text(tempBasalString)
-                            .font(.callout).fontWeight(.bold).fontDesign(.rounded)
+                        if tempBasalString.count > 5 {
+                            Text(tempBasalString)
+                                .font(.callout).fontWeight(.bold).fontDesign(.rounded)
+                                .lineLimit(1)
+                                .minimumScaleFactor(0.85)
+                                .truncationMode(.tail)
+                                .allowsTightening(true)
+                        } else {
+                            // Short strings can just display normally
+                            Text(tempBasalString).font(.callout).fontWeight(.bold).fontDesign(.rounded)
+                        }
                     } else {
                         Image(systemName: "drop.circle")
                             .font(.callout)
@@ -815,8 +851,28 @@ extension Home {
 
                 mainChart(geo: geo)
 
-                timeInterval.padding(.top, UIDevice.adjustPadding(min: 0, max: 12))
-                    .padding(.bottom, UIDevice.adjustPadding(min: 0, max: 12))
+                HStack {
+                    tappableButton(
+                        buttonColor: (colorScheme == .dark ? Color.white : Color.black).opacity(0.8),
+                        label: "Stats",
+                        iconString: statsIconString,
+                        action: { state.showModal(for: .statistics) }
+                    )
+
+                    Spacer()
+
+                    timeIntervalButtons.padding(.top, UIDevice.adjustPadding(min: 0, max: 10))
+                        .padding(.bottom, UIDevice.adjustPadding(min: 0, max: 10))
+
+                    Spacer()
+
+                    tappableButton(
+                        buttonColor: (colorScheme == .dark ? Color.white : Color.black).opacity(0.8),
+                        label: "Info",
+                        iconString: "info",
+                        action: { state.isLegendPresented.toggle() }
+                    )
+                }.padding([.horizontal, .top, .bottom])
 
                 if let progress = state.bolusProgress {
                     bolusView(geo: geo, progress)

+ 8 - 1
FreeAPS/Sources/Modules/ISFEditor/View/ISFEditorRootView.swift

@@ -46,7 +46,14 @@ extension ISFEditor {
                                     state.shouldDisplaySaving = false
                                 }
                             } label: {
-                                Text(state.shouldDisplaySaving ? "Saving..." : "Save").padding(10)
+                                HStack {
+                                    if state.shouldDisplaySaving {
+                                        ProgressView().padding(.trailing, 10)
+                                    }
+                                    Text(state.shouldDisplaySaving ? "Saving..." : "Save")
+                                }
+                                .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
+                                .padding(10)
                             }
                         }
                         .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)

+ 5 - 6
FreeAPS/Sources/Modules/MealSettings/MealSettingsStateModel.swift

@@ -7,10 +7,10 @@ extension MealSettings {
         @Published var maxCarbs: Decimal = 250
         @Published var maxFat: Decimal = 250
         @Published var maxProtein: Decimal = 250
-        @Published var individualAdjustmentFactor: Decimal = 0
-        @Published var timeCap: Decimal = 0
-        @Published var minuteInterval: Decimal = 0
-        @Published var delay: Decimal = 0
+        @Published var individualAdjustmentFactor: Decimal = 0.5
+        @Published var timeCap: Decimal = 8
+        @Published var minuteInterval: Decimal = 30
+        @Published var delay: Decimal = 60
 
         override func subscribe() {
             units = settingsManager.settings.units
@@ -38,8 +38,7 @@ extension MealSettings {
             })
 
             subscribeSetting(\.individualAdjustmentFactor, on: $individualAdjustmentFactor, initial: {
-                let value = max(min($0, 1.2), 0.1)
-                individualAdjustmentFactor = value
+                individualAdjustmentFactor = $0
             }, map: {
                 $0
             })

+ 4 - 4
FreeAPS/Sources/Modules/MealSettings/View/MealSettingsRootView.swift

@@ -288,7 +288,6 @@ extension MealSettings {
                             )
                             Text("Increasing this setting may result in more FPU entries with smaller carb values.")
                             Text("Decreasing this setting may result in fewer FPU entries with larger carb values.")
-                            Text("Note: Accepted range for this setting is 5 - 12 hours.")
                         }
                     )
 
@@ -316,7 +315,6 @@ extension MealSettings {
                             Text("The shorter the interval, the smoother the correlating dosing result.")
                             Text("Increasing this setting may result in fewer FPU entries with larger carb values.")
                             Text("Decreasing this setting may result in more FPU entries with smaller carb values.")
-                            Text("Accepted range for this setting is 5 - 60 minutes.")
                         }
                     )
 
@@ -344,9 +342,11 @@ extension MealSettings {
                                     Text("(Fat × 45%) + (Protein × 20%)")
                                     Text("100% is full effect:").bold()
                                     Text("(Fat × 90%) + (Protein × 40%)")
-                                    Text("200% is double effect:").bold()
-                                    Text("(Fat × 180%) + (Protein x 80%)")
+                                    Text("110% makes fat-to-carbs ratio essentially equal:").bold()
+                                    Text("(Fat × 99%) + (Protein x 44%)")
                                 }
+                                .multilineTextAlignment(.center)
+                                .fixedSize(horizontal: false, vertical: true)
                                 Text(
                                     "Tip: You may find that your normal carb ratio needs to increase to a larger number when you begin adding fat and protein entries. For this reason, it is best to start with a factor of about 50%."
                                 )

+ 7 - 1
FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift

@@ -20,6 +20,7 @@ extension NightscoutConfig {
         @Published var url = ""
         @Published var secret = ""
         @Published var message = ""
+        @Published var isValidURL: Bool = false
         @Published var connecting = false
         @Published var backfilling = false
         @Published var isUploadEnabled = false // Allow uploads
@@ -88,12 +89,17 @@ extension NightscoutConfig {
                 let fixedURL = url.dropLast()
                 url = String(fixedURL)
             }
-            guard let url = URL(string: url) else {
+
+            guard let url = URL(string: url), self.url.hasPrefix("https://") else {
                 message = "Invalid URL"
+                isValidURL = false
                 return
             }
+
             connecting = true
+            isValidURL = true
             message = ""
+
             provider.checkConnection(url: url, secret: secret.isEmpty ? nil : secret)
                 .receive(on: DispatchQueue.main)
                 .sink { completion in

+ 12 - 6
FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutConnectView.swift

@@ -19,17 +19,23 @@ struct NightscoutConnectView: View {
             Section(
                 header: Text("Connect to Nightscout"),
                 content: {
-                    TextField("URL", text: $state.url)
-                        .disableAutocorrection(true)
-                        .textContentType(.URL)
-                        .autocapitalization(.none)
-                        .keyboardType(.URL)
+                    HStack {
+                        TextField("URL", text: $state.url)
+                            .disableAutocorrection(true)
+                            .textContentType(.URL)
+                            .autocapitalization(.none)
+                            .keyboardType(.URL)
+                        if state.message.isNotEmpty && !state.isValidURL {
+                            Image(systemName: "exclamationmark.triangle.fill")
+                                .foregroundStyle(.orange)
+                        }
+                    }
                     SecureField("API secret", text: $state.secret)
                         .disableAutocorrection(true)
                         .autocapitalization(.none)
                         .textContentType(.password)
                         .keyboardType(.asciiCapable)
-                    if !state.message.isEmpty {
+                    if state.message.isNotEmpty {
                         Text(state.message)
                     }
                     if state.connecting {

+ 36 - 39
FreeAPS/Sources/Modules/PumpConfig/View/PumpConfigRootView.swift

@@ -57,25 +57,6 @@ extension PumpConfig {
                                         Spacer()
                                         Button(
                                             action: {
-                                                hintLabel = "Pump Pairing to Trio"
-                                                selectedVerboseHint =
-                                                    AnyView(
-                                                        VStack(alignment: .leading, spacing: 10) {
-                                                            Text(
-                                                                "Current Pump Models Supported:"
-                                                            )
-                                                            VStack(alignment: .leading) {
-                                                                Text("• Medtronic")
-                                                                Text("• Omnipod Eros")
-                                                                Text("• Omnipod Dash")
-                                                                Text("• Dana (RS/-i)")
-                                                                Text("• Pump Simulator")
-                                                            }
-                                                            Text(
-                                                                "Note: If using a pump simulator, you will not have continuous readings from the CGM in Trio. Using a pump simulator is only advisable for becoming familiar with the app user interface. It will not give you insight on how the algorithm will respond."
-                                                            )
-                                                        }
-                                                    )
                                                 shouldDisplayHint.toggle()
                                             },
                                             label: {
@@ -97,12 +78,46 @@ extension PumpConfig {
                 .navigationTitle("Insulin Pump")
                 .navigationBarTitleDisplayMode(.automatic)
                 .navigationBarItems(leading: displayClose ? Button("Close", action: state.hideModal) : nil)
+                .sheet(isPresented: $state.setupPump) {
+                    if let pumpManager = state.provider.apsManager.pumpManager {
+                        PumpSettingsView(
+                            pumpManager: pumpManager,
+                            bluetoothManager: state.provider.apsManager.bluetoothManager!,
+                            completionDelegate: state,
+                            setupDelegate: state
+                        )
+                    } else {
+                        PumpSetupView(
+                            pumpType: state.setupPumpType,
+                            pumpInitialSettings: state.initialSettings,
+                            bluetoothManager: state.provider.apsManager.bluetoothManager!,
+                            completionDelegate: state,
+                            setupDelegate: state
+                        )
+                    }
+                }
                 .sheet(isPresented: $shouldDisplayHint) {
                     SettingInputHintView(
                         hintDetent: $hintDetent,
                         shouldDisplayHint: $shouldDisplayHint,
-                        hintLabel: hintLabel ?? "",
-                        hintText: selectedVerboseHint ?? AnyView(EmptyView()),
+                        hintLabel: "Pump Pairing to Trio",
+                        hintText: AnyView(
+                            VStack(alignment: .leading, spacing: 10) {
+                                Text(
+                                    "Current Pump Models Supported:"
+                                )
+                                VStack(alignment: .leading) {
+                                    Text("• Medtronic")
+                                    Text("• Omnipod Eros")
+                                    Text("• Omnipod Dash")
+                                    Text("• Dana (RS/-i)")
+                                    Text("• Pump Simulator")
+                                }
+                                Text(
+                                    "Note: If using a pump simulator, you will not have continuous readings from the CGM in Trio. Using a pump simulator is only advisable for becoming familiar with the app user interface. It will not give you insight on how the algorithm will respond."
+                                )
+                            }
+                        ),
                         sheetTitle: "Help"
                     )
                 }
@@ -114,24 +129,6 @@ extension PumpConfig {
                     Button("Pump Simulator") { state.addPump(.simulator) }
                 } message: { Text("Select Pump Model") }
             }
-            .sheet(isPresented: $state.setupPump) {
-                if let pumpManager = state.provider.apsManager.pumpManager {
-                    PumpSettingsView(
-                        pumpManager: pumpManager,
-                        bluetoothManager: state.provider.apsManager.bluetoothManager!,
-                        completionDelegate: state,
-                        setupDelegate: state
-                    )
-                } else {
-                    PumpSetupView(
-                        pumpType: state.setupPumpType,
-                        pumpInitialSettings: state.initialSettings,
-                        bluetoothManager: state.provider.apsManager.bluetoothManager!,
-                        completionDelegate: state,
-                        setupDelegate: state
-                    )
-                }
-            }
         }
     }
 }

+ 6 - 1
FreeAPS/Sources/Modules/PumpConfig/View/PumpSetupView.swift

@@ -87,7 +87,12 @@ extension PumpConfig {
             case let .createdAndOnboarded(pumpManagerUI):
                 debug(.default, "Pump manager  created and onboarded")
                 setupDelegate?.pumpManagerOnboarding(didCreatePumpManager: pumpManagerUI)
-                return UIViewController()
+                var vc = pumpManagerUI.settingsViewController(
+                    bluetoothProvider: bluetoothManager,
+                    pumpManagerOnboardingDelegate: setupDelegate
+                )
+                vc.completionDelegate = completionDelegate
+                return vc
             }
         }
 

+ 1 - 1
FreeAPS/Sources/Modules/SMBSettings/View/SMBSettingsRootView.swift

@@ -306,7 +306,7 @@ extension SMBSettings {
                         }
                         VStack(alignment: .leading, spacing: 10) {
                             Text(
-                                "Warning: Increasing this value above 90 minutes may impact Trio's ability to effectively zero temp and prevent lows."
+                                "Warning: Increasing this value above 60 minutes may impact Trio's ability to effectively zero temp and prevent lows."
                             ).bold()
                             Text("Note: UAM SMBs must be enabled to use this limit.")
                         }

+ 2 - 2
FreeAPS/Sources/Modules/Settings/SettingItems.swift

@@ -219,13 +219,13 @@ enum SettingItems {
             title: "Trio Notifications",
             view: .glucoseNotificationSettings,
             searchContents: [
+                "Play Alarm Sound",
                 "Always Notify Pump",
                 "Always Notify CGM",
                 "Always Notify Carb",
                 "Always Notify Algorithm",
                 "Show Glucose App Badge",
-                "Always Notify Glucose",
-                "Play Alarm Sound",
+                "Glucose Notifications",
                 "Add Glucose Source to Alarm",
                 "Low Glucose Alarm Limit",
                 "High Glucose Alarm Limit"

+ 2 - 7
FreeAPS/Sources/Modules/Stat/View/StatsView.swift

@@ -128,13 +128,8 @@ struct StatsView: View {
     var hba1c: some View {
         HStack(spacing: 50) {
             let useUnit: GlucoseUnits = {
-                if units == .mmolL && hbA1cDisplayUnit == .mmolMol {
-                    return .mgdL
-                } else if (units == .mgdL && hbA1cDisplayUnit == .mmolMol) || units == .mmolL {
-                    return .mmolL
-                } else {
-                    return .mgdL
-                }
+                if hbA1cDisplayUnit == .mmolMol { return .mmolL }
+                else { return .mgdL }
             }()
 
             let hba1cs = glucoseStats()

+ 7 - 0
FreeAPS/Sources/Modules/TargetsEditor/TargetsEditorStateModel.swift

@@ -3,6 +3,7 @@ import SwiftUI
 extension TargetsEditor {
     final class StateModel: BaseStateModel<Provider> {
         @Injected() private var nightscout: NightscoutManager!
+        @Injected() private var broadcaster: Broadcaster!
 
         @Published var items: [Item] = []
         @Published var initialItems: [Item] = []
@@ -75,6 +76,12 @@ extension TargetsEditor {
             provider.saveProfile(profile)
             initialItems = items.map { Item(lowIndex: $0.lowIndex, highIndex: $0.highIndex, timeIndex: $0.timeIndex) }
 
+            DispatchQueue.main.async {
+                self.broadcaster.notify(BGTargetsObserver.self, on: .main) {
+                    $0.bgTargetsDidChange(profile)
+                }
+            }
+
             Task.detached(priority: .low) {
                 debug(.nightscout, "Attempting to upload targets to Nightscout")
                 await self.nightscout.uploadProfiles()

+ 22 - 22
FreeAPS/Sources/Modules/TargetsEditor/View/TargetsEditorRootView.swift

@@ -31,30 +31,30 @@ extension TargetsEditor {
 
                 Group {
                     HStack {
-                        HStack {
-                            Button {
-                                let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
-                                impactHeavy.impactOccurred()
-                                state.save()
-
-                                // deactivate saving display after 1.25 seconds
-                                DispatchQueue.main.asyncAfter(deadline: .now() + 1.25) {
-                                    state.shouldDisplaySaving = false
+                        Button(action: {
+                            let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
+                            impactHeavy.impactOccurred()
+                            state.save()
+
+                            // deactivate saving display after 1.25 seconds
+                            DispatchQueue.main.asyncAfter(deadline: .now() + 1.25) {
+                                state.shouldDisplaySaving = false
+                            }
+                        }, label: {
+                            HStack {
+                                if state.shouldDisplaySaving {
+                                    ProgressView().padding(.trailing, 10)
                                 }
-                            } label: {
-                                HStack {
-                                    if state.shouldDisplaySaving {
-                                        ProgressView().padding(.trailing, 10)
-                                    }
-                                    Text(state.shouldDisplaySaving ? "Saving..." : "Save")
-                                }.padding(10)
+                                Text(state.shouldDisplaySaving ? "Saving..." : "Save")
                             }
-                        }
-                        .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
-                        .disabled(shouldDisableButton)
-                        .background(shouldDisableButton ? Color(.systemGray4) : Color(.systemBlue))
-                        .tint(.white)
-                        .clipShape(RoundedRectangle(cornerRadius: 8))
+                            .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
+                            .padding(10)
+                        })
+                            .frame(width: UIScreen.main.bounds.width * 0.9, height: 40, alignment: .center)
+                            .disabled(shouldDisableButton)
+                            .background(shouldDisableButton ? Color(.systemGray4) : Color(.systemBlue))
+                            .tint(.white)
+                            .clipShape(RoundedRectangle(cornerRadius: 8))
                     }
                 }.padding(5)
             }

+ 2 - 1
FreeAPS/Sources/Modules/Treatments/TreatmentsStateModel.swift

@@ -567,7 +567,8 @@ extension Treatments {
                 protein: protein,
                 note: note,
                 enteredBy: CarbsEntry.local,
-                isFPU: false, fpuID: UUID().uuidString
+                isFPU: false,
+                fpuID: fat > 0 || protein > 0 ? UUID().uuidString : nil
             )]
             await carbsStorage.storeCarbs(carbsToStore, areFetchedFromRemote: false)
 

+ 1 - 1
FreeAPS/Sources/Modules/Treatments/View/MealPreset/MealPresetView.swift

@@ -177,7 +177,7 @@ struct MealPresetView: View {
     }
 
     private var noPresetChosen: Bool {
-        state.selection == nil || carbs == 0 // || (state.useFPUconversion && (fat == 0 || protein == 0))
+        state.selection == nil || state.summation.isEmpty
     }
 
     @ViewBuilder private func dishInfos() -> some View {

+ 19 - 17
FreeAPS/Sources/Modules/Treatments/View/TreatmentsRootView.swift

@@ -79,31 +79,31 @@ extension Treatments {
         @ViewBuilder private func proteinAndFat() -> some View {
             HStack {
                 HStack {
-                    Text("Fat")
+                    Text("Protein")
+
                     TextFieldWithToolBar(
-                        text: $state.fat,
+                        text: $state.protein,
                         placeholder: "0",
                         keyboardType: .numberPad,
                         numberFormatter: mealFormatter,
                         previousTextField: { focusOnPreviousTextField(index: 2) },
                         nextTextField: { focusOnNextTextField(index: 2) }
-                    ).focused($focusedField, equals: .fat)
+                    ).focused($focusedField, equals: .protein)
                     Text("g").foregroundColor(.secondary)
                 }
 
                 Divider().foregroundStyle(.primary).fontWeight(.bold).frame(width: 10)
 
                 HStack {
-                    Text("Protein")
-
+                    Text("Fat")
                     TextFieldWithToolBar(
-                        text: $state.protein,
+                        text: $state.fat,
                         placeholder: "0",
                         keyboardType: .numberPad,
                         numberFormatter: mealFormatter,
                         previousTextField: { focusOnPreviousTextField(index: 3) },
                         nextTextField: { focusOnNextTextField(index: 3) }
-                    ).focused($focusedField, equals: .protein)
+                    ).focused($focusedField, equals: .fat)
                     Text("g").foregroundColor(.secondary)
                 }
             }
@@ -316,15 +316,17 @@ extension Treatments {
                         Text("Close")
                     }
                 }
-                ToolbarItem(placement: .topBarTrailing) {
-                    Button(action: {
-                        showPresetSheet = true
-                    }, label: {
-                        HStack {
-                            Text("Presets")
-                            Image(systemName: "plus")
-                        }
-                    })
+                if state.displayPresets {
+                    ToolbarItem(placement: .topBarTrailing) {
+                        Button(action: {
+                            showPresetSheet = true
+                        }, label: {
+                            HStack {
+                                Text("Presets")
+                                Image(systemName: "plus")
+                            }
+                        })
+                    }
                 }
             })
             .onAppear {
@@ -396,7 +398,7 @@ extension Treatments {
 
         private var taskButtonLabel: some View {
             if pumpBolusLimitExceeded {
-                return Text("Max Bolus of \(state.maxBolus.description) U E== 0xceeded")
+                return Text("Max Bolus of \(state.maxBolus.description) U Exceeded")
             } else if externalBolusLimitExceeded {
                 return Text("Max External Bolus of \(state.maxExternal.description) U Exceeded")
             } else if carbLimitExceeded {

+ 1 - 1
FreeAPS/Sources/Modules/UserInterfaceSettings/View/UserInterfaceSettingsRootView.swift

@@ -413,7 +413,7 @@ extension UserInterfaceSettings {
                                                 VStack(alignment: .leading, spacing: 5) {
                                                     Text("Total Insulin in Scope:").bold()
                                                     Text(
-                                                        "Displays the total insulin administered since midnight, both basal and bolus."
+                                                        "Displays the total amount of insulin given as a bolus (manual or SMB) and through temporary basal rates above zero during the selected timeframe of the main chart."
                                                     )
                                                 }
                                             }

+ 14 - 16
FreeAPS/Sources/Modules/WatchConfig/View/WatchConfigGarminView.swift

@@ -19,7 +19,7 @@ struct WatchConfigGarminView: View {
     }
 
     var body: some View {
-        List {
+        Form {
             Section(
                 header: Text("Garmin Configuration"),
                 content:
@@ -43,13 +43,6 @@ struct WatchConfigGarminView: View {
                             Spacer()
                             Button(
                                 action: {
-                                    hintLabel = "Add Device"
-                                    selectedVerboseHint =
-                                        AnyView(
-                                            Text(
-                                                "Add Garmin Device to Trio. Please look at the docs to see which devices are supported."
-                                            )
-                                        )
                                     shouldDisplayHint.toggle()
                                 },
                                 label: {
@@ -64,14 +57,17 @@ struct WatchConfigGarminView: View {
             ).listRowBackground(Color.chart)
 
             if !state.devices.isEmpty {
-                Section(header: Text("Garmin Watch")) {
-                    List {
-                        ForEach(state.devices, id: \.uuid) { device in
-                            Text(device.friendlyName)
+                Section(
+                    header: Text("Garmin Watch"),
+                    content: {
+                        List {
+                            ForEach(state.devices, id: \.uuid) { device in
+                                Text(device.friendlyName)
+                            }
+                            .onDelete(perform: onDelete)
                         }
-                        .onDelete(perform: onDelete)
                     }
-                }.listRowBackground(Color.chart)
+                ).listRowBackground(Color.chart)
             }
         }
         .listSectionSpacing(sectionSpacing)
@@ -79,8 +75,10 @@ struct WatchConfigGarminView: View {
             SettingInputHintView(
                 hintDetent: $hintDetent,
                 shouldDisplayHint: $shouldDisplayHint,
-                hintLabel: hintLabel ?? "",
-                hintText: selectedVerboseHint ?? AnyView(EmptyView()),
+                hintLabel: "Add Device",
+                hintText: Text(
+                    "Add Garmin Device to Trio. Please look at the docs to see which devices are supported."
+                ),
                 sheetTitle: "Help"
             )
         }

+ 5 - 1
FreeAPS/Sources/Router/Router.swift

@@ -60,7 +60,11 @@ final class BaseRouter: Router {
         case .carb:
             guard settings.notificationsCarb else { return false }
         case .glucose:
-            guard settings.glucoseNotificationsAlways else { return false }
+            guard (
+                message.type == .warning &&
+                    settings.glucoseNotificationsOption == GlucoseNotificationsOption.onlyAlarmLimits
+            ) ||
+                settings.glucoseNotificationsOption == GlucoseNotificationsOption.alwaysEveryCGM else { return false }
         case .algorithm:
             guard settings.notificationsAlgorithm else { return false }
         case .misc:

+ 19 - 18
FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift

@@ -245,26 +245,27 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
             for allSamples in carbs {
                 guard let id = allSamples.id else { continue }
                 let fpuID = allSamples.fpuID ?? id
-
                 let startDate = allSamples.actualDate ?? Date()
 
-                // Carbs Sample
+                // Carbs Sample (only if value is greater than 0)
                 let carbValue = allSamples.carbs
-                let carbSample = HKQuantitySample(
-                    type: carbSampleType,
-                    quantity: HKQuantity(unit: .gram(), doubleValue: Double(carbValue)),
-                    start: startDate,
-                    end: startDate,
-                    metadata: [
-                        HKMetadataKeyExternalUUID: id,
-                        HKMetadataKeySyncIdentifier: id,
-                        HKMetadataKeySyncVersion: 1
-                    ]
-                )
-                samples.append(carbSample)
+                if carbValue > 0 {
+                    let carbSample = HKQuantitySample(
+                        type: carbSampleType,
+                        quantity: HKQuantity(unit: .gram(), doubleValue: Double(carbValue)),
+                        start: startDate,
+                        end: startDate,
+                        metadata: [
+                            HKMetadataKeyExternalUUID: id,
+                            HKMetadataKeySyncIdentifier: id,
+                            HKMetadataKeySyncVersion: 1
+                        ]
+                    )
+                    samples.append(carbSample)
+                }
 
-                // Fat Sample (if available)
-                if let fatValue = allSamples.fat {
+                // Fat Sample (only if value is greater than 0)
+                if let fatValue = allSamples.fat, fatValue > 0 {
                     let fatSample = HKQuantitySample(
                         type: fatSampleType,
                         quantity: HKQuantity(unit: .gram(), doubleValue: Double(fatValue)),
@@ -279,8 +280,8 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
                     samples.append(fatSample)
                 }
 
-                // Protein Sample (if available)
-                if let proteinValue = allSamples.protein {
+                // Protein Sample (only if value is greater than 0)
+                if let proteinValue = allSamples.protein, proteinValue > 0 {
                     let proteinSample = HKQuantitySample(
                         type: proteinSampleType,
                         quantity: HKQuantity(unit: .gram(), doubleValue: Double(proteinValue)),

+ 8 - 3
FreeAPS/Sources/Services/Network/Nightscout/NightscoutManager.swift

@@ -1239,13 +1239,18 @@ extension BaseNightscoutManager {
                 }
 
             } else {
-                // Handle everything else, e.g., "minPredBG 39", "COB 29", etc.
+                // Handle everything else, e.g., "minPredBG 39", "Dev: 5", etc.
                 let parts = glucoseValueString.components(separatedBy: .whitespaces)
                 if parts.count >= 2 {
-                    let metric = parts[0]
+                    var metric = parts[0]
                     let value = parts[1]
+
+                    // Add ":" to the metric only if it doesn't already end with ":"
+                    if !metric.hasSuffix(":") {
+                        metric += ":"
+                    }
                     let formattedValue = convertToMmolL(value)
-                    let formattedString = "\(metric): \(formattedValue)"
+                    let formattedString = "\(metric) \(formattedValue)"
                     updatedReason.replaceSubrange(range, with: formattedString)
                 }
             }

+ 1 - 1
FreeAPS/Sources/Services/RemoteControl/TrioRemoteControl+Meal.swift

@@ -69,7 +69,7 @@ extension TrioRemoteControl {
             note: "Remote meal command",
             enteredBy: CarbsEntry.local,
             isFPU: false,
-            fpuID: nil
+            fpuID: fatDecimal ?? 0 > 0 || proteinDecimal ?? 0 > 0 ? UUID().uuidString : nil
         )
 
         await carbsStorage.storeCarbs([mealEntry], areFetchedFromRemote: false)

+ 7 - 11
FreeAPS/Sources/Services/UserNotifications/UserNotificationsManager.swift

@@ -128,7 +128,7 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
     private func addAppBadge(glucose: Int?) {
         guard let glucose = glucose, settingsManager.settings.glucoseBadge else {
             DispatchQueue.main.async {
-                self.center.setBadgeCount(-1) { error in
+                self.center.setBadgeCount(0) { error in
                     guard let error else {
                         return
                     }
@@ -270,8 +270,6 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
 
             addAppBadge(glucose: (glucoseObjects.first?.glucose).map { Int($0) })
 
-            guard glucoseStorage.alarm != nil || settingsManager.settings.glucoseNotificationsAlways else { return }
-
             var titles: [String] = []
             var notificationAlarm = false
             var messageType = MessageType.info
@@ -421,24 +419,22 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
             trigger: trigger,
             action: action
         )
-        if alertPermissionsChecker.notificationsDisabled {
-            router.alertMessage.send(messageCont)
-            return
-        }
-        guard router.allowNotify(messageCont, settingsManager.settings) else { return }
-
         var alertIdentifier = identifier.rawValue
         alertIdentifier = identifier == .pumpNotification ? alertIdentifier + content
             .title : (identifier == .alertMessageNotification ? alertIdentifier + content.body : alertIdentifier)
-        let request = UNNotificationRequest(identifier: alertIdentifier, content: content, trigger: trigger)
-
         if deleteOld {
             DispatchQueue.main.async {
                 self.center.removeDeliveredNotifications(withIdentifiers: [alertIdentifier])
                 self.center.removePendingNotificationRequests(withIdentifiers: [alertIdentifier])
             }
         }
+        if alertPermissionsChecker.notificationsDisabled {
+            router.alertMessage.send(messageCont)
+            return
+        }
+        guard router.allowNotify(messageCont, settingsManager.settings) else { return }
 
+        let request = UNNotificationRequest(identifier: alertIdentifier, content: content, trigger: trigger)
         DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
             self.center.add(request) { error in
                 if let error = error {

+ 1 - 1
Model/Classes+Properties/OpenAPS_Battery+CoreDataProperties.swift

@@ -9,7 +9,7 @@ public extension OpenAPS_Battery {
     @NSManaged var date: Date?
     @NSManaged var display: Bool
     @NSManaged var id: UUID?
-    @NSManaged var percent: Int16
+    @NSManaged var percent: Double
     @NSManaged var status: String?
     @NSManaged var voltage: NSDecimalNumber?
 }

+ 2 - 2
Model/Helper/CarbEntryStored+helper.swift

@@ -9,13 +9,13 @@ extension NSPredicate {
 
     static var carbsForChart: NSPredicate {
         let date = Date.oneDayAgo
-        return NSPredicate(format: "isFPU == false AND date >= %@", date as NSDate)
+        return NSPredicate(format: "isFPU == false AND date >= %@ AND carbs > 0", date as NSDate)
     }
 
     static var carbsNotYetUploadedToNightscout: NSPredicate {
         let date = Date.oneDayAgo
         return NSPredicate(
-            format: "date >= %@ AND isUploadedToNS == %@ AND isFPU == %@",
+            format: "date >= %@ AND isUploadedToNS == %@ AND isFPU == %@ AND carbs > 0",
             date as NSDate,
             false as NSNumber,
             false as NSNumber

+ 5 - 0
Model/Helper/NSPredicates.swift

@@ -66,6 +66,11 @@ extension NSPredicate {
         return NSPredicate(format: "date >= %@", date as NSDate)
     }
 
+    static var carbsHistory: NSPredicate {
+        let date = Date.oneDayAgo
+        return NSPredicate(format: "date >= %@ AND carbs > 0", date as NSDate)
+    }
+
     static var predicateForOneHourAgo: NSPredicate {
         let date = Date.oneHourAgo
         return NSPredicate(format: "date >= %@", date as NSDate)

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

@@ -96,7 +96,7 @@
         <attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
         <attribute name="display" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
-        <attribute name="percent" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
+        <attribute name="percent" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="YES"/>
         <attribute name="status" optional="YES" attributeType="String"/>
         <attribute name="voltage" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <fetchIndex name="byDate">

+ 1 - 1
OmniBLE

@@ -1 +1 @@
-Subproject commit fee22b34644a6c349db92b55ce835114c377e4d7
+Subproject commit 1fa2874419225c8c7af0d9afbd9faf823cda34e5

+ 1 - 1
OmniKit

@@ -1 +1 @@
-Subproject commit 0857ad8d71d4d588ff7f5470e78fe8d6b5055924
+Subproject commit 48a35efa52f42e0b72fe2e984f60d4482a11a75f

+ 11 - 4
oref0_source_version.txt

@@ -1,9 +1,16 @@
-oref0 branch: dev - git version: 22603e2
+oref0 branch: tdd-pumpdataCheck - git version: 46deecb
 
 Last commits:
-22603e2 skip_NetralTemps only if SMB's somehow disabled
-0ff47a3 fix weightedAverage, always calculate TDD
-e274bb0 revert to standard HBT calculation
+46deecb remove dynisf check on pumpdata calc
+2f258b2 Merge pull request #36 from mountrcg/fixTDDcheck
+4998a09 fix condition  to use weightedAverage as TDD
+ff54c14 Merge pull request #35 from mountrcg/skipNTfix
+bbdf258 Merge pull request #34 from mountrcg/TToref-reset
+6e4c8ce Merge pull request #31 from simonp22/calc_tdd
+c5f9a6a Use weightedAverage as tdd if weightPercentage > 1
+7153b43 skip_NetralTemps only if SMB's somehow disabled
+e0caaa0 revert to standard HBT calculation
+06ca64b Always calculate TDD
 363fd11 Merge pull request #28 from bjornoleh/harmonise_defaults
 2d695e1 index.js: set enableUAM to false, and remove whitespace in L11
 8f5f820 Harmonise profile defaults with openaps/oref0

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

@@ -383,7 +383,7 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_
         console.log("Pumphistory is empty!");
         dynISFenabled = false;
         enableDynamicCR = false;
-    } else if (dynISFenabled) {
+    } else {
         let phLastEntry = pumphistory.length - 1;
         var endDate = new Date(pumphistory[phLastEntry].timestamp);
         var startDate = new Date(pumphistory[0].timestamp);
@@ -559,7 +559,6 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_
 
     } else { tddReason = ", TDD: Not enough pumpData (< 21h)"; }
 
-
     var tdd_before = tdd;
 
     // -------------------- END OF TDD ----------------------------------------------------