ソースを参照

Merge branch 'dev' of github.com:nightscout/Trio into core-data-fixes

Marvin Polscheit 5 日 前
コミット
9534b96713
57 ファイル変更2184 行追加1876 行削除
  1. 1 1
      Config.xcconfig
  2. 3 1
      Gemfile
  3. 14 10
      Gemfile.lock
  4. 1 0
      Model/Classes+Properties/TempTargetStored+CoreDataProperties.swift
  5. 0 46
      Model/Helper/GlucoseStored+helper.swift
  6. 1 0
      Model/Helper/PumpEvent+helper.swift
  7. 1 1
      Model/JSONImporter.swift
  8. 3 1
      Model/TrioCoreDataPersistentContainer.xcdatamodeld/TrioCoreDataPersistentContainer.xcdatamodel/contents
  9. 64 0
      Trio.xcodeproj/project.pbxproj
  10. 1 1
      Trio/Sources/APS/CGM/GlucoseSimulatorSource.swift
  11. 1 1
      Trio/Sources/APS/CGM/PluginSource.swift
  12. 1 1
      Trio/Sources/APS/DeviceDataManager.swift
  13. 26 66
      Trio/Sources/APS/Storage/GlucoseStorage.swift
  14. 16 1
      Trio/Sources/APS/Storage/PumpHistoryStorage.swift
  15. 3 2
      Trio/Sources/APS/Storage/TempTargetsStorage.swift
  16. 34 12
      Trio/Sources/Localizations/Main/Localizable.xcstrings
  17. 1 1
      Trio/Sources/Models/AlgorithmGlucose.swift
  18. 26 9
      Trio/Sources/Models/BloodGlucose.swift
  19. 5 0
      Trio/Sources/Models/TrioSettings.swift
  20. 5 0
      Trio/Sources/Modules/Adjustments/AdjustmentsStateModel.swift
  21. 110 0
      Trio/Sources/Modules/Adjustments/View/AdjustmentsRootView.swift
  22. 15 17
      Trio/Sources/Modules/Adjustments/View/Overrides/AdjustmentsRootView+Overrides.swift
  23. 15 15
      Trio/Sources/Modules/Adjustments/View/TempTargets/AdjustmentsRootView+TempTargets.swift
  24. 43 43
      Trio/Sources/Modules/DynamicSettings/View/DynamicSettingsRootView.swift
  25. 161 0
      Trio/Sources/Modules/History/HistoryDataFlow+Models.swift
  26. 3 158
      Trio/Sources/Modules/History/HistoryDataFlow.swift
  27. 65 0
      Trio/Sources/Modules/History/HistoryDeletionTarget.swift
  28. 2 2
      Trio/Sources/Modules/History/HistoryProvider.swift
  29. 291 0
      Trio/Sources/Modules/History/HistoryStateModel+Deletion/HistoryStateModel+CarbEditing.swift
  30. 119 0
      Trio/Sources/Modules/History/HistoryStateModel+Deletion/HistoryStateModel+Carbs.swift
  31. 55 0
      Trio/Sources/Modules/History/HistoryStateModel+Deletion/HistoryStateModel+Glucose.swift
  32. 85 0
      Trio/Sources/Modules/History/HistoryStateModel+Deletion/HistoryStateModel+Insulin.swift
  33. 0 533
      Trio/Sources/Modules/History/HistoryStateModel.swift
  34. 72 0
      Trio/Sources/Modules/History/View/HistoryRootView+AddGlucose.swift
  35. 133 0
      Trio/Sources/Modules/History/View/HistoryRootView+Adjustments.swift
  36. 44 0
      Trio/Sources/Modules/History/View/HistoryRootView+Confirmations.swift
  37. 103 0
      Trio/Sources/Modules/History/View/HistoryRootView+Filters.swift
  38. 79 0
      Trio/Sources/Modules/History/View/HistoryRootView+Glucose.swift
  39. 98 0
      Trio/Sources/Modules/History/View/HistoryRootView+Meals.swift
  40. 103 0
      Trio/Sources/Modules/History/View/HistoryRootView+Treatments.swift
  41. 61 745
      Trio/Sources/Modules/History/View/HistoryRootView.swift
  42. 8 29
      Trio/Sources/Modules/Home/View/HomeRootView.swift
  43. 22 11
      Trio/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift
  44. 12 0
      Trio/Sources/Modules/Onboarding/View/TherapySettingEditorView.swift
  45. 2 1
      Trio/Sources/Modules/Settings/SettingItems.swift
  46. 7 0
      Trio/Sources/Modules/SettingsExport/SettingsExportStateModel.swift
  47. 72 17
      Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift
  48. 126 36
      Trio/Sources/Modules/Treatments/View/TreatmentsRootView.swift
  49. 4 0
      Trio/Sources/Modules/UserInterfaceSettings/UserInterfaceSettingsStateModel.swift
  50. 23 0
      Trio/Sources/Modules/UserInterfaceSettings/View/UserInterfaceSettingsRootView.swift
  51. 0 1
      Trio/Sources/Services/Network/Nightscout/BaseNightscoutManager+Subscribers.swift
  52. 10 5
      Trio/Sources/Services/Network/Nightscout/NightscoutAPI.swift
  53. 6 92
      Trio/Sources/Services/Network/Nightscout/NightscoutManager.swift
  54. 0 1
      Trio/Sources/Services/Network/Nightscout/NightscoutUploadPipeline.swift
  55. 27 0
      Trio/Sources/Views/BolusProgressBar.swift
  56. 0 15
      TrioTests/CoreDataTests/GlucoseStorageTests.swift
  57. 1 1
      scripts/define_common_trio.sh

+ 1 - 1
Config.xcconfig

@@ -19,7 +19,7 @@ TRIO_APP_GROUP_ID = group.org.nightscout.$(DEVELOPMENT_TEAM).trio.trio-app-group
 
 // The developers set the version numbers, please leave them alone
 APP_VERSION = 0.7.0
-APP_DEV_VERSION = 0.7.0
+APP_DEV_VERSION = 0.7.0.14
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 

+ 3 - 1
Gemfile

@@ -1,3 +1,5 @@
 source "https://rubygems.org"
 
-gem "fastlane", "2.231.0"
+gem "fastlane", "2.233.1"
+gem "json", ">=2.19.2"
+gem "addressable", ">=2.9.0"

+ 14 - 10
Gemfile.lock

@@ -3,7 +3,7 @@ GEM
   specs:
     CFPropertyList (3.0.8)
     abbrev (0.1.2)
-    addressable (2.8.8)
+    addressable (2.9.0)
       public_suffix (>= 2.0.2, < 8.0)
     artifactory (3.0.17)
     atomos (0.1.3)
@@ -28,6 +28,7 @@ GEM
       aws-eventstream (~> 1, >= 1.0.2)
     babosa (1.0.4)
     base64 (0.2.0)
+    benchmark (0.5.0)
     bigdecimal (4.0.1)
     claide (1.1.0)
     colored (1.2)
@@ -67,14 +68,15 @@ GEM
     faraday_middleware (1.2.1)
       faraday (~> 1.0)
     fastimage (2.4.0)
-    fastlane (2.231.0)
+    fastlane (2.233.1)
       CFPropertyList (>= 2.3, < 4.0.0)
       abbrev (~> 0.1.2)
       addressable (>= 2.8, < 3.0.0)
       artifactory (~> 3.0)
-      aws-sdk-s3 (~> 1.0)
+      aws-sdk-s3 (~> 1.197)
       babosa (>= 1.0.3, < 2.0.0)
       base64 (~> 0.2.0)
+      benchmark (>= 0.1.0)
       bundler (>= 1.17.3, < 5.0.0)
       colored (~> 1.2)
       commander (~> 4.6)
@@ -86,11 +88,11 @@ GEM
       faraday-cookie_jar (~> 0.0.6)
       faraday_middleware (~> 1.0)
       fastimage (>= 2.1.0, < 3.0.0)
-      fastlane-sirp (>= 1.0.0)
+      fastlane-sirp (>= 1.1.0)
       gh_inspector (>= 1.1.2, < 2.0.0)
       google-apis-androidpublisher_v3 (~> 0.3)
       google-apis-playcustomapp_v1 (~> 0.1)
-      google-cloud-env (>= 1.6.0, < 2.0.0)
+      google-cloud-env (>= 1.6.0, <= 2.1.1)
       google-cloud-storage (~> 1.31)
       highline (~> 2.0)
       http-cookie (~> 1.0.5)
@@ -103,6 +105,7 @@ GEM
       naturally (~> 2.2)
       nkf (~> 0.2.0)
       optparse (>= 0.1.1, < 1.0.0)
+      ostruct (>= 0.1.0)
       plist (>= 3.1.0, < 4.0.0)
       rubyzip (>= 2.0.0, < 3.0.0)
       security (= 0.1.5)
@@ -115,8 +118,7 @@ GEM
       xcodeproj (>= 1.13.0, < 2.0.0)
       xcpretty (~> 0.4.1)
       xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
-    fastlane-sirp (1.0.0)
-      sysrandom (~> 1.0)
+    fastlane-sirp (1.1.0)
     gh_inspector (1.1.3)
     google-apis-androidpublisher_v3 (0.54.0)
       google-apis-core (>= 0.11.0, < 2.a)
@@ -160,7 +162,7 @@ GEM
     httpclient (2.9.0)
       mutex_m
     jmespath (1.6.2)
-    json (2.18.0)
+    json (2.19.4)
     jwt (2.10.2)
       base64
     logger (1.7.0)
@@ -174,6 +176,7 @@ GEM
     nkf (0.2.0)
     optparse (0.8.1)
     os (1.1.4)
+    ostruct (0.6.3)
     plist (3.7.2)
     public_suffix (7.0.2)
     rake (13.3.1)
@@ -195,7 +198,6 @@ GEM
     simctl (1.6.10)
       CFPropertyList
       naturally
-    sysrandom (1.0.5)
     terminal-notifier (2.0.0)
     terminal-table (3.0.2)
       unicode-display_width (>= 1.1.1, < 3)
@@ -224,7 +226,9 @@ PLATFORMS
   ruby
 
 DEPENDENCIES
-  fastlane (= 2.231.0)
+  addressable (>= 2.9.0)
+  fastlane (= 2.233.1)
+  json (>= 2.19.2)
 
 BUNDLED WITH
   4.0.4

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

@@ -17,6 +17,7 @@ public extension TempTargetStored {
     @NSManaged var orderPosition: Int16
     @NSManaged var target: NSDecimalNumber?
     @NSManaged var tempTargetRun: TempTargetRunStored?
+    @NSManaged var enteredBy: String?
 }
 
 extension TempTargetStored: Identifiable {}

+ 0 - 46
Model/Helper/GlucoseStored+helper.swift

@@ -93,16 +93,6 @@ extension NSPredicate {
         return NSPredicate(format: "date >= %@ AND isUploadedToTidepool == %@", date as NSDate, false as NSNumber)
     }
 
-    static var manualGlucoseNotYetUploadedToNightscout: NSPredicate {
-        let date = Date.oneDayAgo
-        return NSPredicate(
-            format: "date >= %@ AND isUploadedToNS == %@ AND isManual == %@",
-            date as NSDate,
-            false as NSNumber,
-            true as NSNumber
-        )
-    }
-
     static var manualGlucoseNotYetUploadedToHealth: NSPredicate {
         let date = Date.oneDayAgo
         return NSPredicate(
@@ -124,42 +114,6 @@ extension NSPredicate {
     }
 }
 
-extension GlucoseStored: Encodable {
-    enum CodingKeys: String, CodingKey {
-        case date
-        case dateString
-        case sgv
-        case glucose
-        case direction
-        case id
-        case type
-    }
-
-    public func encode(to encoder: Encoder) throws {
-        var container = encoder.container(keyedBy: CodingKeys.self)
-
-        let dateFormatter = ISO8601DateFormatter()
-        dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
-
-        try container.encode(dateFormatter.string(from: date ?? Date()), forKey: .dateString)
-
-        let dateAsUnixTimestamp = String(format: "%.0f", (date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000)
-        try container.encode(dateAsUnixTimestamp, forKey: .date)
-
-        try container.encode(direction, forKey: .direction)
-        try container.encode(id, forKey: .id)
-
-        // TODO: Handle the type of the glucose entry conditionally not hardcoded
-        try container.encode("sgv", forKey: .type)
-
-        if isManual {
-            try container.encode(glucose, forKey: .glucose)
-        } else {
-            try container.encode(glucose, forKey: .sgv)
-        }
-    }
-}
-
 // In order to show the correct direction in the bobble we convert the direction property of the NSManagedObject GlucoseStored back to the Direction type
 extension GlucoseStored {
     var directionEnum: BloodGlucose.Direction? {

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

@@ -58,6 +58,7 @@ public extension PumpEventStored {
         case rewind = "Rewind"
         case prime = "Prime"
         case journalCarbs = "JournalEntryMealMarker"
+        case siteChange = "SiteChange"
 
         case nsNote = "Note"
         case nsTempBasal = "Temp Basal"

+ 1 - 1
Model/JSONImporter.swift

@@ -355,7 +355,7 @@ extension BloodGlucose {
         }
 
         let glucoseEntry = GlucoseStored(context: context)
-        glucoseEntry.id = _id.flatMap({ UUID(uuidString: $0) }) ?? UUID()
+        glucoseEntry.id = UUID(uuidString: id) ?? UUID()
         glucoseEntry.date = dateString
         glucoseEntry.glucose = Int16(glucoseValue)
         glucoseEntry.direction = direction?.rawValue

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

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="24512" systemVersion="25D2128" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
+<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23788.4" systemVersion="25B78" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
     <entity name="BolusStored" representedClassName="BolusStored" syncable="YES">
         <attribute name="amount" optional="YES" attributeType="Decimal" defaultValueString="0"/>
         <attribute name="isExternal" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
@@ -237,6 +237,7 @@
     </entity>
     <entity name="TempTargetRunStored" representedClassName="TempTargetRunStored" syncable="YES">
         <attribute name="endDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
+        <attribute name="enteredBy" optional="YES" attributeType="String"/>
         <attribute name="id" optional="YES" attributeType="UUID" defaultValueString="empy" usesScalarValueType="NO"/>
         <attribute name="isUploadedToNS" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
         <attribute name="name" optional="YES" attributeType="String"/>
@@ -251,6 +252,7 @@
         <attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
         <attribute name="duration" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <attribute name="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
+        <attribute name="enteredBy" optional="YES" attributeType="String"/>
         <attribute name="halfBasalTarget" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
         <attribute name="isPreset" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>

+ 64 - 0
Trio.xcodeproj/project.pbxproj

@@ -190,6 +190,7 @@
 		38E98A3725F5509500C0CED0 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E98A3625F5509500C0CED0 /* String+Extensions.swift */; };
 		38EA05DA261F6E7C0064E39B /* SimpleLogReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38EA05D9261F6E7C0064E39B /* SimpleLogReporter.swift */; };
 		38EA0600262091870064E39B /* BolusProgressViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38EA05FF262091870064E39B /* BolusProgressViewStyle.swift */; };
+		DDB0BBA02026050100000001 /* BolusProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB0BBA02026050100000002 /* BolusProgressBar.swift */; };
 		38F37828261260DC009DB701 /* Color+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F37827261260DC009DB701 /* Color+Extensions.swift */; };
 		38F3B2EF25ED8E2A005C48AA /* TempTargetsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F3B2EE25ED8E2A005C48AA /* TempTargetsStorage.swift */; };
 		38FCF3D625E8FDF40078B0D1 /* MD5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FCF3D525E8FDF40078B0D1 /* MD5.swift */; };
@@ -743,6 +744,19 @@
 		FE41E4D629463EE20047FD55 /* NightscoutPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE41E4D529463EE20047FD55 /* NightscoutPreferences.swift */; };
 		FE66D16B291F74F8005D6F77 /* Bundle+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE66D16A291F74F8005D6F77 /* Bundle+Extensions.swift */; };
 		FEFFA7A22929FE49007B8193 /* UIDevice+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEFFA7A12929FE49007B8193 /* UIDevice+Extensions.swift */; };
+		BD175EBE0000100000000001 /* HistoryDataFlow+Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE0000100000000001 /* HistoryDataFlow+Models.swift */; };
+		BD175EBE0000100000000002 /* HistoryDeletionTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE0000100000000002 /* HistoryDeletionTarget.swift */; };
+		BD175EBE0000100000000003 /* HistoryStateModel+Glucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE0000100000000003 /* HistoryStateModel+Glucose.swift */; };
+		BD175EBE0000100000000004 /* HistoryStateModel+Carbs.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE0000100000000004 /* HistoryStateModel+Carbs.swift */; };
+		BD175EBE0000100000000005 /* HistoryStateModel+Insulin.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE0000100000000005 /* HistoryStateModel+Insulin.swift */; };
+		BD175EBE0000100000000006 /* HistoryStateModel+CarbEditing.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE0000100000000006 /* HistoryStateModel+CarbEditing.swift */; };
+		BD175EBE0000100000000007 /* HistoryRootView+Treatments.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE0000100000000007 /* HistoryRootView+Treatments.swift */; };
+		BD175EBE0000100000000008 /* HistoryRootView+Meals.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE0000100000000008 /* HistoryRootView+Meals.swift */; };
+		BD175EBE0000100000000009 /* HistoryRootView+Glucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE0000100000000009 /* HistoryRootView+Glucose.swift */; };
+		BD175EBE000010000000000A /* HistoryRootView+Adjustments.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE000010000000000A /* HistoryRootView+Adjustments.swift */; };
+		BD175EBE000010000000000B /* HistoryRootView+Filters.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE000010000000000B /* HistoryRootView+Filters.swift */; };
+		BD175EBE000010000000000C /* HistoryRootView+AddGlucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE000010000000000C /* HistoryRootView+AddGlucose.swift */; };
+		BD175EBE000010000000000D /* HistoryRootView+Confirmations.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD175EFE000010000000000D /* HistoryRootView+Confirmations.swift */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
@@ -1050,6 +1064,7 @@
 		38E98A3625F5509500C0CED0 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
 		38EA05D9261F6E7C0064E39B /* SimpleLogReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleLogReporter.swift; sourceTree = "<group>"; };
 		38EA05FF262091870064E39B /* BolusProgressViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusProgressViewStyle.swift; sourceTree = "<group>"; };
+		DDB0BBA02026050100000002 /* BolusProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusProgressBar.swift; sourceTree = "<group>"; };
 		38F37827261260DC009DB701 /* Color+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Extensions.swift"; sourceTree = "<group>"; };
 		38F3783A2613555C009DB701 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; };
 		38F3B2EE25ED8E2A005C48AA /* TempTargetsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetsStorage.swift; sourceTree = "<group>"; };
@@ -1584,6 +1599,19 @@
 		FE41E4D529463EE20047FD55 /* NightscoutPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutPreferences.swift; sourceTree = "<group>"; };
 		FE66D16A291F74F8005D6F77 /* Bundle+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Extensions.swift"; sourceTree = "<group>"; };
 		FEFFA7A12929FE49007B8193 /* UIDevice+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDevice+Extensions.swift"; sourceTree = "<group>"; };
+		BD175EFE0000100000000001 /* HistoryDataFlow+Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryDataFlow+Models.swift"; sourceTree = "<group>"; };
+		BD175EFE0000100000000002 /* HistoryDeletionTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryDeletionTarget.swift; sourceTree = "<group>"; };
+		BD175EFE0000100000000003 /* HistoryStateModel+Glucose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryStateModel+Glucose.swift"; sourceTree = "<group>"; };
+		BD175EFE0000100000000004 /* HistoryStateModel+Carbs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryStateModel+Carbs.swift"; sourceTree = "<group>"; };
+		BD175EFE0000100000000005 /* HistoryStateModel+Insulin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryStateModel+Insulin.swift"; sourceTree = "<group>"; };
+		BD175EFE0000100000000006 /* HistoryStateModel+CarbEditing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryStateModel+CarbEditing.swift"; sourceTree = "<group>"; };
+		BD175EFE0000100000000007 /* HistoryRootView+Treatments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryRootView+Treatments.swift"; sourceTree = "<group>"; };
+		BD175EFE0000100000000008 /* HistoryRootView+Meals.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryRootView+Meals.swift"; sourceTree = "<group>"; };
+		BD175EFE0000100000000009 /* HistoryRootView+Glucose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryRootView+Glucose.swift"; sourceTree = "<group>"; };
+		BD175EFE000010000000000A /* HistoryRootView+Adjustments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryRootView+Adjustments.swift"; sourceTree = "<group>"; };
+		BD175EFE000010000000000B /* HistoryRootView+Filters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryRootView+Filters.swift"; sourceTree = "<group>"; };
+		BD175EFE000010000000000C /* HistoryRootView+AddGlucose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryRootView+AddGlucose.swift"; sourceTree = "<group>"; };
+		BD175EFE000010000000000D /* HistoryRootView+Confirmations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HistoryRootView+Confirmations.swift"; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFileSystemSynchronizedRootGroup section */
@@ -1715,6 +1743,13 @@
 			children = (
 				BDA7593D2D37CFC000E649A4 /* CarbEntryEditorView.swift */,
 				881E04BA5E0A003DE8E0A9C6 /* HistoryRootView.swift */,
+				BD175EFE0000100000000007 /* HistoryRootView+Treatments.swift */,
+				BD175EFE0000100000000008 /* HistoryRootView+Meals.swift */,
+				BD175EFE0000100000000009 /* HistoryRootView+Glucose.swift */,
+				BD175EFE000010000000000A /* HistoryRootView+Adjustments.swift */,
+				BD175EFE000010000000000B /* HistoryRootView+Filters.swift */,
+				BD175EFE000010000000000C /* HistoryRootView+AddGlucose.swift */,
+				BD175EFE000010000000000D /* HistoryRootView+Confirmations.swift */,
 			);
 			path = View;
 			sourceTree = "<group>";
