Przeglądaj źródła

Merge branch 'dev' of github.com:nightscout/Trio into feat/refactor-history-module

Deniz Cengiz 2 tygodni temu
rodzic
commit
c0205a2452
36 zmienionych plików z 602 dodań i 383 usunięć
  1. 1 1
      Config.xcconfig
  2. 3 1
      Gemfile
  3. 14 10
      Gemfile.lock
  4. 0 46
      Model/Helper/GlucoseStored+helper.swift
  5. 1 1
      Model/JSONImporter.swift
  6. 4 0
      Trio.xcodeproj/project.pbxproj
  7. 1 1
      Trio/Sources/APS/CGM/GlucoseSimulatorSource.swift
  8. 1 1
      Trio/Sources/APS/CGM/PluginSource.swift
  9. 1 1
      Trio/Sources/APS/DeviceDataManager.swift
  10. 26 63
      Trio/Sources/APS/Storage/GlucoseStorage.swift
  11. 33 0
      Trio/Sources/Localizations/Main/Localizable.xcstrings
  12. 1 1
      Trio/Sources/Models/AlgorithmGlucose.swift
  13. 26 9
      Trio/Sources/Models/BloodGlucose.swift
  14. 5 0
      Trio/Sources/Models/TrioSettings.swift
  15. 5 0
      Trio/Sources/Modules/Adjustments/AdjustmentsStateModel.swift
  16. 110 0
      Trio/Sources/Modules/Adjustments/View/AdjustmentsRootView.swift
  17. 9 14
      Trio/Sources/Modules/Adjustments/View/Overrides/AdjustmentsRootView+Overrides.swift
  18. 8 11
      Trio/Sources/Modules/Adjustments/View/TempTargets/AdjustmentsRootView+TempTargets.swift
  19. 43 43
      Trio/Sources/Modules/DynamicSettings/View/DynamicSettingsRootView.swift
  20. 1 1
      Trio/Sources/Modules/History/HistoryDataFlow.swift
  21. 2 2
      Trio/Sources/Modules/History/HistoryProvider.swift
  22. 2 2
      Trio/Sources/Modules/History/HistoryStateModel+Deletion/HistoryStateModel+Glucose.swift
  23. 8 29
      Trio/Sources/Modules/Home/View/HomeRootView.swift
  24. 22 11
      Trio/Sources/Modules/NightscoutConfig/View/NightscoutConfigRootView.swift
  25. 2 1
      Trio/Sources/Modules/Settings/SettingItems.swift
  26. 7 0
      Trio/Sources/Modules/SettingsExport/SettingsExportStateModel.swift
  27. 70 17
      Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift
  28. 126 36
      Trio/Sources/Modules/Treatments/View/TreatmentsRootView.swift
  29. 4 0
      Trio/Sources/Modules/UserInterfaceSettings/UserInterfaceSettingsStateModel.swift
  30. 23 0
      Trio/Sources/Modules/UserInterfaceSettings/View/UserInterfaceSettingsRootView.swift
  31. 0 1
      Trio/Sources/Services/Network/Nightscout/BaseNightscoutManager+Subscribers.swift
  32. 10 5
      Trio/Sources/Services/Network/Nightscout/NightscoutAPI.swift
  33. 6 59
      Trio/Sources/Services/Network/Nightscout/NightscoutManager.swift
  34. 0 1
      Trio/Sources/Services/Network/Nightscout/NightscoutUploadPipeline.swift
  35. 27 0
      Trio/Sources/Views/BolusProgressBar.swift
  36. 0 15
      TrioTests/CoreDataTests/GlucoseStorageTests.swift

+ 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
 // The developers set the version numbers, please leave them alone
 APP_VERSION = 0.7.0
 APP_VERSION = 0.7.0
-APP_DEV_VERSION = 0.7.0.4
+APP_DEV_VERSION = 0.7.0.11
 APP_BUILD_NUMBER = 1
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 COPYRIGHT_NOTICE =
 
 

+ 3 - 1
Gemfile

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

+ 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)
         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 {
     static var manualGlucoseNotYetUploadedToHealth: NSPredicate {
         let date = Date.oneDayAgo
         let date = Date.oneDayAgo
         return NSPredicate(
         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
 // 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 {
 extension GlucoseStored {
     var directionEnum: BloodGlucose.Direction? {
     var directionEnum: BloodGlucose.Direction? {

+ 1 - 1
Model/JSONImporter.swift

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

+ 4 - 0
Trio.xcodeproj/project.pbxproj

@@ -190,6 +190,7 @@
 		38E98A3725F5509500C0CED0 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E98A3625F5509500C0CED0 /* String+Extensions.swift */; };
 		38E98A3725F5509500C0CED0 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E98A3625F5509500C0CED0 /* String+Extensions.swift */; };
 		38EA05DA261F6E7C0064E39B /* SimpleLogReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38EA05D9261F6E7C0064E39B /* SimpleLogReporter.swift */; };
 		38EA05DA261F6E7C0064E39B /* SimpleLogReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38EA05D9261F6E7C0064E39B /* SimpleLogReporter.swift */; };
 		38EA0600262091870064E39B /* BolusProgressViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38EA05FF262091870064E39B /* BolusProgressViewStyle.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 */; };
 		38F37828261260DC009DB701 /* Color+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F37827261260DC009DB701 /* Color+Extensions.swift */; };
 		38F3B2EF25ED8E2A005C48AA /* TempTargetsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F3B2EE25ED8E2A005C48AA /* TempTargetsStorage.swift */; };
 		38F3B2EF25ED8E2A005C48AA /* TempTargetsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F3B2EE25ED8E2A005C48AA /* TempTargetsStorage.swift */; };
 		38FCF3D625E8FDF40078B0D1 /* MD5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FCF3D525E8FDF40078B0D1 /* MD5.swift */; };
 		38FCF3D625E8FDF40078B0D1 /* MD5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FCF3D525E8FDF40078B0D1 /* MD5.swift */; };
