ソースを参照

Merge branch 'dev' into fix-temp-target-enteredby

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

+ 1 - 1
CGMBLEKit

@@ -1 +1 @@
-Subproject commit ba5d0b7daf83d282b587c8ff0e835162b8c75846
+Subproject commit 98fae7929c8c8e4e849d18a70c1f249dd6c09e5f

+ 135 - 0
CONTRIBUTING.md

@@ -0,0 +1,135 @@
+# Contributing to Trio
+
+Thank you for your interest in contributing to Trio.
+
+Trio is a community effort, and contributions of all kinds are welcome. This document outlines some guidelines, good practices, and expectations for contributing to the project, with the goal of making collaboration and review as smooth as possible.
+
+Whether you are helping other users, improving documentation, translating the app, testing builds, reviewing code, or contributing new features and fixes, your work matters.
+
+## Ways to contribute
+
+There are many ways to support the Trio community:
+
+- **Help others** by answering questions and guiding users in support communities.
+- Improve the **documentation** by updating or expanding TrioDocs.
+- Improve the **app** by contributing code, fixes, features, or tests.
+- Help with **translation and localization** through Crowdin.
+- Support **testing and feedback** by validating changes and reporting issues clearly.
+
+### Pay it forward
+
+If Trio has helped you manage your diabetes successfully, consider paying it forward by helping others. Answering questions in [Discord](https://discord.triodocs.org) or [Facebook](https://facebook.triodocs.org) groups can make a real difference for someone getting started.
+
+### Translate
+
+Trio is translated into multiple languages to make it easier to understand and use around the world. Translation is managed through [Crowdin](https://crowdin.triodocs.org/) and does not require programming experience.
+
+If your preferred language is missing, or you would like to improve an existing translation, please sign up as a translator on [Crowdin](https://crowdin.com/project/trio).
+
+### Develop
+
+Do you work with Swift? UI/UX? Testing? API optimization? Data storage?
+
+Trio is a collaborative project, and contributions of all kinds are welcome. Whether you are writing code, improving the user experience, testing builds, helping with documentation, or contributing in other ways, your help matters.
+
+## General principles
+
+- Start small. Smaller, focused contributions are easier to review, test, and merge.
+- For larger changes or new features, open or reference an issue first so there is a clear place for discussion and progress tracking.
+- Reach out early if you are planning to work on something substantial, especially if it may overlap with work already in progress.
+- Keep discussions constructive, respectful, and focused on improving Trio for the community.
+- Remember that Trio is part of a wider open source AID ecosystem. Collaboration and maintainability matter just as much as shipping features.
+
+## Development guidelines
+
+### Coding conventions
+
+- Use Xcode and follow the existing formatting and style used throughout the codebase.
+- Keep indentation and formatting consistent in every file you change.
+- Format your code before committing.
+- Avoid unrelated formatting-only changes in files you are not otherwise modifying.
+- Prefer clear, readable code over clever or overly compact solutions.
+- Follow existing naming, file organization, and architectural patterns unless there is a good reason not to.
+
+### Strings and localization
+
+- Add new user-facing strings in the appropriate localization mechanism used by the app.
+- Provide English source strings only unless the contribution is specifically about translations.
+- Translation and localization for other languages should go through [Crowdin](https://crowdin.triodocs.org/).
+- Translations from shared pump managers, CGM managers and services are handled by the [Loop lokalise project](https://loopkit.github.io/loopdocs/faqs/app-translation/#code-translation).
+
+### Documentation
+
+- Update docstrings when your change affects setup, configuration, behavior, workflows, or troubleshooting.
+- Keep documentation changes clear and practical.
+- +1: Documentation contributions are just as valuable as code contributions.
+
+## Branches, commits, and pull requests
+
+### Getting started
+
+1. Fork the [Trio repository](https://github.com/nightscout/Trio) on GitHub.
+1. Create a separate branch for each feature or fix with an [appropriate name](#branch-names).
+1. Branch from the most recent appropriate development branch (typically `dev`).
+1. Commit your changes to your fork.
+1. When ready, open a pull request against the upstream repository (`nightscout/Trio`).
+
+### Before opening a pull request
+
+- Rebase or otherwise sync your branch with the latest target branch.
+- Make sure your change is focused and does not include unrelated edits.
+- Test your changes as thoroughly as you reasonably can.
+- Update relevant documentation when needed.
+- Double-check for debug code, commented-out code, accidental version changes, or temporary workarounds left behind.
+
+### Pull request guidance
+
+- Keep pull requests as small and focused as practical.
+- Use a clear title and description.
+- Explain **what** changed and **why**.
+- Link the relevant issue when applicable.
+- Mention any areas that need particular review attention.
+- Be open to feedback and follow-up changes during review.
+- Use AI tools, if at all, as a support for small, well-understood tasks rather than to generate large parts of a contribution
+- Do not submit AI-heavy or "vibe-coded" pull requests; we welcome thoughtful use of tooling, but contributions need to be intentionally designed.
+
+## Naming conventions
+
+### Branch names
+
+Use short, descriptive branch names that make the purpose of the change obvious. For example:
+
+- `fix/watchstate-sync`
+- `feature/onboarding-target-behavior`
+- `refactor/therapy-editor`
+
+### Pull request titles
+
+Use concise, descriptive pull request titles. Good titles usually start with the type of change, for example:
+
+- `Fix watch state sync timing issue`
+- `Add onboarding step for target behavior`
+- `Update build documentation`
+- `Refactor therapy editor validation`
+
+## Communication and coordination
+
+For new ideas, larger features, or work that may affect multiple parts of the app, **discuss it with the community first** — reach out to the contributor core on [Trio Discord](https://discord.triodocs.org/). This helps reduce duplicate work, avoid merge conflicts, and improve the final design.
+
+## Review expectations
+
+Please remember that Trio is maintained by contributors with limited time. Reviews may take time, and some pull requests may require iteration before they are ready to merge.
+
+To help keep reviews efficient:
+
+- Keep the scope narrow.
+- Explain your reasoning clearly.
+- Respond to review comments directly.
+- Avoid force-pushing large unexplained rewrites during active review unless necessary.
+- AI-assisted work is welcome for limited, well-understood tasks, but contributions should remain author-driven and must be code you fully understand, and can explain.
+
+We do not accept pull requests that are largely AI-generated or submitted without careful engineering judgment, testing, and alignment with Trio’s existing patterns.
+
+## Final note
+
+Trio exists because people choose to contribute their time, knowledge, and care to a shared effort. Thank you for helping improve the project and support the broader open source AID community.

+ 2 - 2
Config.xcconfig

@@ -18,8 +18,8 @@ BUNDLE_IDENTIFIER = org.nightscout.$(DEVELOPMENT_TEAM).trio
 TRIO_APP_GROUP_ID = group.org.nightscout.$(DEVELOPMENT_TEAM).trio.trio-app-group
 
 // The developers set the version numbers, please leave them alone
-APP_VERSION = 0.6.0
-APP_DEV_VERSION = 0.6.0.82
+APP_VERSION = 0.7.0
+APP_DEV_VERSION = 0.7.0.13
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 

+ 1 - 1
DanaKit

@@ -1 +1 @@
-Subproject commit 0158fc85391725bb1855ea34469d48cb65667850
+Subproject commit de8ef83124230bb43b3937041538bba1f5e38cec

+ 3 - 1
Gemfile

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

+ 14 - 10
Gemfile.lock

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

+ 1 - 1
MedtrumKit

@@ -1 +1 @@
-Subproject commit b7f3d44c06bb7c580be897e0414e64de2d6dd995
+Subproject commit 7a3cb276b65ebaee7a3d6485878baa525399c3ee

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

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

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

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

+ 1 - 1
Model/JSONImporter.swift

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

+ 1 - 1
OmniBLE

@@ -1 +1 @@
-Subproject commit b0b78e66a6962677970a00e5d37ae4157e548b8a
+Subproject commit 1912793284b736754a0f25cf3d828e2eecd9ff2f

ファイルの差分が大きいため隠しています
+ 35 - 35
README.md


+ 64 - 0
Trio.xcodeproj/project.pbxproj

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

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

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

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

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

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

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

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

@@ -19,7 +19,6 @@ protocol GlucoseStorage {
     func isGlucoseFresh() -> Bool
     func getGlucoseNotYetUploadedToNightscout() async throws -> [BloodGlucose]
     func getCGMStateNotYetUploadedToNightscout() async throws -> [NightscoutTreatment]
-    func getManualGlucoseNotYetUploadedToNightscout() async throws -> [NightscoutTreatment]
     func getGlucoseNotYetUploadedToHealth() async throws -> [BloodGlucose]
     func getManualGlucoseNotYetUploadedToHealth() async throws -> [BloodGlucose]
     func getGlucoseNotYetUploadedToTidepool() async throws -> [StoredGlucoseSample]
@@ -411,64 +410,28 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             }
 
             return fetchedResults.map { result in
-                BloodGlucose(
-                    _id: result.id?.uuidString ?? UUID().uuidString,
-                    sgv: Int(result.glucose),
-                    direction: BloodGlucose.Direction(from: result.direction ?? ""),
-                    date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
-                    dateString: result.date ?? Date(),
-                    unfiltered: Decimal(result.glucose),
-                    filtered: Decimal(result.glucose),
-                    noise: nil,
-                    glucose: Int(result.glucose),
-                    type: "sgv"
-                )
-            }
-        }
-    }
-
-    // Fetch manual glucose that is not uploaded to Nightscout yet
-    /// - Returns: Array of NightscoutTreatment to ensure the correct format for the NS Upload
-    func getManualGlucoseNotYetUploadedToNightscout() async throws -> [NightscoutTreatment] {
-        let 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
                 BloodGlucose(
-                    _id: result.id?.uuidString ?? UUID().uuidString,
+                    id: result.id?.uuidString ?? UUID().uuidString,
                     sgv: Int(result.glucose),
                     direction: BloodGlucose.Direction(from: result.direction ?? ""),
                     date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
@@ -533,7 +496,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
             return fetchedResults.map { result in
                 BloodGlucose(
-                    _id: result.id?.uuidString ?? UUID().uuidString,
+                    id: result.id?.uuidString ?? UUID().uuidString,
                     sgv: Int(result.glucose),
                     direction: BloodGlucose.Direction(from: result.direction ?? ""),
                     date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
@@ -565,7 +528,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
             return fetchedResults.map { result in
                 BloodGlucose(
-                    _id: result.id?.uuidString ?? UUID().uuidString,
+                    id: result.id?.uuidString ?? UUID().uuidString,
                     sgv: Int(result.glucose),
                     direction: BloodGlucose.Direction(from: result.direction ?? ""),
                     date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,
@@ -598,7 +561,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
             return fetchedResults.map { result in
                 BloodGlucose(
-                    _id: result.id?.uuidString ?? UUID().uuidString,
+                    id: result.id?.uuidString ?? UUID().uuidString,
                     sgv: Int(result.glucose),
                     direction: BloodGlucose.Direction(from: result.direction ?? ""),
                     date: Decimal(result.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000,

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

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

ファイルの差分が大きいため隠しています
+ 34 - 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)
 
-        let dateAsUnixTimestamp = String(format: "%.0f", (date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000)
+        let dateAsUnixTimestamp = Int64((date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) * 1000)
         try container.encode(dateAsUnixTimestamp, forKey: .date)
 
         try container.encode(direction, forKey: .direction)

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

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

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

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

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

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

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

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

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

@@ -17,10 +17,13 @@ extension Adjustments.RootView {
         Section {
             ForEach(state.overridePresets) { preset in
                 overridesView(for: preset, showCheckMark: showOverrideCheckmark) {
-                    enactOverridePreset(preset)
+                    requestOverridePresetActivation(preset)
+                }
+                .contextMenu {
+                    actionButtonsForOverrides(for: preset)
                 }
                 .swipeActions(edge: .trailing, allowsFullSwipe: true) {
-                    swipeActionsForOverrides(for: preset)
+                    actionButtonsForOverrides(for: preset)
                 }
             }
             .onMove(perform: state.reorderOverride)
@@ -73,24 +76,19 @@ extension Adjustments.RootView {
         }
     }
 
-    func enactOverridePreset(_ preset: OverrideStored) {
-        Task {
-            let objectID = preset.objectID
-            await state.enactOverridePreset(withID: objectID)
-            state.hideModal()
-            selectedOverridePresetID = preset.id
-            showOverrideCheckmark = true
-
-            // Deactivate checkmark after 3 seconds
-            DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
-                showOverrideCheckmark = false
-            }
-        }
+    private func requestOverridePresetActivation(_ preset: OverrideStored) {
+        let activation = PendingPresetActivation.override(
+            objectID: preset.objectID,
+            presetID: preset.id,
+            name: preset.name ?? ""
+        )
+
+        requestPresetActivation(activation)
     }
 
-    func swipeActionsForOverrides(for preset: OverrideStored) -> some View {
+    func actionButtonsForOverrides(for preset: OverrideStored) -> some View {
         Group {
-            Button(role: .none) {
+            Button(role: .destructive) {
                 selectedOverride = preset
                 isConfirmDeletePresented = true
             } label: {

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -0,0 +1,291 @@
+import CoreData
+import Foundation
+
+extension History.StateModel {
+    // MARK: - Entry Management
+
+    /// Updates a carb/FPU entry with new values and handles the necessary cleanup and recreation of FPU entries
+    /// - Parameters:
+    ///   - treatmentObjectID: The ID of the entry to update
+    ///   - newCarbs: The new carbs value
+    ///   - newFat: The new fat value
+    ///   - newProtein: The new protein value
+    ///   - newNote: The new note text
+    ///   - newDate: The new date for the entry
+    func updateEntry(
+        _ treatmentObjectID: NSManagedObjectID,
+        newCarbs: Decimal,
+        newFat: Decimal,
+        newProtein: Decimal,
+        newNote: String,
+        newDate: Date
+    ) {
+        Task {
+            do {
+                // Get original date from entry to re-create the entry later with the updated values and the same date
+                guard let originalEntry = await getOriginalEntryValues(treatmentObjectID) else { return }
+
+                // Deletion logic for carb and FPU entries
+                try await deleteOldEntries(
+                    treatmentObjectID,
+                    originalEntry: originalEntry,
+                    newCarbs: newCarbs,
+                    newFat: newFat,
+                    newProtein: newProtein,
+                    newNote: newNote
+                )
+
+                try await createNewEntries(
+                    originalDate: newDate,
+                    newCarbs: newCarbs,
+                    newFat: newFat,
+                    newProtein: newProtein,
+                    newNote: newNote
+                )
+
+                await syncWithServices()
+
+                // Perform a determine basal sync to update cob
+                try await apsManager.determineBasalSync()
+
+            } catch {
+                debug(.default, "\(DebuggingIdentifiers.failed) failed to update entry: \(error)")
+            }
+        }
+    }
+
+    private func createNewEntries(
+        originalDate: Date,
+        newCarbs: Decimal,
+        newFat: Decimal,
+        newProtein: Decimal,
+        newNote: String
+    ) async throws {
+        let newEntry = CarbsEntry(
+            id: UUID().uuidString,
+            createdAt: Date(),
+            actualDate: originalDate,
+            carbs: newCarbs,
+            fat: newFat,
+            protein: newProtein,
+            note: newNote,
+            enteredBy: CarbsEntry.local,
+            isFPU: false,
+            fpuID: newFat > 0 || newProtein > 0 ? UUID().uuidString : nil
+        )
+
+        // Handles internally whether to create fake carbs or not based on whether fat > 0 or protein > 0
+        try await carbsStorage.storeCarbs([newEntry], areFetchedFromRemote: false)
+    }
+
+    /// Deletes the old carb/ FPU entries and creates new ones with updated values
+    /// - Parameters:
+    ///   - treatmentObjectID: The ID of the entry to delete
+    ///   - originalDate: The original date to preserve
+    ///   - newCarbs: The new carbs value
+    ///   - newFat: The new fat value
+    ///   - newProtein: The new protein value
+    ///   - newNote: The new note text
+    private func deleteOldEntries(
+        _ treatmentObjectID: NSManagedObjectID,
+        originalEntry: (
+            entryValues: (date: Date, carbs: Double, fat: Double, protein: Double)?,
+            entryId: NSManagedObjectID
+        ),
+        newCarbs _: Decimal,
+        newFat _: Decimal,
+        newProtein _: Decimal,
+        newNote _: String
+    ) async throws {
+        if ((originalEntry.entryValues?.carbs ?? 0) == 0 && (originalEntry.entryValues?.fat ?? 0) > 0) ||
+            ((originalEntry.entryValues?.carbs ?? 0) == 0 && (originalEntry.entryValues?.protein ?? 0) > 0)
+        {
+            // Delete the zero-carb-entry and all its carb equivalents connected by the same fpuID from remote services and Core Data
+            // Use fpuID
+            try await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: true)
+        } else if ((originalEntry.entryValues?.carbs ?? 0) > 0 && (originalEntry.entryValues?.fat ?? 0) > 0) ||
+            ((originalEntry.entryValues?.carbs ?? 0) > 0 && (originalEntry.entryValues?.protein ?? 0) > 0)
+        {
+            // Delete carb entry and carb equivalents that are all connected by the same fpuID from remote services and Core Data
+            // Use fpuID
+            try await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: true)
+
+        } else {
+            // Delete just the carb entry since there are no carb equivalents
+            // Use NSManagedObjectID
+            try await deleteCarbs(treatmentObjectID)
+        }
+    }
+
+    /// Retrieves the original entry values
+    /// - Parameter objectID: The ID of the entry
+    /// - Returns: A tuple of the old entry values and its original date and the objectID or nil
+    private func getOriginalEntryValues(_ objectID: NSManagedObjectID) async
+        -> (entryValues: (date: Date, carbs: Double, fat: Double, protein: Double)?, entryId: NSManagedObjectID)?
+    {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "updateContext"
+        context.transactionAuthor = "updateEntry"
+
+        return await context.perform {
+            do {
+                guard let entry = try context.existingObject(with: objectID) as? CarbEntryStored, let entryDate = entry.date
+                else { return nil }
+
+                return (
+                    entryValues: (date: entryDate, carbs: entry.carbs, fat: entry.fat, protein: entry.protein),
+                    entryId: entry.objectID
+                )
+            } catch let error as NSError {
+                debugPrint("\(DebuggingIdentifiers.failed) Failed to get original date with error: \(error.userInfo)")
+                return nil
+            }
+        }
+    }
+
+    /// Synchronizes the FPU/ Carb entry with all remote services in parallel
+    private func syncWithServices() async {
+        async let nightscoutUpload: () = provider.nightscoutManager.uploadCarbs()
+        async let healthKitUpload: () = provider.healthkitManager.uploadCarbs()
+        async let tidepoolUpload: () = provider.tidepoolManager.uploadCarbs()
+
+        _ = await [nightscoutUpload, healthKitUpload, tidepoolUpload]
+    }
+
+    // MARK: - Entry Loading
+
+    /// Loads the values of a carb or FPU entry from Core Data
+    /// - Parameter objectID: The ID of the entry to load
+    /// - Returns: A tuple containing the entry's values, or nil if not found
+    func loadEntryValues(from objectID: NSManagedObjectID) async
+        -> (carbs: Decimal, fat: Decimal, protein: Decimal, note: String, date: Date)?
+    {
+        let context = CoreDataStack.shared.persistentContainer.viewContext
+
+        return await context.perform {
+            do {
+                guard let entry = try context.existingObject(with: objectID) as? CarbEntryStored,
+                      let entryDate = entry.date
+                else { return nil }
+
+                return (
+                    carbs: Decimal(entry.carbs),
+                    fat: Decimal(entry.fat),
+                    protein: Decimal(entry.protein),
+                    note: entry.note ?? "",
+                    date: entryDate
+                )
+            } catch {
+                debugPrint("\(DebuggingIdentifiers.failed) Failed to load entry: \(error)")
+                return nil
+            }
+        }
+    }
+
+    // MARK: - FPU Entry Handling
+
+    /// Handles the loading of FPU entries based on their type
+    /// If the user taps on an FPU entry in the DataTable list, there are two cases:
+    /// - the User has entered this FPU entry WITH carbs
+    /// - the User has entered this FPU entry WITHOUT carbs
+    /// In the first case, we simply need to load the corresponding carb entry. For this case THIS is the entry we want to edit.
+    /// In the second case, we need to load the zero-carb entry that actually holds the FPU values (and the carbs). For this case THIS is the entry we want to edit.
+    /// - Parameter objectID: The ID of the FPU entry
+    /// - Returns: A tuple containing the entry values and ID, or nil if not found
+    func handleFPUEntry(_ objectID: NSManagedObjectID) async
+        -> (
+            entryValues: (carbs: Decimal, fat: Decimal, protein: Decimal, note: String, date: Date)?,
+            entryID: NSManagedObjectID?
+        )?
+    {
+        // Case 1: FPU entry WITH carbs
+        if let correspondingCarbEntryID = await getCorrespondingCarbEntry(objectID) {
+            if let values = await loadEntryValues(from: correspondingCarbEntryID) {
+                return (values, correspondingCarbEntryID)
+            }
+        }
+        // Case 2: FPU entry WITHOUT carbs
+        else if let originalEntryID = await getZeroCarbNonFPUEntry(objectID) {
+            if let values = await loadEntryValues(from: originalEntryID) {
+                return (values, originalEntryID)
+            }
+        }
+        return nil
+    }
+
+    /// Retrieves the original zero-carb non-FPU entry for a given FPU entry.
+    /// This is used when the user has entered a FPU entry WITHOUT carbs.
+    /// - Parameter treatmentObjectID: The ID of the FPU entry
+    /// - Returns: The ID of the original entry, or nil if not found
+    func getZeroCarbNonFPUEntry(_ treatmentObjectID: NSManagedObjectID) async -> NSManagedObjectID? {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "fpuContext"
+
+        return await context.perform {
+            do {
+                // Get the fpuID from the selected entry
+                guard let selectedEntry = try context.existingObject(with: treatmentObjectID) as? CarbEntryStored,
+                      let fpuID = selectedEntry.fpuID
+                else { return nil }
+
+                // Fetch the original zero-carb entry (non-FPU) with the same fpuID
+                let last24Hours = Date().addingTimeInterval(-60 * 60 * 24)
+                let request = CarbEntryStored.fetchRequest()
+                request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
+                    NSPredicate(format: "date >= %@", last24Hours as NSDate),
+                    NSPredicate(format: "fpuID == %@", fpuID as CVarArg),
+                    NSPredicate(format: "isFPU == NO"),
+                    NSPredicate(format: "carbs == 0")
+                ])
+                request.fetchLimit = 1
+
+                let originalEntry = try context.fetch(request).first
+                debugPrint("FPU fetch result: \(originalEntry != nil ? "Entry found" : "No entry found")")
+                return originalEntry?.objectID
+
+            } catch let error as NSError {
+                debugPrint("\(DebuggingIdentifiers.failed) Failed to fetch original FPU entry: \(error.userInfo)")
+                return nil
+            }
+        }
+    }
+
+    /// Retrieves the corresponding carb entry for a given FPU entry.
+    /// This is used when the user has entered a carb entry WITH FPUs all at once.
+    /// - Parameter treatmentObjectID: The ID of the FPU entry
+    /// - Returns: The ID of the corresponding carb entry, or nil if not found
+    func getCorrespondingCarbEntry(_ treatmentObjectID: NSManagedObjectID) async -> NSManagedObjectID? {
+        let context = CoreDataStack.shared.newTaskContext()
+        context.name = "carbContext"
+
+        return await context.perform {
+            do {
+                // Get the fpuID from the selected entry
+                guard let selectedEntry = try context.existingObject(with: treatmentObjectID) as? CarbEntryStored,
+                      let fpuID = selectedEntry.fpuID
+                else { return nil }
+
+                // Fetch the corresponding carb entry with the same fpuID
+                let last24Hours = Date().addingTimeInterval(-24.hours.timeInterval)
+                let request = CarbEntryStored.fetchRequest()
+                request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
+                    NSPredicate(format: "date >= %@", last24Hours as NSDate),
+                    NSPredicate(format: "fpuID == %@", fpuID as CVarArg),
+                    NSPredicate(format: "isFPU == NO"),
+                    NSPredicate(format: "(carbs > 0) OR (fat > 0) OR (protein > 0)")
+                ])
+                request.fetchLimit = 1
+
+                let correspondingCarbEntry = try context.fetch(request).first
+                debugPrint(
+                    "Corresponding carb entry fetch result: \(correspondingCarbEntry != nil ? "Entry found" : "No entry found")"
+                )
+                return correspondingCarbEntry?.objectID
+
+            } catch let error as NSError {
+                debugPrint("\(DebuggingIdentifiers.failed) Failed to fetch corresponding carb entry: \(error.userInfo)")
+                return nil
+            }
+        }
+    }
+}

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

@@ -0,0 +1,119 @@
+import CoreData
+import Foundation
+import HealthKit
+
+extension History.StateModel {
+    // Carb and FPU deletion from history
+    /// - **Parameter**: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
+    func invokeCarbDeletionTask(_ treatmentObjectID: NSManagedObjectID, isFpuOrComplexMeal: Bool = false) {
+        Task {
+            do {
+                /// Set the variables that control the CustomProgressView BEFORE the actual deletion
+                /// otherwise the determineBasalSync gets executed first, sets waitForSuggestion to false and afterwards waitForSuggestion is set in this function to true, leading to an endless animation
+                await MainActor.run {
+                    carbEntryDeleted = true
+                    waitForSuggestion = true
+                }
+
+                try await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: isFpuOrComplexMeal)
+
+            } catch {
+                debug(.default, "\(DebuggingIdentifiers.failed) Failed to delete carbs: \(error)")
+                await MainActor.run {
+                    carbEntryDeleted = false
+                    waitForSuggestion = false
+                }
+            }
+        }
+    }
+
+    func deleteCarbs(_ treatmentObjectID: NSManagedObjectID, isFpuOrComplexMeal: Bool = false) async throws {
+        // Delete from Nightscout/Apple Health/Tidepool
+        await deleteFromServices(treatmentObjectID, isFPUDeletion: isFpuOrComplexMeal)
+
+        // Delete carbs from Core Data
+        await carbsStorage.deleteCarbsEntryStored(treatmentObjectID)
+
+        // Perform a determine basal sync to update cob
+        try await apsManager.determineBasalSync()
+    }
+
+    /// Deletes carb and FPU entries from all connected services (Nightscout, HealthKit, Tidepool)
+    /// - Parameters:
+    ///   - treatmentObjectID: The Core Data object ID of the entry to delete
+    ///   - isFPUDeletion: Flag indicating if this is a FPU deletion that requires special handling
+    ///     - If true: Will first fetch the corresponding carb entry and then delete both FPU and carb entries
+    ///     - If false: Will delete the entry directly as a standard carb deletion
+    /// - Note: This function handles three scenarios:
+    ///   1. Standard carb deletion (isFPUDeletion = false)
+    ///   2. FPU-only deletion (isFPUDeletion = true)
+    ///   3. Combined carb+FPU deletion (isFPUDeletion = true)
+    func deleteFromServices(_ treatmentObjectID: NSManagedObjectID, isFPUDeletion: Bool = false) async {
+        let taskContext = CoreDataStack.shared.newTaskContext()
+        taskContext.name = "deleteContext"
+        taskContext.transactionAuthor = "deleteCarbsFromServices"
+
+        var carbEntry: CarbEntryStored?
+        var objectIDToDelete = treatmentObjectID
+
+        // For FPU deletions, first get the corresponding carb entry
+        if isFPUDeletion {
+            guard let correspondingEntry: (
+                entryValues: (carbs: Decimal, fat: Decimal, protein: Decimal, note: String, date: Date)?,
+                entryID: NSManagedObjectID?
+            ) = await handleFPUEntry(treatmentObjectID),
+                let nsManagedObjectID = correspondingEntry.entryID
+            else { return }
+
+            objectIDToDelete = nsManagedObjectID
+        }
+
+        // Delete entries from all services
+        await taskContext.perform {
+            do {
+                carbEntry = try taskContext.existingObject(with: objectIDToDelete) as? CarbEntryStored
+                guard let carbEntry = carbEntry else {
+                    debugPrint("Carb entry for deletion not found. \(DebuggingIdentifiers.failed)")
+                    return
+                }
+
+                // Delete FPU related entries if they exist
+                if let fpuID = carbEntry.fpuID {
+                    // Delete Fat and Protein entries from Nightscout
+                    self.provider.deleteCarbsFromNightscout(withID: fpuID.uuidString)
+
+                    // Delete Fat and Protein entries from Apple Health
+                    let healthObjectsToDelete: [HKSampleType?] = [
+                        AppleHealthConfig.healthFatObject,
+                        AppleHealthConfig.healthProteinObject
+                    ]
+
+                    for sampleType in healthObjectsToDelete {
+                        if let validSampleType = sampleType {
+                            self.provider.deleteMealDataFromHealth(byID: fpuID.uuidString, sampleType: validSampleType)
+                        }
+                    }
+                }
+
+                // Delete carb entries if they exist
+                if let id = carbEntry.id, let entryDate = carbEntry.date {
+                    self.provider.deleteCarbsFromNightscout(withID: id.uuidString)
+
+                    // Delete carbs from Apple Health
+                    if let sampleType = AppleHealthConfig.healthCarbObject {
+                        self.provider.deleteMealDataFromHealth(byID: id.uuidString, sampleType: sampleType)
+                    }
+
+                    self.provider.deleteCarbsFromTidepool(
+                        withSyncId: id,
+                        carbs: Decimal(carbEntry.carbs),
+                        at: entryDate,
+                        enteredBy: CarbsEntry.local
+                    )
+                }
+            } catch {
+                debugPrint("\(DebuggingIdentifiers.failed) Error deleting entries: \(error)")
+            }
+        }
+    }
+}

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

@@ -0,0 +1,55 @@
+import CoreData
+import Foundation
+
+extension History.StateModel {
+    /// Initiates the glucose deletion process asynchronously
+    /// - Parameter treatmentObjectID: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
+    func invokeGlucoseDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
+        Task {
+            await deleteGlucose(treatmentObjectID)
+        }
+    }
+
+    func deleteGlucose(_ treatmentObjectID: NSManagedObjectID) async {
+        // Delete from Apple Health/Tidepool
+        await deleteGlucoseFromServices(treatmentObjectID)
+
+        // Delete from Core Data
+        await glucoseStorage.deleteGlucose(treatmentObjectID)
+    }
+
+    func deleteGlucoseFromServices(_ treatmentObjectID: NSManagedObjectID) async {
+        let taskContext = CoreDataStack.shared.newTaskContext()
+        taskContext.name = "deleteContext"
+        taskContext.transactionAuthor = "deleteGlucoseFromServices"
+
+        await taskContext.perform {
+            do {
+                let result = try taskContext.existingObject(with: treatmentObjectID) as? GlucoseStored
+
+                guard let glucoseToDelete = result else {
+                    debugPrint("Data Table State: \(#function) \(DebuggingIdentifiers.failed) glucose not found in core data")
+                    return
+                }
+
+                // Delete from Nightscout
+                if let id = glucoseToDelete.id?.uuidString, let date = glucoseToDelete.date {
+                    self.provider.deleteGlucoseFromNightscout(withID: id, withDate: date)
+                }
+
+                // Delete from Apple Health
+                if let id = glucoseToDelete.id?.uuidString {
+                    self.provider.deleteGlucoseFromHealth(withSyncID: id)
+                }
+
+                debugPrint(
+                    "\(#file) \(#function) \(DebuggingIdentifiers.succeeded) deleted glucose from remote service(s) (Nightscout, Apple Health, Tidepool)"
+                )
+            } catch {
+                debugPrint(
+                    "\(#file) \(#function) \(DebuggingIdentifiers.failed) error while deleting glucose remote service(s) (Nightscout, Apple Health, Tidepool) with error: \(error)"
+                )
+            }
+        }
+    }
+}

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

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

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

@@ -42,57 +42,6 @@ extension History {
             glucoseStorage.isGlucoseDataFresh(glucoseDate)
         }
 
-        /// Initiates the glucose deletion process asynchronously
-        /// - Parameter treatmentObjectID: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
-        func invokeGlucoseDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
-            Task {
-                await deleteGlucose(treatmentObjectID)
-            }
-        }
-
-        func deleteGlucose(_ treatmentObjectID: NSManagedObjectID) async {
-            // Delete from Apple Health/Tidepool
-            await deleteGlucoseFromServices(treatmentObjectID)
-
-            // Delete from Core Data
-            await glucoseStorage.deleteGlucose(treatmentObjectID)
-        }
-
-        func deleteGlucoseFromServices(_ treatmentObjectID: NSManagedObjectID) async {
-            let taskContext = CoreDataStack.shared.newTaskContext()
-            taskContext.name = "deleteContext"
-            taskContext.transactionAuthor = "deleteGlucoseFromServices"
-
-            await taskContext.perform {
-                do {
-                    let result = try taskContext.existingObject(with: treatmentObjectID) as? GlucoseStored
-
-                    guard let glucoseToDelete = result else {
-                        debugPrint("Data Table State: \(#function) \(DebuggingIdentifiers.failed) glucose not found in core data")
-                        return
-                    }
-
-                    // Delete from Nightscout
-                    if let id = glucoseToDelete.id?.uuidString {
-                        self.provider.deleteManualGlucoseFromNightscout(withID: id)
-                    }
-
-                    // Delete from Apple Health
-                    if let id = glucoseToDelete.id?.uuidString {
-                        self.provider.deleteGlucoseFromHealth(withSyncID: id)
-                    }
-
-                    debugPrint(
-                        "\(#file) \(#function) \(DebuggingIdentifiers.succeeded) deleted glucose from remote service(s) (Nightscout, Apple Health, Tidepool)"
-                    )
-                } catch {
-                    debugPrint(
-                        "\(#file) \(#function) \(DebuggingIdentifiers.failed) error while deleting glucose remote service(s) (Nightscout, Apple Health, Tidepool) with error: \(error)"
-                    )
-                }
-            }
-        }
-
         func addManualGlucose() {
             // Always save value in mg/dL
             let glucose = units == .mmolL ? manualGlucose.asMgdL : manualGlucose
@@ -100,488 +49,6 @@ extension History {
 
             glucoseStorage.addManualGlucose(glucose: glucoseAsInt)
         }
-
-        // Carb and FPU deletion from history
-        /// - **Parameter**: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
-        func invokeCarbDeletionTask(_ treatmentObjectID: NSManagedObjectID, isFpuOrComplexMeal: Bool = false) {
-            Task {
-                do {
-                    /// Set the variables that control the CustomProgressView BEFORE the actual deletion
-                    /// otherwise the determineBasalSync gets executed first, sets waitForSuggestion to false and afterwards waitForSuggestion is set in this function to true, leading to an endless animation
-                    await MainActor.run {
-                        carbEntryDeleted = true
-                        waitForSuggestion = true
-                    }
-
-                    try await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: isFpuOrComplexMeal)
-
-                } catch {
-                    debug(.default, "\(DebuggingIdentifiers.failed) Failed to delete carbs: \(error)")
-                    await MainActor.run {
-                        carbEntryDeleted = false
-                        waitForSuggestion = false
-                    }
-                }
-            }
-        }
-
-        func deleteCarbs(_ treatmentObjectID: NSManagedObjectID, isFpuOrComplexMeal: Bool = false) async throws {
-            // Delete from Nightscout/Apple Health/Tidepool
-            await deleteFromServices(treatmentObjectID, isFPUDeletion: isFpuOrComplexMeal)
-
-            // Delete carbs from Core Data
-            await carbsStorage.deleteCarbsEntryStored(treatmentObjectID)
-
-            // Perform a determine basal sync to update cob
-            try await apsManager.determineBasalSync()
-        }
-
-        /// Deletes carb and FPU entries from all connected services (Nightscout, HealthKit, Tidepool)
-        /// - Parameters:
-        ///   - treatmentObjectID: The Core Data object ID of the entry to delete
-        ///   - isFPUDeletion: Flag indicating if this is a FPU deletion that requires special handling
-        ///     - If true: Will first fetch the corresponding carb entry and then delete both FPU and carb entries
-        ///     - If false: Will delete the entry directly as a standard carb deletion
-        /// - Note: This function handles three scenarios:
-        ///   1. Standard carb deletion (isFPUDeletion = false)
-        ///   2. FPU-only deletion (isFPUDeletion = true)
-        ///   3. Combined carb+FPU deletion (isFPUDeletion = true)
-        func deleteFromServices(_ treatmentObjectID: NSManagedObjectID, isFPUDeletion: Bool = false) async {
-            let taskContext = CoreDataStack.shared.newTaskContext()
-            taskContext.name = "deleteContext"
-            taskContext.transactionAuthor = "deleteCarbsFromServices"
-
-            var carbEntry: CarbEntryStored?
-            var objectIDToDelete = treatmentObjectID
-
-            // For FPU deletions, first get the corresponding carb entry
-            if isFPUDeletion {
-                guard let correspondingEntry: (
-                    entryValues: (carbs: Decimal, fat: Decimal, protein: Decimal, note: String, date: Date)?,
-                    entryID: NSManagedObjectID?
-                ) = await handleFPUEntry(treatmentObjectID),
-                    let nsManagedObjectID = correspondingEntry.entryID
-                else { return }
-
-                objectIDToDelete = nsManagedObjectID
-            }
-
-            // Delete entries from all services
-            await taskContext.perform {
-                do {
-                    carbEntry = try taskContext.existingObject(with: objectIDToDelete) as? CarbEntryStored
-                    guard let carbEntry = carbEntry else {
-                        debugPrint("Carb entry for deletion not found. \(DebuggingIdentifiers.failed)")
-                        return
-                    }
-
-                    // Delete FPU related entries if they exist
-                    if let fpuID = carbEntry.fpuID {
-                        // Delete Fat and Protein entries from Nightscout
-                        self.provider.deleteCarbsFromNightscout(withID: fpuID.uuidString)
-
-                        // Delete Fat and Protein entries from Apple Health
-                        let healthObjectsToDelete: [HKSampleType?] = [
-                            AppleHealthConfig.healthFatObject,
-                            AppleHealthConfig.healthProteinObject
-                        ]
-
-                        for sampleType in healthObjectsToDelete {
-                            if let validSampleType = sampleType {
-                                self.provider.deleteMealDataFromHealth(byID: fpuID.uuidString, sampleType: validSampleType)
-                            }
-                        }
-                    }
-
-                    // Delete carb entries if they exist
-                    if let id = carbEntry.id, let entryDate = carbEntry.date {
-                        self.provider.deleteCarbsFromNightscout(withID: id.uuidString)
-
-                        // Delete carbs from Apple Health
-                        if let sampleType = AppleHealthConfig.healthCarbObject {
-                            self.provider.deleteMealDataFromHealth(byID: id.uuidString, sampleType: sampleType)
-                        }
-
-                        self.provider.deleteCarbsFromTidepool(
-                            withSyncId: id,
-                            carbs: Decimal(carbEntry.carbs),
-                            at: entryDate,
-                            enteredBy: CarbsEntry.local
-                        )
-                    }
-                } catch {
-                    debugPrint("\(DebuggingIdentifiers.failed) Error deleting entries: \(error)")
-                }
-            }
-        }
-
-        // Insulin deletion from history
-        /// - **Parameter**: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
-        func invokeInsulinDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
-            Task {
-                do {
-                    try await invokeInsulinDeletion(treatmentObjectID)
-                } catch {
-                    debug(.default, "\(DebuggingIdentifiers.failed) Failed to delete insulin entry: \(error)")
-                }
-            }
-        }
-
-        func invokeInsulinDeletion(_ treatmentObjectID: NSManagedObjectID) async throws {
-            do {
-                let authenticated = try await unlockmanager.unlock()
-
-                guard authenticated else {
-                    debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Authentication Error")
-                    return
-                }
-
-                /// Set variables that control the CustomProgressView to true AFTER the authentication and BEFORE the actual determineBasalSync
-                /// We definitely need to set the variables BEFORE the actual sync
-                /// otherwise the determineBasalSync gets executed first, sets waitForSuggestion to false and afterwards waitForSuggestion is set in this function to true, leading to an endless animation
-                /// But we also want it AFTER the authentication
-                /// otherwise the animation would pop up even before the authentication prompt appears to the user
-                await MainActor.run {
-                    insulinEntryDeleted = true
-                    waitForSuggestion = true
-                }
-
-                // Delete from remote service(s) (i.e. Nightscout, Apple Health, Tidepool)
-                await deleteInsulinFromServices(with: treatmentObjectID)
-
-                // Delete from Core Data
-                await CoreDataStack.shared.deleteObject(identifiedBy: treatmentObjectID)
-
-                // Perform a determine basal sync to update iob
-                try await apsManager.determineBasalSync()
-            } catch {
-                debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Error while Insulin Deletion Task: \(error)"
-                )
-                await MainActor.run {
-                    insulinEntryDeleted = false
-                    waitForSuggestion = false
-                }
-            }
-        }
-
-        func deleteInsulinFromServices(with treatmentObjectID: NSManagedObjectID) async {
-            let taskContext = CoreDataStack.shared.newTaskContext()
-            taskContext.name = "deleteContext"
-            taskContext.transactionAuthor = "deleteInsulinFromServices"
-
-            await taskContext.perform {
-                do {
-                    guard let treatmentToDelete = try taskContext.existingObject(with: treatmentObjectID) as? PumpEventStored
-                    else {
-                        debug(.default, "Could not cast the object to PumpEventStored")
-                        return
-                    }
-
-                    if let id = treatmentToDelete.id, let timestamp = treatmentToDelete.timestamp,
-                       let bolus = treatmentToDelete.bolus, let bolusAmount = bolus.amount
-                    {
-                        self.provider.deleteInsulinFromNightscout(withID: id)
-                        self.provider.deleteInsulinFromHealth(withSyncID: id)
-                        self.provider.deleteInsulinFromTidepool(withSyncId: id, amount: bolusAmount as Decimal, at: timestamp)
-                    }
-
-                    taskContext.delete(treatmentToDelete)
-                    try taskContext.save()
-
-                    debug(.default, "Successfully deleted the treatment object.")
-                } catch {
-                    debug(.default, "Failed to delete the treatment object: \(error)")
-                }
-            }
-        }
-
-        // MARK: - Entry Management
-
-        /// Updates a carb/FPU entry with new values and handles the necessary cleanup and recreation of FPU entries
-        /// - Parameters:
-        ///   - treatmentObjectID: The ID of the entry to update
-        ///   - newCarbs: The new carbs value
-        ///   - newFat: The new fat value
-        ///   - newProtein: The new protein value
-        ///   - newNote: The new note text
-        ///   - newDate: The new date for the entry
-        func updateEntry(
-            _ treatmentObjectID: NSManagedObjectID,
-            newCarbs: Decimal,
-            newFat: Decimal,
-            newProtein: Decimal,
-            newNote: String,
-            newDate: Date
-        ) {
-            Task {
-                do {
-                    // Get original date from entry to re-create the entry later with the updated values and the same date
-                    guard let originalEntry = await getOriginalEntryValues(treatmentObjectID) else { return }
-
-                    // Deletion logic for carb and FPU entries
-                    try await deleteOldEntries(
-                        treatmentObjectID,
-                        originalEntry: originalEntry,
-                        newCarbs: newCarbs,
-                        newFat: newFat,
-                        newProtein: newProtein,
-                        newNote: newNote
-                    )
-
-                    try await createNewEntries(
-                        originalDate: newDate,
-                        newCarbs: newCarbs,
-                        newFat: newFat,
-                        newProtein: newProtein,
-                        newNote: newNote
-                    )
-
-                    await syncWithServices()
-
-                    // Perform a determine basal sync to update cob
-                    try await apsManager.determineBasalSync()
-
-                } catch {
-                    debug(.default, "\(DebuggingIdentifiers.failed) failed to update entry: \(error)")
-                }
-            }
-        }
-
-        private func createNewEntries(
-            originalDate: Date,
-            newCarbs: Decimal,
-            newFat: Decimal,
-            newProtein: Decimal,
-            newNote: String
-        ) async throws {
-            let newEntry = CarbsEntry(
-                id: UUID().uuidString,
-                createdAt: Date(),
-                actualDate: originalDate,
-                carbs: newCarbs,
-                fat: newFat,
-                protein: newProtein,
-                note: newNote,
-                enteredBy: CarbsEntry.local,
-                isFPU: false,
-                fpuID: newFat > 0 || newProtein > 0 ? UUID().uuidString : nil
-            )
-
-            // Handles internally whether to create fake carbs or not based on whether fat > 0 or protein > 0
-            try await carbsStorage.storeCarbs([newEntry], areFetchedFromRemote: false)
-        }
-
-        /// Deletes the old carb/ FPU entries and creates new ones with updated values
-        /// - Parameters:
-        ///   - treatmentObjectID: The ID of the entry to delete
-        ///   - originalDate: The original date to preserve
-        ///   - newCarbs: The new carbs value
-        ///   - newFat: The new fat value
-        ///   - newProtein: The new protein value
-        ///   - newNote: The new note text
-        private func deleteOldEntries(
-            _ treatmentObjectID: NSManagedObjectID,
-            originalEntry: (
-                entryValues: (date: Date, carbs: Double, fat: Double, protein: Double)?,
-                entryId: NSManagedObjectID
-            ),
-            newCarbs _: Decimal,
-            newFat _: Decimal,
-            newProtein _: Decimal,
-            newNote _: String
-        ) async throws {
-            if ((originalEntry.entryValues?.carbs ?? 0) == 0 && (originalEntry.entryValues?.fat ?? 0) > 0) ||
-                ((originalEntry.entryValues?.carbs ?? 0) == 0 && (originalEntry.entryValues?.protein ?? 0) > 0)
-            {
-                // Delete the zero-carb-entry and all its carb equivalents connected by the same fpuID from remote services and Core Data
-                // Use fpuID
-                try await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: true)
-            } else if ((originalEntry.entryValues?.carbs ?? 0) > 0 && (originalEntry.entryValues?.fat ?? 0) > 0) ||
-                ((originalEntry.entryValues?.carbs ?? 0) > 0 && (originalEntry.entryValues?.protein ?? 0) > 0)
-            {
-                // Delete carb entry and carb equivalents that are all connected by the same fpuID from remote services and Core Data
-                // Use fpuID
-                try await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: true)
-
-            } else {
-                // Delete just the carb entry since there are no carb equivalents
-                // Use NSManagedObjectID
-                try await deleteCarbs(treatmentObjectID)
-            }
-        }
-
-        /// Retrieves the original entry values
-        /// - Parameter objectID: The ID of the entry
-        /// - Returns: A tuple of the old entry values and its original date and the objectID or nil
-        private func getOriginalEntryValues(_ objectID: NSManagedObjectID) async
-            -> (entryValues: (date: Date, carbs: Double, fat: Double, protein: Double)?, entryId: NSManagedObjectID)?
-        {
-            let context = CoreDataStack.shared.newTaskContext()
-            context.name = "updateContext"
-            context.transactionAuthor = "updateEntry"
-
-            return await context.perform {
-                do {
-                    guard let entry = try context.existingObject(with: objectID) as? CarbEntryStored, let entryDate = entry.date
-                    else { return nil }
-
-                    return (
-                        entryValues: (date: entryDate, carbs: entry.carbs, fat: entry.fat, protein: entry.protein),
-                        entryId: entry.objectID
-                    )
-                } catch let error as NSError {
-                    debugPrint("\(DebuggingIdentifiers.failed) Failed to get original date with error: \(error.userInfo)")
-                    return nil
-                }
-            }
-        }
-
-        /// Synchronizes the FPU/ Carb entry with all remote services in parallel
-        private func syncWithServices() async {
-            async let nightscoutUpload: () = provider.nightscoutManager.uploadCarbs()
-            async let healthKitUpload: () = provider.healthkitManager.uploadCarbs()
-            async let tidepoolUpload: () = provider.tidepoolManager.uploadCarbs()
-
-            _ = await [nightscoutUpload, healthKitUpload, tidepoolUpload]
-        }
-
-        // MARK: - Entry Loading
-
-        /// Loads the values of a carb or FPU entry from Core Data
-        /// - Parameter objectID: The ID of the entry to load
-        /// - Returns: A tuple containing the entry's values, or nil if not found
-        func loadEntryValues(from objectID: NSManagedObjectID) async
-            -> (carbs: Decimal, fat: Decimal, protein: Decimal, note: String, date: Date)?
-        {
-            let context = CoreDataStack.shared.persistentContainer.viewContext
-
-            return await context.perform {
-                do {
-                    guard let entry = try context.existingObject(with: objectID) as? CarbEntryStored,
-                          let entryDate = entry.date
-                    else { return nil }
-
-                    return (
-                        carbs: Decimal(entry.carbs),
-                        fat: Decimal(entry.fat),
-                        protein: Decimal(entry.protein),
-                        note: entry.note ?? "",
-                        date: entryDate
-                    )
-                } catch {
-                    debugPrint("\(DebuggingIdentifiers.failed) Failed to load entry: \(error)")
-                    return nil
-                }
-            }
-        }
-
-        // MARK: - FPU Entry Handling
-
-        /// Handles the loading of FPU entries based on their type
-        /// If the user taps on an FPU entry in the DataTable list, there are two cases:
-        /// - the User has entered this FPU entry WITH carbs
-        /// - the User has entered this FPU entry WITHOUT carbs
-        /// In the first case, we simply need to load the corresponding carb entry. For this case THIS is the entry we want to edit.
-        /// In the second case, we need to load the zero-carb entry that actually holds the FPU values (and the carbs). For this case THIS is the entry we want to edit.
-        /// - Parameter objectID: The ID of the FPU entry
-        /// - Returns: A tuple containing the entry values and ID, or nil if not found
-        func handleFPUEntry(_ objectID: NSManagedObjectID) async
-            -> (
-                entryValues: (carbs: Decimal, fat: Decimal, protein: Decimal, note: String, date: Date)?,
-                entryID: NSManagedObjectID?
-            )?
-        {
-            // Case 1: FPU entry WITH carbs
-            if let correspondingCarbEntryID = await getCorrespondingCarbEntry(objectID) {
-                if let values = await loadEntryValues(from: correspondingCarbEntryID) {
-                    return (values, correspondingCarbEntryID)
-                }
-            }
-            // Case 2: FPU entry WITHOUT carbs
-            else if let originalEntryID = await getZeroCarbNonFPUEntry(objectID) {
-                if let values = await loadEntryValues(from: originalEntryID) {
-                    return (values, originalEntryID)
-                }
-            }
-            return nil
-        }
-
-        /// Retrieves the original zero-carb non-FPU entry for a given FPU entry.
-        /// This is used when the user has entered a FPU entry WITHOUT carbs.
-        /// - Parameter treatmentObjectID: The ID of the FPU entry
-        /// - Returns: The ID of the original entry, or nil if not found
-        func getZeroCarbNonFPUEntry(_ treatmentObjectID: NSManagedObjectID) async -> NSManagedObjectID? {
-            let context = CoreDataStack.shared.newTaskContext()
-            context.name = "fpuContext"
-
-            return await context.perform {
-                do {
-                    // Get the fpuID from the selected entry
-                    guard let selectedEntry = try context.existingObject(with: treatmentObjectID) as? CarbEntryStored,
-                          let fpuID = selectedEntry.fpuID
-                    else { return nil }
-
-                    // Fetch the original zero-carb entry (non-FPU) with the same fpuID
-                    let last24Hours = Date().addingTimeInterval(-60 * 60 * 24)
-                    let request = CarbEntryStored.fetchRequest()
-                    request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
-                        NSPredicate(format: "date >= %@", last24Hours as NSDate),
-                        NSPredicate(format: "fpuID == %@", fpuID as CVarArg),
-                        NSPredicate(format: "isFPU == NO"),
-                        NSPredicate(format: "carbs == 0")
-                    ])
-                    request.fetchLimit = 1
-
-                    let originalEntry = try context.fetch(request).first
-                    debugPrint("FPU fetch result: \(originalEntry != nil ? "Entry found" : "No entry found")")
-                    return originalEntry?.objectID
-
-                } catch let error as NSError {
-                    debugPrint("\(DebuggingIdentifiers.failed) Failed to fetch original FPU entry: \(error.userInfo)")
-                    return nil
-                }
-            }
-        }
-
-        /// Retrieves the corresponding carb entry for a given FPU entry.
-        /// This is used when the user has entered a carb entry WITH FPUs all at once.
-        /// - Parameter treatmentObjectID: The ID of the FPU entry
-        /// - Returns: The ID of the corresponding carb entry, or nil if not found
-        func getCorrespondingCarbEntry(_ treatmentObjectID: NSManagedObjectID) async -> NSManagedObjectID? {
-            let context = CoreDataStack.shared.newTaskContext()
-            context.name = "carbContext"
-
-            return await context.perform {
-                do {
-                    // Get the fpuID from the selected entry
-                    guard let selectedEntry = try context.existingObject(with: treatmentObjectID) as? CarbEntryStored,
-                          let fpuID = selectedEntry.fpuID
-                    else { return nil }
-
-                    // Fetch the corresponding carb entry with the same fpuID
-                    let last24Hours = Date().addingTimeInterval(-24.hours.timeInterval)
-                    let request = CarbEntryStored.fetchRequest()
-                    request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
-                        NSPredicate(format: "date >= %@", last24Hours as NSDate),
-                        NSPredicate(format: "fpuID == %@", fpuID as CVarArg),
-                        NSPredicate(format: "isFPU == NO"),
-                        NSPredicate(format: "(carbs > 0) OR (fat > 0) OR (protein > 0)")
-                    ])
-                    request.fetchLimit = 1
-
-                    let correspondingCarbEntry = try context.fetch(request).first
-                    debugPrint(
-                        "Corresponding carb entry fetch result: \(correspondingCarbEntry != nil ? "Entry found" : "No entry found")"
-                    )
-                    return correspondingCarbEntry?.objectID
-
-                } catch let error as NSError {
-                    debugPrint("\(DebuggingIdentifiers.failed) Failed to fetch corresponding carb entry: \(error.userInfo)")
-                    return nil
-                }
-            }
-        }
     }
 }
 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -124,6 +124,7 @@ extension Treatments {
         let viewContext = CoreDataStack.shared.persistentContainer.viewContext
         let glucoseFetchContext = CoreDataStack.shared.newTaskContext()
         let determinationFetchContext = CoreDataStack.shared.newTaskContext()
+        let pumpHistoryFetchContext = CoreDataStack.shared.newTaskContext()
 
         var isActive: Bool = false
 
@@ -137,8 +138,9 @@ extension Treatments {
 
         typealias PumpEvent = PumpEventStored.EventType
 
-        var isBolusInProgress: Bool = false
-        private var bolusProgressCancellable: AnyCancellable?
+        var bolusProgress: Decimal?
+        var isBolusInProgress: Bool { bolusProgress != nil }
+        var lastPumpBolus: PumpEventStored?
 
         func unsubscribe() {
             subscriptions.forEach { $0.cancel() }
@@ -173,7 +175,7 @@ extension Treatments {
             hasCleanedUp = true
 
             unsubscribe()
-            bolusProgressCancellable?.cancel()
+            lifetime = Lifetime()
 
             broadcaster?.unregister(DeterminationObserver.self, observer: self)
             broadcaster?.unregister(BolusFailureObserver.self, observer: self)
@@ -198,6 +200,9 @@ extension Treatments {
                         group.addTask {
                             self.registerObservers()
                         }
+                        group.addTask {
+                            self.setupLastBolus()
+                        }
 
                         // Wait for all tasks to complete
                         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() {
-            bolusProgressCancellable = apsManager.bolusProgress
+            apsManager.bolusProgress
                 .receive(on: DispatchQueue.main)
-                .sink { [weak self] progressValue in
-                    guard let self = self else { return }
-                    // If progressValue is non-nil, a bolus is in progress.
-                    self.isBolusInProgress = (progressValue != nil)
-                }
+                .weakAssign(to: \.bolusProgress, on: self)
+                .store(in: &lifetime)
+        }
+
+        func cancelBolus() {
+            Task {
+                await apsManager.cancelBolus(nil)
+                try? await apsManager.determineBasalSync()
+            }
         }
 
         // MARK: - Basal
@@ -744,6 +748,11 @@ extension Treatments.StateModel {
             guard let self = self else { return }
             self.setupGlucoseArray()
         }.store(in: &subscriptions)
+
+        // Refresh `lastPumpBolus` whenever a new pump event lands (mirrors HomeStateModel)
+        coreDataPublisher?.filteredByEntityName("PumpEventStored").sink { [weak self] _ in
+            self?.setupLastBolus()
+        }.store(in: &subscriptions)
     }
 
     private func registerSubscribers() {
@@ -999,3 +1008,47 @@ private extension Predictions {
         iob == nil && zt == nil && cob == nil && uam == nil
     }
 }
+
+// MARK: - Last Pump Bolus
+
+extension Treatments.StateModel {
+    /// Mirrors `HomeStateModel.setupLastBolus` so the in-progress visualizer can show the
+    /// running pump-bolus's amount as the denominator (not the user's pending entry).
+    /// Filters out external boluses via `NSPredicate.lastPumpBolus`.
+    func setupLastBolus() {
+        Task {
+            do {
+                guard let id = try await fetchLastBolus() else { return }
+                await updateLastBolus(with: id)
+            } catch {
+                debug(.default, "\(DebuggingIdentifiers.failed) Error setting up last bolus: \(error)")
+            }
+        }
+    }
+
+    private func fetchLastBolus() async throws -> NSManagedObjectID? {
+        let 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
         }
 
+        private var bolusProgressFormatter: NumberFormatter {
+            let fractionDigits: Int = switch state.settingsManager.preferences.bolusIncrement {
+            case 0.1: 1
+            case 0.025: 3
+            default: 2
+            }
+
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            formatter.minimum = 0
+            formatter.maximumFractionDigits = fractionDigits
+            formatter.minimumFractionDigits = fractionDigits
+            formatter.allowsFloats = true
+            formatter.roundingIncrement = Double(state.settingsManager.preferences.bolusIncrement) as NSNumber
+            return formatter
+        }
+
         private var mealFormatter: NumberFormatter {
             let formatter = NumberFormatter()
             formatter.numberStyle = .decimal
@@ -475,6 +492,9 @@ extension Treatments {
         }
 
         var treatmentButton: some View {
+            let shouldDisplayBolusProgress = state.isBolusInProgress && state.amount > 0 &&
+                !state.externalInsulin && (state.carbs == 0 || state.fat == 0 || state.protein == 0)
+
             var treatmentButtonBackground = Color(.systemBlue)
             if limitExceeded {
                 treatmentButtonBackground = Color(.systemRed)
@@ -483,41 +503,43 @@ extension Treatments {
             }
 
             return Section {
-                Button {
-                    if bolusWarning.shouldConfirm {
-                        showConfirmDialogForBolusing = true
-                    } else {
-                        state.invokeTreatmentsTask()
-                    }
-                } label: {
-                    HStack {
-                        if state.isBolusInProgress && state.amount > 0 &&
-                            !state.externalInsulin && (state.carbs == 0 || state.fat == 0 || state.protein == 0)
-                        {
-                            ProgressView()
+                if shouldDisplayBolusProgress {
+                    bolusInProgressView
+                        .listRowBackground(Color.clear)
+                        .listRowInsets(EdgeInsets(top: 4, leading: 0, bottom: 4, trailing: 0))
+                } else {
+                    Button {
+                        if bolusWarning.shouldConfirm {
+                            showConfirmDialogForBolusing = true
+                        } else {
+                            state.invokeTreatmentsTask()
                         }
-                        taskButtonLabel
+                    } label: {
+                        HStack {
+                            taskButtonLabel
+                        }
+                        .font(.headline)
+                        .foregroundStyle(Color.white)
+                        .frame(maxWidth: .infinity, alignment: .center)
+                        .frame(height: 35)
                     }
-                    .font(.headline)
-                    .foregroundStyle(Color.white)
-                    .frame(maxWidth: .infinity, alignment: .center)
-                    .frame(height: 35)
-                }
-                .disabled(disableTaskButton)
-                .listRowBackground(treatmentButtonBackground)
-                .shadow(radius: 3)
-                .clipShape(RoundedRectangle(cornerRadius: 8))
-                .confirmationDialog(
-                    bolusWarning.warningMessage + " Bolus \(state.amount.description) U?",
-                    isPresented: $showConfirmDialogForBolusing,
-                    titleVisibility: .visible
-                ) {
-                    Button("Cancel", role: .cancel) {}
-                    Button(
-                        bolusWarning.warningMessage.isEmpty ? "Enact Bolus" : "Ignore Warning and Enact Bolus",
-                        role: bolusWarning.warningMessage.isEmpty ? nil : .destructive
+                    .disabled(disableTaskButton)
+                    .listRowBackground(treatmentButtonBackground)
+                    .shadow(radius: 3)
+                    .clipShape(RoundedRectangle(cornerRadius: 8))
+                    .confirmationDialog(
+                        bolusWarning.warningMessage + " Bolus \(state.amount.description) U?",
+                        isPresented: $showConfirmDialogForBolusing,
+                        titleVisibility: .visible
                     ) {
-                        state.invokeTreatmentsTask()
+                        Button("Cancel", role: .cancel) {}
+                        Button(
+                            bolusWarning.warningMessage
+                                .isEmpty ? String(localized: "Enact Bolus") : String(localized: "Ignore Warning and Enact Bolus"),
+                            role: bolusWarning.warningMessage.isEmpty ? nil : .destructive
+                        ) {
+                            state.invokeTreatmentsTask()
+                        }
                     }
                 }
             } header: {
@@ -532,6 +554,75 @@ extension Treatments {
             }
         }
 
+        /// Card-style in-progress visualizer matching Home's `bolusView` look:
+        /// insulin-tinted background, cross.vial.fill icon, "Bolusing" + "X of Y U" text,
+        /// xmark.app cancel, gradient progress bar overlaid at the bottom.
+        @ViewBuilder private var bolusInProgressView: some View {
+            let progress = state.bolusProgress ?? 0
+            let bolusTotal = state.lastPumpBolus?.bolus?.amount as Decimal?
+            let bolusFraction = (bolusTotal ?? 0) * progress
+            let bolusString: String = {
+                guard let bolusTotal = bolusTotal else { return String(localized: "Bolus In Progress...") }
+                return (bolusProgressFormatter.string(from: bolusFraction as NSNumber) ?? "0")
+                    + String(localized: " of ", comment: "Bolus string partial message: 'x U of y U' in home view")
+                    + (Formatter.decimalFormatterWithThreeFractionDigits.string(from: bolusTotal as NSNumber) ?? "0")
+                    + String(localized: " U", comment: "Insulin unit")
+            }()
+
+            ZStack {
+                // background card
+                RoundedRectangle(cornerRadius: 15)
+                    .fill(
+                        colorScheme == .dark
+                            ? Color(red: 0.03921568627, green: 0.133333333, blue: 0.2156862745)
+                            : Color.insulin.opacity(0.2)
+                    )
+                    .frame(height: 56)
+                    .shadow(
+                        color: colorScheme == .dark
+                            ? Color(red: 0.02745098039, green: 0.1098039216, blue: 0.1411764706)
+                            : Color.black.opacity(0.33),
+                        radius: 3
+                    )
+
+                // bolus content
+                HStack {
+                    Image(systemName: "cross.vial.fill")
+                        .font(.system(size: 25))
+
+                    Spacer()
+
+                    VStack {
+                        Text("Bolusing")
+                            .font(.subheadline)
+                            .frame(maxWidth: .infinity, alignment: .leading)
+                        Text(bolusString)
+                            .font(.caption)
+                            .frame(maxWidth: .infinity, alignment: .leading)
+                    }
+                    .padding(.leading, 5)
+
+                    Spacer()
+
+                    Button { state.cancelBolus() } label: {
+                        Image(systemName: "xmark.app")
+                            .font(.system(size: 25))
+                    }.tint(Color.tabBar)
+                        .buttonStyle(.borderless)
+                        .accessibilityLabel("Cancel bolus")
+                }
+                .padding(.horizontal, 10)
+                .padding(.trailing, 8)
+            }
+            .padding(.horizontal, 10)
+            .overlay(alignment: .bottom) {
+                BolusProgressBar(progress: progress)
+                    .padding(.horizontal, 18)
+                    .padding(.bottom, 1)
+            }
+            .clipShape(RoundedRectangle(cornerRadius: 15))
+        }
+
         private var taskButtonLabel: some View {
             if pumpBolusLimitExceeded {
                 return Text("Max Bolus of \(state.maxBolus.description) U Exceeded")
@@ -550,9 +641,8 @@ extension Treatments {
             let hasFatOrProtein = state.fat > 0 || state.protein > 0
             let bolusString = state.externalInsulin ? String(localized: "External Insulin") : String(localized: "Enact Bolus")
 
-            if state.isBolusInProgress && hasInsulin && !state.externalInsulin && (!hasCarbs || !hasFatOrProtein) {
-                return Text("Bolus In Progress...")
-            }
+            // Note: when a pump bolus is in progress, the row is rendered by `bolusInProgressView`
+            // (Home-style card), so this label's in-progress branch is intentionally absent.
 
             switch (hasInsulin, hasCarbs, hasFatOrProtein) {
             case (true, true, true):
@@ -562,7 +652,7 @@ extension Treatments {
             case (true, false, true):
                 return Text("Log FPU and \(bolusString)")
             case (true, false, false):
-                return Text(state.externalInsulin ? "Log External Insulin" : "Enact Bolus")
+                return Text(state.externalInsulin ? String(localized: "Log External Insulin") : String(localized: "Enact Bolus"))
             case (false, true, true):
                 return Text("Log Meal")
             case (false, true, false):

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

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

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

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

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

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

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

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

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

@@ -11,14 +11,13 @@ protocol NightscoutManager: GlucoseSource {
     func fetchTempTargets() async -> [TempTarget]
     func deleteCarbs(withID id: String) async
     func deleteInsulin(withID id: String) async
-    func deleteManualGlucose(withID id: String) async
+    func deleteGlucose(withID id: String, withDate date: Date) async
     func uploadDeviceStatus() async throws
     func uploadGlucose() async
     func uploadCarbs() async
     func uploadPumpHistory() async
     func uploadOverrides() async
     func uploadTempTargets() async
-    func uploadManualGlucose() async
     func uploadProfiles() async throws
     func uploadNoteTreatment(note: String) async
     func importSettings() async -> ScheduledNightscoutProfile?
@@ -54,7 +53,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     /// coalesce into a single upload run for that pipeline.
     let uploadPipelineInterval: [NightscoutUploadPipeline: TimeInterval] = [
         .carbs: 2, .pumpHistory: 2, .overrides: 2, .tempTargets: 2,
-        .glucose: 2, .manualGlucose: 2, .deviceStatus: 2
+        .glucose: 2, .deviceStatus: 2
     ]
 
     /// Subjects used to request an upload pipeline. The pipeline applies a throttle so
@@ -95,7 +94,6 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         case .overrides: await uploadOverrides()
         case .tempTargets: await uploadTempTargets()
         case .glucose: await uploadGlucose()
-        case .manualGlucose: await uploadManualGlucose()
         case .deviceStatus:
             do { try await uploadDeviceStatus() }
             catch { debug(.nightscout, "deviceStatus upload failed: \(error)") }
@@ -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 }
 
         do {
-            try await nightscout.deleteManualGlucose(withId: id)
+            try await nightscout.deleteGlucose(withId: id, withDate: date)
+            debug(.nightscout, "Glucose deleted")
         } catch {
             debug(
                 .nightscout,
-                "\(DebuggingIdentifiers.failed) Failed to delete Manual Glucose from Nightscout with error: \(error)"
+                "\(DebuggingIdentifiers.failed) Failed to delete Glucose from Nightscout with error: \(error)"
             )
         }
     }
@@ -582,10 +581,6 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         } catch {
             debug(.nightscout, String(describing: error))
         }
-
-        Task.detached {
-            await self.uploadPodAge()
-        }
     }
 
     private func updateOrefDeterminationAsUploaded(_ determination: [Determination]) async {
@@ -610,33 +605,6 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         }
     }
 
-    func uploadPodAge() async {
-        let uploadedPodAge = storage.retrieve(OpenAPS.Nightscout.uploadedPodAge, as: [NightscoutTreatment].self) ?? []
-        if let podAge = storage.retrieve(OpenAPS.Monitor.podAge, as: Date.self),
-           uploadedPodAge.last?.createdAt == nil || podAge != uploadedPodAge.last!.createdAt!
-        {
-            let siteTreatment = NightscoutTreatment(
-                duration: nil,
-                rawDuration: nil,
-                rawRate: nil,
-                absolute: nil,
-                rate: nil,
-                eventType: .nsSiteChange,
-                createdAt: podAge,
-                enteredBy: NightscoutTreatment.local,
-                bolus: nil,
-                insulin: nil,
-                notes: nil,
-                carbs: nil,
-                fat: nil,
-                protein: nil,
-                targetTop: nil,
-                targetBottom: nil
-            )
-            await uploadNonCoreDataTreatments([siteTreatment])
-        }
-    }
-
     func uploadProfiles() async throws {
         if isUploadEnabled {
             do {
@@ -803,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 {
         do {
             try await uploadPumpHistory(pumpHistoryStorage.getPumpHistoryNotYetUploadedToNightscout())
@@ -959,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 {
         guard !treatments.isEmpty, let nightscout = nightscoutAPI, isUploadEnabled else {
             return

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

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

+ 2 - 1
Trio/Sources/Services/Network/TidepoolManager.swift

@@ -320,11 +320,12 @@ extension BaseTidepoolManager {
                                     .processTempBasalEvent(event, existingTempBasalEntries: existingTempBasalEntries)
                             )
                     case .bolus:
+                        guard let amount = event.amount else { return result }
                         let bolusDoseEntry = DoseEntry(
                             type: .bolus,
                             startDate: event.timestamp,
                             endDate: event.timestamp,
-                            value: Double(event.amount!),
+                            value: Double(amount),
                             unit: .units,
                             deliveredUnits: nil,
                             syncIdentifier: event.id,

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

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

+ 0 - 15
TrioTests/CoreDataTests/GlucoseStorageTests.swift

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

+ 1 - 1
scripts/define_common_trio.sh

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