@@ -2321,6 +2356,7 @@
 				383420D825FFEB3F002D46C1 /* Popup.swift */,
 				389ECDFD2601061500D86C4F /* View+Snapshot.swift */,
 				38EA05FF262091870064E39B /* BolusProgressViewStyle.swift */,
+				DDB0BBA02026050100000002 /* BolusProgressBar.swift */,
 				38DF1785276A73D400B3528F /* TagCloudView.swift */,
 				DD88C8E12C50420800F2D558 /* DefinitionRow.swift */,
 				DD1745282C55642100211FAC /* SettingInputSection.swift */,
@@ -2849,13 +2885,27 @@
 			isa = PBXGroup;
 			children = (
 				A401509D21F7F35D4E109EDA /* HistoryDataFlow.swift */,
+				BD175EFE0000100000000001 /* HistoryDataFlow+Models.swift */,
+				BD175EFE0000100000000002 /* HistoryDeletionTarget.swift */,
 				60744C3E9BB3652895C908CC /* HistoryProvider.swift */,
 				9455FA2D92E77A6C4AFED8A3 /* HistoryStateModel.swift */,
+				BD175EC00000100000000001 /* HistoryStateModel+Deletion */,
 				0EE66DD474AFFD4FD787D5B9 /* View */,
 			);
 			path = History;
 			sourceTree = "<group>";
 		};