@@ -1063,6 +1064,7 @@
 		38E98A3625F5509500C0CED0 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		38F3B2EE25ED8E2A005C48AA /* TempTargetsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetsStorage.swift; sourceTree = "<group>"; };
@@ -2354,6 +2356,7 @@
 				383420D825FFEB3F002D46C1 /* Popup.swift */,
 				383420D825FFEB3F002D46C1 /* Popup.swift */,
 				389ECDFD2601061500D86C4F /* View+Snapshot.swift */,
 				389ECDFD2601061500D86C4F /* View+Snapshot.swift */,
 				38EA05FF262091870064E39B /* BolusProgressViewStyle.swift */,
 				38EA05FF262091870064E39B /* BolusProgressViewStyle.swift */,
+				DDB0BBA02026050100000002 /* BolusProgressBar.swift */,
 				38DF1785276A73D400B3528F /* TagCloudView.swift */,
 				38DF1785276A73D400B3528F /* TagCloudView.swift */,
 				DD88C8E12C50420800F2D558 /* DefinitionRow.swift */,
 				DD88C8E12C50420800F2D558 /* DefinitionRow.swift */,
 				DD1745282C55642100211FAC /* SettingInputSection.swift */,
 				DD1745282C55642100211FAC /* SettingInputSection.swift */,
@@ -4603,6 +4606,7 @@
 				582DF9792C8CE1E5001F516D /* MainChartHelper.swift in Sources */,
 				582DF9792C8CE1E5001F516D /* MainChartHelper.swift in Sources */,
 				E06B911A275B5EEA003C04B6 /* Array+Extension.swift in Sources */,
 				E06B911A275B5EEA003C04B6 /* Array+Extension.swift in Sources */,
 				38EA0600262091870064E39B /* BolusProgressViewStyle.swift in Sources */,
 				38EA0600262091870064E39B /* BolusProgressViewStyle.swift in Sources */,
+				DDB0BBA02026050100000001 /* BolusProgressBar.swift in Sources */,
 				389ECDFE2601061500D86C4F /* View+Snapshot.swift in Sources */,
 				389ECDFE2601061500D86C4F /* View+Snapshot.swift in Sources */,
 				38FEF3FE2738083E00574A46 /* CGMSettingsProvider.swift in Sources */,
 				38FEF3FE2738083E00574A46 /* CGMSettingsProvider.swift in Sources */,
 				38E98A3725F5509500C0CED0 /* String+Extensions.swift in Sources */,
 				38E98A3725F5509500C0CED0 /* String+Extensions.swift in Sources */,

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

@@ -217,7 +217,7 @@ class OscillatingGenerator: BloodGlucoseGenerator {
 
 
             // Create BloodGlucose with the correct constructor
             // Create BloodGlucose with the correct constructor
             let bloodGlucose = BloodGlucose(
             let bloodGlucose = BloodGlucose(
-                _id: UUID().uuidString,
+                id: UUID().uuidString,
                 sgv: glucose,
                 sgv: glucose,
                 direction: direction,
                 direction: direction,
                 date: Decimal(Int(currentDate.timeIntervalSince1970) * 1000),
                 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))
                 let value = Int(quantity.doubleValue(for: .milligramsPerDeciliter))
                 return BloodGlucose(
                 return BloodGlucose(
-                    _id: UUID().uuidString,
+                    id: UUID().uuidString,
                     sgv: value,
                     sgv: value,
                     direction: .init(trendType: newGlucoseSample.trend),
                     direction: .init(trendType: newGlucoseSample.trend),
                     date: Decimal(Int(newGlucoseSample.date.timeIntervalSince1970 * 1000)),
                     date: Decimal(Int(newGlucoseSample.date.timeIntervalSince1970 * 1000)),

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

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

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

@@ -19,7 +19,6 @@ protocol GlucoseStorage {
     func isGlucoseFresh() -> Bool
     func isGlucoseFresh() -> Bool
     func getGlucoseNotYetUploadedToNightscout() async throws -> [BloodGlucose]
     func getGlucoseNotYetUploadedToNightscout() async throws -> [BloodGlucose]
     func getCGMStateNotYetUploadedToNightscout() async throws -> [NightscoutTreatment]
     func getCGMStateNotYetUploadedToNightscout() async throws -> [NightscoutTreatment]
-    func getManualGlucoseNotYetUploadedToNightscout() async throws -> [NightscoutTreatment]
     func getGlucoseNotYetUploadedToHealth() async throws -> [BloodGlucose]
     func getGlucoseNotYetUploadedToHealth() async throws -> [BloodGlucose]
     func getManualGlucoseNotYetUploadedToHealth() async throws -> [BloodGlucose]
     func getManualGlucoseNotYetUploadedToHealth() async throws -> [BloodGlucose]
     func getGlucoseNotYetUploadedToTidepool() async throws -> [StoredGlucoseSample]
     func getGlucoseNotYetUploadedToTidepool() async throws -> [StoredGlucoseSample]
@@ -411,64 +410,28 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             }
             }
 
 
             return fetchedResults.map { result in
             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 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"
+                    )
+                }
             }
             }
         }
         }
     }
     }