+		BD175EC00000100000000001 /* HistoryStateModel+Deletion */ = {
+			isa = PBXGroup;
+			children = (
+				BD175EFE0000100000000003 /* HistoryStateModel+Glucose.swift */,
+				BD175EFE0000100000000004 /* HistoryStateModel+Carbs.swift */,
+				BD175EFE0000100000000005 /* HistoryStateModel+Insulin.swift */,
+				BD175EFE0000100000000006 /* HistoryStateModel+CarbEditing.swift */,
+			);
+			path = "HistoryStateModel+Deletion";
+			sourceTree = "<group>";
+		};
 		A42F1FEDFFD0DDE00AAD54D3 /* BasalProfileEditor */ = {
 			isa = PBXGroup;
 			children = (
@@ -4556,6 +4606,7 @@
 				582DF9792C8CE1E5001F516D /* MainChartHelper.swift in Sources */,
 				E06B911A275B5EEA003C04B6 /* Array+Extension.swift in Sources */,
 				38EA0600262091870064E39B /* BolusProgressViewStyle.swift in Sources */,
+				DDB0BBA02026050100000001 /* BolusProgressBar.swift in Sources */,
 				389ECDFE2601061500D86C4F /* View+Snapshot.swift in Sources */,
 				38FEF3FE2738083E00574A46 /* CGMSettingsProvider.swift in Sources */,
 				38E98A3725F5509500C0CED0 /* String+Extensions.swift in Sources */,
@@ -4792,6 +4843,19 @@
 				8194B80890CDD6A3C13B0FEE /* SnoozeStateModel.swift in Sources */,
 				BDA25EE42D260CD500035F34 /* AppleWatchManager.swift in Sources */,
 				0437CE46C12535A56504EC19 /* SnoozeRootView.swift in Sources */,
+				BD175EBE0000100000000001 /* HistoryDataFlow+Models.swift in Sources */,
+				BD175EBE0000100000000002 /* HistoryDeletionTarget.swift in Sources */,
+				BD175EBE0000100000000003 /* HistoryStateModel+Glucose.swift in Sources */,
+				BD175EBE0000100000000004 /* HistoryStateModel+Carbs.swift in Sources */,
+				BD175EBE0000100000000005 /* HistoryStateModel+Insulin.swift in Sources */,
+				BD175EBE0000100000000006 /* HistoryStateModel+CarbEditing.swift in Sources */,
+				BD175EBE0000100000000007 /* HistoryRootView+Treatments.swift in Sources */,
+				BD175EBE0000100000000008 /* HistoryRootView+Meals.swift in Sources */,
+				BD175EBE0000100000000009 /* HistoryRootView+Glucose.swift in Sources */,
+				BD175EBE000010000000000A /* HistoryRootView+Adjustments.swift in Sources */,
+				BD175EBE000010000000000B /* HistoryRootView+Filters.swift in Sources */,
+				BD175EBE000010000000000C /* HistoryRootView+AddGlucose.swift in Sources */,
+				BD175EBE000010000000000D /* HistoryRootView+Confirmations.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};

+ 1 - 1
Trio/Sources/APS/CGM/GlucoseSimulatorSource.swift

@@ -217,7 +217,7 @@ class OscillatingGenerator: BloodGlucoseGenerator {
 
             // Create BloodGlucose with the correct constructor
             let bloodGlucose = BloodGlucose(
-                _id: UUID().uuidString,
+                id: UUID().uuidString,
                 sgv: glucose,
                 direction: direction,
                 date: Decimal(Int(currentDate.timeIntervalSince1970) * 1000),

+ 1 - 1
Trio/Sources/APS/CGM/PluginSource.swift

@@ -236,7 +236,7 @@ extension PluginSource: CGMManagerDelegate {
 
                 let value = Int(quantity.doubleValue(for: .milligramsPerDeciliter))
                 return BloodGlucose(
-                    _id: UUID().uuidString,
+                    id: UUID().uuidString,
                     sgv: value,
                     direction: .init(trendType: newGlucoseSample.trend),
                     date: Decimal(Int(newGlucoseSample.date.timeIntervalSince1970 * 1000)),

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

@@ -360,7 +360,7 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
                         let results = glucose.enumerated().map { index, sample -> BloodGlucose in
                             let value = Int(sample.quantity.doubleValue(for: .milligramsPerDeciliter))
                             return BloodGlucose(
-                                _id: sample.syncIdentifier,
+                                id: sample.syncIdentifier,
                                 sgv: value,
                                 direction: directions[index],
                                 date: Decimal(Int(sample.date.timeIntervalSince1970 * 1000)),

+ 26 - 66
Trio/Sources/APS/Storage/GlucoseStorage.swift

@@ -19,7 +19,6 @@ protocol GlucoseStorage {
     func isGlucoseFresh() -> Bool
     func getGlucoseNotYetUploadedToNightscout() async throws -> [BloodGlucose]
     func getCGMStateNotYetUploadedToNightscout() async throws -> [NightscoutTreatment]
-    func getManualGlucoseNotYetUploadedToNightscout() async throws -> [NightscoutTreatment]
     func getGlucoseNotYetUploadedToHealth() async throws -> [BloodGlucose]
     func getManualGlucoseNotYetUploadedToHealth() async throws -> [BloodGlucose]
     func getGlucoseNotYetUploadedToTidepool() async throws -> [StoredGlucoseSample]
@@ -436,67 +435,28 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             }
 
             return fetchedResults.map { result in
-                BloodGlucose(
-                    _id: result.id?.uuidString ?? UUID().uuidString,
-                    sgv: Int(result.glucose),
-                    direction: BloodGlucose.Direction(from: result.direction ?? ""),
-                    date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
-                    dateString: result.date ?? Date(),
-                    unfiltered: Decimal(result.glucose),
-                    filtered: Decimal(result.glucose),
-                    noise: nil,
-                    glucose: Int(result.glucose),
-                    type: "sgv"
-                )
-            }
-        }
-    }
-
-    // Fetch manual glucose that is not uploaded to Nightscout yet
-    /// - Returns: Array of NightscoutTreatment to ensure the correct format for the NS Upload
-    func getManualGlucoseNotYetUploadedToNightscout() async throws -> [NightscoutTreatment] {
-        let context = makeContext()
-        context.name = "getManualGlucoseNotYetUploadedToNightscout"
-
-        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: GlucoseStored.self,
-            onContext: context,
-            predicate: NSPredicate.manualGlucoseNotYetUploadedToNightscout,
-            key: "date",
-            ascending: false
-        )
-
-        return try await context.perform {
-            guard let fetchedResults = results as? [GlucoseStored] else {
-                throw CoreDataError.fetchError(function: #function, file: #file)
-            }
-
-            return fetchedResults.map { result in
-                NightscoutTreatment(
-                    duration: nil,
-                    rawDuration: nil,
-                    rawRate: nil,
-                    absolute: nil,
-                    rate: nil,
-                    eventType: .capillaryGlucose,
-                    createdAt: result.date,
-                    enteredBy: CarbsEntry.local,
-                    bolus: nil,
-                    insulin: nil,
-                    notes: "Trio User",
-                    carbs: nil,
-                    fat: nil,
-                    protein: nil,
-                    foodType: nil,
-                    targetTop: nil,
-                    targetBottom: nil,
-                    glucoseType: "Manual",
-                    glucose: self.settingsManager.settings
-                        .units == .mgdL ? (self.glucoseFormatter.string(from: Int(result.glucose) as NSNumber) ?? "")
-                        : (self.glucoseFormatter.string(from: Decimal(result.glucose).asMmolL as NSNumber) ?? ""),
-                    units: self.settingsManager.settings.units == .mmolL ? "mmol" : "mg/dl",
-                    id: result.id?.uuidString
-                )
+                if result.isManual {
+                    BloodGlucose(
+                        id: result.id?.uuidString ?? UUID().uuidString,
+                        mbg: Int(result.glucose),
+                        date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
+                        dateString: result.date ?? Date(),
+                        type: "mbg"
+                    )
+                } else {
+                    BloodGlucose(
+                        id: result.id?.uuidString ?? UUID().uuidString,
+                        sgv: Int(result.glucose),
+                        direction: BloodGlucose.Direction(from: result.direction ?? ""),
+                        date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
+                        dateString: result.date ?? Date(),
+                        unfiltered: Decimal(result.glucose),
+                        filtered: Decimal(result.glucose),
+                        noise: nil,
+                        glucose: Int(result.glucose),
+                        type: "sgv"
+                    )
+                }
             }
         }
     }
@@ -532,7 +492,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
             return fetchedResults.map { result in
                 BloodGlucose(
-                    _id: result.id?.uuidString ?? UUID().uuidString,
+                    id: result.id?.uuidString ?? UUID().uuidString,
                     sgv: Int(result.glucose),
                     direction: BloodGlucose.Direction(from: result.direction ?? ""),
                     date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
@@ -567,7 +527,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
             return fetchedResults.map { result in
                 BloodGlucose(
-                    _id: result.id?.uuidString ?? UUID().uuidString,
+                    id: result.id?.uuidString ?? UUID().uuidString,
                     sgv: Int(result.glucose),
                     direction: BloodGlucose.Direction(from: result.direction ?? ""),
                     date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
@@ -602,7 +562,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
             return fetchedResults.map { result in
                 BloodGlucose(
-                    _id: result.id?.uuidString ?? UUID().uuidString,
+                    id: result.id?.uuidString ?? UUID().uuidString,
                     sgv: Int(result.glucose),
                     direction: BloodGlucose.Direction(from: result.direction ?? ""),
                     date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
@@ -638,7 +598,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
             return fetchedResults.map { result in
                 BloodGlucose(
-                    _id: result.id?.uuidString ?? UUID().uuidString,
+                    id: result.id?.uuidString ?? UUID().uuidString,
                     sgv: Int(result.glucose),
                     direction: BloodGlucose.Direction(from: result.direction ?? ""),
                     date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,

+ 16 - 1
Trio/Sources/APS/Storage/PumpHistoryStorage.swift

@@ -207,6 +207,21 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                     newPumpEvent.isUploadedToTidepool = false
                     newPumpEvent.note = event.title
 
+                case .replaceComponent(componentType: .infusionSet),
+                     .replaceComponent(componentType: .pump):
+                    guard existingEvents.isEmpty else {
+                        // Duplicate found, do not store the event
+                        debug(.coreData, "Duplicate event found with timestamp: \(event.date)")
+                        continue
+                    }
+                    let newPumpEvent = PumpEventStored(context: context)
+                    newPumpEvent.id = UUID().uuidString
+                    newPumpEvent.timestamp = event.date
+                    newPumpEvent.type = PumpEvent.siteChange.rawValue
+                    newPumpEvent.isUploadedToNS = false
+                    newPumpEvent.isUploadedToHealth = false
+                    newPumpEvent.isUploadedToTidepool = false
+
                 default:
                     continue
                 }
@@ -428,7 +443,7 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                         targetTop: nil,
                         targetBottom: nil
                     )
-                case PumpEvent.prime.rawValue:
+                case PumpEvent.siteChange.rawValue:
                     return NightscoutTreatment(
                         duration: nil,
                         rawDuration: nil,

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

@@ -151,6 +151,7 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
             newTempTarget.name = tempTarget.name
             newTempTarget.target = NSDecimalNumber(decimal: tempTarget.targetTop ?? 0)
             newTempTarget.isPreset = tempTarget.isPreset ?? false
+            newTempTarget.enteredBy = tempTarget.enteredBy
 
             // Nullify half basal target to ensure the latest HBT is used via OpenAPS Manager when sending TT data to oref
             newTempTarget.halfBasalTarget = nil
@@ -313,7 +314,7 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
                     rate: nil,
                     eventType: .nsTempTarget,
                     createdAt: tempTarget.date ?? Date(),
-                    enteredBy: TempTarget.local,
+                    enteredBy: tempTarget.enteredBy ?? TempTarget.local,
                     bolus: nil,
                     insulin: nil,
                     notes: tempTarget.name ?? TempTarget.custom,
@@ -359,7 +360,7 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
                     rate: nil,
                     eventType: .nsTempTarget,
                     createdAt: (tempTargetRun.startDate ?? tempTargetRun.tempTarget?.date) ?? Date(),
-                    enteredBy: TempTarget.local,
+                    enteredBy: tempTargetRun.tempTarget?.enteredBy ?? TempTarget.local,
                     bolus: nil,
                     insulin: nil,
                     notes: tempTargetRun.tempTarget?.name ?? TempTarget.custom,

ファイルの差分が大きいため隠しています
+ 34 - 12
Trio/Sources/Localizations/Main/Localizable.xcstrings


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

@@ -68,7 +68,7 @@ struct AlgorithmGlucose: Codable {
 
         try container.encode(dateFormatter.string(from: date ?? Date()), forKey: .dateString)
 
-        let dateAsUnixTimestamp = String(format: "%.0f", (date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000)
+        let dateAsUnixTimestamp = Int64((date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000)
         try container.encode(dateAsUnixTimestamp, forKey: .date)
 
         try container.encode(direction, forKey: .direction)

+ 26 - 9
Trio/Sources/Models/BloodGlucose.swift

@@ -60,8 +60,10 @@ struct BloodGlucose: JSON, Identifiable, Hashable, Codable {
     }
 
     enum CodingKeys: String, CodingKey {
-        case _id
+        case legacyId = "_id"
+        case id
         case sgv
+        case mbg
         case direction
         case date
         case dateString
@@ -77,7 +79,12 @@ struct BloodGlucose: JSON, Identifiable, Hashable, Codable {
 
     init(from decoder: Decoder) throws {
         let container = try decoder.container(keyedBy: CodingKeys.self)
-        _id = try container.decode(String.self, forKey: ._id)
+
+        let legacyId = try container.decodeIfPresent(String.self, forKey: .legacyId)
+        let explicitId = try container.decodeIfPresent(String.self, forKey: .id)
+
+        self.legacyId = legacyId
+        id = explicitId ?? legacyId ?? UUID().uuidString
 
         sgv = try? container.decodeIfPresent(Int.self, forKey: .sgv)
         if sgv == nil {
@@ -87,6 +94,14 @@ struct BloodGlucose: JSON, Identifiable, Hashable, Codable {
             }
             // If both attempts fail, sgv remains nil
         }
+        mbg = try? container.decodeIfPresent(Int.self, forKey: .mbg)
+        if mbg == nil {
+            // The nightscout API might return a double instead of an int, or the key might be missing
+            if let doubleValue = try? container.decodeIfPresent(Double.self, forKey: .mbg) {
+                mbg = Int(doubleValue)
+            }
+            // If both attempts fail, sgv remains nil
+        }
 
         direction = try container.decodeIfPresent(Direction.self, forKey: .direction)
         date = try container.decode(Decimal.self, forKey: .date)
@@ -102,8 +117,10 @@ struct BloodGlucose: JSON, Identifiable, Hashable, Codable {
     }
 
     init(
-        _id: String = UUID().uuidString,
+        id: String = UUID().uuidString,
+        legacyId: String? = nil,
         sgv: Int? = nil,
+        mbg: Int? = nil,
         direction: Direction? = nil,
         date: Decimal,
         dateString: Date,
@@ -116,8 +133,10 @@ struct BloodGlucose: JSON, Identifiable, Hashable, Codable {
         sessionStartDate: Date? = nil,
         transmitterID: String? = nil
     ) {
-        self._id = _id
+        self.id = id
+        self.legacyId = legacyId
         self.sgv = sgv
+        self.mbg = mbg
         self.direction = direction
         self.date = date
         self.dateString = dateString
@@ -131,12 +150,10 @@ struct BloodGlucose: JSON, Identifiable, Hashable, Codable {
         self.transmitterID = transmitterID
     }
 
-    var _id: String?
-    var id: String {
-        _id ?? UUID().uuidString
-    }
-
+    let legacyId: String?
+    var id: String
     var sgv: Int?
+    var mbg: Int?
     var direction: Direction?
     let date: Decimal
     let dateString: Date

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

@@ -75,6 +75,7 @@ struct TrioSettings: JSON, Equatable, Encodable {
     var smartStackView: LockScreenView = .simple
     var bolusShortcut: BolusShortcutLimit = .notAllowed
     var timeInRangeType: TimeInRangeType = .timeInTightRange
+    var requireAdjustmentsConfirmation: Bool = false
 
     /// Selected Garmin watchface (Trio or SwissAlpine)
     var garminWatchface: GarminWatchface = .trio
@@ -358,6 +359,10 @@ extension TrioSettings: Decodable {
             settings.timeInRangeType = timeInRangeType
         }
 
+        if let requireAdjustmentsConfirmation = try? container.decode(Bool.self, forKey: .requireAdjustmentsConfirmation) {
+            settings.requireAdjustmentsConfirmation = requireAdjustmentsConfirmation
+        }
+
         if let garminWatchface = try? container.decode(GarminWatchface.self, forKey: .garminWatchface) {
             settings.garminWatchface = garminWatchface
         }

+ 5 - 0
Trio/Sources/Modules/Adjustments/AdjustmentsStateModel.swift

@@ -13,6 +13,9 @@ extension Adjustments {
         @ObservationIgnored @Injected() var overrideStorage: OverrideStorage!
         @ObservationIgnored @Injected() var nightscoutManager: NightscoutManager!
 
+        var requireAdjustmentsConfirmation: Bool = false
+        var shouldDisplayPresetStartConfirmDialog: Bool = false
+
         // MARK: - Override and Temp Target Properties
 
         var overridePercentage: Double = 100
@@ -161,6 +164,7 @@ extension Adjustments {
                 target: tempTargetTarget,
                 autosensMax: autosensMax
             )
+            requireAdjustmentsConfirmation = settingsManager.settings.requireAdjustmentsConfirmation
             Task {
                 await getCurrentGlucoseTarget()
             }
@@ -255,6 +259,7 @@ extension Adjustments.StateModel: SettingsObserver, PreferencesObserver {
     /// Updates settings when they change.
     func settingsDidChange(_: TrioSettings) {
         units = settingsManager.settings.units
+        requireAdjustmentsConfirmation = settingsManager.settings.requireAdjustmentsConfirmation
         Task {
             await getCurrentGlucoseTarget()
         }

+ 110 - 0
Trio/Sources/Modules/Adjustments/View/AdjustmentsRootView.swift

@@ -23,6 +23,7 @@ extension Adjustments {
         @State var isEditingTT = false
         @State var showCancelOverrideConfirmDialog = false
         @State var showCancelTempTargetConfirmDialog = false
+        @State var pendingPresetActivation: PendingPresetActivation?
 
         private var shouldDisplayStickyOverrideStopButton: Bool {
             state.isOverrideEnabled && state.activeOverrideName.isNotEmpty
@@ -171,6 +172,25 @@ extension Adjustments {
                 } message: {
                     Text("Stop the Temp Target \"\(state.currentActiveTempTarget?.name ?? "")\"?")
                 }
+                .confirmationDialog(
+                    "Activate Preset",
+                    isPresented: presetActivationConfirmationBinding
+                ) {
+                    Button("Activate") {
+                        if let activation = pendingPresetActivation {
+                            activatePreset(activation)
+                        }
+                    }
+
+                    Button("Cancel", role: .cancel) {
+                        state.shouldDisplayPresetStartConfirmDialog = false
+                        pendingPresetActivation = nil
+                    }
+                } message: {
+                    if let activation = pendingPresetActivation {
+                        Text(activation.confirmationMessage)
+                    }
+                }
             }).background(appState.trioBackgroundColor(for: colorScheme))
         }
 
@@ -291,3 +311,93 @@ extension Adjustments {
         }
     }
 }
+
+// MARK: Preset Activation Handling
+
+extension Adjustments.RootView: View {
+    enum PendingPresetActivation {
+        case override(objectID: NSManagedObjectID, presetID: String?, name: String)
+        case tempTarget(objectID: NSManagedObjectID, presetID: String?, name: String)
+
+        var name: String {
+            switch self {
+            case let .override(_, _, name),
+                 let .tempTarget(_, _, name):
+                return name
+            }
+        }
+
+        var adjustmentType: String {
+            switch self {
+            case .override:
+                return String(localized: "Override")
+            case .tempTarget:
+                return String(localized: "Temp Target")
+            }
+        }
+
+        var confirmationMessage: String {
+            String(localized: "Start the \(adjustmentType) \"\(name)\"?", comment: "Confirmation message for starting a preset")
+        }
+    }
+
+    private var presetActivationConfirmationBinding: Binding<Bool> {
+        Binding(
+            get: {
+                state.requireAdjustmentsConfirmation &&
+                    state.shouldDisplayPresetStartConfirmDialog &&
+                    pendingPresetActivation != nil
+            },
+            set: { isPresented in
+                if !isPresented {
+                    state.shouldDisplayPresetStartConfirmDialog = false
+                    pendingPresetActivation = nil
+                }
+            }
+        )
+    }
+
+    func requestPresetActivation(_ activation: PendingPresetActivation) {
+        if state.requireAdjustmentsConfirmation {
+            pendingPresetActivation = activation
+            state.shouldDisplayPresetStartConfirmDialog = true
+        } else {
+            activatePreset(activation)
+        }
+    }
+
+    func activatePreset(_ activation: PendingPresetActivation) {
+        Task {
+            switch activation {
+            case let .override(objectID, presetID, _):
+                await state.enactOverridePreset(withID: objectID)
+
+                await MainActor.run {
+                    state.hideModal()
+                    selectedOverridePresetID = presetID
+                    showOverrideCheckmark = true
+                    state.shouldDisplayPresetStartConfirmDialog = false
+                    pendingPresetActivation = nil
+                }
+
+                DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
+                    showOverrideCheckmark = false
+                }
+
+            case let .tempTarget(objectID, presetID, _):
+                await state.enactTempTargetPreset(withID: objectID)
+
+                await MainActor.run {
+                    selectedTempTargetPresetID = presetID
+                    showTempTargetCheckmark = true
+                    state.shouldDisplayPresetStartConfirmDialog = false
+                    pendingPresetActivation = nil
+                }
+
+                DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
+                    showTempTargetCheckmark = false
+                }
+            }
+        }
+    }
+}

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

@@ -17,10 +17,13 @@ extension Adjustments.RootView {
         Section {
             ForEach(state.overridePresets) { preset in
                 overridesView(for: preset, showCheckMark: showOverrideCheckmark) {
-                    enactOverridePreset(preset)
+                    requestOverridePresetActivation(preset)
+                }
+                .contextMenu {
+                    actionButtonsForOverrides(for: preset)
                 }
                 .swipeActions(edge: .trailing, allowsFullSwipe: true) {
-                    swipeActionsForOverrides(for: preset)
+                    actionButtonsForOverrides(for: preset)
                 }
             }
             .onMove(perform: state.reorderOverride)
@@ -73,24 +76,19 @@ extension Adjustments.RootView {
         }
     }
 
-    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
-            }
-        }
+    private func requestOverridePresetActivation(_ preset: OverrideStored) {
+        let activation = PendingPresetActivation.override(
+            objectID: preset.objectID,
+            presetID: preset.id,
+            name: preset.name ?? ""
+        )
+
+        requestPresetActivation(activation)
     }
 
-    func swipeActionsForOverrides(for preset: OverrideStored) -> some View {
+    func actionButtonsForOverrides(for preset: OverrideStored) -> some View {
         Group {
-            Button(role: .none) {
+            Button(role: .destructive) {
                 selectedOverride = preset
                 isConfirmDeletePresented = true
             } label: {

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

@@ -21,7 +21,7 @@ extension Adjustments.RootView {
             ForEach(state.scheduledTempTargets) { tempTarget in
                 tempTargetView(for: tempTarget)
                     .swipeActions(edge: .trailing, allowsFullSwipe: true) {
-                        swipeActionsForTempTargets(for: tempTarget)
+                        actionButtonsForTempTargets(for: tempTarget)
                     }
             }
             .listRowBackground(Color.chart)
@@ -34,10 +34,13 @@ extension Adjustments.RootView {
         Section {
             ForEach(state.tempTargetPresets) { preset in
                 tempTargetView(for: preset, showCheckmark: showTempTargetCheckmark) {
-                    enactTempTargetPreset(preset)
+                    requestTempTargetPresetActivation(preset)
+                }
+                .contextMenu {
+                    actionButtonsForTempTargets(for: preset)
                 }
                 .swipeActions(edge: .trailing, allowsFullSwipe: true) {
-                    swipeActionsForTempTargets(for: preset)
+                    actionButtonsForTempTargets(for: preset)
                 }
             }
             .onMove(perform: state.reorderTempTargets)
@@ -61,22 +64,19 @@ extension Adjustments.RootView {
         }
     }
 
-    private func enactTempTargetPreset(_ preset: TempTargetStored) {
-        Task {
-            let objectID = preset.objectID
-            await state.enactTempTargetPreset(withID: objectID)
-            selectedTempTargetPresetID = preset.id?.uuidString
-            showTempTargetCheckmark = true
+    private func requestTempTargetPresetActivation(_ preset: TempTargetStored) {
+        let activation = PendingPresetActivation.tempTarget(
+            objectID: preset.objectID,
+            presetID: preset.id?.uuidString,
+            name: preset.name ?? ""
+        )
 
-            DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
-                showTempTargetCheckmark = false
-            }
-        }
+        requestPresetActivation(activation)
     }
 
-    private func swipeActionsForTempTargets(for tempTarget: TempTargetStored) -> some View {
+    private func actionButtonsForTempTargets(for tempTarget: TempTargetStored) -> some View {
         Group {
-            Button {
+            Button(role: .destructive) {
                 Task {
                     selectedTempTarget = tempTarget
                     isConfirmDeletePresented = true

+ 43 - 43
Trio/Sources/Modules/DynamicSettings/View/DynamicSettingsRootView.swift

@@ -5,13 +5,37 @@ extension DynamicSettings {
     struct RootView: BaseView {
         let resolver: Resolver
         @StateObject var state = StateModel()
-        @State private var shouldDisplayHint: Bool = false
         @State var hintDetent = PresentationDetent.large
-        @State var selectedVerboseHint: AnyView?
-        @State var hintLabel: String?
+        @State private var hintPayload: HintPayload?
         @State private var decimalPlaceholder: Decimal = 0.0
         @State private var booleanPlaceholder: Bool = false
 
+        private struct HintPayload: Identifiable {
+            let id = UUID()
+            let label: String
+            let content: AnyView
+        }
+
+        private var shouldDisplayHintBinding: Binding<Bool> {
+            Binding(
+                get: { hintPayload != nil },
+                set: { newValue in if !newValue { hintPayload = nil } }
+            )
+        }
+
+        private func verboseHintBinding(label: String) -> Binding<(any View)?> {
+            Binding(
+                get: { hintPayload?.content },
+                set: { newView in
+                    if let view = newView {
+                        hintPayload = HintPayload(label: label, content: AnyView(view))
+                    } else {
+                        hintPayload = nil
+                    }
+                }
+            )
+        }
+
         private var conversionFormatter: NumberFormatter {
             let formatter = NumberFormatter()
             formatter.numberStyle = .decimal
@@ -76,9 +100,9 @@ extension DynamicSettings {
                                 Spacer()
                                 Button(
                                     action: {
-                                        hintLabel = String(localized: "Time in Range Chart Style")
-                                        selectedVerboseHint =
-                                            AnyView(
+                                        hintPayload = HintPayload(
+                                            label: String(localized: "Dynamic Insulin Sensitivity"),
+                                            content: AnyView(
                                                 VStack(alignment: .leading, spacing: 10) {
                                                     Text("Default: Disabled").bold()
                                                     Text(
@@ -124,7 +148,7 @@ extension DynamicSettings {
                                                     }
                                                 }
                                             )
-                                        shouldDisplayHint.toggle()
+                                        )
                                     },
                                     label: {
                                         HStack {
@@ -142,14 +166,8 @@ extension DynamicSettings {
                         SettingInputSection(
                             decimalValue: $state.adjustmentFactor,
                             booleanValue: $booleanPlaceholder,
-                            shouldDisplayHint: $shouldDisplayHint,
-                            selectedVerboseHint: Binding(
-                                get: { selectedVerboseHint },
-                                set: {
-                                    selectedVerboseHint = $0.map { AnyView($0) }
-                                    hintLabel = String(localized: "Adjustment Factor (AF)")
-                                }
-                            ),
+                            shouldDisplayHint: shouldDisplayHintBinding,
+                            selectedVerboseHint: verboseHintBinding(label: String(localized: "Adjustment Factor (AF)")),
                             // TODO?: include conditional links to Desmos logarithmic graphs based on which .glucose setting is used
                             units: state.units,
                             type: .decimal("adjustmentFactor"),
@@ -173,14 +191,8 @@ extension DynamicSettings {
                         SettingInputSection(
                             decimalValue: $state.adjustmentFactorSigmoid,
                             booleanValue: $booleanPlaceholder,
-                            shouldDisplayHint: $shouldDisplayHint,
-                            selectedVerboseHint: Binding(
-                                get: { selectedVerboseHint },
-                                set: {
-                                    selectedVerboseHint = $0.map { AnyView($0) }
-                                    hintLabel = String(localized: "Sigmoid Adjustment Factor")
-                                }
-                            ),
+                            shouldDisplayHint: shouldDisplayHintBinding,
+                            selectedVerboseHint: verboseHintBinding(label: String(localized: "Sigmoid Adjustment Factor")),
                             units: state.units,
                             type: .decimal("adjustmentFactorSigmoid"),
                             label: String(localized: "Sigmoid Adjustment Factor"),
@@ -207,14 +219,8 @@ extension DynamicSettings {
                     SettingInputSection(
                         decimalValue: $state.weightPercentage,
                         booleanValue: $booleanPlaceholder,
-                        shouldDisplayHint: $shouldDisplayHint,
-                        selectedVerboseHint: Binding(
-                            get: { selectedVerboseHint },
-                            set: {
-                                selectedVerboseHint = $0.map { AnyView($0) }
-                                hintLabel = String(localized: "Weighted Average of TDD")
-                            }
-                        ),
+                        shouldDisplayHint: shouldDisplayHintBinding,
+                        selectedVerboseHint: verboseHintBinding(label: String(localized: "Weighted Average of TDD")),
                         units: state.units,
                         type: .decimal("weightPercentage"),
                         label: String(localized: "Weighted Average of TDD"),
@@ -236,14 +242,8 @@ extension DynamicSettings {
                     SettingInputSection(
                         decimalValue: $decimalPlaceholder,
                         booleanValue: $state.tddAdjBasal,
-                        shouldDisplayHint: $shouldDisplayHint,
-                        selectedVerboseHint: Binding(
-                            get: { selectedVerboseHint },
-                            set: {
-                                selectedVerboseHint = $0.map { AnyView($0) }
-                                hintLabel = String(localized: "Adjust Basal")
-                            }
-                        ),
+                        shouldDisplayHint: shouldDisplayHintBinding,
+                        selectedVerboseHint: verboseHintBinding(label: String(localized: "Adjust Basal")),
                         units: state.units,
                         type: .boolean,
                         label: String(localized: "Adjust Basal"),
@@ -264,12 +264,12 @@ extension DynamicSettings {
                 }
             }
             .listSectionSpacing(sectionSpacing)
-            .sheet(isPresented: $shouldDisplayHint) {
+            .sheet(item: $hintPayload) { payload in
                 SettingInputHintView(
                     hintDetent: $hintDetent,
-                    shouldDisplayHint: $shouldDisplayHint,
-                    hintLabel: hintLabel ?? "",
-                    hintText: selectedVerboseHint ?? AnyView(EmptyView()),
+                    shouldDisplayHint: shouldDisplayHintBinding,
+                    hintLabel: payload.label,
+                    hintText: payload.content,
                     sheetTitle: String(localized: "Help", comment: "Help sheet title")
                 )
             }

+ 161 - 0
Trio/Sources/Modules/History/HistoryDataFlow+Models.swift

@@ -0,0 +1,161 @@
+import Foundation
+import SwiftUI
+
+extension History {
+    class Treatment: Identifiable, Hashable, Equatable {
+        let id: String
+        let idPumpEvent: String?
+        let units: GlucoseUnits
+        let type: DataType
+        let date: Date
+        let amount: Decimal?
+        let secondAmount: Decimal?
+        let duration: Decimal?
+        let isFPU: Bool?
+        let fpuID: String?
+        let note: String?
+        let isSMB: Bool?
+        let isExternal: Bool?
+
+        private var numberFormatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            formatter.maximumFractionDigits = 2
+            return formatter
+        }
+
+        private var tempTargetFormater: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            formatter.maximumFractionDigits = 1
+            return formatter
+        }
+
+        init(
+            units: GlucoseUnits,
+            type: DataType,
+            date: Date,
+            amount: Decimal? = nil,
+            secondAmount: Decimal? = nil,
+            duration: Decimal? = nil,
+            id: String? = nil,
+            idPumpEvent: String? = nil,
+            isFPU: Bool? = nil,
+            fpuID: String? = nil,
+            note: String? = nil,
+            isSMB: Bool? = nil,
+            isExternal: Bool? = nil
+        ) {
+            self.units = units
+            self.type = type
+            self.date = date
+            self.amount = amount
+            self.secondAmount = secondAmount
+            self.duration = duration
+            self.id = id ?? UUID().uuidString
+            self.idPumpEvent = idPumpEvent
+            self.isFPU = isFPU
+            self.fpuID = fpuID
+            self.note = note
+            self.isSMB = isSMB
+            self.isExternal = isExternal
+        }
+
+        static func == (lhs: Treatment, rhs: Treatment) -> Bool {
+            lhs.id == rhs.id
+        }
+
+        func hash(into hasher: inout Hasher) {
+            hasher.combine(id)
+        }
+
+        var amountText: String {
+            guard let amount = amount else {
+                return ""
+            }
+
+            if amount == 0, duration == 0 {
+                return "Cancel temp"
+            }
+
+            switch type {
+            case .carbs:
+                return numberFormatter
+                    .string(from: amount as NSNumber)! + String(localized: " g", comment: "gram of carbs")
+            case .fpus:
+                return numberFormatter
+                    .string(from: amount as NSNumber)! + String(localized: " g", comment: "gram of carb equilvalents")
+            case .bolus:
+                var bolusText = " "
+                if isSMB ?? false {}
+                else if isExternal ?? false {
+                    bolusText += String(localized: "External", comment: "External Insulin")
+                } else {
+                    bolusText += String(localized: "Manual", comment: "Manual Bolus")
+                }
+
+                return numberFormatter
+                    .string(from: amount as NSNumber)! + String(localized: " U", comment: "Insulin unit") + bolusText
+            case .tempBasal:
+                return numberFormatter
+                    .string(from: amount as NSNumber)! + String(localized: " U/hr", comment: "Unit insulin per hour")
+            case .tempTarget:
+                var converted = amount
+                if units == .mmolL {
+                    converted = converted.asMmolL
+                }
+
+                guard var secondAmount = secondAmount else {
+                    return numberFormatter.string(from: converted as NSNumber)! + " \(units.rawValue)"
+                }
+                if units == .mmolL {
+                    secondAmount = secondAmount.asMmolL
+                }
+
+                return tempTargetFormater.string(from: converted as NSNumber)! + " - " + tempTargetFormater
+                    .string(from: secondAmount as NSNumber)! + " \(units.rawValue)"
+            case .resume,
+                 .suspend:
+                return type.name
+            }
+        }
+
+        var color: Color {
+            switch type {
+            case .carbs:
+                return .loopYellow
+            case .fpus:
+                return .orange.opacity(0.5)
+            case .bolus:
+                return Color.insulin
+            case .tempBasal:
+                return Color.insulin.opacity(0.4)
+            case .resume,
+                 .suspend,
+                 .tempTarget:
+                return .loopGray
+            }
+        }
+
+        var durationText: String? {
+            guard let duration = duration, duration > 0 else {
+                return nil
+            }
+            return numberFormatter.string(from: duration as NSNumber)! + " min"
+        }
+    }
+
+    class Glucose: Identifiable, Hashable, Equatable {
+        static func == (lhs: History.Glucose, rhs: History.Glucose) -> Bool {
+            lhs.glucose == rhs.glucose
+        }
+
+        let glucose: BloodGlucose
+
+        init(glucose: BloodGlucose) {
+            self.glucose = glucose
+        }
+
+        var id: String { glucose.id }
+    }
+}

+ 3 - 158
Trio/Sources/Modules/History/HistoryDataFlow.swift

@@ -82,170 +82,15 @@ enum History {
             }
         }
     }
-
-    class Treatment: Identifiable, Hashable, Equatable {
-        let id: String
-        let idPumpEvent: String?
-        let units: GlucoseUnits
-        let type: DataType
-        let date: Date
-        let amount: Decimal?
-        let secondAmount: Decimal?
-        let duration: Decimal?
-        let isFPU: Bool?
-        let fpuID: String?
-        let note: String?
-        let isSMB: Bool?
-        let isExternal: Bool?
-
-        private var numberFormatter: NumberFormatter {
-            let formatter = NumberFormatter()
-            formatter.numberStyle = .decimal
-            formatter.maximumFractionDigits = 2
-            return formatter
-        }
-
-        private var tempTargetFormater: NumberFormatter {
-            let formatter = NumberFormatter()
-            formatter.numberStyle = .decimal
-            formatter.maximumFractionDigits = 1
-            return formatter
-        }
-
-        init(
-            units: GlucoseUnits,
-            type: DataType,
-            date: Date,
-            amount: Decimal? = nil,
-            secondAmount: Decimal? = nil,
-            duration: Decimal? = nil,
-            id: String? = nil,
-            idPumpEvent: String? = nil,
-            isFPU: Bool? = nil,
-            fpuID: String? = nil,
-            note: String? = nil,
-            isSMB: Bool? = nil,
-            isExternal: Bool? = nil
-        ) {
-            self.units = units
-            self.type = type
-            self.date = date
-            self.amount = amount
-            self.secondAmount = secondAmount
-            self.duration = duration
-            self.id = id ?? UUID().uuidString
-            self.idPumpEvent = idPumpEvent
-            self.isFPU = isFPU
-            self.fpuID = fpuID
-            self.note = note
-            self.isSMB = isSMB
-            self.isExternal = isExternal
-        }
-
-        static func == (lhs: Treatment, rhs: Treatment) -> Bool {
-            lhs.id == rhs.id
-        }
-
-        func hash(into hasher: inout Hasher) {
-            hasher.combine(id)
-        }
-
-        var amountText: String {
-            guard let amount = amount else {
-                return ""
-            }
-
-            if amount == 0, duration == 0 {
-                return "Cancel temp"
-            }
-
-            switch type {
-            case .carbs:
-                return numberFormatter
-                    .string(from: amount as NSNumber)! + String(localized: " g", comment: "gram of carbs")
-            case .fpus:
-                return numberFormatter
-                    .string(from: amount as NSNumber)! + String(localized: " g", comment: "gram of carb equilvalents")
-            case .bolus:
-                var bolusText = " "
-                if isSMB ?? false {}
-                else if isExternal ?? false {
-                    bolusText += String(localized: "External", comment: "External Insulin")
-                } else {
-                    bolusText += String(localized: "Manual", comment: "Manual Bolus")
-                }
-
-                return numberFormatter
-                    .string(from: amount as NSNumber)! + String(localized: " U", comment: "Insulin unit") + bolusText
-            case .tempBasal:
-                return numberFormatter
-                    .string(from: amount as NSNumber)! + String(localized: " U/hr", comment: "Unit insulin per hour")
-            case .tempTarget:
-                var converted = amount
-                if units == .mmolL {
-                    converted = converted.asMmolL
-                }
-
-                guard var secondAmount = secondAmount else {
-                    return numberFormatter.string(from: converted as NSNumber)! + " \(units.rawValue)"
-                }
-                if units == .mmolL {
-                    secondAmount = secondAmount.asMmolL
-                }
-
-                return tempTargetFormater.string(from: converted as NSNumber)! + " - " + tempTargetFormater
-                    .string(from: secondAmount as NSNumber)! + " \(units.rawValue)"
-            case .resume,
-                 .suspend:
-                return type.name
-            }
-        }
-
-        var color: Color {
-            switch type {
-            case .carbs:
-                return .loopYellow
-            case .fpus:
-                return .orange.opacity(0.5)
-            case .bolus:
-                return Color.insulin
-            case .tempBasal:
-                return Color.insulin.opacity(0.4)
-            case .resume,
-                 .suspend,
-                 .tempTarget:
-                return .loopGray
-            }
-        }
-
-        var durationText: String? {
-            guard let duration = duration, duration > 0 else {
-                return nil
-            }
-            return numberFormatter.string(from: duration as NSNumber)! + " min"
-        }
-    }
-
-    class Glucose: Identifiable, Hashable, Equatable {
-        static func == (lhs: History.Glucose, rhs: History.Glucose) -> Bool {
-            lhs.glucose == rhs.glucose
-        }
-
-        let glucose: BloodGlucose
-
-        init(glucose: BloodGlucose) {
-            self.glucose = glucose
-        }
-
-        var id: String { glucose.id }
-    }
 }
 
 protocol HistoryProvider: Provider {
     func deleteCarbsFromNightscout(withID id: String)
     func deleteInsulinFromNightscout(withID id: String)
-    func deleteManualGlucoseFromNightscout(withID id: String)
+    func deleteGlucoseFromNightscout(withID id: String, withDate date: Date)
     func deleteGlucoseFromHealth(withSyncID id: String)
     func deleteMealDataFromHealth(byID id: String, sampleType: HKSampleType)
     func deleteInsulinFromHealth(withSyncID id: String)
+    func deleteInsulinFromTidepool(withSyncId id: String, amount: Decimal, at: Date)
+    func deleteCarbsFromTidepool(withSyncId id: UUID, carbs: Decimal, at: Date, enteredBy: String)
 }

+ 65 - 0
Trio/Sources/Modules/History/HistoryDeletionTarget.swift

@@ -0,0 +1,65 @@
+import CoreData
+import Foundation
+
+extension History {
+    enum DeletionTarget: Identifiable {
+        case glucose(GlucoseStored)
+        case insulin(PumpEventStored)
+        case carbs(CarbEntryStored)
+
+        var id: NSManagedObjectID {
+            switch self {
+            case let .glucose(glucose): return glucose.objectID
+            case let .insulin(pumpEvent): return pumpEvent.objectID
+            case let .carbs(carbEntry): return carbEntry.objectID
+            }
+        }
+
+        func title(units _: GlucoseUnits) -> String {
+            switch self {
+            case .glucose:
+                return String(localized: "Delete Glucose?", comment: "Alert title for deleting glucose")
+            case .insulin:
+                return String(localized: "Delete Insulin?", comment: "Alert title for deleting insulin")
+            case let .carbs(carbEntry):
+                if carbEntry.fpuID == nil {
+                    return String(localized: "Delete Carbs?", comment: "Alert title for deleting carbs")
+                }
+                return carbEntry.isFPU
+                    ? String(localized: "Delete Carbs Equivalents?", comment: "Alert title for deleting carb equivalents")
+                    : String(localized: "Delete Carbs?", comment: "Alert title for deleting carbs")
+            }
+        }
+
+        func message(units: GlucoseUnits) -> String? {
+            switch self {
+            case let .glucose(glucose):
+                let glucoseToDisplay = units == .mgdL
+                    ? glucose.glucose.description
+                    : Int(glucose.glucose).formattedAsMmolL
+                return Formatter.dateFormatter.string(from: glucose.date ?? Date())
+                    + ", " + glucoseToDisplay + " " + units.rawValue
+            case let .insulin(pumpEvent):
+                var text = Formatter.dateFormatter.string(from: pumpEvent.timestamp ?? Date())
+                    + ", "
+                    + (Formatter.decimalFormatterWithThreeFractionDigits.string(from: pumpEvent.bolus?.amount ?? 0) ?? "0")
+                    + String(localized: " U", comment: "Insulin unit")
+                if let bolus = pumpEvent.bolus, bolus.isSMB {
+                    text += String(localized: " SMB", comment: "Super Micro Bolus indicator in delete alert")
+                }
+                return text
+            case let .carbs(carbEntry):
+                if carbEntry.fpuID == nil {
+                    return Formatter.dateFormatter.string(from: carbEntry.date ?? Date())
+                        + ", "
+                        + (Formatter.decimalFormatterWithTwoFractionDigits.string(for: carbEntry.carbs) ?? "0")
+                        + String(localized: " g", comment: "gram of carbs")
+                }
+                return String(
+                    localized: "All FPUs and the carbs of the meal will be deleted.",
+                    comment: "Alert message for meal deletion"
+                )
+            }
+        }
+    }
+}

+ 2 - 2
Trio/Sources/Modules/History/HistoryProvider.swift

@@ -35,10 +35,10 @@ extension History {
             }
         }
 
-        func deleteManualGlucoseFromNightscout(withID id: String) {
+        func deleteGlucoseFromNightscout(withID id: String, withDate date: Date) {
             Task.detached { [weak self] in
                 guard let self = self else { return }
-                await self.nightscoutManager.deleteManualGlucose(withID: id)
+                await self.nightscoutManager.deleteGlucose(withID: id, withDate: date)
             }
         }
 

+ 291 - 0
Trio/Sources/Modules/History/HistoryStateModel+Deletion/HistoryStateModel+CarbEditing.swift

@@ -0,0 +1,291 @@
+import CoreData
+import Foundation
+
+extension History.StateModel {
+    // 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 {
+            do {
+                // 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
+                try await deleteOldEntries(
+                    treatmentObjectID,
+                    originalEntry: originalEntry,
+                    newCarbs: newCarbs,
+                    newFat: newFat,
+                    newProtein: newProtein,
+                    newNote: newNote
+                )
+
+                try await createNewEntries(
+                    originalDate: newDate,
+                    newCarbs: newCarbs,
+                    newFat: newFat,
+                    newProtein: newProtein,
+                    newNote: newNote
+                )
+
+                await syncWithServices()
+
+                // Perform a determine basal sync to update cob
+                try await apsManager.determineBasalSync()
+
+            } catch {
+                debug(.default, "\(DebuggingIdentifiers.failed) failed to update entry: \(error)")
+            }
+        }
+    }
+
+    private func createNewEntries(
+        originalDate: Date,
+        newCarbs: Decimal,
+        newFat: Decimal,
+        newProtein: Decimal,
+        newNote: String
+    ) async throws {
+        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
+        try 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 throws {
+        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
+            try 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
+            try await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: true)
+
+        } else {
+            // Delete just the carb entry since there are no carb equivalents
+            // Use NSManagedObjectID
+            try 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)")
+                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
+            }
+        }
+    }
+}

+ 119 - 0
Trio/Sources/Modules/History/HistoryStateModel+Deletion/HistoryStateModel+Carbs.swift

@@ -0,0 +1,119 @@
+import CoreData
+import Foundation
+import HealthKit
+
+extension History.StateModel {
+    // 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, isFpuOrComplexMeal: Bool = false) {
+        Task {
+            do {
+                /// Set the variables that control the CustomProgressView BEFORE the actual deletion
+                /// otherwise the determineBasalSync gets executed first, sets waitForSuggestion to false and afterwards waitForSuggestion is set in this function to true, leading to an endless animation
+                await MainActor.run {
+                    carbEntryDeleted = true
+                    waitForSuggestion = true
+                }
+
+                try await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: isFpuOrComplexMeal)
+
+            } catch {
+                debug(.default, "\(DebuggingIdentifiers.failed) Failed to delete carbs: \(error)")
+                await MainActor.run {
+                    carbEntryDeleted = false
+                    waitForSuggestion = false
+                }
+            }
+        }
+    }
+
+    func deleteCarbs(_ treatmentObjectID: NSManagedObjectID, isFpuOrComplexMeal: Bool = false) async throws {
+        // Delete from Nightscout/Apple Health/Tidepool
+        await deleteFromServices(treatmentObjectID, isFPUDeletion: isFpuOrComplexMeal)
+
+        // Delete carbs from Core Data
+        await carbsStorage.deleteCarbsEntryStored(treatmentObjectID)
+
+        // Perform a determine basal sync to update cob
+        try await apsManager.determineBasalSync()
+    }
+
+    /// 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 entries from all services
+        await taskContext.perform {
+            do {
+                carbEntry = try taskContext.existingObject(with: objectIDToDelete) as? CarbEntryStored
+                guard let carbEntry = carbEntry else {
+                    debugPrint("Carb entry for deletion not found. \(DebuggingIdentifiers.failed)")
+                    return
+                }
+
+                // 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)
+
+                    // Delete Fat and Protein entries from Apple Health
+                    let healthObjectsToDelete: [HKSampleType?] = [
+                        AppleHealthConfig.healthFatObject,
+                        AppleHealthConfig.healthProteinObject
+                    ]
+
+                    for sampleType in healthObjectsToDelete {
+                        if let validSampleType = sampleType {
+                            self.provider.deleteMealDataFromHealth(byID: fpuID.uuidString, sampleType: validSampleType)
+                        }
+                    }
+                }
+
+                // Delete carb entries if they exist
+                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)
+                    }
+
+                    self.provider.deleteCarbsFromTidepool(
+                        withSyncId: id,
+                        carbs: Decimal(carbEntry.carbs),
+                        at: entryDate,
+                        enteredBy: CarbsEntry.local
+                    )
+                }
+            } catch {
+                debugPrint("\(DebuggingIdentifiers.failed) Error deleting entries: \(error)")
+            }
+        }
+    }
+}

+ 55 - 0
Trio/Sources/Modules/History/HistoryStateModel+Deletion/HistoryStateModel+Glucose.swift

@@ -0,0 +1,55 @@
+import CoreData
+import Foundation
+
+extension History.StateModel {
+    /// 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)
+        }
+    }
+
+    func deleteGlucose(_ treatmentObjectID: NSManagedObjectID) async {
+        // Delete from Apple Health/Tidepool
+        await deleteGlucoseFromServices(treatmentObjectID)
+
+        // Delete from Core Data
+        await glucoseStorage.deleteGlucose(treatmentObjectID)
+    }
+
+    func deleteGlucoseFromServices(_ treatmentObjectID: NSManagedObjectID) async {
+        let taskContext = CoreDataStack.shared.newTaskContext()
+        taskContext.name = "deleteContext"
+        taskContext.transactionAuthor = "deleteGlucoseFromServices"
+
+        await taskContext.perform {
+            do {
+                let result = try taskContext.existingObject(with: treatmentObjectID) as? GlucoseStored
+
+                guard let glucoseToDelete = result else {
+                    debugPrint("Data Table State: \(#function) \(DebuggingIdentifiers.failed) glucose not found in core data")
+                    return
+                }
+
+                // Delete from Nightscout
+                if let id = glucoseToDelete.id?.uuidString, let date = glucoseToDelete.date {
+                    self.provider.deleteGlucoseFromNightscout(withID: id, withDate: date)
+                }
+
+                // Delete from Apple Health
+                if let id = glucoseToDelete.id?.uuidString {
+                    self.provider.deleteGlucoseFromHealth(withSyncID: id)
+                }
+
+                debugPrint(
+                    "\(#file) \(#function) \(DebuggingIdentifiers.succeeded) deleted glucose from remote service(s) (Nightscout, Apple Health, Tidepool)"
+                )
+            } catch {
+                debugPrint(
+                    "\(#file) \(#function) \(DebuggingIdentifiers.failed) error while deleting glucose remote service(s) (Nightscout, Apple Health, Tidepool) with error: \(error)"
+                )
+            }
+        }
+    }
+}

+ 85 - 0
Trio/Sources/Modules/History/HistoryStateModel+Deletion/HistoryStateModel+Insulin.swift

@@ -0,0 +1,85 @@
+import CoreData
+import Foundation
+
+extension History.StateModel {
+    // Insulin deletion from history
+    /// - **Parameter**: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
+    func invokeInsulinDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
+        Task {
+            do {
+                try await invokeInsulinDeletion(treatmentObjectID)
+            } catch {
+                debug(.default, "\(DebuggingIdentifiers.failed) Failed to delete insulin entry: \(error)")
+            }
+        }
+    }
+
+    func invokeInsulinDeletion(_ treatmentObjectID: NSManagedObjectID) async throws {
+        do {
+            let authenticated = try await unlockmanager.unlock()
+
+            guard authenticated else {
+                debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Authentication Error")
+                return
+            }
+
+            /// Set variables that control the CustomProgressView to true AFTER the authentication and BEFORE the actual determineBasalSync
+            /// We definitely need to set the variables BEFORE the actual sync
+            /// otherwise the determineBasalSync gets executed first, sets waitForSuggestion to false and afterwards waitForSuggestion is set in this function to true, leading to an endless animation
+            /// But we also want it AFTER the authentication
+            /// otherwise the animation would pop up even before the authentication prompt appears to the user
+            await MainActor.run {
+                insulinEntryDeleted = true
+                waitForSuggestion = true
+            }
+
+            // Delete from remote service(s) (i.e. Nightscout, Apple Health, Tidepool)
+            await deleteInsulinFromServices(with: treatmentObjectID)
+
+            // Delete from Core Data
+            await CoreDataStack.shared.deleteObject(identifiedBy: treatmentObjectID)
+
+            // Perform a determine basal sync to update iob
+            try await apsManager.determineBasalSync()
+        } catch {
+            debugPrint(
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Error while Insulin Deletion Task: \(error)"
+            )
+            await MainActor.run {
+                insulinEntryDeleted = false
+                waitForSuggestion = false
+            }
+        }
+    }
+
+    func deleteInsulinFromServices(with treatmentObjectID: NSManagedObjectID) async {
+        let taskContext = CoreDataStack.shared.newTaskContext()
+        taskContext.name = "deleteContext"
+        taskContext.transactionAuthor = "deleteInsulinFromServices"
+
+        await taskContext.perform {
+            do {
+                guard let treatmentToDelete = try taskContext.existingObject(with: treatmentObjectID) as? PumpEventStored
+                else {
+                    debug(.default, "Could not cast the object to PumpEventStored")
+                    return
+                }
+
+                if let id = treatmentToDelete.id, let timestamp = treatmentToDelete.timestamp,
+                   let bolus = treatmentToDelete.bolus, let bolusAmount = bolus.amount
+                {
+                    self.provider.deleteInsulinFromNightscout(withID: id)
+                    self.provider.deleteInsulinFromHealth(withSyncID: id)
+                    self.provider.deleteInsulinFromTidepool(withSyncId: id, amount: bolusAmount as Decimal, at: timestamp)
+                }
+
+                taskContext.delete(treatmentToDelete)
+                try taskContext.save()
+
+                debug(.default, "Successfully deleted the treatment object.")
+            } catch {
+                debug(.default, "Failed to delete the treatment object: \(error)")
+            }
+        }
+    }
+}

+ 0 - 533
Trio/Sources/Modules/History/HistoryStateModel.swift

@@ -40,57 +40,6 @@ extension History {
             glucoseStorage.isGlucoseDataFresh(glucoseDate)
         }
 
-        /// 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)
-            }
-        }
-
-        func deleteGlucose(_ treatmentObjectID: NSManagedObjectID) async {
-            // Delete from Apple Health/Tidepool
-            await deleteGlucoseFromServices(treatmentObjectID)
-
-            // Delete from Core Data
-            await glucoseStorage.deleteGlucose(treatmentObjectID)
-        }
-
-        func deleteGlucoseFromServices(_ treatmentObjectID: NSManagedObjectID) async {
-            let taskContext = CoreDataStack.shared.newTaskContext()
-            taskContext.name = "deleteContext"
-            taskContext.transactionAuthor = "deleteGlucoseFromServices"
-
-            await taskContext.perform {
-                do {
-                    let result = try taskContext.existingObject(with: treatmentObjectID) as? GlucoseStored
-
-                    guard let glucoseToDelete = result else {
-                        debugPrint("Data Table State: \(#function) \(DebuggingIdentifiers.failed) glucose not found in core data")
-                        return
-                    }
-
-                    // Delete from Nightscout
-                    if let id = glucoseToDelete.id?.uuidString {
-                        self.provider.deleteManualGlucoseFromNightscout(withID: id)
-                    }
-
-                    // Delete from Apple Health
-                    if let id = glucoseToDelete.id?.uuidString {
-                        self.provider.deleteGlucoseFromHealth(withSyncID: id)
-                    }
-
-                    debugPrint(
-                        "\(#file) \(#function) \(DebuggingIdentifiers.succeeded) deleted glucose from remote service(s) (Nightscout, Apple Health, Tidepool)"
-                    )
-                } catch {
-                    debugPrint(
-                        "\(#file) \(#function) \(DebuggingIdentifiers.failed) error while deleting glucose remote service(s) (Nightscout, Apple Health, Tidepool) with error: \(error)"
-                    )
-                }
-            }
-        }
-
         func addManualGlucose() {
             // Always save value in mg/dL
             let glucose = units == .mmolL ? manualGlucose.asMgdL : manualGlucose
@@ -98,488 +47,6 @@ extension History {
 
             glucoseStorage.addManualGlucose(glucose: glucoseAsInt)
         }
-
-        // 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, isFpuOrComplexMeal: Bool = false) {
-            Task {
-                do {
-                    /// Set the variables that control the CustomProgressView BEFORE the actual deletion
-                    /// otherwise the determineBasalSync gets executed first, sets waitForSuggestion to false and afterwards waitForSuggestion is set in this function to true, leading to an endless animation
-                    await MainActor.run {
-                        carbEntryDeleted = true
-                        waitForSuggestion = true
-                    }
-
-                    try await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: isFpuOrComplexMeal)
-
-                } catch {
-                    debug(.default, "\(DebuggingIdentifiers.failed) Failed to delete carbs: \(error)")
-                    await MainActor.run {
-                        carbEntryDeleted = false
-                        waitForSuggestion = false
-                    }
-                }
-            }
-        }
-
-        func deleteCarbs(_ treatmentObjectID: NSManagedObjectID, isFpuOrComplexMeal: Bool = false) async throws {
-            // Delete from Nightscout/Apple Health/Tidepool
-            await deleteFromServices(treatmentObjectID, isFPUDeletion: isFpuOrComplexMeal)
-
-            // Delete carbs from Core Data
-            await carbsStorage.deleteCarbsEntryStored(treatmentObjectID)
-
-            // Perform a determine basal sync to update cob
-            try await apsManager.determineBasalSync()
-        }
-
-        /// 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 entries from all services
-            await taskContext.perform {
-                do {
-                    carbEntry = try taskContext.existingObject(with: objectIDToDelete) as? CarbEntryStored
-                    guard let carbEntry = carbEntry else {
-                        debugPrint("Carb entry for deletion not found. \(DebuggingIdentifiers.failed)")
-                        return
-                    }
-
-                    // 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)
-
-                        // Delete Fat and Protein entries from Apple Health
-                        let healthObjectsToDelete: [HKSampleType?] = [
-                            AppleHealthConfig.healthFatObject,
-                            AppleHealthConfig.healthProteinObject
-                        ]
-
-                        for sampleType in healthObjectsToDelete {
-                            if let validSampleType = sampleType {
-                                self.provider.deleteMealDataFromHealth(byID: fpuID.uuidString, sampleType: validSampleType)
-                            }
-                        }
-                    }
-
-                    // Delete carb entries if they exist
-                    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)
-                        }
-
-                        self.provider.deleteCarbsFromTidepool(
-                            withSyncId: id,
-                            carbs: Decimal(carbEntry.carbs),
-                            at: entryDate,
-                            enteredBy: CarbsEntry.local
-                        )
-                    }
-                } catch {
-                    debugPrint("\(DebuggingIdentifiers.failed) Error deleting entries: \(error)")
-                }
-            }
-        }
-
-        // Insulin deletion from history
-        /// - **Parameter**: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
-        func invokeInsulinDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
-            Task {
-                do {
-                    try await invokeInsulinDeletion(treatmentObjectID)
-                } catch {
-                    debug(.default, "\(DebuggingIdentifiers.failed) Failed to delete insulin entry: \(error)")
-                }
-            }
-        }
-
-        func invokeInsulinDeletion(_ treatmentObjectID: NSManagedObjectID) async throws {
-            do {
-                let authenticated = try await unlockmanager.unlock()
-
-                guard authenticated else {
-                    debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Authentication Error")
-                    return
-                }
-
-                /// Set variables that control the CustomProgressView to true AFTER the authentication and BEFORE the actual determineBasalSync
-                /// We definitely need to set the variables BEFORE the actual sync
-                /// otherwise the determineBasalSync gets executed first, sets waitForSuggestion to false and afterwards waitForSuggestion is set in this function to true, leading to an endless animation
-                /// But we also want it AFTER the authentication
-                /// otherwise the animation would pop up even before the authentication prompt appears to the user
-                await MainActor.run {
-                    insulinEntryDeleted = true
-                    waitForSuggestion = true
-                }
-
-                // Delete from remote service(s) (i.e. Nightscout, Apple Health, Tidepool)
-                await deleteInsulinFromServices(with: treatmentObjectID)
-
-                // Delete from Core Data
-                await CoreDataStack.shared.deleteObject(identifiedBy: treatmentObjectID)
-
-                // Perform a determine basal sync to update iob
-                try await apsManager.determineBasalSync()
-            } catch {
-                debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Error while Insulin Deletion Task: \(error)"
-                )
-                await MainActor.run {
-                    insulinEntryDeleted = false
-                    waitForSuggestion = false
-                }
-            }
-        }
-
-        func deleteInsulinFromServices(with treatmentObjectID: NSManagedObjectID) async {
-            let taskContext = CoreDataStack.shared.newTaskContext()
-            taskContext.name = "deleteContext"
-            taskContext.transactionAuthor = "deleteInsulinFromServices"
-
-            await taskContext.perform {
-                do {
-                    guard let treatmentToDelete = try taskContext.existingObject(with: treatmentObjectID) as? PumpEventStored
-                    else {
-                        debug(.default, "Could not cast the object to PumpEventStored")
-                        return
-                    }
-
-                    if let id = treatmentToDelete.id, let timestamp = treatmentToDelete.timestamp,
-                       let bolus = treatmentToDelete.bolus, let bolusAmount = bolus.amount
-                    {
-                        self.provider.deleteInsulinFromNightscout(withID: id)
-                        self.provider.deleteInsulinFromHealth(withSyncID: id)
-                        self.provider.deleteInsulinFromTidepool(withSyncId: id, amount: bolusAmount as Decimal, at: timestamp)
-                    }
-
-                    taskContext.delete(treatmentToDelete)
-                    try taskContext.save()
-
-                    debug(.default, "Successfully deleted the treatment object.")
-                } catch {
-                    debug(.default, "Failed to delete the treatment object: \(error)")
-                }
-            }
-        }
-
-        // 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 {
-                do {
-                    // 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
-                    try await deleteOldEntries(
-                        treatmentObjectID,
-                        originalEntry: originalEntry,
-                        newCarbs: newCarbs,
-                        newFat: newFat,
-                        newProtein: newProtein,
-                        newNote: newNote
-                    )
-
-                    try await createNewEntries(
-                        originalDate: newDate,
-                        newCarbs: newCarbs,
-                        newFat: newFat,
-                        newProtein: newProtein,
-                        newNote: newNote
-                    )
-
-                    await syncWithServices()
-
-                    // Perform a determine basal sync to update cob
-                    try await apsManager.determineBasalSync()
-
-                } catch {
-                    debug(.default, "\(DebuggingIdentifiers.failed) failed to update entry: \(error)")
-                }
-            }
-        }
-
-        private func createNewEntries(
-            originalDate: Date,
-            newCarbs: Decimal,
-            newFat: Decimal,
-            newProtein: Decimal,
-            newNote: String
-        ) async throws {
-            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
-            try 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 throws {
-            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
-                try 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
-                try await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: true)
-
-            } else {
-                // Delete just the carb entry since there are no carb equivalents
-                // Use NSManagedObjectID
-                try 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)")
-                    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
-                }
-            }
-        }
     }
 }
 

+ 72 - 0
Trio/Sources/Modules/History/View/HistoryRootView+AddGlucose.swift

@@ -0,0 +1,72 @@
+import SwiftUI
+
+extension History.RootView {
+    var manualGlucoseFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        if state.units == .mgdL {
+            formatter.maximumIntegerDigits = 3
+            formatter.maximumFractionDigits = 0
+        } else {
+            formatter.maximumIntegerDigits = 2
+            formatter.minimumFractionDigits = 0
+            formatter.maximumFractionDigits = 1
+        }
+        formatter.roundingMode = .halfUp
+        return formatter
+    }
+
+    @ViewBuilder func addGlucoseView() -> some View {
+        let limitLow: Decimal = state.units == .mgdL ? Decimal(14) : 14.asMmolL
+        let limitHigh: Decimal = state.units == .mgdL ? Decimal(720) : 720.asMmolL
+
+        NavigationView {
+            VStack {
+                Form {
+                    Section {
+                        HStack {
+                            Text("New Glucose")
+                            TextFieldWithToolBar(
+                                text: $state.manualGlucose,
+                                placeholder: " ... ",
+                                keyboardType: state.units == .mgdL ? .numberPad : .decimalPad,
+                                numberFormatter: manualGlucoseFormatter,
+                                initialFocus: true,
+                                unitsText: state.units.rawValue
+                            )
+                        }
+                    }.listRowBackground(Color.chart)
+
+                    Section {
+                        HStack {
+                            Button {
+                                state.addManualGlucose()
+                                isAmountUnconfirmed = false
+                                showManualGlucose = false
+                                state.mode = .glucose
+                            }
+                            label: { Text("Save") }
+                                .frame(maxWidth: .infinity, alignment: .center)
+                                .disabled(state.manualGlucose < limitLow || state.manualGlucose > limitHigh)
+                        }
+                    }
+                    .listRowBackground(
+                        state.manualGlucose < limitLow || state
+                            .manualGlucose > limitHigh ? Color(.systemGray4) : Color(.systemBlue)
+                    )
+                    .tint(.white)
+                }.scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
+            }
+            .onAppear(perform: configureView)
+            .navigationTitle("Add Glucose")
+            .navigationBarTitleDisplayMode(.inline)
+            .toolbar {
+                ToolbarItem(placement: .topBarLeading) {
+                    Button("Close") {
+                        showManualGlucose = false
+                    }
+                }
+            }
+        }
+    }
+}

+ 133 - 0
Trio/Sources/Modules/History/View/HistoryRootView+Adjustments.swift

@@ -0,0 +1,133 @@
+import CoreData
+import SwiftUI
+
+extension History.RootView {
+    var adjustmentsList: some View {
+        List {
+            HStack {
+                Text("Adjustment").foregroundStyle(.secondary)
+                Spacer()
+            }
+            if !combinedAdjustments.isEmpty {
+                ForEach(combinedAdjustments) { item in
+                    adjustmentView(for: item)
+                }
+            } else {
+                ContentUnavailableView(
+                    String(localized: "No data."),
+                    systemImage: "clock.arrow.2.circlepath"
+                )
+            }
+        }
+        .listRowBackground(Color.chart)
+    }
+
+    fileprivate var combinedAdjustments: [AdjustmentItem] {
+        let overrides = overrideRunStored.map { override -> AdjustmentItem in
+            AdjustmentItem(
+                id: override.objectID,
+                name: override.name ?? String(localized: "Override"),
+                startDate: override.startDate ?? Date(),
+                endDate: override.endDate ?? Date(),
+                target: override.target?.decimalValue,
+                type: .override
+            )
+        }
+
+        let tempTargets = tempTargetRunStored.map { tempTarget -> AdjustmentItem in
+            AdjustmentItem(
+                id: tempTarget.objectID,
+                name: tempTarget.name ?? String(localized: "Temp Target"),
+                startDate: tempTarget.startDate ?? Date(),
+                endDate: tempTarget.endDate ?? Date(),
+                target: tempTarget.target?.decimalValue,
+                type: .tempTarget
+            )
+        }
+
+        let combined = overrides + tempTargets
+        return combined.sorted {
+            if $0.startDate == $1.startDate {
+                return $0.endDate > $1.endDate
+            }
+            return $0.startDate > $1.startDate
+        }
+    }
+
+    fileprivate struct AdjustmentItem: Identifiable {
+        let id: NSManagedObjectID
+        let name: String
+        let startDate: Date
+        let endDate: Date
+        let target: Decimal?
+        let type: AdjustmentType
+    }
+
+    fileprivate enum AdjustmentType {
+        case override
+        case tempTarget
+
+        var symbolName: String {
+            switch self {
+            case .override:
+                return "clock.arrow.2.circlepath"
+            case .tempTarget:
+                return "target"
+            }
+        }
+
+        var symbolColor: Color {
+            switch self {
+            case .override:
+                return .orange
+            case .tempTarget:
+                return .blue
+            }
+        }
+    }
+
+    @ViewBuilder fileprivate func adjustmentView(for item: AdjustmentItem) -> some View {
+        let formattedDates =
+            "\(Formatter.dateFormatter.string(from: item.startDate)) - \(Formatter.dateFormatter.string(from: item.endDate))"
+
+        let targetDescription: String = {
+            guard let target = item.target, target != 0 else {
+                return ""
+            }
+            return "\(state.units == .mgdL ? target : target.asMmolL) \(state.units.rawValue)"
+        }()
+
+        let labels: [String] = [
+            targetDescription,
+            formattedDates
+        ].filter { !$0.isEmpty }
+
+        ZStack(alignment: .trailing) {
+            HStack {
+                VStack(alignment: .leading) {
+                    HStack {
+                        Image(systemName: item.type.symbolName)
+                            .foregroundStyle(item.type == .override ? Color.purple : Color.green)
+                        Text(item.name)
+                            .font(.headline)
+                        Spacer()
+                    }
+                    HStack(spacing: 5) {
+                        ForEach(labels, id: \.self) { label in
+                            Text(label)
+                            if label != labels.last {
+                                Divider()
+                            }
+                        }
+                        Spacer()
+                    }
+                    .padding(.top, 2)
+                    .foregroundColor(.secondary)
+                    .font(.caption)
+                }
+                .contentShape(Rectangle())
+            }
+        }
+        .padding(.vertical, 8)
+    }
+}

+ 44 - 0
Trio/Sources/Modules/History/View/HistoryRootView+Confirmations.swift

@@ -0,0 +1,44 @@
+import SwiftUI
+
+extension History.RootView {
+    func requestDelete(_ target: History.DeletionTarget) {
+        deletionTarget = target
+    }
+
+    @ViewBuilder func historyConfirmations(_ content: some View) -> some View {
+        content
+            .confirmationDialog(
+                deletionTarget?.title(units: state.units) ?? "",
+                isPresented: Binding(
+                    get: { deletionTarget != nil },
+                    set: { if !$0 { deletionTarget = nil } }
+                ),
+                titleVisibility: .visible,
+                presenting: deletionTarget
+            ) { target in
+                Button("Delete", role: .destructive) {
+                    switch target {
+                    case let .glucose(glucose):
+                        state.invokeGlucoseDeletionTask(glucose.objectID)
+                    case let .insulin(pumpEvent):
+                        state.invokeInsulinDeletionTask(pumpEvent.objectID)
+                    case let .carbs(carbEntry):
+                        state.invokeCarbDeletionTask(
+                            carbEntry.objectID,
+                            isFpuOrComplexMeal: carbEntry.isFPU || carbEntry.fat > 0 || carbEntry.protein > 0
+                        )
+                    }
+                }
+                Button("Cancel", role: .cancel) {}
+            } message: { target in
+                if let message = target.message(units: state.units) {
+                    Text(message)
+                }
+            }
+            .alert("Error", isPresented: $showErrorAlert) {
+                Button("OK", role: .cancel) {}
+            } message: {
+                Text(errorMessage)
+            }
+    }
+}

+ 103 - 0
Trio/Sources/Modules/History/View/HistoryRootView+Filters.swift

@@ -0,0 +1,103 @@
+import SwiftUI
+
+extension History.RootView {
+    var filterTreatmentsButton: some View {
+        Button(action: {
+            showTreatmentTypeFilter.toggle()
+        }) {
+            HStack {
+                Text("Filter")
+                Image(
+                    systemName: selectedTreatmentTypes.count == History.TreatmentType.allCases.count
+                        ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill"
+                )
+                if selectedTreatmentTypes.count < History.TreatmentType.allCases.count {
+                    Text(verbatim: "(\(selectedTreatmentTypes.count)/\(History.TreatmentType.allCases.count))")
+                }
+            }.foregroundColor(Color.accentColor)
+        }
+        .popover(isPresented: $showTreatmentTypeFilter, arrowEdge: .top) {
+            VStack(alignment: .leading, spacing: 20) {
+                Button(action: {
+                    if selectedTreatmentTypes.count == History.TreatmentType.allCases.count {
+                        // Deselect all - keep at least one selected
+                        selectedTreatmentTypes = []
+                    } else {
+                        // Select all
+                        selectedTreatmentTypes = Set(History.TreatmentType.allCases)
+                    }
+                }) {
+                    HStack(spacing: 20) {
+                        Image(
+                            systemName: selectedTreatmentTypes.count == History.TreatmentType.allCases.count
+                                ? "checkmark.square.fill" : "square"
+                        )
+                        .frame(width: 20)
+                        .foregroundColor(Color.accentColor)
+                        Text(
+                            selectedTreatmentTypes.count == History.TreatmentType.allCases
+                                .count ? String(localized: "Deselect All") : String(localized: "Select All")
+                        )
+                        .foregroundColor(Color.primary)
+                    }.padding(4)
+                }
+                .buttonStyle(.borderless)
+
+                Divider()
+
+                ForEach(History.TreatmentType.allCases, id: \.rawValue) { treatmentType in
+                    Button(action: {
+                        toggleTreatmentType(treatmentType)
+                    }) {
+                        HStack(spacing: 20) {
+                            Image(
+                                systemName: selectedTreatmentTypes
+                                    .contains(treatmentType) ? "checkmark.square.fill" : "square"
+                            )
+                            .frame(width: 20)
+                            .foregroundColor(Color.accentColor)
+                            Text(treatmentType.displayName)
+                                .foregroundColor(Color.primary)
+                        }.padding(4)
+                    }
+                    .buttonStyle(.borderless)
+                }
+
+                Divider()
+
+                Button("Done") {
+                    showTreatmentTypeFilter = false
+                }
+                .frame(maxWidth: .infinity)
+                .buttonStyle(.borderless)
+            }
+            .padding()
+            .presentationCompactAdaptation(.popover)
+            .background(Color.chart)
+        }
+    }
+
+    var filterFutureEntriesButton: some View {
+        Button(
+            action: {
+                showFutureEntries.toggle()
+            },
+            label: {
+                HStack {
+                    Text(showFutureEntries ? String(localized: "Hide Future") : String(localized: "Show Future"))
+                        .foregroundColor(Color.accentColor)
+                    Image(systemName: showFutureEntries ? "eye.slash" : "eye")
+                        .foregroundColor(Color.accentColor)
+                }
+            }
+        ).buttonStyle(.borderless)
+    }
+
+    func toggleTreatmentType(_ type: History.TreatmentType) {
+        if selectedTreatmentTypes.contains(type) {
+            selectedTreatmentTypes.remove(type)
+        } else {
+            selectedTreatmentTypes.insert(type)
+        }
+    }
+}

+ 79 - 0
Trio/Sources/Modules/History/View/HistoryRootView+Glucose.swift

@@ -0,0 +1,79 @@
+import CoreData
+import SwiftUI
+
+extension History.RootView {
+    var glucoseList: some View {
+        List {
+            HStack {
+                Text("Values")
+                Spacer()
+                Text("Time")
+            }.foregroundStyle(.secondary)
+
+            if !glucoseStored.isEmpty {
+                ForEach(glucoseStored) { glucose in
+                    HStack {
+                        Text(formatGlucose(Decimal(glucose.glucose), isManual: glucose.isManual))
+
+                        /// check for manual glucose
+                        if glucose.isManual {
+                            Image(systemName: "drop.fill").symbolRenderingMode(.monochrome).foregroundStyle(.red)
+                        } else {
+                            Text("\(glucose.directionEnum?.symbol ?? "--")")
+                        }
+
+                        if state.settingsManager.settings.smoothGlucose, !glucose.isManual,
+                           let smoothedGlucose = glucose.smoothedGlucose, smoothedGlucose != 0
+                        {
+                            let smoothedGlucoseForDisplay = state.units == .mgdL ? smoothedGlucose
+                                .description : smoothedGlucose.decimalValue
+                                .formattedAsMmolL
+
+                            (
+                                Text("(") +
+                                    Text(Image(systemName: "sparkles")) +
+                                    Text(" ") +
+                                    Text("\(smoothedGlucoseForDisplay)") +
+                                    Text(")")
+                            ).foregroundStyle(.secondary)
+                                .padding(.leading, 10)
+                        }
+
+                        Spacer()
+
+                        Text(Formatter.dateFormatter.string(from: glucose.date ?? Date()))
+                    }
+                    .contextMenu {
+                        Button(
+                            "Delete",
+                            systemImage: "trash.fill",
+                            role: .destructive,
+                            action: { requestDelete(.glucose(glucose)) }
+                        ).tint(.red)
+                    }
+                    .swipeActions {
+                        Button(
+                            "Delete",
+                            systemImage: "trash.fill",
+                            role: .none,
+                            action: { requestDelete(.glucose(glucose)) }
+                        ).tint(.red)
+                    }
+                }
+            } else {
+                ContentUnavailableView(
+                    String(localized: "No data."),
+                    systemImage: "drop.fill"
+                )
+            }
+        }.listRowBackground(Color.chart)
+    }
+
+    func formatGlucose(_ value: Decimal, isManual: Bool) -> String {
+        let formatter = isManual ? manualGlucoseFormatter : Formatter.glucoseFormatter(for: state.units)
+        let glucoseValue = state.units == .mmolL ? value.asMmolL : value
+        let formattedValue = formatter.string(from: glucoseValue as NSNumber) ?? "--"
+
+        return formattedValue
+    }
+}

+ 98 - 0
Trio/Sources/Modules/History/View/HistoryRootView+Meals.swift

@@ -0,0 +1,98 @@
+import CoreData
+import SwiftUI
+
+extension History.RootView {
+    var mealsList: some View {
+        List {
+            HStack {
+                Text("Type").foregroundStyle(.secondary)
+                Spacer()
+                filterFutureEntriesButton
+            }
+            if !carbEntryStored.isEmpty {
+                ForEach(carbEntryStored.filter({ !showFutureEntries ? $0.date ?? Date() <= Date() : true })) { item in
+                    mealView(item)
+                }
+            } else {
+                ContentUnavailableView(
+                    String(localized: "No data."),
+                    systemImage: "fork.knife"
+                )
+            }
+        }.listRowBackground(Color.chart)
+    }
+
+    @ViewBuilder func mealView(_ meal: CarbEntryStored) -> some View {
+        VStack {
+            HStack {
+                if meal.isFPU {
+                    Image(systemName: "circle.fill").foregroundColor(Color.orange.opacity(0.5))
+                    Text("Fat / Protein")
+                    Text(
+                        (Formatter.decimalFormatterWithTwoFractionDigits.string(for: meal.carbs) ?? "0") +
+                            String(localized: " g", comment: "gram of carbs")
+                    )
+                } else {
+                    Image(systemName: "circle.fill").foregroundColor(Color.loopYellow)
+                    Text("Carbs")
+                    Text(
+                        (Formatter.decimalFormatterWithTwoFractionDigits.string(for: meal.carbs) ?? "0") +
+                            String(localized: " g", comment: "gram of carb equilvalents")
+                    )
+                }
+
+                Spacer()
+
+                Text(Formatter.dateFormatter.string(from: meal.date ?? Date()))
+                    .moveDisabled(true)
+            }
+            if let note = meal.note, note != "" {
+                HStack {
+                    Image(systemName: "square.and.pencil")
+                    Text(note)
+                    Spacer()
+                }.padding(.top, 5).foregroundColor(.secondary)
+            }
+        }
+        .contextMenu {
+            Button(
+                "Delete",
+                systemImage: "trash.fill",
+                role: .destructive,
+                action: { requestDelete(.carbs(meal)) }
+            ).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)
+        }
+        .swipeActions {
+            Button(
+                "Delete",
+                systemImage: "trash.fill",
+                role: .none,
+                action: { requestDelete(.carbs(meal)) }
+            ).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)
+        }
+    }
+}

+ 103 - 0
Trio/Sources/Modules/History/View/HistoryRootView+Treatments.swift

@@ -0,0 +1,103 @@
+import CoreData
+import SwiftUI
+
+extension History.RootView {
+    var treatmentsList: some View {
+        List {
+            HStack {
+                filterTreatmentsButton
+                Spacer()
+                Text("Time").foregroundStyle(.secondary)
+            }
+            if !filteredPumpEvents.isEmpty {
+                ForEach(filteredPumpEvents) { item in
+                    treatmentView(item)
+                }
+            } else {
+                ContentUnavailableView(
+                    String(localized: "No data."),
+                    systemImage: "syringe"
+                )
+            }
+        }.listRowBackground(Color.chart)
+    }
+
+    var filteredPumpEvents: [PumpEventStored] {
+        pumpEventStored.filter { item in
+            // First filter by date
+            let passesDateFilter = !showFutureEntries ? item.timestamp ?? Date() <= Date() : true
+
+            guard passesDateFilter else { return false }
+
+            // Then filter by treatment type
+            if let bolus = item.bolus {
+                if bolus.isSMB {
+                    return selectedTreatmentTypes.contains(.smb)
+                } else if bolus.isExternal {
+                    return selectedTreatmentTypes.contains(.externalBolus)
+                } else {
+                    return selectedTreatmentTypes.contains(.bolus)
+                }
+            } else if item.tempBasal != nil {
+                return selectedTreatmentTypes.contains(.tempBasal)
+            } else if item.type == "PumpSuspend" {
+                return selectedTreatmentTypes.contains(.suspend)
+            } else {
+                return selectedTreatmentTypes.contains(.other)
+            }
+        }
+    }
+
+    @ViewBuilder func treatmentView(_ item: PumpEventStored) -> some View {
+        HStack {
+            if let bolus = item.bolus, let amount = bolus.amount {
+                Image(systemName: "circle.fill").foregroundColor(Color.insulin)
+                Text(bolus.isSMB ? "SMB" : item.type ?? "Bolus")
+                Text(
+                    (Formatter.decimalFormatterWithThreeFractionDigits.string(from: amount) ?? "0") +
+                        String(localized: " U", comment: "Insulin unit")
+                )
+                .foregroundColor(.secondary)
+                if bolus.isExternal {
+                    Text(String(localized: "External", comment: "External Insulin")).foregroundColor(.secondary)
+                }
+            } else if let tempBasal = item.tempBasal, let rate = tempBasal.rate {
+                Image(systemName: "circle.fill").foregroundColor(Color.insulin.opacity(0.4))
+                Text("Temp Basal")
+                Text(
+                    (Formatter.decimalFormatterWithThreeFractionDigits.string(from: rate) ?? "0") +
+                        String(localized: " U/hr", comment: "Unit insulin per hour")
+                )
+                .foregroundColor(.secondary)
+                if tempBasal.duration > 0 {
+                    Text("\(tempBasal.duration.string) min").foregroundColor(.secondary)
+                }
+            } else {
+                Image(systemName: "circle.fill").foregroundColor(Color.loopGray)
+                Text(item.type ?? "Pump Event")
+            }
+            Spacer()
+            Text(Formatter.dateFormatter.string(from: item.timestamp ?? Date())).moveDisabled(true)
+        }
+        .contextMenu {
+            if item.bolus != nil {
+                Button(
+                    "Delete",
+                    systemImage: "trash.fill",
+                    role: .destructive,
+                    action: { requestDelete(.insulin(item)) }
+                ).tint(.red)
+            }
+        }
+        .swipeActions {
+            if item.bolus != nil {
+                Button(
+                    "Delete",
+                    systemImage: "trash.fill",
+                    role: .none,
+                    action: { requestDelete(.insulin(item)) }
+                ).tint(.red)
+            }
+        }
+    }
+}

+ 61 - 745
Trio/Sources/Modules/History/View/HistoryRootView.swift

@@ -8,19 +8,14 @@ extension History {
 
         @State var state = StateModel()
 
-        @State private var isRemoveHistoryItemAlertPresented: Bool = false
-        @State private var alertTitle: String = ""
-        @State private var alertMessage: String = ""
-        @State private var alertTreatmentToDelete: PumpEventStored?
-        @State private var alertCarbEntryToDelete: CarbEntryStored?
-        @State private var alertGlucoseToDelete: GlucoseStored?
-        @State private var showAlert = false
-        @State private var showFutureEntries: Bool = false // default to hide future entries
-        @State private var showManualGlucose: Bool = false
-        @State private var isAmountUnconfirmed: Bool = true
-        @State private var showTreatmentTypeFilter = false
-        @State private var selectedTreatmentTypes: Set<TreatmentType> = Set(TreatmentType.allCases)
-        @State private var filterPopoverAnchor: CGRect = .zero
+        @State var deletionTarget: History.DeletionTarget?
+        @State var showErrorAlert: Bool = false
+        @State var errorMessage: String = ""
+        @State var showFutureEntries: Bool = false // default to hide future entries
+        @State var showManualGlucose: Bool = false
+        @State var isAmountUnconfirmed: Bool = true
+        @State var showTreatmentTypeFilter = false
+        @State var selectedTreatmentTypes: Set<TreatmentType> = Set(TreatmentType.allCases)
 
         @Environment(\.colorScheme) var colorScheme
         @Environment(\.managedObjectContext) var context
@@ -61,76 +56,63 @@ extension History {
             animation: .bouncy
         ) var tempTargetRunStored: FetchedResults<TempTargetRunStored>
 
-        private var manualGlucoseFormatter: NumberFormatter {
-            let formatter = NumberFormatter()
-            formatter.numberStyle = .decimal
-            if state.units == .mgdL {
-                formatter.maximumIntegerDigits = 3
-                formatter.maximumFractionDigits = 0
-            } else {
-                formatter.maximumIntegerDigits = 2
-                formatter.minimumFractionDigits = 0
-                formatter.maximumFractionDigits = 1
-            }
-            formatter.roundingMode = .halfUp
-            return formatter
-        }
-
         var body: some View {
-            ZStack(alignment: .center, content: {
-                VStack {
-                    Picker("Mode", selection: $state.mode) {
-                        ForEach(
-                            Mode.allCases.indexed(),
-                            id: \.1
-                        ) { index, item in
-                            Text(item.name).tag(index)
+            historyConfirmations(
+                ZStack(alignment: .center, content: {
+                    VStack {
+                        Picker("Mode", selection: $state.mode) {
+                            ForEach(
+                                Mode.allCases.indexed(),
+                                id: \.1
+                            ) { index, item in
+                                Text(item.name).tag(index)
+                            }
                         }
+                        .pickerStyle(SegmentedPickerStyle())
+                        .padding(.horizontal)
+
+                        Form {
+                            switch state.mode {
+                            case .treatments: treatmentsList
+                            case .glucose: glucoseList
+                            case .meals: mealsList
+                            case .adjustments: adjustmentsList
+                            }
+                        }.scrollContentBackground(.hidden)
+                            .background(appState.trioBackgroundColor(for: colorScheme))
+                    }.blur(radius: state.waitForSuggestion ? 8 : 0)
+
+                    // Show custom progress view
+                    /// don't show it if glucose is stale as it will block the UI
+                    if state.waitForSuggestion && state.isGlucoseDataFresh(glucoseStored.first?.date) {
+                        CustomProgressView(text: progressText.displayName)
+                    }
+                })
+                    .background(appState.trioBackgroundColor(for: colorScheme))
+                    .onAppear(perform: configureView)
+                    .onDisappear {
+                        state.carbEntryDeleted = false
+                        state.insulinEntryDeleted = false
+                    }
+                    .navigationTitle("History")
+                    .navigationBarTitleDisplayMode(.large)
+                    .toolbar {
+                        ToolbarItem(placement: .topBarTrailing, content: {
+                            addButton({
+                                showManualGlucose = true
+                                state.manualGlucose = 0
+                            })
+                        })
                     }
-                    .pickerStyle(SegmentedPickerStyle())
-                    .padding(.horizontal)
-
-                    Form {
-                        switch state.mode {
-                        case .treatments: treatmentsList
-                        case .glucose: glucoseList
-                        case .meals: mealsList
-                        case .adjustments: adjustmentsList
+                    .sheet(isPresented: $showManualGlucose) {
+                        addGlucoseView()
+                    }
+                    .sheet(isPresented: $state.showCarbEntryEditor) {
+                        if let carbEntry = state.carbEntryToEdit {
+                            CarbEntryEditorView(state: state, carbEntry: carbEntry)
                         }
-                    }.scrollContentBackground(.hidden)
-                        .background(appState.trioBackgroundColor(for: colorScheme))
-                }.blur(radius: state.waitForSuggestion ? 8 : 0)
-
-                // Show custom progress view
-                /// don't show it if glucose is stale as it will block the UI
-                if state.waitForSuggestion && state.isGlucoseDataFresh(glucoseStored.first?.date) {
-                    CustomProgressView(text: progressText.displayName)
-                }
-            })
-                .background(appState.trioBackgroundColor(for: colorScheme))
-                .onAppear(perform: configureView)
-                .onDisappear {
-                    state.carbEntryDeleted = false
-                    state.insulinEntryDeleted = false
-                }
-                .navigationTitle("History")
-                .navigationBarTitleDisplayMode(.large)
-                .toolbar {
-                    ToolbarItem(placement: .topBarTrailing, content: {
-                        addButton({
-                            showManualGlucose = true
-                            state.manualGlucose = 0
-                        })
-                    })
-                }
-                .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 {
@@ -146,7 +128,7 @@ extension History {
             )
         }
 
-        private var progressText: ProgressText {
+        var progressText: ProgressText {
             switch (state.carbEntryDeleted, state.insulinEntryDeleted) {
             case (true, false):
                 return .updatingCOB
@@ -156,671 +138,5 @@ extension History {
                 return .updatingHistory
             }
         }
-
-        private var logGlucoseButton: some View {
-            Button(
-                action: {
-                    showManualGlucose = true
-                    state.manualGlucose = 0
-                },
-                label: {
-                    Text("Log Glucose")
-                        .foregroundColor(Color.accentColor)
-                    Image(systemName: "plus")
-                        .foregroundColor(Color.accentColor)
-                }
-            ).buttonStyle(.borderless)
-        }
-
-        private var filterTreatmentsButton: some View {
-            Button(action: {
-                showTreatmentTypeFilter.toggle()
-            }) {
-                HStack {
-                    Text("Filter")
-                    Image(
-                        systemName: selectedTreatmentTypes.count == TreatmentType.allCases.count
-                            ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill"
-                    )
-                    if selectedTreatmentTypes.count < TreatmentType.allCases.count {
-                        Text(verbatim: "(\(selectedTreatmentTypes.count)/\(TreatmentType.allCases.count))")
-                    }
-                }.foregroundColor(Color.accentColor)
-            }
-            .popover(isPresented: $showTreatmentTypeFilter, arrowEdge: .top) {
-                VStack(alignment: .leading, spacing: 20) {
-                    Button(action: {
-                        if selectedTreatmentTypes.count == TreatmentType.allCases.count {
-                            // Deselect all - keep at least one selected
-                            selectedTreatmentTypes = []
-                        } else {
-                            // Select all
-                            selectedTreatmentTypes = Set(TreatmentType.allCases)
-                        }
-                    }) {
-                        HStack(spacing: 20) {
-                            Image(
-                                systemName: selectedTreatmentTypes.count == TreatmentType.allCases.count
-                                    ? "checkmark.square.fill" : "square"
-                            )
-                            .frame(width: 20)
-                            .foregroundColor(Color.accentColor)
-                            Text(
-                                selectedTreatmentTypes.count == TreatmentType.allCases
-                                    .count ? String(localized: "Deselect All") : String(localized: "Select All")
-                            )
-                            .foregroundColor(Color.primary)
-                        }.padding(4)
-                    }
-                    .buttonStyle(.borderless)
-
-                    Divider()
-
-                    ForEach(TreatmentType.allCases, id: \.rawValue) { treatmentType in
-                        Button(action: {
-                            toggleTreatmentType(treatmentType)
-                        }) {
-                            HStack(spacing: 20) {
-                                Image(
-                                    systemName: selectedTreatmentTypes
-                                        .contains(treatmentType) ? "checkmark.square.fill" : "square"
-                                )
-                                .frame(width: 20)
-                                .foregroundColor(Color.accentColor)
-                                Text(treatmentType.displayName)
-                                    .foregroundColor(Color.primary)
-                            }.padding(4)
-                        }
-                        .buttonStyle(.borderless)
-                    }
-
-                    Divider()
-
-                    Button("Done") {
-                        showTreatmentTypeFilter = false
-                    }
-                    .frame(maxWidth: .infinity)
-                    .buttonStyle(.borderless)
-                }
-                .padding()
-                .presentationCompactAdaptation(.popover)
-                .background(Color.chart)
-            }
-        }
-
-        private var filterFutureEntriesButton: some View {
-            Button(
-                action: {
-                    showFutureEntries.toggle()
-                },
-                label: {
-                    HStack {
-                        Text(showFutureEntries ? String(localized: "Hide Future") : String(localized: "Show Future"))
-                            .foregroundColor(Color.accentColor)
-                        Image(systemName: showFutureEntries ? "eye.slash" : "eye")
-                            .foregroundColor(Color.accentColor)
-                    }
-                }
-            ).buttonStyle(.borderless)
-        }
-
-        private func toggleTreatmentType(_ type: TreatmentType) {
-            if selectedTreatmentTypes.contains(type) {
-                selectedTreatmentTypes.remove(type)
-            } else {
-                selectedTreatmentTypes.insert(type)
-            }
-        }
-
-        private var filteredPumpEvents: [PumpEventStored] {
-            pumpEventStored.filter { item in
-                // First filter by date
-                let passesDateFilter = !showFutureEntries ? item.timestamp ?? Date() <= Date() : true
-
-                guard passesDateFilter else { return false }
-
-                // Then filter by treatment type
-                if let bolus = item.bolus {
-                    if bolus.isSMB {
-                        return selectedTreatmentTypes.contains(.smb)
-                    } else if bolus.isExternal {
-                        return selectedTreatmentTypes.contains(.externalBolus)
-                    } else {
-                        return selectedTreatmentTypes.contains(.bolus)
-                    }
-                } else if item.tempBasal != nil {
-                    return selectedTreatmentTypes.contains(.tempBasal)
-                } else if item.type == "PumpSuspend" {
-                    return selectedTreatmentTypes.contains(.suspend)
-                } else {
-                    return selectedTreatmentTypes.contains(.other)
-                }
-            }
-        }
-
-        private var treatmentsList: some View {
-            List {
-                HStack {
-                    filterTreatmentsButton
-                    Spacer()
-                    Text("Time").foregroundStyle(.secondary)
-                }
-                if !filteredPumpEvents.isEmpty {
-                    ForEach(filteredPumpEvents) { item in
-                        treatmentView(item)
-                    }
-                } else {
-                    ContentUnavailableView(
-                        String(localized: "No data."),
-                        systemImage: "syringe"
-                    )
-                }
-            }.listRowBackground(Color.chart)
-        }
-
-        private var mealsList: some View {
-            List {
-                HStack {
-                    Text("Type").foregroundStyle(.secondary)
-                    Spacer()
-                    filterFutureEntriesButton
-                }
-                if !carbEntryStored.isEmpty {
-                    ForEach(carbEntryStored.filter({ !showFutureEntries ? $0.date ?? Date() <= Date() : true })) { item in
-                        mealView(item)
-                    }
-                } else {
-                    ContentUnavailableView(
-                        String(localized: "No data."),
-                        systemImage: "fork.knife"
-                    )
-                }
-            }.listRowBackground(Color.chart)
-        }
-
-        private var adjustmentsList: some View {
-            List {
-                HStack {
-                    Text("Adjustment").foregroundStyle(.secondary)
-                    Spacer()
-                }
-                if !combinedAdjustments.isEmpty {
-                    ForEach(combinedAdjustments) { item in
-                        adjustmentView(for: item)
-                    }
-                } else {
-                    ContentUnavailableView(
-                        String(localized: "No data."),
-                        systemImage: "clock.arrow.2.circlepath"
-                    )
-                }
-            }
-            .listRowBackground(Color.chart)
-        }
-
-        private var combinedAdjustments: [AdjustmentItem] {
-            let overrides = overrideRunStored.map { override -> AdjustmentItem in
-                AdjustmentItem(
-                    id: override.objectID,
-                    name: override.name ?? String(localized: "Override"),
-                    startDate: override.startDate ?? Date(),
-                    endDate: override.endDate ?? Date(),
-                    target: override.target?.decimalValue,
-                    type: .override
-                )
-            }
-
-            let tempTargets = tempTargetRunStored.map { tempTarget -> AdjustmentItem in
-                AdjustmentItem(
-                    id: tempTarget.objectID,
-                    name: tempTarget.name ?? String(localized: "Temp Target"),
-                    startDate: tempTarget.startDate ?? Date(),
-                    endDate: tempTarget.endDate ?? Date(),
-                    target: tempTarget.target?.decimalValue,
-                    type: .tempTarget
-                )
-            }
-
-            let combined = overrides + tempTargets
-            return combined.sorted {
-                if $0.startDate == $1.startDate {
-                    return $0.endDate > $1.endDate
-                }
-                return $0.startDate > $1.startDate
-            } }
-
-        private struct AdjustmentItem: Identifiable {
-            let id: NSManagedObjectID
-            let name: String
-            let startDate: Date
-            let endDate: Date
-            let target: Decimal?
-            let type: AdjustmentType
-        }
-
-        private enum AdjustmentType {
-            case override
-            case tempTarget
-
-            var symbolName: String {
-                switch self {
-                case .override:
-                    return "clock.arrow.2.circlepath"
-                case .tempTarget:
-                    return "target"
-                }
-            }
-
-            var symbolColor: Color {
-                switch self {
-                case .override:
-                    return .orange
-                case .tempTarget:
-                    return .blue
-                }
-            }
-        }
-
-        @ViewBuilder private func adjustmentView(for item: AdjustmentItem) -> some View {
-            let formattedDates =
-                "\(Formatter.dateFormatter.string(from: item.startDate)) - \(Formatter.dateFormatter.string(from: item.endDate))"
-
-            let targetDescription: String = {
-                guard let target = item.target, target != 0 else {
-                    return ""
-                }
-                return "\(state.units == .mgdL ? target : target.asMmolL) \(state.units.rawValue)"
-            }()
-
-            let labels: [String] = [
-                targetDescription,
-                formattedDates
-            ].filter { !$0.isEmpty }
-
-            ZStack(alignment: .trailing) {
-                HStack {
-                    VStack(alignment: .leading) {
-                        HStack {
-                            Image(systemName: item.type.symbolName)
-                                .foregroundStyle(item.type == .override ? Color.purple : Color.green)
-                            Text(item.name)
-                                .font(.headline)
-                            Spacer()
-                        }
-                        HStack(spacing: 5) {
-                            ForEach(labels, id: \.self) { label in
-                                Text(label)
-                                if label != labels.last {
-                                    Divider()
-                                }
-                            }
-                            Spacer()
-                        }
-                        .padding(.top, 2)
-                        .foregroundColor(.secondary)
-                        .font(.caption)
-                    }
-                    .contentShape(Rectangle())
-                }
-            }
-            .padding(.vertical, 8)
-        }
-
-        private var glucoseList: some View {
-            List {
-                HStack {
-                    Text("Values")
-                    Spacer()
-                    Text("Time")
-                }.foregroundStyle(.secondary)
-
-                if !glucoseStored.isEmpty {
-                    ForEach(glucoseStored) { glucose in
-                        HStack {
-                            Text(formatGlucose(Decimal(glucose.glucose), isManual: glucose.isManual))
-
-                            /// check for manual glucose
-                            if glucose.isManual {
-                                Image(systemName: "drop.fill").symbolRenderingMode(.monochrome).foregroundStyle(.red)
-                            } else {
-                                Text("\(glucose.directionEnum?.symbol ?? "--")")
-                            }
-
-                            if state.settingsManager.settings.smoothGlucose, !glucose.isManual,
-                               let smoothedGlucose = glucose.smoothedGlucose, smoothedGlucose != 0
-                            {
-                                let smoothedGlucoseForDisplay = state.units == .mgdL ? smoothedGlucose
-                                    .description : smoothedGlucose.decimalValue
-                                    .formattedAsMmolL
-
-                                (
-                                    Text("(") +
-                                        Text(Image(systemName: "sparkles")) +
-                                        Text(" ") +
-                                        Text("\(smoothedGlucoseForDisplay)") +
-                                        Text(")")
-                                ).foregroundStyle(.secondary)
-                                    .padding(.leading, 10)
-                            }
-
-                            Spacer()
-
-                            Text(Formatter.dateFormatter.string(from: glucose.date ?? Date()))
-                        }.swipeActions {
-                            Button(
-                                "Delete",
-                                systemImage: "trash.fill",
-                                role: .none,
-                                action: {
-                                    alertGlucoseToDelete = glucose
-
-                                    let glucoseToDisplay = state.units == .mgdL ? glucose.glucose
-                                        .description : Int(glucose.glucose).formattedAsMmolL
-                                    alertTitle = String(localized: "Delete Glucose?", comment: "Alert title for deleting glucose")
-                                    alertMessage = Formatter.dateFormatter
-                                        .string(from: glucose.date ?? Date()) + ", " + glucoseToDisplay + " " + state.units
-                                        .rawValue
-
-                                    isRemoveHistoryItemAlertPresented = true
-                                }
-                            ).tint(.red)
-                        }
-                        .alert(
-                            Text(alertTitle),
-                            isPresented: $isRemoveHistoryItemAlertPresented
-                        ) {
-                            Button("Cancel", role: .cancel) {}
-                            Button("Delete", role: .destructive) {
-                                guard let glucoseToDelete = alertGlucoseToDelete else {
-                                    debug(.default, "Cannot gracefully unwrap alertCarbEntryToDelete!")
-                                    return
-                                }
-                                let glucoseToDeleteObjectID = glucoseToDelete.objectID
-                                state.invokeGlucoseDeletionTask(glucoseToDeleteObjectID)
-                            }
-                        } message: {
-                            Text("\n" + alertMessage)
-                        }
-                    }
-                } else {
-                    ContentUnavailableView(
-                        String(localized: "No data."),
-                        systemImage: "drop.fill"
-                    )
-                }
-            }.listRowBackground(Color.chart)
-                .alert(isPresented: $showAlert) {
-                    Alert(title: Text("Error"), message: Text(alertMessage), dismissButton: .default(Text("OK")))
-                }
-        }
-
-        private func deleteGlucose(at offsets: IndexSet) {
-            for index in offsets {
-                let glucoseToDelete = glucoseStored[index]
-                context.delete(glucoseToDelete)
-            }
-
-            do {
-                try context.save()
-                debugPrint("Data Table Root View: \(#function) \(DebuggingIdentifiers.succeeded) deleted glucose from core data")
-            } catch {
-                debugPrint(
-                    "Data Table Root View: \(#function) \(DebuggingIdentifiers.failed) error while deleting glucose from core data"
-                )
-                alertMessage = "Failed to delete glucose data: \(error.localizedDescription)"
-                showAlert = true
-            }
-        }
-
-        @ViewBuilder private func addGlucoseView() -> some View {
-            let limitLow: Decimal = state.units == .mgdL ? Decimal(14) : 14.asMmolL
-            let limitHigh: Decimal = state.units == .mgdL ? Decimal(720) : 720.asMmolL
-
-            NavigationView {
-                VStack {
-                    Form {
-                        Section {
-                            HStack {
-                                Text("New Glucose")
-                                TextFieldWithToolBar(
-                                    text: $state.manualGlucose,
-                                    placeholder: " ... ",
-                                    keyboardType: state.units == .mgdL ? .numberPad : .decimalPad,
-                                    numberFormatter: manualGlucoseFormatter,
-                                    initialFocus: true,
-                                    unitsText: state.units.rawValue
-                                )
-                            }
-                        }.listRowBackground(Color.chart)
-
-                        Section {
-                            HStack {
-                                Button {
-                                    state.addManualGlucose()
-                                    isAmountUnconfirmed = false
-                                    showManualGlucose = false
-                                    state.mode = .glucose
-                                }
-                                label: { Text("Save") }
-                                    .frame(maxWidth: .infinity, alignment: .center)
-                                    .disabled(state.manualGlucose < limitLow || state.manualGlucose > limitHigh)
-                            }
-                        }
-                        .listRowBackground(
-                            state.manualGlucose < limitLow || state
-                                .manualGlucose > limitHigh ? Color(.systemGray4) : Color(.systemBlue)
-                        )
-                        .tint(.white)
-                    }.scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
-                }
-                .onAppear(perform: configureView)
-                .navigationTitle("Add Glucose")
-                .navigationBarTitleDisplayMode(.inline)
-                .toolbar {
-                    ToolbarItem(placement: .topBarLeading) {
-                        Button("Close") {
-                            showManualGlucose = false
-                        }
-                    }
-                }
-            }
-        }
-
-        private var filterEntriesButton: some View {
-            Button(action: { showFutureEntries.toggle() }, label: {
-                HStack {
-                    Text(showFutureEntries ? String(localized: "Hide Future") : String(localized: "Show Future"))
-                        .foregroundColor(Color.secondary)
-                    Image(systemName: showFutureEntries ? "calendar.badge.minus" : "calendar.badge.plus")
-                }.frame(maxWidth: .infinity, alignment: .trailing)
-            }).buttonStyle(.borderless)
-        }
-
-        @ViewBuilder private func treatmentView(_ item: PumpEventStored) -> some View {
-            HStack {
-                if let bolus = item.bolus, let amount = bolus.amount {
-                    Image(systemName: "circle.fill").foregroundColor(Color.insulin)
-                    Text(bolus.isSMB ? "SMB" : item.type ?? "Bolus")
-                    Text(
-                        (Formatter.decimalFormatterWithThreeFractionDigits.string(from: amount) ?? "0") +
-                            String(localized: " U", comment: "Insulin unit")
-                    )
-                    .foregroundColor(.secondary)
-                    if bolus.isExternal {
-                        Text(String(localized: "External", comment: "External Insulin")).foregroundColor(.secondary)
-                    }
-                } else if let tempBasal = item.tempBasal, let rate = tempBasal.rate {
-                    Image(systemName: "circle.fill").foregroundColor(Color.insulin.opacity(0.4))
-                    Text("Temp Basal")
-                    Text(
-                        (Formatter.decimalFormatterWithThreeFractionDigits.string(from: rate) ?? "0") +
-                            String(localized: " U/hr", comment: "Unit insulin per hour")
-                    )
-                    .foregroundColor(.secondary)
-                    if tempBasal.duration > 0 {
-                        Text("\(tempBasal.duration.string) min").foregroundColor(.secondary)
-                    }
-                } else {
-                    Image(systemName: "circle.fill").foregroundColor(Color.loopGray)
-                    Text(item.type ?? "Pump Event")
-                }
-                Spacer()
-                Text(Formatter.dateFormatter.string(from: item.timestamp ?? Date())).moveDisabled(true)
-            }
-            .swipeActions {
-                if item.bolus != nil {
-                    Button(
-                        "Delete",
-                        systemImage: "trash.fill",
-                        role: .none,
-                        action: {
-                            alertTreatmentToDelete = item
-                            alertTitle = String(localized: "Delete Insulin?", comment: "Alert title for deleting insulin")
-                            alertMessage = Formatter.dateFormatter
-                                .string(from: item.timestamp ?? Date()) + ", " +
-                                (Formatter.decimalFormatterWithThreeFractionDigits.string(from: item.bolus?.amount ?? 0) ?? "0") +
-                                String(localized: " U", comment: "Insulin unit")
-
-                            if let bolus = item.bolus {
-                                // Add text snippet, so that alert message is more descriptive for SMBs
-                                alertMessage += bolus.isSMB ? String(
-                                    localized: " SMB",
-                                    comment: "Super Micro Bolus indicator in delete alert"
-                                )
-                                    : ""
-                            }
-
-                            isRemoveHistoryItemAlertPresented = true
-                        }
-                    ).tint(.red)
-                }
-            }
-            .alert(
-                Text(alertTitle),
-                isPresented: $isRemoveHistoryItemAlertPresented
-            ) {
-                Button("Cancel", role: .cancel) {}
-                Button("Delete", role: .destructive) {
-                    guard let treatmentToDelete = alertTreatmentToDelete else {
-                        debug(.default, "Cannot gracefully unwrap alertTreatmentToDelete!")
-                        return
-                    }
-                    let treatmentObjectID = treatmentToDelete.objectID
-
-                    state.invokeInsulinDeletionTask(treatmentObjectID)
-                }
-            } message: {
-                Text("\n" + alertMessage)
-            }
-        }
-
-        @ViewBuilder private func mealView(_ meal: CarbEntryStored) -> some View {
-            VStack {
-                HStack {
-                    if meal.isFPU {
-                        Image(systemName: "circle.fill").foregroundColor(Color.orange.opacity(0.5))
-                        Text("Fat / Protein")
-                        Text(
-                            (Formatter.decimalFormatterWithTwoFractionDigits.string(for: meal.carbs) ?? "0") +
-                                String(localized: " g", comment: "gram of carbs")
-                        )
-                    } else {
-                        Image(systemName: "circle.fill").foregroundColor(Color.loopYellow)
-                        Text("Carbs")
-                        Text(
-                            (Formatter.decimalFormatterWithTwoFractionDigits.string(for: meal.carbs) ?? "0") +
-                                String(localized: " g", comment: "gram of carb equilvalents")
-                        )
-                    }
-
-                    Spacer()
-
-                    Text(Formatter.dateFormatter.string(from: meal.date ?? Date()))
-                        .moveDisabled(true)
-                }
-                if let note = meal.note, note != "" {
-                    HStack {
-                        Image(systemName: "square.and.pencil")
-                        Text(note)
-                        Spacer()
-                    }.padding(.top, 5).foregroundColor(.secondary)
-                }
-            }
-            .swipeActions {
-                Button(
-                    "Delete",
-                    systemImage: "trash.fill",
-                    role: .none,
-                    action: {
-                        alertCarbEntryToDelete = meal
-
-                        // meal is carb-only
-                        if meal.fpuID == nil {
-                            alertTitle = String(localized: "Delete Carbs?", comment: "Alert title for deleting carbs")
-                            alertMessage = Formatter.dateFormatter
-                                .string(from: meal.date ?? Date()) + ", " +
-                                (Formatter.decimalFormatterWithTwoFractionDigits.string(for: meal.carbs) ?? "0") +
-                                String(localized: " g", comment: "gram of carbs")
-                        }
-                        // meal is complex-meal or fpu-only
-                        else {
-                            alertTitle = meal.isFPU ? String(
-                                localized: "Delete Carbs Equivalents?",
-                                comment: "Alert title for deleting carb equivalents"
-                            )
-                                : String(localized: "Delete Carbs?", comment: "Alert title for deleting carbs")
-                            alertMessage = String(
-                                localized: "All FPUs and the carbs of the meal will be deleted.",
-                                comment: "Alert message for meal deletion"
-                            )
-                        }
-
-                        isRemoveHistoryItemAlertPresented = true
-                    }
-                ).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(alertTitle),
-                isPresented: $isRemoveHistoryItemAlertPresented
-            ) {
-                Button("Cancel", role: .cancel) {}
-                Button("Delete", role: .destructive) {
-                    guard let carbEntryToDelete = alertCarbEntryToDelete else {
-                        debug(.default, "Cannot gracefully unwrap alertCarbEntryToDelete!")
-                        return
-                    }
-                    let treatmentObjectID = carbEntryToDelete.objectID
-
-                    state.invokeCarbDeletionTask(
-                        treatmentObjectID,
-                        isFpuOrComplexMeal: carbEntryToDelete.isFPU || carbEntryToDelete.fat > 0 || carbEntryToDelete.protein > 0
-                    )
-                }
-            } message: {
-                Text("\n" + alertMessage)
-            }
-        }
-
-        // MARK: - Format glucose
-
-        private func formatGlucose(_ value: Decimal, isManual: Bool) -> String {
-            let formatter = isManual ? manualGlucoseFormatter : Formatter.glucoseFormatter(for: state.units)
-            let glucoseValue = state.units == .mmolL ? value.asMmolL : value
-            let formattedValue = formatter.string(from: glucoseValue as NSNumber) ?? "--"
-
-            return formattedValue
-        }
     }
 }

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

@@ -773,27 +773,6 @@ extension Home {
             }.padding(.horizontal, 10).padding(.bottom, UIDevice.adjustPadding(min: nil, max: 10))
         }
 
-        @ViewBuilder func bolusProgressBar(_ progress: Decimal) -> some View {
-            GeometryReader { geo in
-                RoundedRectangle(cornerRadius: 15)
-                    .frame(height: 6)
-                    .foregroundColor(.clear)
-                    .background(
-                        LinearGradient(colors: [
-                            Color(red: 0.7215686275, green: 0.3411764706, blue: 1),
-                            Color(red: 0.6235294118, green: 0.4235294118, blue: 0.9803921569),
-                            Color(red: 0.4862745098, green: 0.5450980392, blue: 0.9529411765),
-                            Color(red: 0.3411764706, green: 0.6666666667, blue: 0.9254901961),
-                            Color(red: 0.262745098, green: 0.7333333333, blue: 0.9137254902)
-                        ], startPoint: .leading, endPoint: .trailing)
-                            .mask(alignment: .leading) {
-                                RoundedRectangle(cornerRadius: 15)
-                                    .frame(width: geo.size.width * CGFloat(progress))
-                            }
-                    )
-            }
-        }
-
         @ViewBuilder func bolusView(geo: GeometryProxy, _ progress: Decimal) -> some View {
             /// ensure that state.lastPumpBolus has a value, i.e. there is a last bolus done by the pump and not an external bolus
             /// - TRUE:  show the pump bolus
@@ -849,14 +828,14 @@ extension Home {
                         }
                     }.padding(.horizontal, 10)
                         .padding(.trailing, 8)
-
-                }.padding(.horizontal, 10).padding(.bottom, UIDevice.adjustPadding(min: nil, max: 10))
-                    .overlay(alignment: .bottom) {
-                        // Use a geo-based offset here to position progress bar independent of device size
-                        let offset = geo.size.height * 0.0725
-                        bolusProgressBar(progress).padding(.horizontal, 18)
-                            .offset(y: offset)
-                    }.clipShape(RoundedRectangle(cornerRadius: 15))
+                }
+                .padding(.horizontal, 10)
+                .padding(.bottom, UIDevice.adjustPadding(min: nil, max: 10))
+                .overlay(alignment: .bottom) {
+                    BolusProgressBar(progress: progress)
+                        .padding(.horizontal, 18)
+                        .padding(.bottom, 9)
+                }.clipShape(RoundedRectangle(cornerRadius: 15))
             }
         }
 

+ 22 - 11
Trio/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift

@@ -7,15 +7,26 @@ extension NightscoutConfig {
         let resolver: Resolver
         let displayClose: Bool
         @StateObject var state = StateModel()
-        @State private var shouldDisplayHint: Bool = false
         @State var hintDetent = PresentationDetent.large
-        @State var selectedVerboseHint: AnyView?
-        @State var hintLabel: String?
+        @State private var hintPayload: HintPayload?
         @State private var decimalPlaceholder: Decimal = 0.0
         @State private var booleanPlaceholder: Bool = false
         @State var backfillAlert: Alert?
         @State var isBackfillAlertPresented = false
 
+        private struct HintPayload: Identifiable {
+            let id = UUID()
+            let label: String
+            let content: AnyView
+        }
+
+        private var shouldDisplayHintBinding: Binding<Bool> {
+            Binding(
+                get: { hintPayload != nil },
+                set: { newValue in if !newValue { hintPayload = nil } }
+            )
+        }
+
         @Environment(\.colorScheme) var colorScheme
         @Environment(AppState.self) var appState
 
@@ -79,14 +90,14 @@ extension NightscoutConfig {
                                     Spacer()
                                     Button(
                                         action: {
-                                            hintLabel = String(localized: "Backfill Glucose from Nightscout")
-                                            selectedVerboseHint =
-                                                AnyView(
+                                            hintPayload = HintPayload(
+                                                label: String(localized: "Backfill Glucose from Nightscout"),
+                                                content: AnyView(
                                                     Text(
                                                         "This will backfill 24 hours of glucose data from your connected Nightscout URL to Trio"
                                                     )
                                                 )
-                                            shouldDisplayHint.toggle()
+                                            )
                                         },
                                         label: {
                                             HStack {
@@ -104,12 +115,12 @@ extension NightscoutConfig {
                 }
                 .listSectionSpacing(sectionSpacing)
             }
-            .sheet(isPresented: $shouldDisplayHint) {
+            .sheet(item: $hintPayload) { payload in
                 SettingInputHintView(
                     hintDetent: $hintDetent,
-                    shouldDisplayHint: $shouldDisplayHint,
-                    hintLabel: hintLabel ?? "",
-                    hintText: selectedVerboseHint ?? AnyView(EmptyView()),
+                    shouldDisplayHint: shouldDisplayHintBinding,
+                    hintLabel: payload.label,
+                    hintText: payload.content,
                     sheetTitle: String(localized: "Help", comment: "Help sheet title")
                 )
             }

+ 12 - 0
Trio/Sources/Modules/Onboarding/View/TherapySettingEditorView.swift

@@ -96,6 +96,18 @@ struct TherapySettingEditorView: View {
                                 .transition(.slide)
                             }
                         }
+                        .contextMenu {
+                            if let index = items.firstIndex(where: { $0.id == item.id }), items.count > 1 {
+                                Button(role: .destructive) {
+                                    items.remove(at: index)
+                                    selectedItemID = nil
+                                    validateTherapySettingItems()
+                                } label: {
+                                    Label("Delete", systemImage: "trash")
+                                }
+                                .tint(.red)
+                            }
+                        }
                         .swipeActions(edge: .trailing, allowsFullSwipe: true) {
                             if let index = items.firstIndex(where: { $0.id == item.id }), items.count > 1 {
                                 Button(role: .destructive) {

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

@@ -240,7 +240,8 @@ enum SettingItems {
                 "Glucose Color Scheme",
                 "Time in Range Type",
                 "Time in Tight Range (TITR)",
-                "Time in Normoglycemia (TING)"
+                "Time in Normoglycemia (TING)",
+                "Require Adjustments Confirmation"
             ],
             path: ["Features", "User Interface"]
         ),

+ 7 - 0
Trio/Sources/Modules/SettingsExport/SettingsExportStateModel.swift

@@ -809,6 +809,13 @@ extension SettingsExport {
                     name: String(localized: "Time in Range Type"),
                     value: trioSettings.timeInRangeType.rawValue
                 )
+                addSetting(
+                    category: featuresCategory,
+                    subcategory: userInterfaceSubcategory,
+                    name: String(localized: "Require Adjustments Confirmation"),
+                    value: trioSettings
+                        .requireAdjustmentsConfirmation ? String(localized: "Enabled") : String(localized: "Disabled")
+                )
 
                 // Appearance setting from UserDefaults
                 let colorSchemePreference = UserDefaults.standard.string(forKey: "colorSchemePreference") ?? "systemDefault"

+ 72 - 17
Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift

@@ -135,8 +135,9 @@ extension Treatments {
 
         typealias PumpEvent = PumpEventStored.EventType
 
-        var isBolusInProgress: Bool = false
-        private var bolusProgressCancellable: AnyCancellable?
+        var bolusProgress: Decimal?
+        var isBolusInProgress: Bool { bolusProgress != nil }
+        var lastPumpBolus: PumpEventStored?
 
         func unsubscribe() {
             subscriptions.forEach { $0.cancel() }
@@ -171,7 +172,7 @@ extension Treatments {
             hasCleanedUp = true
 
             unsubscribe()
-            bolusProgressCancellable?.cancel()
+            lifetime = Lifetime()
 
             broadcaster?.unregister(DeterminationObserver.self, observer: self)
             broadcaster?.unregister(BolusFailureObserver.self, observer: self)
@@ -196,6 +197,9 @@ extension Treatments {
                         group.addTask {
                             self.registerObservers()
                         }
+                        group.addTask {
+                            self.setupLastBolus()
+                        }
 
                         // Wait for all tasks to complete
                         try await group.waitForAll()
@@ -206,22 +210,21 @@ extension Treatments {
             }
         }
 
-        /// Observes changes to the `bolusProgress` published by the `apsManager` to update the `isBolusInProgress` property in real time.
-        ///
-        /// - Important:
-        ///   - `apsManager.bolusProgress` is a `CurrentValueSubject<Decimal?, Never>`.
-        ///   - When a bolus starts, this subject emits `0` (or a fraction like `0.1, 0.5, etc.`).
-        ///   - When the bolus finishes, the subject is typically set to `nil`.
-        ///   - This treats ANY non-nil value as "bolus in progress."
-        ///
+        /// Mirrors `apsManager.bolusProgress` (a `CurrentValueSubject<Decimal?, Never>`) directly into the
+        /// state model so the View can read both the progress fraction (0.0–1.0) and a derived in-progress
+        /// flag. Stored in `lifetime` to match the Home module's pattern (HomeStateModel.registerObservers).
         private func subscribeToBolusProgress() {
-            bolusProgressCancellable = apsManager.bolusProgress
+            apsManager.bolusProgress
                 .receive(on: DispatchQueue.main)
-                .sink { [weak self] progressValue in
-                    guard let self = self else { return }
-                    // If progressValue is non-nil, a bolus is in progress.
-                    self.isBolusInProgress = (progressValue != nil)
-                }
+                .weakAssign(to: \.bolusProgress, on: self)
+                .store(in: &lifetime)
+        }
+
+        func cancelBolus() {
+            Task {
+                await apsManager.cancelBolus(nil)
+                try? await apsManager.determineBasalSync()
+            }
         }
 
         // MARK: - Basal
@@ -742,6 +745,11 @@ extension Treatments.StateModel {
             guard let self = self else { return }
             self.setupGlucoseArray()
         }.store(in: &subscriptions)
+
+        // Refresh `lastPumpBolus` whenever a new pump event lands (mirrors HomeStateModel)
+        coreDataPublisher?.filteredByEntityName("PumpEventStored").sink { [weak self] _ in
+            self?.setupLastBolus()
+        }.store(in: &subscriptions)
     }
 
     private func registerSubscribers() {
@@ -999,3 +1007,50 @@ private extension Predictions {
         iob == nil && zt == nil && cob == nil && uam == nil
     }
 }
+
+// MARK: - Last Pump Bolus
+
+extension Treatments.StateModel {
+    /// Mirrors `HomeStateModel.setupLastBolus` so the in-progress visualizer can show the
+    /// running pump-bolus's amount as the denominator (not the user's pending entry).
+    /// Filters out external boluses via `NSPredicate.lastPumpBolus`.
+    func setupLastBolus() {
+        Task {
+            do {
+                guard let id = try await fetchLastBolus() else { return }
+                await updateLastBolus(with: id)
+            } catch {
+                debug(.default, "\(DebuggingIdentifiers.failed) Error setting up last bolus: \(error)")
+            }
+        }
+    }
+
+    private func fetchLastBolus() async throws -> NSManagedObjectID? {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "fetchLastBolus"
+
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: PumpEventStored.self,
+            onContext: context,
+            predicate: NSPredicate.lastPumpBolus,
+            key: "timestamp",
+            ascending: false,
+            fetchLimit: 1
+        )
+
+        return try await context.perform {
+            guard let fetched = results as? [PumpEventStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
+            return fetched.map(\.objectID).first
+        }
+    }
+
+    @MainActor private func updateLastBolus(with id: NSManagedObjectID) {
+        do {
+            lastPumpBolus = try viewContext.existingObject(with: id) as? PumpEventStored
+        } catch {
+            debug(.default, "\(DebuggingIdentifiers.failed) updateLastBolus: \(error)")
+        }
+    }
+}

+ 126 - 36
Trio/Sources/Modules/Treatments/View/TreatmentsRootView.swift

@@ -42,6 +42,23 @@ extension Treatments {
             return formatter
         }
 
+        private var bolusProgressFormatter: NumberFormatter {
+            let fractionDigits: Int = switch state.settingsManager.preferences.bolusIncrement {
+            case 0.1: 1
+            case 0.025: 3
+            default: 2
+            }
+
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            formatter.minimum = 0
+            formatter.maximumFractionDigits = fractionDigits
+            formatter.minimumFractionDigits = fractionDigits
+            formatter.allowsFloats = true
+            formatter.roundingIncrement = Double(state.settingsManager.preferences.bolusIncrement) as NSNumber
+            return formatter
+        }
+
         private var mealFormatter: NumberFormatter {
             let formatter = NumberFormatter()
             formatter.numberStyle = .decimal
@@ -475,6 +492,9 @@ extension Treatments {
         }
 
         var treatmentButton: some View {
+            let shouldDisplayBolusProgress = state.isBolusInProgress && state.amount > 0 &&
+                !state.externalInsulin && (state.carbs == 0 || state.fat == 0 || state.protein == 0)
+
             var treatmentButtonBackground = Color(.systemBlue)
             if limitExceeded {
                 treatmentButtonBackground = Color(.systemRed)
@@ -483,41 +503,43 @@ extension Treatments {
             }
 
             return Section {
-                Button {
-                    if bolusWarning.shouldConfirm {
-                        showConfirmDialogForBolusing = true
-                    } else {
-                        state.invokeTreatmentsTask()
-                    }
-                } label: {
-                    HStack {
-                        if state.isBolusInProgress && state.amount > 0 &&
-                            !state.externalInsulin && (state.carbs == 0 || state.fat == 0 || state.protein == 0)
-                        {
-                            ProgressView()
+                if shouldDisplayBolusProgress {
+                    bolusInProgressView
+                        .listRowBackground(Color.clear)
+                        .listRowInsets(EdgeInsets(top: 4, leading: 0, bottom: 4, trailing: 0))
+                } else {
+                    Button {
+                        if bolusWarning.shouldConfirm {
+                            showConfirmDialogForBolusing = true
+                        } else {
+                            state.invokeTreatmentsTask()
                         }
-                        taskButtonLabel
+                    } label: {
+                        HStack {
+                            taskButtonLabel
+                        }
+                        .font(.headline)
+                        .foregroundStyle(Color.white)
+                        .frame(maxWidth: .infinity, alignment: .center)
+                        .frame(height: 35)
                     }
-                    .font(.headline)
-                    .foregroundStyle(Color.white)
-                    .frame(maxWidth: .infinity, alignment: .center)
-                    .frame(height: 35)
-                }
-                .disabled(disableTaskButton)
-                .listRowBackground(treatmentButtonBackground)
-                .shadow(radius: 3)
-                .clipShape(RoundedRectangle(cornerRadius: 8))
-                .confirmationDialog(
-                    bolusWarning.warningMessage + " Bolus \(state.amount.description) U?",
-                    isPresented: $showConfirmDialogForBolusing,
-                    titleVisibility: .visible
-                ) {
-                    Button("Cancel", role: .cancel) {}
-                    Button(
-                        bolusWarning.warningMessage.isEmpty ? "Enact Bolus" : "Ignore Warning and Enact Bolus",
-                        role: bolusWarning.warningMessage.isEmpty ? nil : .destructive
+                    .disabled(disableTaskButton)
+                    .listRowBackground(treatmentButtonBackground)
+                    .shadow(radius: 3)
+                    .clipShape(RoundedRectangle(cornerRadius: 8))
+                    .confirmationDialog(
+                        bolusWarning.warningMessage + " Bolus \(state.amount.description) U?",
+                        isPresented: $showConfirmDialogForBolusing,
+                        titleVisibility: .visible
                     ) {
-                        state.invokeTreatmentsTask()
+                        Button("Cancel", role: .cancel) {}
+                        Button(
+                            bolusWarning.warningMessage
+                                .isEmpty ? String(localized: "Enact Bolus") : String(localized: "Ignore Warning and Enact Bolus"),
+                            role: bolusWarning.warningMessage.isEmpty ? nil : .destructive
+                        ) {
+                            state.invokeTreatmentsTask()
+                        }
                     }
                 }
             } header: {
@@ -532,6 +554,75 @@ extension Treatments {
             }
         }
 
+        /// Card-style in-progress visualizer matching Home's `bolusView` look:
+        /// insulin-tinted background, cross.vial.fill icon, "Bolusing" + "X of Y U" text,
+        /// xmark.app cancel, gradient progress bar overlaid at the bottom.
+        @ViewBuilder private var bolusInProgressView: some View {
+            let progress = state.bolusProgress ?? 0
+            let bolusTotal = state.lastPumpBolus?.bolus?.amount as Decimal?
+            let bolusFraction = (bolusTotal ?? 0) * progress
+            let bolusString: String = {
+                guard let bolusTotal = bolusTotal else { return String(localized: "Bolus In Progress...") }
+                return (bolusProgressFormatter.string(from: bolusFraction as NSNumber) ?? "0")
+                    + String(localized: " of ", comment: "Bolus string partial message: 'x U of y U' in home view")
+                    + (Formatter.decimalFormatterWithThreeFractionDigits.string(from: bolusTotal as NSNumber) ?? "0")
+                    + String(localized: " U", comment: "Insulin unit")
+            }()
+
+            ZStack {
+                // background card
+                RoundedRectangle(cornerRadius: 15)
+                    .fill(
+                        colorScheme == .dark
+                            ? Color(red: 0.03921568627, green: 0.133333333, blue: 0.2156862745)
+                            : Color.insulin.opacity(0.2)
+                    )
+                    .frame(height: 56)
+                    .shadow(
+                        color: colorScheme == .dark
+                            ? Color(red: 0.02745098039, green: 0.1098039216, blue: 0.1411764706)
+                            : Color.black.opacity(0.33),
+                        radius: 3
+                    )
+
+                // bolus content
+                HStack {
+                    Image(systemName: "cross.vial.fill")
+                        .font(.system(size: 25))
+
+                    Spacer()
+
+                    VStack {
+                        Text("Bolusing")
+                            .font(.subheadline)
+                            .frame(maxWidth: .infinity, alignment: .leading)
+                        Text(bolusString)
+                            .font(.caption)
+                            .frame(maxWidth: .infinity, alignment: .leading)
+                    }
+                    .padding(.leading, 5)
+
+                    Spacer()
+
+                    Button { state.cancelBolus() } label: {
+                        Image(systemName: "xmark.app")
+                            .font(.system(size: 25))
+                    }.tint(Color.tabBar)
+                        .buttonStyle(.borderless)
+                        .accessibilityLabel("Cancel bolus")
+                }
+                .padding(.horizontal, 10)
+                .padding(.trailing, 8)
+            }
+            .padding(.horizontal, 10)
+            .overlay(alignment: .bottom) {
+                BolusProgressBar(progress: progress)
+                    .padding(.horizontal, 18)
+                    .padding(.bottom, 1)
+            }
+            .clipShape(RoundedRectangle(cornerRadius: 15))
+        }
+
         private var taskButtonLabel: some View {
             if pumpBolusLimitExceeded {
                 return Text("Max Bolus of \(state.maxBolus.description) U Exceeded")
@@ -550,9 +641,8 @@ extension Treatments {
             let hasFatOrProtein = state.fat > 0 || state.protein > 0
             let bolusString = state.externalInsulin ? String(localized: "External Insulin") : String(localized: "Enact Bolus")
 
-            if state.isBolusInProgress && hasInsulin && !state.externalInsulin && (!hasCarbs || !hasFatOrProtein) {
-                return Text("Bolus In Progress...")
-            }
+            // Note: when a pump bolus is in progress, the row is rendered by `bolusInProgressView`
+            // (Home-style card), so this label's in-progress branch is intentionally absent.
 
             switch (hasInsulin, hasCarbs, hasFatOrProtein) {
             case (true, true, true):
@@ -562,7 +652,7 @@ extension Treatments {
             case (true, false, true):
                 return Text("Log FPU and \(bolusString)")
             case (true, false, false):
-                return Text(state.externalInsulin ? "Log External Insulin" : "Enact Bolus")
+                return Text(state.externalInsulin ? String(localized: "Log External Insulin") : String(localized: "Enact Bolus"))
             case (false, true, true):
                 return Text("Log Meal")
             case (false, true, false):

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

@@ -14,6 +14,7 @@ extension UserInterfaceSettings {
         @Published var glucoseColorScheme: GlucoseColorScheme = .staticColor
         @Published var eA1cDisplayUnit: EstimatedA1cDisplayUnit = .percent
         @Published var timeInRangeType: TimeInRangeType = .timeInTightRange
+        @Published var requireAdjustmentsConfirmation: Bool = false
 
         var units: GlucoseUnits = .mgdL
 
@@ -44,6 +45,9 @@ extension UserInterfaceSettings {
             subscribeSetting(\.eA1cDisplayUnit, on: $eA1cDisplayUnit) { eA1cDisplayUnit = $0 }
 
             subscribeSetting(\.timeInRangeType, on: $timeInRangeType) { timeInRangeType = $0 }
+
+            subscribeSetting(\.requireAdjustmentsConfirmation, on: $requireAdjustmentsConfirmation) {
+                requireAdjustmentsConfirmation = $0 }
         }
     }
 }

+ 23 - 0
Trio/Sources/Modules/UserInterfaceSettings/View/UserInterfaceSettingsRootView.swift

@@ -561,6 +561,29 @@ extension UserInterfaceSettings {
                     ),
                     headerText: String(localized: "Carbs Required Badge")
                 )
+
+                SettingInputSection(
+                    decimalValue: $decimalPlaceholder,
+                    booleanValue: $state.requireAdjustmentsConfirmation,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    selectedVerboseHint: Binding(
+                        get: { selectedVerboseHint },
+                        set: {
+                            selectedVerboseHint = $0.map { AnyView($0) }
+                            hintLabel = String(localized: "Require Adjustments Confirmation")
+                        }
+                    ),
+                    units: state.units,
+                    type: .boolean,
+                    label: String(localized: "Require Adjustments Confirmation"),
+                    miniHint: String(
+                        localized: "If enabled, a confirmation dialog will be shown when activating adjustment presets."
+                    ),
+                    verboseHint: Text(
+                        "Turning this on will show a confirmation dialog when you activate an Override or Temporary Target preset. This is for users who would like avoid accidentally activating a preset by mistake."
+                    ),
+                    headerText: String(localized: "Adjustments")
+                )
             }
             .listSectionSpacing(sectionSpacing)
             .sheet(isPresented: $shouldDisplayHint) {

+ 0 - 1
Trio/Sources/Services/Network/Nightscout/BaseNightscoutManager+Subscribers.swift

@@ -73,7 +73,6 @@ extension BaseNightscoutManager {
             .filteredByEntityName("GlucoseStored")
             .sink { [weak self] _ in
                 self?.requestUpload(.glucose)
-                self?.requestUpload(.manualGlucose)
             }
             .store(in: &subscriptions)
     }

+ 10 - 5
Trio/Sources/Services/Network/Nightscout/NightscoutAPI.swift

@@ -188,14 +188,21 @@ extension NightscoutAPI {
         return
     }
 
-    func deleteManualGlucose(withId id: String) async throws {
+    func deleteGlucose(withId id: String, withDate date: Date) async throws {
         var components = URLComponents()
         components.scheme = url.scheme
         components.host = url.host
         components.port = url.port
-        components.path = Config.treatmentsPath
+        components.path = Config.uploadEntriesPath
         components.queryItems = [
-            URLQueryItem(name: "find[id][$eq]", value: id)
+            URLQueryItem(
+                name: "find[$or][0][id][$eq]",
+                value: id
+            ),
+            URLQueryItem(
+                name: "find[$or][1][dateString][$eq]",
+                value: Formatter.iso8601withFractionalSeconds.string(from: date)
+            )
         ]
 
         guard let url = components.url else {
@@ -216,8 +223,6 @@ extension NightscoutAPI {
         guard let httpResponse = response as? HTTPURLResponse, (200 ... 299).contains(httpResponse.statusCode) else {
             throw URLError(.badServerResponse)
         }
-
-        debugPrint("Delete successful for ID \(id)")
     }
 
     func deleteInsulin(withId id: String) async throws {

+ 6 - 92
Trio/Sources/Services/Network/Nightscout/NightscoutManager.swift

@@ -11,14 +11,13 @@ protocol NightscoutManager: GlucoseSource {
     func fetchTempTargets() async -> [TempTarget]
     func deleteCarbs(withID id: String) async
     func deleteInsulin(withID id: String) async
-    func deleteManualGlucose(withID id: String) async
+    func deleteGlucose(withID id: String, withDate date: Date) async
     func uploadDeviceStatus() async throws
     func uploadGlucose() async
     func uploadCarbs() async
     func uploadPumpHistory() async
     func uploadOverrides() async
     func uploadTempTargets() async
-    func uploadManualGlucose() async
     func uploadProfiles() async throws
     func uploadNoteTreatment(note: String) async
     func importSettings() async -> ScheduledNightscoutProfile?
@@ -51,7 +50,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     /// coalesce into a single upload run for that pipeline.
     let uploadPipelineInterval: [NightscoutUploadPipeline: TimeInterval] = [
         .carbs: 2, .pumpHistory: 2, .overrides: 2, .tempTargets: 2,
-        .glucose: 2, .manualGlucose: 2, .deviceStatus: 2
+        .glucose: 2, .deviceStatus: 2
     ]
 
     /// Subjects used to request an upload pipeline. The pipeline applies a throttle so
@@ -92,7 +91,6 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         case .overrides: await uploadOverrides()
         case .tempTargets: await uploadTempTargets()
         case .glucose: await uploadGlucose()
-        case .manualGlucose: await uploadManualGlucose()
         case .deviceStatus:
             do { try await uploadDeviceStatus() }
             catch { debug(.nightscout, "deviceStatus upload failed: \(error)") }
@@ -336,15 +334,16 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         }
     }
 
-    func deleteManualGlucose(withID id: String) async {
+    func deleteGlucose(withID id: String, withDate date: Date) async {
         guard let nightscout = nightscoutAPI, isUploadEnabled else { return }
 
         do {
-            try await nightscout.deleteManualGlucose(withId: id)
+            try await nightscout.deleteGlucose(withId: id, withDate: date)
+            debug(.nightscout, "Glucose deleted")
         } catch {
             debug(
                 .nightscout,
-                "\(DebuggingIdentifiers.failed) Failed to delete Manual Glucose from Nightscout with error: \(error)"
+                "\(DebuggingIdentifiers.failed) Failed to delete Glucose from Nightscout with error: \(error)"
             )
         }
     }
@@ -583,10 +582,6 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         } catch {
             debug(.nightscout, String(describing: error))
         }
-
-        Task.detached {
-            await self.uploadPodAge()
-        }
     }
 
     private func updateOrefDeterminationAsUploaded(_ determination: [Determination]) async {
@@ -613,33 +608,6 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         }
     }
 
-    func uploadPodAge() async {
-        let uploadedPodAge = storage.retrieve(OpenAPS.Nightscout.uploadedPodAge, as: [NightscoutTreatment].self) ?? []
-        if let podAge = storage.retrieve(OpenAPS.Monitor.podAge, as: Date.self),
-           uploadedPodAge.last?.createdAt == nil || podAge != uploadedPodAge.last!.createdAt!
-        {
-            let siteTreatment = NightscoutTreatment(
-                duration: nil,
-                rawDuration: nil,
-                rawRate: nil,
-                absolute: nil,
-                rate: nil,
-                eventType: .nsSiteChange,
-                createdAt: podAge,
-                enteredBy: NightscoutTreatment.local,
-                bolus: nil,
-                insulin: nil,
-                notes: nil,
-                carbs: nil,
-                fat: nil,
-                protein: nil,
-                targetTop: nil,
-                targetBottom: nil
-            )
-            await uploadNonCoreDataTreatments([siteTreatment])
-        }
-    }
-
     func uploadProfiles() async throws {
         if isUploadEnabled {
             do {
@@ -806,17 +774,6 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         }
     }
 
-    func uploadManualGlucose() async {
-        do {
-            try await uploadManualGlucose(glucoseStorage.getManualGlucoseNotYetUploadedToNightscout())
-        } catch {
-            debug(
-                .nightscout,
-                "\(DebuggingIdentifiers.failed) failed to upload manual glucose with error: \(error)"
-            )
-        }
-    }
-
     func uploadPumpHistory() async {
         do {
             try await uploadPumpHistory(pumpHistoryStorage.getPumpHistoryNotYetUploadedToNightscout())
@@ -966,49 +923,6 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         }
     }
 
-    private func uploadManualGlucose(_ treatments: [NightscoutTreatment]) async {
-        guard !treatments.isEmpty, let nightscout = nightscoutAPI, isUploadEnabled else {
-            return
-        }
-
-        do {
-            for chunk in treatments.chunks(ofCount: 100) {
-                try await nightscout.uploadTreatments(Array(chunk))
-            }
-
-            // If successful, update the isUploadedToNS property of the GlucoseStored objects
-            await updateManualGlucoseAsUploaded(treatments)
-
-            debug(.nightscout, "Treatments uploaded")
-        } catch {
-            debug(.nightscout, String(describing: error))
-        }
-    }
-
-    private func updateManualGlucoseAsUploaded(_ treatments: [NightscoutTreatment]) async {
-        let context = CoreDataStack.shared.newTaskContext()
-        context.name = "updateManualGlucoseAsUploaded"
-        await context.perform {
-            let ids = treatments.map(\.id) as NSArray
-            let fetchRequest: NSFetchRequest<GlucoseStored> = GlucoseStored.fetchRequest()
-            fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
-
-            do {
-                let results = try context.fetch(fetchRequest)
-                for result in results {
-                    result.isUploadedToNS = true
-                }
-
-                guard context.hasChanges else { return }
-                try context.save()
-            } catch let error as NSError {
-                debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToNS: \(error.userInfo)"
-                )
-            }
-        }
-    }
-
     private func uploadCarbs(_ treatments: [NightscoutTreatment]) async {
         guard !treatments.isEmpty, let nightscout = nightscoutAPI, isUploadEnabled else {
             return

+ 0 - 1
Trio/Sources/Services/Network/Nightscout/NightscoutUploadPipeline.swift

@@ -9,7 +9,6 @@ public enum NightscoutUploadPipeline: String, CaseIterable {
     case overrides
     case tempTargets
     case glucose
-    case manualGlucose
     case deviceStatus
 }
 

+ 27 - 0
Trio/Sources/Views/BolusProgressBar.swift

@@ -0,0 +1,27 @@
+import SwiftUI
+
+struct BolusProgressBar: View {
+    let progress: Decimal
+
+    var body: some View {
+        GeometryReader { geo in
+            RoundedRectangle(cornerRadius: 15)
+                .frame(height: 6)
+                .foregroundColor(.clear)
+                .background(
+                    LinearGradient(colors: [
+                        Color(red: 0.7215686275, green: 0.3411764706, blue: 1),
+                        Color(red: 0.6235294118, green: 0.4235294118, blue: 0.9803921569),
+                        Color(red: 0.4862745098, green: 0.5450980392, blue: 0.9529411765),
+                        Color(red: 0.3411764706, green: 0.6666666667, blue: 0.9254901961),
+                        Color(red: 0.262745098, green: 0.7333333333, blue: 0.9137254902)
+                    ], startPoint: .leading, endPoint: .trailing)
+                        .mask(alignment: .leading) {
+                            RoundedRectangle(cornerRadius: 15)
+                                .frame(width: geo.size.width * CGFloat(progress))
+                        }
+                )
+        }
+        .frame(height: 6)
+    }
+}

+ 0 - 15
TrioTests/CoreDataTests/GlucoseStorageTests.swift

@@ -127,21 +127,6 @@ import Testing
         #expect(notUploadedEntries[0].glucose == 160, "Glucose value should match")
     }
 
-    @Test("Get manual glucose not yet uploaded to Nightscout") func testGetManualGlucoseNotYetUploadedToNightscout() async throws {
-        // Given
-        storage.addManualGlucose(glucose: 180)
-
-        // When
-        let notUploadedEntries = try await storage.getManualGlucoseNotYetUploadedToNightscout()
-
-        // Then
-        #expect(!notUploadedEntries.isEmpty, "Should have manual entries not uploaded to NS")
-        let entry = notUploadedEntries[0]
-        #expect(entry.glucose == "180", "Glucose value should match")
-        #expect(entry.glucoseType == "Manual", "Type should be mbg for manual entries")
-        #expect(entry.eventType == .capillaryGlucose, "Type should be capillaryGlucose")
-    }
-
     @Test(
         "Test glucose alarms",
         .enabled(if: false, "Flaky test, disabled while investigating")

+ 1 - 1
scripts/define_common_trio.sh

@@ -31,6 +31,6 @@ TRIO_PROJECTS=( \
     loopandlearn:RileyLinkKit:dev \
     loopandlearn:TidepoolService:dev \
     loopandlearn:DanaKit:dev \
-    loopandlearn:EversenseKit:dev \
     loopandlearn:MedtrumKit:dev \
+    loopandlearn:EversenseKit:dev \
 )