@@ -501,7 +464,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
 
             return fetchedResults.map { result in
             return fetchedResults.map { result in
                 BloodGlucose(
                 BloodGlucose(
-                    _id: result.id?.uuidString ?? UUID().uuidString,
+                    id: result.id?.uuidString ?? UUID().uuidString,
                     sgv: Int(result.glucose),
                     sgv: Int(result.glucose),
                     direction: BloodGlucose.Direction(from: result.direction ?? ""),
                     direction: BloodGlucose.Direction(from: result.direction ?? ""),
                     date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
                     date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
@@ -533,7 +496,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
 
             return fetchedResults.map { result in
             return fetchedResults.map { result in
                 BloodGlucose(
                 BloodGlucose(
-                    _id: result.id?.uuidString ?? UUID().uuidString,
+                    id: result.id?.uuidString ?? UUID().uuidString,
                     sgv: Int(result.glucose),
                     sgv: Int(result.glucose),
                     direction: BloodGlucose.Direction(from: result.direction ?? ""),
                     direction: BloodGlucose.Direction(from: result.direction ?? ""),
                     date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
                     date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
@@ -565,7 +528,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
 
             return fetchedResults.map { result in
             return fetchedResults.map { result in
                 BloodGlucose(
                 BloodGlucose(
-                    _id: result.id?.uuidString ?? UUID().uuidString,
+                    id: result.id?.uuidString ?? UUID().uuidString,
                     sgv: Int(result.glucose),
                     sgv: Int(result.glucose),
                     direction: BloodGlucose.Direction(from: result.direction ?? ""),
                     direction: BloodGlucose.Direction(from: result.direction ?? ""),
                     date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
                     date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
@@ -598,7 +561,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
 
             return fetchedResults.map { result in
             return fetchedResults.map { result in
                 BloodGlucose(
                 BloodGlucose(
-                    _id: result.id?.uuidString ?? UUID().uuidString,
+                    id: result.id?.uuidString ?? UUID().uuidString,
                     sgv: Int(result.glucose),
                     sgv: Int(result.glucose),
                     direction: BloodGlucose.Direction(from: result.direction ?? ""),
                     direction: BloodGlucose.Direction(from: result.direction ?? ""),
                     date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
                     date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,

Plik diff jest za duży
+ 33 - 0
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)
         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(dateAsUnixTimestamp, forKey: .date)
 
 
         try container.encode(direction, forKey: .direction)
         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 {
     enum CodingKeys: String, CodingKey {
-        case _id
+        case legacyId = "_id"
+        case id
         case sgv
         case sgv
+        case mbg
         case direction
         case direction
         case date
         case date
         case dateString
         case dateString
@@ -77,7 +79,12 @@ struct BloodGlucose: JSON, Identifiable, Hashable, Codable {
 
 
     init(from decoder: Decoder) throws {
     init(from decoder: Decoder) throws {
         let container = try decoder.container(keyedBy: CodingKeys.self)
         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)
         sgv = try? container.decodeIfPresent(Int.self, forKey: .sgv)
         if sgv == nil {
         if sgv == nil {
@@ -87,6 +94,14 @@ struct BloodGlucose: JSON, Identifiable, Hashable, Codable {
             }
             }
             // If both attempts fail, sgv remains nil
             // 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)
         direction = try container.decodeIfPresent(Direction.self, forKey: .direction)
         date = try container.decode(Decimal.self, forKey: .date)
         date = try container.decode(Decimal.self, forKey: .date)
@@ -102,8 +117,10 @@ struct BloodGlucose: JSON, Identifiable, Hashable, Codable {
     }
     }
 
 
     init(
     init(
-        _id: String = UUID().uuidString,
+        id: String = UUID().uuidString,
+        legacyId: String? = nil,
         sgv: Int? = nil,
         sgv: Int? = nil,
+        mbg: Int? = nil,
         direction: Direction? = nil,
         direction: Direction? = nil,
         date: Decimal,
         date: Decimal,
         dateString: Date,
         dateString: Date,
@@ -116,8 +133,10 @@ struct BloodGlucose: JSON, Identifiable, Hashable, Codable {
         sessionStartDate: Date? = nil,
         sessionStartDate: Date? = nil,
         transmitterID: String? = nil
         transmitterID: String? = nil
     ) {
     ) {
-        self._id = _id
+        self.id = id
+        self.legacyId = legacyId
         self.sgv = sgv
         self.sgv = sgv
+        self.mbg = mbg
         self.direction = direction
         self.direction = direction
         self.date = date
         self.date = date
         self.dateString = dateString
         self.dateString = dateString
@@ -131,12 +150,10 @@ struct BloodGlucose: JSON, Identifiable, Hashable, Codable {
         self.transmitterID = transmitterID
         self.transmitterID = transmitterID
     }
     }
 
 
-    var _id: String?
-    var id: String {
-        _id ?? UUID().uuidString
-    }
-
+    let legacyId: String?
+    var id: String
     var sgv: Int?
     var sgv: Int?
+    var mbg: Int?
     var direction: Direction?
     var direction: Direction?
     let date: Decimal
     let date: Decimal
     let dateString: Date
     let dateString: Date

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

@@ -75,6 +75,7 @@ struct TrioSettings: JSON, Equatable, Encodable {
     var smartStackView: LockScreenView = .simple
     var smartStackView: LockScreenView = .simple
     var bolusShortcut: BolusShortcutLimit = .notAllowed
     var bolusShortcut: BolusShortcutLimit = .notAllowed
     var timeInRangeType: TimeInRangeType = .timeInTightRange
     var timeInRangeType: TimeInRangeType = .timeInTightRange
+    var requireAdjustmentsConfirmation: Bool = false
 
 
     /// Selected Garmin watchface (Trio or SwissAlpine)
     /// Selected Garmin watchface (Trio or SwissAlpine)
     var garminWatchface: GarminWatchface = .trio
     var garminWatchface: GarminWatchface = .trio
@@ -358,6 +359,10 @@ extension TrioSettings: Decodable {
             settings.timeInRangeType = timeInRangeType
             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) {
         if let garminWatchface = try? container.decode(GarminWatchface.self, forKey: .garminWatchface) {
             settings.garminWatchface = 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 overrideStorage: OverrideStorage!
         @ObservationIgnored @Injected() var nightscoutManager: NightscoutManager!
         @ObservationIgnored @Injected() var nightscoutManager: NightscoutManager!
 
 
+        var requireAdjustmentsConfirmation: Bool = false
+        var shouldDisplayPresetStartConfirmDialog: Bool = false
+
         // MARK: - Override and Temp Target Properties
         // MARK: - Override and Temp Target Properties
 
 
         var overridePercentage: Double = 100
         var overridePercentage: Double = 100
@@ -162,6 +165,7 @@ extension Adjustments {
                 target: tempTargetTarget,
                 target: tempTargetTarget,
                 autosensMax: autosensMax
                 autosensMax: autosensMax
             )
             )
+            requireAdjustmentsConfirmation = settingsManager.settings.requireAdjustmentsConfirmation
             Task {
             Task {
                 await getCurrentGlucoseTarget()
                 await getCurrentGlucoseTarget()
             }
             }
@@ -256,6 +260,7 @@ extension Adjustments.StateModel: SettingsObserver, PreferencesObserver {
     /// Updates settings when they change.
     /// Updates settings when they change.
     func settingsDidChange(_: TrioSettings) {
     func settingsDidChange(_: TrioSettings) {
         units = settingsManager.settings.units
         units = settingsManager.settings.units
+        requireAdjustmentsConfirmation = settingsManager.settings.requireAdjustmentsConfirmation
         Task {
         Task {
             await getCurrentGlucoseTarget()
             await getCurrentGlucoseTarget()
         }
         }

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

@@ -23,6 +23,7 @@ extension Adjustments {
         @State var isEditingTT = false
         @State var isEditingTT = false
         @State var showCancelOverrideConfirmDialog = false
         @State var showCancelOverrideConfirmDialog = false
         @State var showCancelTempTargetConfirmDialog = false
         @State var showCancelTempTargetConfirmDialog = false
+        @State var pendingPresetActivation: PendingPresetActivation?
 
 
         private var shouldDisplayStickyOverrideStopButton: Bool {
         private var shouldDisplayStickyOverrideStopButton: Bool {
             state.isOverrideEnabled && state.activeOverrideName.isNotEmpty
             state.isOverrideEnabled && state.activeOverrideName.isNotEmpty
@@ -171,6 +172,25 @@ extension Adjustments {
                 } message: {
                 } message: {
                     Text("Stop the Temp Target \"\(state.currentActiveTempTarget?.name ?? "")\"?")
                     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))
             }).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
+                }
+            }
+        }
+    }
+}

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

@@ -17,7 +17,7 @@ extension Adjustments.RootView {
         Section {
         Section {
             ForEach(state.overridePresets) { preset in
             ForEach(state.overridePresets) { preset in
                 overridesView(for: preset, showCheckMark: showOverrideCheckmark) {
                 overridesView(for: preset, showCheckMark: showOverrideCheckmark) {
-                    enactOverridePreset(preset)
+                    requestOverridePresetActivation(preset)
                 }
                 }
                 .contextMenu {
                 .contextMenu {
                     actionButtonsForOverrides(for: preset)
                     actionButtonsForOverrides(for: preset)
@@ -76,19 +76,14 @@ 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 actionButtonsForOverrides(for preset: OverrideStored) -> some View {
     func actionButtonsForOverrides(for preset: OverrideStored) -> some View {

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

@@ -34,7 +34,7 @@ extension Adjustments.RootView {
         Section {
         Section {
             ForEach(state.tempTargetPresets) { preset in
             ForEach(state.tempTargetPresets) { preset in
                 tempTargetView(for: preset, showCheckmark: showTempTargetCheckmark) {
                 tempTargetView(for: preset, showCheckmark: showTempTargetCheckmark) {
-                    enactTempTargetPreset(preset)
+                    requestTempTargetPresetActivation(preset)
                 }
                 }
                 .contextMenu {
                 .contextMenu {
                     actionButtonsForTempTargets(for: preset)
                     actionButtonsForTempTargets(for: preset)
@@ -64,17 +64,14 @@ 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 actionButtonsForTempTargets(for tempTarget: TempTargetStored) -> some View {
     private func actionButtonsForTempTargets(for tempTarget: TempTargetStored) -> some View {

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

@@ -5,13 +5,37 @@ extension DynamicSettings {
     struct RootView: BaseView {
     struct RootView: BaseView {
         let resolver: Resolver
         let resolver: Resolver
         @StateObject var state = StateModel()
         @StateObject var state = StateModel()
-        @State private var shouldDisplayHint: Bool = false
         @State var hintDetent = PresentationDetent.large
         @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 decimalPlaceholder: Decimal = 0.0
         @State private var booleanPlaceholder: Bool = false
         @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 {
         private var conversionFormatter: NumberFormatter {
             let formatter = NumberFormatter()
             let formatter = NumberFormatter()
             formatter.numberStyle = .decimal
             formatter.numberStyle = .decimal
@@ -76,9 +100,9 @@ extension DynamicSettings {
                                 Spacer()
                                 Spacer()
                                 Button(
                                 Button(
                                     action: {
                                     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) {
                                                 VStack(alignment: .leading, spacing: 10) {
                                                     Text("Default: Disabled").bold()
                                                     Text("Default: Disabled").bold()
                                                     Text(
                                                     Text(
@@ -124,7 +148,7 @@ extension DynamicSettings {
                                                     }
                                                     }
                                                 }
                                                 }
                                             )
                                             )
-                                        shouldDisplayHint.toggle()
+                                        )
                                     },
                                     },
                                     label: {
                                     label: {
                                         HStack {
                                         HStack {
@@ -142,14 +166,8 @@ extension DynamicSettings {
                         SettingInputSection(
                         SettingInputSection(
                             decimalValue: $state.adjustmentFactor,
                             decimalValue: $state.adjustmentFactor,
                             booleanValue: $booleanPlaceholder,
                             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
                             // TODO?: include conditional links to Desmos logarithmic graphs based on which .glucose setting is used
                             units: state.units,
                             units: state.units,
                             type: .decimal("adjustmentFactor"),
                             type: .decimal("adjustmentFactor"),
@@ -173,14 +191,8 @@ extension DynamicSettings {
                         SettingInputSection(
                         SettingInputSection(
                             decimalValue: $state.adjustmentFactorSigmoid,
                             decimalValue: $state.adjustmentFactorSigmoid,
                             booleanValue: $booleanPlaceholder,
                             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,
                             units: state.units,
                             type: .decimal("adjustmentFactorSigmoid"),
                             type: .decimal("adjustmentFactorSigmoid"),
                             label: String(localized: "Sigmoid Adjustment Factor"),
                             label: String(localized: "Sigmoid Adjustment Factor"),
@@ -207,14 +219,8 @@ extension DynamicSettings {
                     SettingInputSection(
                     SettingInputSection(
                         decimalValue: $state.weightPercentage,
                         decimalValue: $state.weightPercentage,
                         booleanValue: $booleanPlaceholder,
                         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,
                         units: state.units,
                         type: .decimal("weightPercentage"),
                         type: .decimal("weightPercentage"),
                         label: String(localized: "Weighted Average of TDD"),
                         label: String(localized: "Weighted Average of TDD"),
@@ -236,14 +242,8 @@ extension DynamicSettings {
                     SettingInputSection(
                     SettingInputSection(
                         decimalValue: $decimalPlaceholder,
                         decimalValue: $decimalPlaceholder,
                         booleanValue: $state.tddAdjBasal,
                         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,
                         units: state.units,
                         type: .boolean,
                         type: .boolean,
                         label: String(localized: "Adjust Basal"),
                         label: String(localized: "Adjust Basal"),
@@ -264,12 +264,12 @@ extension DynamicSettings {
                 }
                 }
             }
             }
             .listSectionSpacing(sectionSpacing)
             .listSectionSpacing(sectionSpacing)
-            .sheet(isPresented: $shouldDisplayHint) {
+            .sheet(item: $hintPayload) { payload in
                 SettingInputHintView(
                 SettingInputHintView(
                     hintDetent: $hintDetent,
                     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")
                     sheetTitle: String(localized: "Help", comment: "Help sheet title")
                 )
                 )
             }
             }

+ 1 - 1
Trio/Sources/Modules/History/HistoryDataFlow.swift

@@ -87,7 +87,7 @@ enum History {
 protocol HistoryProvider: Provider {
 protocol HistoryProvider: Provider {
     func deleteCarbsFromNightscout(withID id: String)
     func deleteCarbsFromNightscout(withID id: String)
     func deleteInsulinFromNightscout(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 deleteGlucoseFromHealth(withSyncID id: String)
     func deleteMealDataFromHealth(byID id: String, sampleType: HKSampleType)
     func deleteMealDataFromHealth(byID id: String, sampleType: HKSampleType)
     func deleteInsulinFromHealth(withSyncID id: String)
     func deleteInsulinFromHealth(withSyncID id: String)

+ 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
             Task.detached { [weak self] in
                 guard let self = self else { return }
                 guard let self = self else { return }
-                await self.nightscoutManager.deleteManualGlucose(withID: id)
+                await self.nightscoutManager.deleteGlucose(withID: id, withDate: date)
             }
             }
         }
         }
 
 

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

@@ -33,8 +33,8 @@ extension History.StateModel {
                 }
                 }
 
 
                 // Delete from Nightscout
                 // Delete from Nightscout
-                if let id = glucoseToDelete.id?.uuidString {
-                    self.provider.deleteManualGlucoseFromNightscout(withID: id)
+                if let id = glucoseToDelete.id?.uuidString, let date = glucoseToDelete.date {
+                    self.provider.deleteGlucoseFromNightscout(withID: id, withDate: date)
                 }
                 }
 
 
                 // Delete from Apple Health
                 // Delete from Apple Health

+ 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))
             }.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 {
         @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
             /// 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
             /// - TRUE:  show the pump bolus
@@ -849,14 +828,14 @@ extension Home {
                         }
                         }
                     }.padding(.horizontal, 10)
                     }.padding(.horizontal, 10)
                         .padding(.trailing, 8)
                         .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 resolver: Resolver
         let displayClose: Bool
         let displayClose: Bool
         @StateObject var state = StateModel()
         @StateObject var state = StateModel()
-        @State private var shouldDisplayHint: Bool = false
         @State var hintDetent = PresentationDetent.large
         @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 decimalPlaceholder: Decimal = 0.0
         @State private var booleanPlaceholder: Bool = false
         @State private var booleanPlaceholder: Bool = false
         @State var backfillAlert: Alert?
         @State var backfillAlert: Alert?
         @State var isBackfillAlertPresented = false
         @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(\.colorScheme) var colorScheme
         @Environment(AppState.self) var appState
         @Environment(AppState.self) var appState
 
 
@@ -79,14 +90,14 @@ extension NightscoutConfig {
                                     Spacer()
                                     Spacer()
                                     Button(
                                     Button(
                                         action: {
                                         action: {
-                                            hintLabel = String(localized: "Backfill Glucose from Nightscout")
-                                            selectedVerboseHint =
-                                                AnyView(
+                                            hintPayload = HintPayload(
+                                                label: String(localized: "Backfill Glucose from Nightscout"),
+                                                content: AnyView(
                                                     Text(
                                                     Text(
                                                         "This will backfill 24 hours of glucose data from your connected Nightscout URL to Trio"
                                                         "This will backfill 24 hours of glucose data from your connected Nightscout URL to Trio"
                                                     )
                                                     )
                                                 )
                                                 )
-                                            shouldDisplayHint.toggle()
+                                            )
                                         },
                                         },
                                         label: {
                                         label: {
                                             HStack {
                                             HStack {
@@ -104,12 +115,12 @@ extension NightscoutConfig {
                 }
                 }
                 .listSectionSpacing(sectionSpacing)
                 .listSectionSpacing(sectionSpacing)
             }
             }
-            .sheet(isPresented: $shouldDisplayHint) {
+            .sheet(item: $hintPayload) { payload in
                 SettingInputHintView(
                 SettingInputHintView(
                     hintDetent: $hintDetent,
                     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")
                     sheetTitle: String(localized: "Help", comment: "Help sheet title")
                 )
                 )
             }
             }

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

@@ -240,7 +240,8 @@ enum SettingItems {
                 "Glucose Color Scheme",
                 "Glucose Color Scheme",
                 "Time in Range Type",
                 "Time in Range Type",
                 "Time in Tight Range (TITR)",
                 "Time in Tight Range (TITR)",
-                "Time in Normoglycemia (TING)"
+                "Time in Normoglycemia (TING)",
+                "Require Adjustments Confirmation"
             ],
             ],
             path: ["Features", "User Interface"]
             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"),
                     name: String(localized: "Time in Range Type"),
                     value: trioSettings.timeInRangeType.rawValue
                     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
                 // Appearance setting from UserDefaults
                 let colorSchemePreference = UserDefaults.standard.string(forKey: "colorSchemePreference") ?? "systemDefault"
                 let colorSchemePreference = UserDefaults.standard.string(forKey: "colorSchemePreference") ?? "systemDefault"

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

@@ -124,6 +124,7 @@ extension Treatments {
         let viewContext = CoreDataStack.shared.persistentContainer.viewContext
         let viewContext = CoreDataStack.shared.persistentContainer.viewContext
         let glucoseFetchContext = CoreDataStack.shared.newTaskContext()
         let glucoseFetchContext = CoreDataStack.shared.newTaskContext()
         let determinationFetchContext = CoreDataStack.shared.newTaskContext()
         let determinationFetchContext = CoreDataStack.shared.newTaskContext()
+        let pumpHistoryFetchContext = CoreDataStack.shared.newTaskContext()
 
 
         var isActive: Bool = false
         var isActive: Bool = false
 
 
@@ -137,8 +138,9 @@ extension Treatments {
 
 
         typealias PumpEvent = PumpEventStored.EventType
         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() {
         func unsubscribe() {
             subscriptions.forEach { $0.cancel() }
             subscriptions.forEach { $0.cancel() }
@@ -173,7 +175,7 @@ extension Treatments {
             hasCleanedUp = true
             hasCleanedUp = true
 
 
             unsubscribe()
             unsubscribe()
-            bolusProgressCancellable?.cancel()
+            lifetime = Lifetime()
 
 
             broadcaster?.unregister(DeterminationObserver.self, observer: self)
             broadcaster?.unregister(DeterminationObserver.self, observer: self)
             broadcaster?.unregister(BolusFailureObserver.self, observer: self)
             broadcaster?.unregister(BolusFailureObserver.self, observer: self)
@@ -198,6 +200,9 @@ extension Treatments {
                         group.addTask {
                         group.addTask {
                             self.registerObservers()
                             self.registerObservers()
                         }
                         }
+                        group.addTask {
+                            self.setupLastBolus()
+                        }
 
 
                         // Wait for all tasks to complete
                         // Wait for all tasks to complete
                         try await group.waitForAll()
                         try await group.waitForAll()
@@ -208,22 +213,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() {
         private func subscribeToBolusProgress() {
-            bolusProgressCancellable = apsManager.bolusProgress
+            apsManager.bolusProgress
                 .receive(on: DispatchQueue.main)
                 .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
         // MARK: - Basal
@@ -744,6 +748,11 @@ extension Treatments.StateModel {
             guard let self = self else { return }
             guard let self = self else { return }
             self.setupGlucoseArray()
             self.setupGlucoseArray()
         }.store(in: &subscriptions)
         }.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() {
     private func registerSubscribers() {
@@ -999,3 +1008,47 @@ private extension Predictions {
         iob == nil && zt == nil && cob == nil && uam == nil
         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 results = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: PumpEventStored.self,
+            onContext: pumpHistoryFetchContext,
+            predicate: NSPredicate.lastPumpBolus,
+            key: "timestamp",
+            ascending: false,
+            fetchLimit: 1
+        )
+
+        return try await pumpHistoryFetchContext.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
             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 {
         private var mealFormatter: NumberFormatter {
             let formatter = NumberFormatter()
             let formatter = NumberFormatter()
             formatter.numberStyle = .decimal
             formatter.numberStyle = .decimal
@@ -475,6 +492,9 @@ extension Treatments {
         }
         }
 
 
         var treatmentButton: some View {
         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)
             var treatmentButtonBackground = Color(.systemBlue)
             if limitExceeded {
             if limitExceeded {
                 treatmentButtonBackground = Color(.systemRed)
                 treatmentButtonBackground = Color(.systemRed)
@@ -483,41 +503,43 @@ extension Treatments {
             }
             }
 
 
             return Section {
             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: {
             } 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 {
         private var taskButtonLabel: some View {
             if pumpBolusLimitExceeded {
             if pumpBolusLimitExceeded {
                 return Text("Max Bolus of \(state.maxBolus.description) U Exceeded")
                 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 hasFatOrProtein = state.fat > 0 || state.protein > 0
             let bolusString = state.externalInsulin ? String(localized: "External Insulin") : String(localized: "Enact Bolus")
             let bolusString = state.externalInsulin ? String(localized: "External Insulin") : String(localized: "Enact Bolus")
 
 
-            if state.isBolusInProgress && hasInsulin && !state.externalInsulin && (!hasCarbs || !hasFatOrProtein) {
-                return Text("Bolus In Progress...")
-            }
+            // 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) {
             switch (hasInsulin, hasCarbs, hasFatOrProtein) {
             case (true, true, true):
             case (true, true, true):
@@ -562,7 +652,7 @@ extension Treatments {
             case (true, false, true):
             case (true, false, true):
                 return Text("Log FPU and \(bolusString)")
                 return Text("Log FPU and \(bolusString)")
             case (true, false, false):
             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):
             case (false, true, true):
                 return Text("Log Meal")
                 return Text("Log Meal")
             case (false, true, false):
             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 glucoseColorScheme: GlucoseColorScheme = .staticColor
         @Published var eA1cDisplayUnit: EstimatedA1cDisplayUnit = .percent
         @Published var eA1cDisplayUnit: EstimatedA1cDisplayUnit = .percent
         @Published var timeInRangeType: TimeInRangeType = .timeInTightRange
         @Published var timeInRangeType: TimeInRangeType = .timeInTightRange
+        @Published var requireAdjustmentsConfirmation: Bool = false
 
 
         var units: GlucoseUnits = .mgdL
         var units: GlucoseUnits = .mgdL
 
 
@@ -44,6 +45,9 @@ extension UserInterfaceSettings {
             subscribeSetting(\.eA1cDisplayUnit, on: $eA1cDisplayUnit) { eA1cDisplayUnit = $0 }
             subscribeSetting(\.eA1cDisplayUnit, on: $eA1cDisplayUnit) { eA1cDisplayUnit = $0 }
 
 
             subscribeSetting(\.timeInRangeType, on: $timeInRangeType) { timeInRangeType = $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")
                     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)
             .listSectionSpacing(sectionSpacing)
             .sheet(isPresented: $shouldDisplayHint) {
             .sheet(isPresented: $shouldDisplayHint) {

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

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

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

@@ -188,14 +188,21 @@ extension NightscoutAPI {
         return
         return
     }
     }
 
 
-    func deleteManualGlucose(withId id: String) async throws {
+    func deleteGlucose(withId id: String, withDate date: Date) async throws {
         var components = URLComponents()
         var components = URLComponents()
         components.scheme = url.scheme
         components.scheme = url.scheme
         components.host = url.host
         components.host = url.host
         components.port = url.port
         components.port = url.port
-        components.path = Config.treatmentsPath
+        components.path = Config.uploadEntriesPath
         components.queryItems = [
         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 {
         guard let url = components.url else {
@@ -216,8 +223,6 @@ extension NightscoutAPI {
         guard let httpResponse = response as? HTTPURLResponse, (200 ... 299).contains(httpResponse.statusCode) else {
         guard let httpResponse = response as? HTTPURLResponse, (200 ... 299).contains(httpResponse.statusCode) else {
             throw URLError(.badServerResponse)
             throw URLError(.badServerResponse)
         }
         }
-
-        debugPrint("Delete successful for ID \(id)")
     }
     }
 
 
     func deleteInsulin(withId id: String) async throws {
     func deleteInsulin(withId id: String) async throws {

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

@@ -11,14 +11,13 @@ protocol NightscoutManager: GlucoseSource {
     func fetchTempTargets() async -> [TempTarget]
     func fetchTempTargets() async -> [TempTarget]
     func deleteCarbs(withID id: String) async
     func deleteCarbs(withID id: String) async
     func deleteInsulin(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 uploadDeviceStatus() async throws
     func uploadGlucose() async
     func uploadGlucose() async
     func uploadCarbs() async
     func uploadCarbs() async
     func uploadPumpHistory() async
     func uploadPumpHistory() async
     func uploadOverrides() async
     func uploadOverrides() async
     func uploadTempTargets() async
     func uploadTempTargets() async
-    func uploadManualGlucose() async
     func uploadProfiles() async throws
     func uploadProfiles() async throws
     func uploadNoteTreatment(note: String) async
     func uploadNoteTreatment(note: String) async
     func importSettings() async -> ScheduledNightscoutProfile?
     func importSettings() async -> ScheduledNightscoutProfile?
@@ -54,7 +53,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     /// coalesce into a single upload run for that pipeline.
     /// coalesce into a single upload run for that pipeline.
     let uploadPipelineInterval: [NightscoutUploadPipeline: TimeInterval] = [
     let uploadPipelineInterval: [NightscoutUploadPipeline: TimeInterval] = [
         .carbs: 2, .pumpHistory: 2, .overrides: 2, .tempTargets: 2,
         .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
     /// Subjects used to request an upload pipeline. The pipeline applies a throttle so
@@ -95,7 +94,6 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         case .overrides: await uploadOverrides()
         case .overrides: await uploadOverrides()
         case .tempTargets: await uploadTempTargets()
         case .tempTargets: await uploadTempTargets()
         case .glucose: await uploadGlucose()
         case .glucose: await uploadGlucose()
-        case .manualGlucose: await uploadManualGlucose()
         case .deviceStatus:
         case .deviceStatus:
             do { try await uploadDeviceStatus() }
             do { try await uploadDeviceStatus() }
             catch { debug(.nightscout, "deviceStatus upload failed: \(error)") }
             catch { debug(.nightscout, "deviceStatus upload failed: \(error)") }
@@ -339,15 +337,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 }
         guard let nightscout = nightscoutAPI, isUploadEnabled else { return }
 
 
         do {
         do {
-            try await nightscout.deleteManualGlucose(withId: id)
+            try await nightscout.deleteGlucose(withId: id, withDate: date)
+            debug(.nightscout, "Glucose deleted")
         } catch {
         } catch {
             debug(
             debug(
                 .nightscout,
                 .nightscout,
-                "\(DebuggingIdentifiers.failed) Failed to delete Manual Glucose from Nightscout with error: \(error)"
+                "\(DebuggingIdentifiers.failed) Failed to delete Glucose from Nightscout with error: \(error)"
             )
             )
         }
         }
     }
     }
@@ -772,17 +771,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 {
     func uploadPumpHistory() async {
         do {
         do {
             try await uploadPumpHistory(pumpHistoryStorage.getPumpHistoryNotYetUploadedToNightscout())
             try await uploadPumpHistory(pumpHistoryStorage.getPumpHistoryNotYetUploadedToNightscout())
@@ -928,47 +916,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 {
-        await backgroundContext.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 self.backgroundContext.fetch(fetchRequest)
-                for result in results {
-                    result.isUploadedToNS = true
-                }
-
-                guard self.backgroundContext.hasChanges else { return }
-                try self.backgroundContext.save()
-            } catch let error as NSError {
-                debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToNS: \(error.userInfo)"
-                )
-            }
-        }
-    }
-
     private func uploadCarbs(_ treatments: [NightscoutTreatment]) async {
     private func uploadCarbs(_ treatments: [NightscoutTreatment]) async {
         guard !treatments.isEmpty, let nightscout = nightscoutAPI, isUploadEnabled else {
         guard !treatments.isEmpty, let nightscout = nightscoutAPI, isUploadEnabled else {
             return
             return

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

@@ -9,7 +9,6 @@ public enum NightscoutUploadPipeline: String, CaseIterable {
     case overrides
     case overrides
     case tempTargets
     case tempTargets
     case glucose
     case glucose
-    case manualGlucose
     case deviceStatus
     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")
         #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(
         "Test glucose alarms",
         "Test glucose alarms",
         .enabled(if: false, "Flaky test, disabled while investigating")
         .enabled(if: false, "Flaky test, disabled while investigating")