Przeglądaj źródła

Merge branch 'nightscout:dev' into dev

Bastiaan Verhaar 11 miesięcy temu
rodzic
commit
6b10933808
49 zmienionych plików z 15784 dodań i 1286 usunięć
  1. 29 14
      .github/workflows/build_trio.yml
  2. 1 1
      Config.xcconfig
  3. 1 1
      Model/Helper/CarbEntryStored+helper.swift
  4. 34 31
      README.md
  5. 24 4
      Trio.xcodeproj/project.pbxproj
  6. 19 0
      Trio.xcodeproj/xcshareddata/xcschemes/Trio Tests.xcscheme
  7. 17 10
      Trio/Sources/APS/APSManager.swift
  8. 1 1
      Trio/Sources/APS/DeviceDataManager.swift
  9. 7 0
      Trio/Sources/APS/Extensions/DecimalExtensions.swift
  10. 3 3
      Trio/Sources/APS/FetchGlucoseManager.swift
  11. 8 8
      Trio/Sources/APS/FetchTreatmentsManager.swift
  12. 8 4
      Trio/Sources/APS/OpenAPS/OpenAPS.swift
  13. 1 1
      Trio/Sources/APS/OpenAPS/Script.swift
  14. 18 10
      Trio/Sources/APS/Storage/CarbsStorage.swift
  15. 1 1
      Trio/Sources/APS/Storage/GlucoseStorage.swift
  16. 9 4
      Trio/Sources/APS/Storage/TDDStorage.swift
  17. 273 0
      Trio/Sources/Application/LockedResolver.swift
  18. 6 5
      Trio/Sources/Application/TrioApp.swift
  19. 42 0
      Trio/Sources/Helpers/TimeAgoFormatter.swift
  20. 14590 1002
      Trio/Sources/Localizations/Main/Localizable.xcstrings
  21. 35 1
      Trio/Sources/Logger/Logger.swift
  22. 1 1
      Trio/Sources/Models/DecimalPickerSettings.swift
  23. 6 11
      Trio/Sources/Models/TrioSettings.swift
  24. 9 7
      Trio/Sources/Modules/BolusCalculatorConfig/View/BolusCalculatorConfigRootView.swift
  25. 3 1
      Trio/Sources/Modules/ContactImage/View/AddContactImageSheet.swift
  26. 3 1
      Trio/Sources/Modules/ContactImage/View/ContactImageDetailView.swift
  27. 1 1
      Trio/Sources/Modules/DynamicSettings/View/DynamicSettingsRootView.swift
  28. 1 1
      Trio/Sources/Modules/Home/HomeStateModel.swift
  29. 1 11
      Trio/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift
  30. 2 6
      Trio/Sources/Modules/Home/View/Header/LoopView.swift
  31. 20 2
      Trio/Sources/Modules/Home/View/Header/PumpView.swift
  32. 9 11
      Trio/Sources/Modules/Home/View/HomeRootView.swift
  33. 13 24
      Trio/Sources/Modules/MealSettings/MealSettingsStateModel.swift
  34. 47 14
      Trio/Sources/Modules/Onboarding/OnboardingStateModel.swift
  35. 1 1
      Trio/Sources/Modules/Onboarding/View/OnboardingView+AlgorithmUtil.swift
  36. 1 1
      Trio/Sources/Modules/SMBSettings/View/SMBSettingsRootView.swift
  37. 6 7
      Trio/Sources/Modules/Settings/SettingItems.swift
  38. 112 10
      Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift
  39. 16 3
      Trio/Sources/Modules/Treatments/View/ForecastChart.swift
  40. 22 6
      Trio/Sources/Modules/Treatments/View/PopupView.swift
  41. 11 3
      Trio/Sources/Modules/Treatments/View/TreatmentsRootView.swift
  42. 28 17
      Trio/Sources/Services/BolusCalculator/BolusCalculationManager.swift
  43. 30 20
      Trio/Sources/Services/Network/Nightscout/NightscoutAPI.swift
  44. 1 5
      Trio/Sources/Services/UnlockManager/UnlockManager.swift
  45. 3 1
      Trio/Sources/Services/WatchManager/AppleWatchManager.swift
  46. 2 2
      Trio/Sources/Shortcuts/Carbs/AddCarbPresetIntent.swift
  47. 183 18
      TrioTests/BolusCalculatorTests/BolusCalculatorTests.swift
  48. 48 0
      TrioTests/DynamicISFEnableTests.swift
  49. 77 0
      TrioTests/LocalizationTests.swift

+ 29 - 14
.github/workflows/build_trio.yml

@@ -55,29 +55,36 @@ jobs:
 
       - name: Check for alive branches
         if: steps.workflow-permission.outputs.has_permission == 'true'
+        id: check-alive
         env:
           GITHUB_TOKEN: ${{ secrets.GH_PAT }}
         run: |
-          if [[ $(gh api -H "Accept: application/vnd.github+json" /repos/${{ github.repository_owner }}/Trio/branches | jq --raw-output '[.[] | select(.name == "alive-main" or .name == "alive-dev")] | length > 0') == "true" ]]; then
-            echo "Branches 'alive-main' or 'alive-dev' exist."
-            echo "ALIVE_BRANCH_EXISTS=true" >> $GITHUB_ENV
+          branch_list=$(gh api -H "Accept: application/vnd.github+json" /repos/${{ github.repository_owner }}/Trio/branches | jq -r '.[].name')
+      
+          if echo "$branch_list" | grep -q '^alive-main$'; then
+            echo "alive-main exists"
+            echo "ALIVE_MAIN_EXISTS=true" >> $GITHUB_ENV
+          else
+            echo "alive-main missing"
+            echo "ALIVE_MAIN_EXISTS=false" >> $GITHUB_ENV
+          fi
+      
+          if echo "$branch_list" | grep -q '^alive-dev$'; then
+            echo "alive-dev exists"
+            echo "ALIVE_DEV_EXISTS=true" >> $GITHUB_ENV
           else
-            echo "Branches 'alive-main' and 'alive-dev' do not exist."
-            echo "ALIVE_BRANCH_EXISTS=false" >> $GITHUB_ENV
+            echo "alive-dev missing"
+            echo "ALIVE_DEV_EXISTS=false" >> $GITHUB_ENV
           fi
 
-      - name: Create alive branches
-        if: env.ALIVE_BRANCH_EXISTS == 'false'
+      - name: Create alive-main branch if missing
+        if: env.ALIVE_MAIN_EXISTS == 'false'
         env:
           GITHUB_TOKEN: ${{ secrets.GH_PAT }}
         run: |
-          # Get ref for UPSTREAM_REPO:main
           SHA_MAIN=$(curl -sS -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/repos/${{ env.UPSTREAM_REPO }}/git/refs/heads/main | jq -r '.object.sha')
-
-          # Get ref for UPSTREAM_REPO:dev
-          SHA_DEV=$(curl -sS -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/repos/${{ env.UPSTREAM_REPO }}/git/refs/heads/dev | jq -r '.object.sha')
-
-          # Create alive-main branch in Trio fork based on UPSTREAM_REPO:main
+      
+          echo "Creating alive-main from upstream main"
           gh api \
             --method POST \
             -H "Authorization: token $GITHUB_TOKEN" \
@@ -86,7 +93,14 @@ jobs:
             -f ref='refs/heads/alive-main' \
             -f sha=$SHA_MAIN
 
-          # Create alive-dev branch in Trio fork based on UPSTREAM_REPO:dev
+      - name: Create alive-dev branch if missing
+        if: env.ALIVE_DEV_EXISTS == 'false'
+        env:
+          GITHUB_TOKEN: ${{ secrets.GH_PAT }}
+        run: |
+          SHA_DEV=$(curl -sS -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/repos/${{ env.UPSTREAM_REPO }}/git/refs/heads/dev | jq -r '.object.sha')
+      
+          echo "Creating alive-dev from upstream dev"
           gh api \
             --method POST \
             -H "Authorization: token $GITHUB_TOKEN" \
@@ -95,6 +109,7 @@ jobs:
             -f ref='refs/heads/alive-dev' \
             -f sha=$SHA_DEV
 
+                  
   # Checks for changes in upstream repository; if changes exist prompts sync for build
   # Performs keepalive to avoid stale fork
   check_latest_from_upstream:

+ 1 - 1
Config.xcconfig

@@ -1,6 +1,6 @@
 APP_DISPLAY_NAME = Trio
 APP_VERSION = 0.5.0
-APP_DEV_VERSION = 0.5.0.3
+APP_DEV_VERSION = 0.5.0.26
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 DEVELOPER_TEAM = ##TEAM_ID##

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

@@ -14,7 +14,7 @@ extension NSPredicate {
 
     static var carbsForStats: NSPredicate {
         let date = Date.threeMonthsAgo
-        return NSPredicate(format: "date >= %@", date as NSDate)
+        return NSPredicate(format: "date >= %@ AND isFPU == %@", date as NSDate, false as NSNumber)
     }
 
     static var carbsNotYetUploadedToNightscout: NSPredicate {

+ 34 - 31
README.md

@@ -16,7 +16,7 @@ You can either use the Build Script or you can run each command manually.
 
 ### Build Script:
 
-If you copy, paste, and run the following script in Terminal, it will guide you through downloading and installing Trio. More information about the script can be found [here](https://docs.diy-trio.org/operate/build/#build-trio-with-script).
+If you copy, paste, and run the following script in Terminal, it will guide you through downloading and installing Trio. More information about the script can be found [here](https://triodocs.org/0.2.x/operate/build/#build-trio-with-script).
 
 ```
 /bin/bash -c "$(curl -fsSL \
@@ -34,67 +34,70 @@ git clone --branch=<branch> --recurse-submodules https://github.com/nightscout/T
 Create a ConfigOverride.xcconfig file that contains your Apple Developer ID (something like `123A4BCDE5`). This will automate signing of the build targets in Xcode:
 
 Copy the command below, and replace `xxxxxxxxxx` by your Apple Developer ID before running the command in Terminal.
+
 ```
 echo 'DEVELOPER_TEAM = xxxxxxxxxx' > ConfigOverride.xcconfig
 ```
 
 Then launch Xcode and build the Trio app:
+
 ```
 xed .
 ```
 
 ## To build directly in GitHub, without using Xcode:
 
-Instructions:
+**Instructions**:
 
-For main branch:
-* https://github.com/nightscout/Trio/blob/main/fastlane/testflight.md   
+- For **`main`** branch:  
+   https://github.com/nightscout/Trio/blob/main/fastlane/testflight.md
+- For **`dev`** branch:
+  https://github.com/nightscout/Trio/blob/dev/fastlane/testflight.md
 
-For dev branch:
-* https://github.com/nightscout/Trio/blob/dev/fastlane/testflight.md   
+Instructions in **greater detail**, but **not Trio-specific**:
 
-Instructions in greater detail, but not Trio-specific:  
-* https://loopkit.github.io/loopdocs/gh-actions/gh-overview/
+- https://loopkit.github.io/loopdocs/gh-actions/gh-overview/
 
 ## Please understand that Trio is:
+
 - an open-source system developed by enthusiasts and for use at your own risk
 - not CE or FDA approved for therapy.
 
+## Documentation
 
-# Documentation
-
-[Discord Trio - Server ](http://discord.triodocs.org)
-
-[Trio documentation](https://triodocs.org/)
+- [Discord Trio - Server ](https://discord.triodocs.org/)
+- [Trio documentation](https://triodocs.org/)
+- [OpenAPS documentation](https://openaps.readthedocs.io/en/latest/)
+- [Crowdin](https://crowdin.triodocs.org/) is the collaborative platform we are using to manage the **translation** and localization of the Trio App.
+<!--   TODO: Add status graphic for the Crowdin Project -->
 
-TODO: Add link: Trio Website (under development, not existing yet)
+## Support
 
-[OpenAPS documentation](https://openaps.readthedocs.io/en/latest/)
+- [Trio Facebook Group](https://facebook.triodocs.org/)
+- [Loop and Learn Facebook Group](https://m.facebook.com/groups/LOOPandLEARN/)
+- [Looped Facebook Group](https://m.facebook.com/groups/TheLoopedGroup/)
 
-TODO: Add link and status graphic: Crowdin Project for translation of Trio (not existing yet)
+## Contribute
 
-# Support
-
-[Trio Facebook Group](https://facebook.triodocs.org)
+If you would like to give something back to the Trio community, there are several ways to contribute:
 
-[Loop and Learn Facebook Group](https://m.facebook.com/groups/LOOPandLEARN/)
+- **Help others**: assist users by answering questions and guiding them in support communities.
+- Improve the **documentation**: update or expand TrioDocs to help users build and use Trio.
+- Improve the **app**: contribute **code**, features, or fixes to the Trio iOS app.
 
-[Looped Facebook Group](https://m.facebook.com/groups/TheLoopedGroup/)
+### Pay it forward
 
-# Contribute
+When you have successfully built Trio and managed to get it working well for your diabetes management, it's time to pay it forward.
+You can start by **responding to questions** in the **Facebook or Discord** support groups, **helping others** make the best out of Trio.
 
-If you would like to give something back to the Trio community, there are several ways to contribute:
+### Translate
 
-## Pay it forward
-When you have successfully built Trio and managed to get it working well for your diabetes management, it's time to pay it forward. 
-You can start by responding to questions in the Facebook or Discord support groups, helping others make the best out of Trio.
-
-## Translate
-Trio is translated into several languages to make sure it's easy to understand and use all over the world. 
-Translation is done using [Crowdin](https://crowdin.com/project/trio), and does not require any programming skills.
+Trio is translated into several languages to make sure it's easy to understand and use all over the world.
+Translation is done using [Crowdin](https://crowdin.triodocs.org/), and does not require any programming skills.
 If your preferred language is missing or you'd like to improve the translation, please sign up as a translator on [Crowdin](https://crowdin.com/project/trio).
 
-## Develop
+### Develop
+
 Do you speak JS or Swift? Do you have UI/UX skills? Do you know how to optimize API calls or improve data storage? Do you have experience with testing and release management?
 Trio is a collaborative project. We always welcome fellow enthusiasts who can contribute with new code, UI/UX improvements, code reviews, testing and release management.
 If you want to contribute to the development of Trio, please reach out on Discord or Facebook.

+ 24 - 4
Trio.xcodeproj/project.pbxproj

@@ -201,6 +201,7 @@
 		38FEF3FC2737E53800574A46 /* MainStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF3FB2737E53800574A46 /* MainStateModel.swift */; };
 		38FEF3FE2738083E00574A46 /* CGMSettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF3FD2738083E00574A46 /* CGMSettingsProvider.swift */; };
 		38FEF413273B317A00574A46 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF412273B317A00574A46 /* HKUnit.swift */; };
+		3B0B4E6C2DE1439F005C6627 /* LockedResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B0B4E6B2DE1439A005C6627 /* LockedResolver.swift */; };
 		3B2F77862D7E52ED005ED9FA /* TDD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B2F77852D7E52ED005ED9FA /* TDD.swift */; };
 		3B2F77882D7E5387005ED9FA /* CurrentTDDSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B2F77872D7E5387005ED9FA /* CurrentTDDSetup.swift */; };
 		3B3B57C92DA07B3400849D16 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3B57C82DA07B3400849D16 /* GoogleService-Info.plist */; };
@@ -247,6 +248,8 @@
 		3B997DCB2DC00849006B6BB2 /* JSONImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B997DCA2DC00849006B6BB2 /* JSONImporter.swift */; };
 		3B997DCF2DC00A3A006B6BB2 /* JSONImporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B997DCE2DC00A3A006B6BB2 /* JSONImporterTests.swift */; };
 		3B997DD32DC02AEF006B6BB2 /* glucose.json in Resources */ = {isa = PBXBuildFile; fileRef = 3B997DD12DC02AEF006B6BB2 /* glucose.json */; };
+		3BA8D1B32DDB87150006191F /* DecimalExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BA8D1B22DDB870F0006191F /* DecimalExtensions.swift */; };
+		3BAAE60C2DE7766C0049589B /* DynamicISFEnableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAAE60B2DE776630049589B /* DynamicISFEnableTests.swift */; };
 		3BAD36B22D7CDC1A00CC298D /* MainLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAD36B12D7CDC1400CC298D /* MainLoadingView.swift */; };
 		3BAD36CC2D7D420E00CC298D /* CoreDataInitializationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAD36CB2D7D420500CC298D /* CoreDataInitializationCoordinator.swift */; };
 		3BCA5F7C2DC7B16400A7EAC7 /* pumphistory-with-external.json in Resources */ = {isa = PBXBuildFile; fileRef = 3BCA5F7B2DC7B15400A7EAC7 /* pumphistory-with-external.json */; };
@@ -615,6 +618,7 @@
 		DDB37CC72D05127500D99BF4 /* FontExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB37CC62D05127500D99BF4 /* FontExtensions.swift */; };
 		DDBD53FC2DAA903100F940A6 /* OverviewStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDBD53FB2DAA903100F940A6 /* OverviewStepView.swift */; };
 		DDC38E102D9B377800ADCB46 /* OnboardingView+Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC38E0F2D9B376900ADCB46 /* OnboardingView+Util.swift */; };
+		DDC6CA6D2DD90A2A0060EE25 /* LocalizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA6C2DD90A2A0060EE25 /* LocalizationTests.swift */; };
 		DDCAE8332D78D4A800B1BB51 /* TherapySettingsUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCAE8322D78D49C00B1BB51 /* TherapySettingsUtil.swift */; };
 		DDCE790F2D6F97FC000A4D7A /* SubmodulesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCE790E2D6F97F7000A4D7A /* SubmodulesView.swift */; };
 		DDCEBF5B2CC1B76400DF4C36 /* LiveActivity+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCEBF5A2CC1B76400DF4C36 /* LiveActivity+Helper.swift */; };
@@ -625,6 +629,7 @@
 		DDD1631A2C4C695E00CD525A /* EditOverrideForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163192C4C695E00CD525A /* EditOverrideForm.swift */; };
 		DDD1631C2C4C697400CD525A /* AddOverrideForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD1631B2C4C697400CD525A /* AddOverrideForm.swift */; };
 		DDD1631F2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DDD1631D2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodeld */; };
+		DDD5889D2DDDC9A900C8848D /* TimeAgoFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD5889C2DDDC9A900C8848D /* TimeAgoFormatter.swift */; };
 		DDD6D4D32CDE90720029439A /* EstimatedA1cDisplayUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD6D4D22CDE90720029439A /* EstimatedA1cDisplayUnit.swift */; };
 		DDD78A912DC4064800AC63F3 /* carbhistory.json in Resources */ = {isa = PBXBuildFile; fileRef = DDD78A902DC4064800AC63F3 /* carbhistory.json */; };
 		DDD78AD92DC421B500AC63F3 /* enacted.json in Resources */ = {isa = PBXBuildFile; fileRef = DDD78AD72DC421B500AC63F3 /* enacted.json */; };
@@ -1032,6 +1037,7 @@
 		38FEF3FB2737E53800574A46 /* MainStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainStateModel.swift; sourceTree = "<group>"; };
 		38FEF3FD2738083E00574A46 /* CGMSettingsProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGMSettingsProvider.swift; sourceTree = "<group>"; };
 		38FEF412273B317A00574A46 /* HKUnit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HKUnit.swift; sourceTree = "<group>"; };
+		3B0B4E6B2DE1439A005C6627 /* LockedResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockedResolver.swift; sourceTree = "<group>"; };
 		3B2F77852D7E52ED005ED9FA /* TDD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TDD.swift; sourceTree = "<group>"; };
 		3B2F77872D7E5387005ED9FA /* CurrentTDDSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentTDDSetup.swift; sourceTree = "<group>"; };
 		3B3B57C82DA07B3400849D16 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
@@ -1056,6 +1062,8 @@
 		3B997DCA2DC00849006B6BB2 /* JSONImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONImporter.swift; sourceTree = "<group>"; };
 		3B997DCE2DC00A3A006B6BB2 /* JSONImporterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONImporterTests.swift; sourceTree = "<group>"; };
 		3B997DD12DC02AEF006B6BB2 /* glucose.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = glucose.json; sourceTree = "<group>"; };
+		3BA8D1B22DDB870F0006191F /* DecimalExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecimalExtensions.swift; sourceTree = "<group>"; };
+		3BAAE60B2DE776630049589B /* DynamicISFEnableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicISFEnableTests.swift; sourceTree = "<group>"; };
 		3BAD36B12D7CDC1400CC298D /* MainLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainLoadingView.swift; sourceTree = "<group>"; };
 		3BAD36CB2D7D420500CC298D /* CoreDataInitializationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataInitializationCoordinator.swift; sourceTree = "<group>"; };
 		3BCA5F7B2DC7B15400A7EAC7 /* pumphistory-with-external.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "pumphistory-with-external.json"; sourceTree = "<group>"; };
@@ -1430,6 +1438,7 @@
 		DDB37CC62D05127500D99BF4 /* FontExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontExtensions.swift; sourceTree = "<group>"; };
 		DDBD53FB2DAA903100F940A6 /* OverviewStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverviewStepView.swift; sourceTree = "<group>"; };
 		DDC38E0F2D9B376900ADCB46 /* OnboardingView+Util.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingView+Util.swift"; sourceTree = "<group>"; };
+		DDC6CA6C2DD90A2A0060EE25 /* LocalizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationTests.swift; sourceTree = "<group>"; };
 		DDCAE8322D78D49C00B1BB51 /* TherapySettingsUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TherapySettingsUtil.swift; sourceTree = "<group>"; };
 		DDCE790E2D6F97F7000A4D7A /* SubmodulesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmodulesView.swift; sourceTree = "<group>"; };
 		DDCEBF5A2CC1B76400DF4C36 /* LiveActivity+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LiveActivity+Helper.swift"; sourceTree = "<group>"; };
@@ -1440,6 +1449,7 @@
 		DDD163192C4C695E00CD525A /* EditOverrideForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditOverrideForm.swift; sourceTree = "<group>"; };
 		DDD1631B2C4C697400CD525A /* AddOverrideForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddOverrideForm.swift; sourceTree = "<group>"; };
 		DDD1631E2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = TrioCoreDataPersistentContainer.xcdatamodel; sourceTree = "<group>"; };
+		DDD5889C2DDDC9A900C8848D /* TimeAgoFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeAgoFormatter.swift; sourceTree = "<group>"; };
 		DDD6D4D22CDE90720029439A /* EstimatedA1cDisplayUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EstimatedA1cDisplayUnit.swift; sourceTree = "<group>"; };
 		DDD78A902DC4064800AC63F3 /* carbhistory.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = carbhistory.json; sourceTree = "<group>"; };
 		DDD78AD72DC421B500AC63F3 /* enacted.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = enacted.json; sourceTree = "<group>"; };
@@ -2098,8 +2108,9 @@
 			isa = PBXGroup;
 			children = (
 				38E4451D274DB04600EC9A94 /* AppDelegate.swift */,
-				388E595B25AD948C0019842D /* TrioApp.swift */,
 				BD4ED4FC2CF9D5E8000EDC9C /* AppState.swift */,
+				3B0B4E6B2DE1439A005C6627 /* LockedResolver.swift */,
+				388E595B25AD948C0019842D /* TrioApp.swift */,
 			);
 			path = Application;
 			sourceTree = "<group>";
@@ -2382,6 +2393,7 @@
 		388E5A5A25B6F05F0019842D /* Helpers */ = {
 			isa = PBXGroup;
 			children = (
+				DDD5889C2DDDC9A900C8848D /* TimeAgoFormatter.swift */,
 				DD82D4B72DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift */,
 				DDCAE8322D78D49C00B1BB51 /* TherapySettingsUtil.swift */,
 				BD249DA62D42FE3800412DEB /* Calendar+GlucoseStatsChart.swift */,
@@ -2442,13 +2454,14 @@
 		38A504F625DDA0E200C5B9E8 /* Extensions */ = {
 			isa = PBXGroup;
 			children = (
+				CE2FAD39297D93F0001A872C /* BloodGlucoseExtensions.swift */,
+				3BA8D1B22DDB870F0006191F /* DecimalExtensions.swift */,
 				DDB37CC62D05127500D99BF4 /* FontExtensions.swift */,
-				38A5049125DD9C4000C5B9E8 /* UserDefaultsExtensions.swift */,
-				38BF021625E7CBBC00579895 /* PumpManagerExtensions.swift */,
 				CEB434E628B9053300B70274 /* LoopUIColorPalette+Default.swift */,
 				CE48C86328CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift */,
 				CE48C86528CA6B48007C0598 /* OmniPodManagerExtensions.swift */,
-				CE2FAD39297D93F0001A872C /* BloodGlucoseExtensions.swift */,
+				38BF021625E7CBBC00579895 /* PumpManagerExtensions.swift */,
+				38A5049125DD9C4000C5B9E8 /* UserDefaultsExtensions.swift */,
 			);
 			path = Extensions;
 			sourceTree = "<group>";
@@ -2557,11 +2570,13 @@
 		38FCF3EE25E9028E0078B0D1 /* TrioTests */ = {
 			isa = PBXGroup;
 			children = (
+				DDC6CA6C2DD90A2A0060EE25 /* LocalizationTests.swift */,
 				3B997DD22DC02AEF006B6BB2 /* JSONImporterData */,
 				BD8FC05C2D6618BE00B95AED /* BolusCalculatorTests */,
 				BD8FC0552D66187700B95AED /* CoreDataTests */,
 				38FCF3F125E9028E0078B0D1 /* Info.plist */,
 				CEE9A65D2BBC9F6500EB5194 /* CalibrationsTests.swift */,
+				3BAAE60B2DE776630049589B /* DynamicISFEnableTests.swift */,
 				38FCF3F825E902C20078B0D1 /* FileStorageTests.swift */,
 				3B997DCE2DC00A3A006B6BB2 /* JSONImporterTests.swift */,
 				CE1F6DD82BADF4620064EB8D /* PluginManagerTests.swift */,
@@ -4148,6 +4163,7 @@
 				CEB434E328B8F9DB00B70274 /* BluetoothStateManager.swift in Sources */,
 				BDC2EA452C3043B000E5BBD0 /* OverrideStorage.swift in Sources */,
 				3811DE4225C9D4A100A708ED /* SettingsDataFlow.swift in Sources */,
+				DDD5889D2DDDC9A900C8848D /* TimeAgoFormatter.swift in Sources */,
 				CEE9A6562BBB418300EB5194 /* CalibrationsRootView.swift in Sources */,
 				3811DE2525C9D48300A708ED /* MainRootView.swift in Sources */,
 				CE94598229E9E3D30047C9C6 /* WatchConfigProvider.swift in Sources */,
@@ -4248,6 +4264,7 @@
 				3811DE0C25C9D32F00A708ED /* BaseProvider.swift in Sources */,
 				CE95BF5A2BA62E4A00DC3DE3 /* PluginSource.swift in Sources */,
 				DD21FCB52C6952AD00AF2C25 /* DecimalPickerSettings.swift in Sources */,
+				3B0B4E6C2DE1439F005C6627 /* LockedResolver.swift in Sources */,
 				3811DE5C25C9D4D500A708ED /* Formatters.swift in Sources */,
 				3871F39F25ED895A0013ECB5 /* Decimal+Extensions.swift in Sources */,
 				CEE9A6592BBB418300EB5194 /* CalibrationsDataFlow.swift in Sources */,
@@ -4535,6 +4552,7 @@
 				DDD163122C4C689900CD525A /* AdjustmentsStateModel.swift in Sources */,
 				BD47FDD72D8B64D20043966B /* CarbRatioStepView.swift in Sources */,
 				3B2F77862D7E52ED005ED9FA /* TDD.swift in Sources */,
+				3BA8D1B32DDB87150006191F /* DecimalExtensions.swift in Sources */,
 				DD3F1F892D9E078D00DCE7B3 /* TherapySettingEditorView.swift in Sources */,
 				DD1745132C54169400211FAC /* DevicesView.swift in Sources */,
 				7F7B756BE8543965D9FDF1A2 /* DataTableDataFlow.swift in Sources */,
@@ -4632,9 +4650,11 @@
 				CEE9A65E2BBC9F6500EB5194 /* CalibrationsTests.swift in Sources */,
 				BD8FC0622D6619E600B95AED /* OverrideStorageTests.swift in Sources */,
 				BD8FC0592D66189700B95AED /* TestAssembly.swift in Sources */,
+				DDC6CA6D2DD90A2A0060EE25 /* LocalizationTests.swift in Sources */,
 				3B997DCF2DC00A3A006B6BB2 /* JSONImporterTests.swift in Sources */,
 				BD8FC0662D661A0000B95AED /* GlucoseStorageTests.swift in Sources */,
 				BD8FC05B2D6618AF00B95AED /* DeterminationStorageTests.swift in Sources */,
+				3BAAE60C2DE7766C0049589B /* DynamicISFEnableTests.swift in Sources */,
 				CE1F6DD92BADF4620064EB8D /* PluginManagerTests.swift in Sources */,
 				38FCF3F925E902C20078B0D1 /* FileStorageTests.swift in Sources */,
 				BD8FC0602D6619DB00B95AED /* CarbsStorageTests.swift in Sources */,

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

@@ -36,6 +36,16 @@
       debugDocumentVersioning = "YES"
       debugServiceExtension = "internal"
       allowLocationSimulation = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "388E595725AD948C0019842D"
+            BuildableName = "Trio.app"
+            BlueprintName = "Trio"
+            ReferencedContainer = "container:Trio.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
    </LaunchAction>
    <ProfileAction
       buildConfiguration = "Release"
@@ -43,6 +53,15 @@
       savedToolIdentifier = ""
       useCustomWorkingDirectory = "NO"
       debugDocumentVersioning = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "388E595725AD948C0019842D"
+            BuildableName = "Trio.app"
+            BlueprintName = "Trio"
+            ReferencedContainer = "container:Trio.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
    </ProfileAction>
    <AnalyzeAction
       buildConfiguration = "Debug">

+ 17 - 10
Trio/Sources/APS/APSManager.swift

@@ -22,7 +22,11 @@ protocol APSManager {
     func enactTempBasal(rate: Double, duration: TimeInterval) async
     func determineBasal() async throws
     func determineBasalSync() async throws
-    func simulateDetermineBasal(simulatedCarbsAmount: Decimal, simulatedBolusAmount: Decimal) async -> Determination?
+    func simulateDetermineBasal(
+        simulatedCarbsAmount: Decimal,
+        simulatedBolusAmount: Decimal,
+        simulatedCarbsDate: Date?
+    ) async -> Determination?
     func roundBolus(amount: Decimal) -> Decimal
     var lastError: CurrentValueSubject<Error?, Never> { get }
     func cancelBolus(_ callback: ((Bool, String) -> Void)?) async
@@ -445,12 +449,6 @@ final class BaseAPSManager: APSManager, Injectable {
             return true
         }
 
-        guard isValidGlucoseData else {
-            debug(.apsManager, "Glucose validation failed")
-            processError(APSError.glucoseError(message: "Glucose validation failed"))
-            return
-        }
-
         do {
             let now = Date()
 
@@ -462,6 +460,10 @@ final class BaseAPSManager: APSManager, Injectable {
             try await openAPS.createProfiles()
             let determination = try await openAPS.determineBasal(currentTemp: await currentTemp, clock: now)
 
+            guard isValidGlucoseData else {
+                throw APSError.glucoseError(message: "Glucose validation failed")
+            }
+
             if let determination = determination {
                 // Capture weak self in closure
                 await MainActor.run { [weak self] in
@@ -480,7 +482,11 @@ final class BaseAPSManager: APSManager, Injectable {
         _ = try await determineBasal()
     }
 
-    func simulateDetermineBasal(simulatedCarbsAmount: Decimal, simulatedBolusAmount: Decimal) async -> Determination? {
+    func simulateDetermineBasal(
+        simulatedCarbsAmount: Decimal,
+        simulatedBolusAmount: Decimal,
+        simulatedCarbsDate: Date? = nil
+    ) async -> Determination? {
         do {
             let temp = try await fetchCurrentTempBasal(date: Date.now)
             return try await openAPS.determineBasal(
@@ -488,11 +494,12 @@ final class BaseAPSManager: APSManager, Injectable {
                 clock: Date(),
                 simulatedCarbsAmount: simulatedCarbsAmount,
                 simulatedBolusAmount: simulatedBolusAmount,
+                simulatedCarbsDate: simulatedCarbsDate,
                 simulation: true
             )
         } catch {
             debugPrint(
-                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Error occurred in invokeDummyDetermineBasalSync: \(error)"
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Error occurred in simulateDetermineBasal: \(error)"
             )
             return nil
         }
@@ -1270,7 +1277,7 @@ extension BaseAPSManager: PumpManagerStatusObserver {
                 guard self.privateContext.hasChanges else { return }
                 try self.privateContext.save()
             } catch {
-                print("Failed to fetch or save battery: \(error.localizedDescription)")
+                debug(.apsManager, "Failed to fetch or save battery: \(error)")
             }
         }
         // TODO: - remove this after ensuring that NS still gets the same infos from Core Data

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

@@ -176,7 +176,7 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
                             try self.privateContext.save()
 
                         } catch {
-                            print("Failed to delete OpenAPS_Battery entries: \(error.localizedDescription)")
+                            debug(.deviceManager, "Failed to delete OpenAPS_Battery entries: \(error)")
                         }
                     }
                 }

+ 7 - 0
Trio/Sources/APS/Extensions/DecimalExtensions.swift

@@ -0,0 +1,7 @@
+import Foundation
+
+extension Decimal {
+    func clamp(to pickerSetting: PickerSetting) -> Decimal {
+        max(min(self, pickerSetting.max), pickerSetting.min)
+    }
+}

+ 3 - 3
Trio/Sources/APS/FetchGlucoseManager.swift

@@ -11,7 +11,7 @@ protocol FetchGlucoseManager: SourceInfoProvider {
     func updateGlucoseSource(cgmGlucoseSourceType: CGMType, cgmGlucosePluginId: String, newManager: CGMManagerUI?)
     func deleteGlucoseSource() async
     func removeCalibrations()
-    var glucoseSource: GlucoseSource! { get }
+    var glucoseSource: GlucoseSource? { get }
     var cgmManager: CGMManagerUI? { get }
     var cgmGlucoseSourceType: CGMType { get set }
     var cgmGlucosePluginId: String { get }
@@ -113,7 +113,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         timer.resume()
     }
 
-    var glucoseSource: GlucoseSource!
+    var glucoseSource: GlucoseSource?
 
     func removeCalibrations() {
         calibrationService.removeAllCalibrations()
@@ -286,7 +286,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     }
 
     func sourceInfo() -> [String: Any]? {
-        glucoseSource.sourceInfo()
+        glucoseSource?.sourceInfo()
     }
 
     private func overcalibrate(entries: [BloodGlucose]) -> [BloodGlucose] {

+ 8 - 8
Trio/Sources/APS/FetchTreatmentsManager.swift

@@ -35,17 +35,17 @@ final class BaseFetchTreatmentsManager: FetchTreatmentsManager, Injectable {
                         async let carbs = self.nightscoutManager.fetchCarbs()
                         async let tempTargets = self.nightscoutManager.fetchTempTargets()
 
-                        // Filter and store if not from "Trio"
-                        let filteredCarbs = await carbs.filter { $0.enteredBy != CarbsEntry.local }
-                        if filteredCarbs.isNotEmpty {
-                            try await self.carbsStorage.storeCarbs(filteredCarbs, areFetchedFromRemote: true)
+                        // Store carbs directly (no filtering, as it's done in fetchCarbs)
+                        let fetchedCarbs = await carbs
+                        if fetchedCarbs.isNotEmpty {
+                            try await self.carbsStorage.storeCarbs(fetchedCarbs, areFetchedFromRemote: true)
                         }
 
-                        // Filter and store if not from Trio
-                        let filteredTargets = await tempTargets.filter { $0.enteredBy != TempTarget.local }
-                        if filteredTargets.isNotEmpty {
+                        // Store temp targets directly (no filtering, as it's done in fetchTempTargets)
+                        let fetchedTargets = await tempTargets
+                        if fetchedTargets.isNotEmpty {
                             // Sort temp targets by creation date
-                            let sortedTargets = filteredTargets.sorted { $0.createdAt < $1.createdAt }
+                            let sortedTargets = fetchedTargets.sorted { $0.createdAt < $1.createdAt }
 
                             // Iterate and store each temp target
                             for (index, tempTarget) in sortedTargets.enumerated() {

+ 8 - 4
Trio/Sources/APS/OpenAPS/OpenAPS.swift

@@ -119,7 +119,7 @@ final class OpenAPS {
         }
     }
 
-    private func fetchAndProcessCarbs(additionalCarbs: Decimal? = nil) async throws -> String {
+    private func fetchAndProcessCarbs(additionalCarbs: Decimal? = nil, carbsDate: Date? = nil) async throws -> String {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
             onContext: context,
@@ -136,13 +136,16 @@ final class OpenAPS {
             var jsonArray = self.jsonConverter.convertToJSON(carbResults)
 
             if let additionalCarbs = additionalCarbs {
+                let formattedDate = carbsDate.map { ISO8601DateFormatter().string(from: $0) } ?? ISO8601DateFormatter()
+                    .string(from: Date())
+
                 let additionalEntry = [
                     "carbs": Double(additionalCarbs),
-                    "actualDate": ISO8601DateFormatter().string(from: Date()),
+                    "actualDate": formattedDate,
                     "id": UUID().uuidString,
                     "note": NSNull(),
                     "protein": 0,
-                    "created_at": ISO8601DateFormatter().string(from: Date()),
+                    "created_at": formattedDate,
                     "isFPU": false,
                     "fat": 0,
                     "enteredBy": "Trio"
@@ -278,6 +281,7 @@ final class OpenAPS {
         clock: Date = Date(),
         simulatedCarbsAmount: Decimal? = nil,
         simulatedBolusAmount: Decimal? = nil,
+        simulatedCarbsDate: Date? = nil,
         simulation: Bool = false
     ) async throws -> Determination? {
         debug(.openAPS, "Start determineBasal")
@@ -287,7 +291,7 @@ final class OpenAPS {
 
         // Perform asynchronous calls in parallel
         async let pumpHistoryObjectIDs = fetchPumpHistoryObjectIDs() ?? []
-        async let carbs = fetchAndProcessCarbs(additionalCarbs: simulatedCarbsAmount ?? 0)
+        async let carbs = fetchAndProcessCarbs(additionalCarbs: simulatedCarbsAmount ?? 0, carbsDate: simulatedCarbsDate)
         async let glucose = fetchAndProcessGlucose()
         async let oref2 = oref2()
         async let profileAsync = loadFileFromStorageAsync(name: Settings.profile)

+ 1 - 1
Trio/Sources/APS/OpenAPS/Script.swift

@@ -10,7 +10,7 @@ struct Script {
             do {
                 body = try String(contentsOf: url)
             } catch {
-                print("Error loading script: \(error.localizedDescription)")
+                debug(.openAPS, "Error loading script: \(error)")
                 body = "Error loading script"
             }
         } else {

+ 18 - 10
Trio/Sources/APS/Storage/CarbsStorage.swift

@@ -27,6 +27,8 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
 
     private let updateSubject = PassthroughSubject<Void, Never>()
 
+    private let settingsProvider = PickerSettingsProvider.shared
+
     var updatePublisher: AnyPublisher<Void, Never> {
         updateSubject.eraseToAnyPublisher()
     }
@@ -111,7 +113,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
 
      - Returns: The computed duration in hours.
      */
-    private func calculateComputedDuration(fpus: Decimal, timeCap: Int) -> Int {
+    private func calculateComputedDuration(fpus: Decimal, timeCap: Decimal) -> Decimal {
         switch fpus {
         case ..<2:
             return 3
@@ -145,22 +147,25 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         createdAt: Date,
         actualDate: Date?
     ) -> ([CarbsEntry], Decimal) {
-        let interval = settings.settings.minuteInterval
-        let timeCap = settings.settings.timeCap
-        let adjustment = settings.settings.individualAdjustmentFactor
-        let delay = settings.settings.delay
+        let trioSettings = settings.settings
+        let providerSettings = settingsProvider.settings
+
+        let interval = trioSettings.minuteInterval.clamp(to: providerSettings.minuteInterval)
+        let timeCap = trioSettings.timeCap.clamp(to: providerSettings.timeCap)
+        let adjustment = trioSettings.individualAdjustmentFactor.clamp(to: providerSettings.individualAdjustmentFactor)
+        let delay = trioSettings.delay.clamp(to: providerSettings.delay)
 
         let kcal = protein * 4 + fat * 9
         let carbEquivalents = (kcal / 10) * adjustment
         let fpus = carbEquivalents / 10
         var computedDuration = calculateComputedDuration(fpus: fpus, timeCap: timeCap)
 
-        var carbEquivalentSize: Decimal = carbEquivalents / Decimal(computedDuration)
-        carbEquivalentSize /= Decimal(60 / interval)
+        var carbEquivalentSize: Decimal = carbEquivalents / computedDuration
+        carbEquivalentSize /= Decimal(60) / interval
 
         if carbEquivalentSize < 1.0 {
             carbEquivalentSize = 1.0
-            computedDuration = Int(carbEquivalents / carbEquivalentSize)
+            computedDuration = min(carbEquivalents / carbEquivalentSize, timeCap)
         }
 
         let roundedEquivalent: Double = round(Double(carbEquivalentSize * 10)) / 10
@@ -172,9 +177,12 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         var futureCarbArray = [CarbsEntry]()
         var firstIndex = true
 
+        // convert Decimal minutes to TimeInterval in seconds
+        let delayTimeInterval = TimeInterval(delay * 60)
+        let intervalTimeInterval = TimeInterval(interval * 60)
         while carbEquivalents > 0, numberOfEquivalents > 0 {
-            useDate = firstIndex ? useDate.addingTimeInterval(delay.minutes.timeInterval) : useDate
-                .addingTimeInterval(interval.minutes.timeInterval)
+            useDate = firstIndex ? useDate.addingTimeInterval(delayTimeInterval) : useDate
+                .addingTimeInterval(intervalTimeInterval)
             firstIndex = false
 
             let eachCarbEntry = CarbsEntry(

+ 1 - 1
Trio/Sources/APS/Storage/GlucoseStorage.swift

@@ -287,7 +287,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                 let results = try self.context.fetch(fr)
                 date = results.first?.date
             } catch let error as NSError {
-                print("Fetch error: \(DebuggingIdentifiers.failed) \(error.localizedDescription), \(error.userInfo)")
+                debug(.storage, "Fetch error: \(DebuggingIdentifiers.failed) \(error), \(error.userInfo)")
             }
         }
 

+ 9 - 4
Trio/Sources/APS/Storage/TDDStorage.swift

@@ -643,13 +643,18 @@ final class BaseTDDStorage: TDDStorage, Injectable {
     /// - The record's date is within the last 7 days.
     /// - The total value is greater than 0.
     ///
-    /// It then checks if at least 85% of the expected data points are present,
+    /// It then checks if at least 75% of the expected data points are present,
     /// assuming at least 288 expected entries per day (one every 5 minutes).
     ///
     /// - Returns: `true` if sufficient TDD data is available, otherwise `false`.
     /// - Throws: An error if the Core Data count operation fails.
     func hasSufficientTDD() async throws -> Bool {
-        try await privateContext.perform {
+        try await BaseTDDStorage.hasSufficientTDD(context: privateContext)
+    }
+
+    /// internal function with context exposed to enable testing
+    static func hasSufficientTDD(context: NSManagedObjectContext) async throws -> Bool {
+        try await context.perform {
             let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "TDDStored")
             fetchRequest.predicate = NSPredicate(
                 format: "date > %@ AND total > 0",
@@ -657,8 +662,8 @@ final class BaseTDDStorage: TDDStorage, Injectable {
             )
             fetchRequest.resultType = .countResultType
 
-            let count = try self.privateContext.count(for: fetchRequest)
-            let threshold = Int(Double(7 * 288) * 0.85)
+            let count = try context.count(for: fetchRequest)
+            let threshold = Int(Double(7 * 288) * 0.75)
             return count >= threshold
         }
     }

+ 273 - 0
Trio/Sources/Application/LockedResolver.swift

@@ -0,0 +1,273 @@
+import Foundation
+import Swinject
+
+/// This class adds a simple wrapper around a Swinject resolver to ensure that only one thread can
+/// access it at any given time.
+struct LockedResolver: Resolver {
+    let resolver: Resolver
+    let lock: NSRecursiveLock
+
+    func resolve<Service, Arg1>(_ serviceType: Service.Type, argument: Arg1) -> Service? {
+        lock.lock()
+        let service = resolver.resolve(serviceType, argument: argument)
+        lock.unlock()
+        return service
+    }
+
+    func resolve<Service, Arg1>(_ serviceType: Service.Type, name: String?, argument: Arg1) -> Service? {
+        lock.lock()
+        let service = resolver.resolve(serviceType, name: name, argument: argument)
+        lock.unlock()
+        return service
+    }
+
+    func resolve<Service, Arg1, Arg2>(_ serviceType: Service.Type, arguments arg1: Arg1, _ arg2: Arg2) -> Service? {
+        lock.lock()
+        let service = resolver.resolve(serviceType, arguments: arg1, arg2)
+        lock.unlock()
+        return service
+    }
+
+    func resolve<Service, Arg1, Arg2>(
+        _ serviceType: Service.Type,
+        name: String?,
+        arguments arg1: Arg1,
+        _ arg2: Arg2
+    ) -> Service? {
+        lock.lock()
+        let service = resolver.resolve(serviceType, name: name, arguments: arg1, arg2)
+        lock.unlock()
+        return service
+    }
+
+    func resolve<Service, Arg1, Arg2, Arg3>(
+        _ serviceType: Service.Type,
+        arguments arg1: Arg1,
+        _ arg2: Arg2,
+        _ arg3: Arg3
+    ) -> Service? {
+        lock.lock()
+        let service = resolver.resolve(serviceType, arguments: arg1, arg2, arg3)
+        lock.unlock()
+        return service
+    }
+
+    func resolve<Service, Arg1, Arg2, Arg3>(
+        _ serviceType: Service.Type,
+        name: String?,
+        arguments arg1: Arg1,
+        _ arg2: Arg2,
+        _ arg3: Arg3
+    ) -> Service? {
+        lock.lock()
+        let service = resolver.resolve(serviceType, name: name, arguments: arg1, arg2, arg3)
+        lock.unlock()
+        return service
+    }
+
+    func resolve<Service, Arg1, Arg2, Arg3, Arg4>(
+        _ serviceType: Service.Type,
+        arguments arg1: Arg1,
+        _ arg2: Arg2,
+        _ arg3: Arg3,
+        _ arg4: Arg4
+    ) -> Service? {
+        lock.lock()
+        let service = resolver.resolve(serviceType, arguments: arg1, arg2, arg3, arg4)
+        lock.unlock()
+        return service
+    }
+
+    func resolve<Service, Arg1, Arg2, Arg3, Arg4>(
+        _ serviceType: Service.Type,
+        name: String?,
+        arguments arg1: Arg1,
+        _ arg2: Arg2,
+        _ arg3: Arg3,
+        _ arg4: Arg4
+    ) -> Service? {
+        lock.lock()
+        let service = resolver.resolve(serviceType, name: name, arguments: arg1, arg2, arg3, arg4)
+        lock.unlock()
+        return service
+    }
+
+    func resolve<Service, Arg1, Arg2, Arg3, Arg4, Arg5>(
+        _ serviceType: Service.Type,
+        arguments arg1: Arg1,
+        _ arg2: Arg2,
+        _ arg3: Arg3,
+        _ arg4: Arg4,
+        _ arg5: Arg5
+    ) -> Service? {
+        lock.lock()
+        let service = resolver.resolve(serviceType, arguments: arg1, arg2, arg3, arg4, arg5)
+        lock.unlock()
+        return service
+    }
+
+    func resolve<Service, Arg1, Arg2, Arg3, Arg4, Arg5>(
+        _ serviceType: Service.Type,
+        name: String?,
+        arguments arg1: Arg1,
+        _ arg2: Arg2,
+        _ arg3: Arg3,
+        _ arg4: Arg4,
+        _ arg5: Arg5
+    ) -> Service? {
+        lock.lock()
+        let service = resolver.resolve(serviceType, name: name, arguments: arg1, arg2, arg3, arg4, arg5)
+        lock.unlock()
+        return service
+    }
+
+    func resolve<Service, Arg1, Arg2, Arg3, Arg4, Arg5, Arg6>(
+        _ serviceType: Service.Type,
+        arguments arg1: Arg1,
+        _ arg2: Arg2,
+        _ arg3: Arg3,
+        _ arg4: Arg4,
+        _ arg5: Arg5,
+        _ arg6: Arg6
+    ) -> Service? {
+        lock.lock()
+        let service = resolver.resolve(serviceType, arguments: arg1, arg2, arg3, arg4, arg5, arg6)
+        lock.unlock()
+        return service
+    }
+
+    func resolve<Service, Arg1, Arg2, Arg3, Arg4, Arg5, Arg6>(
+        _ serviceType: Service.Type,
+        name: String?,
+        arguments arg1: Arg1,
+        _ arg2: Arg2,
+        _ arg3: Arg3,
+        _ arg4: Arg4,
+        _ arg5: Arg5,
+        _ arg6: Arg6
+    ) -> Service? {
+        lock.lock()
+        let service = resolver.resolve(serviceType, name: name, arguments: arg1, arg2, arg3, arg4, arg5, arg6)
+        lock.unlock()
+        return service
+    }
+
+    func resolve<Service, Arg1, Arg2, Arg3, Arg4, Arg5, Arg6, Arg7>(
+        _ serviceType: Service.Type,
+        arguments arg1: Arg1,
+        _ arg2: Arg2,
+        _ arg3: Arg3,
+        _ arg4: Arg4,
+        _ arg5: Arg5,
+        _ arg6: Arg6,
+        _ arg7: Arg7
+    ) -> Service? {
+        lock.lock()
+        let service = resolver.resolve(serviceType, arguments: arg1, arg2, arg3, arg4, arg5, arg6, arg7)
+        lock.unlock()
+        return service
+    }
+
+    func resolve<Service, Arg1, Arg2, Arg3, Arg4, Arg5, Arg6, Arg7>(
+        _ serviceType: Service.Type,
+        name: String?,
+        arguments arg1: Arg1,
+        _ arg2: Arg2,
+        _ arg3: Arg3,
+        _ arg4: Arg4,
+        _ arg5: Arg5,
+        _ arg6: Arg6,
+        _ arg7: Arg7
+    ) -> Service? {
+        lock.lock()
+        let service = resolver.resolve(serviceType, name: name, arguments: arg1, arg2, arg3, arg4, arg5, arg6, arg7)
+        lock.unlock()
+        return service
+    }
+
+    func resolve<Service, Arg1, Arg2, Arg3, Arg4, Arg5, Arg6, Arg7, Arg8>(
+        _ serviceType: Service.Type,
+        arguments arg1: Arg1,
+        _ arg2: Arg2,
+        _ arg3: Arg3,
+        _ arg4: Arg4,
+        _ arg5: Arg5,
+        _ arg6: Arg6,
+        _ arg7: Arg7,
+        _ arg8: Arg8
+    ) -> Service? {
+        lock.lock()
+        let service = resolver.resolve(serviceType, arguments: arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8)
+        lock.unlock()
+        return service
+    }
+
+    func resolve<Service, Arg1, Arg2, Arg3, Arg4, Arg5, Arg6, Arg7, Arg8>(
+        _ serviceType: Service.Type,
+        name: String?,
+        arguments arg1: Arg1,
+        _ arg2: Arg2,
+        _ arg3: Arg3,
+        _ arg4: Arg4,
+        _ arg5: Arg5,
+        _ arg6: Arg6,
+        _ arg7: Arg7,
+        _ arg8: Arg8
+    ) -> Service? {
+        lock.lock()
+        let service = resolver.resolve(serviceType, name: name, arguments: arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8)
+        lock.unlock()
+        return service
+    }
+
+    func resolve<Service, Arg1, Arg2, Arg3, Arg4, Arg5, Arg6, Arg7, Arg8, Arg9>(
+        _ serviceType: Service.Type,
+        arguments arg1: Arg1,
+        _ arg2: Arg2,
+        _ arg3: Arg3,
+        _ arg4: Arg4,
+        _ arg5: Arg5,
+        _ arg6: Arg6,
+        _ arg7: Arg7,
+        _ arg8: Arg8,
+        _ arg9: Arg9
+    ) -> Service? {
+        lock.lock()
+        let service = resolver.resolve(serviceType, arguments: arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)
+        lock.unlock()
+        return service
+    }
+
+    func resolve<Service, Arg1, Arg2, Arg3, Arg4, Arg5, Arg6, Arg7, Arg8, Arg9>(
+        _ serviceType: Service.Type,
+        name: String?,
+        arguments arg1: Arg1,
+        _ arg2: Arg2,
+        _ arg3: Arg3,
+        _ arg4: Arg4,
+        _ arg5: Arg5,
+        _ arg6: Arg6,
+        _ arg7: Arg7,
+        _ arg8: Arg8,
+        _ arg9: Arg9
+    ) -> Service? {
+        lock.lock()
+        let service = resolver.resolve(serviceType, name: name, arguments: arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)
+        lock.unlock()
+        return service
+    }
+
+    func resolve<Service>(_ serviceType: Service.Type) -> Service? {
+        lock.lock()
+        let service = resolver.resolve(serviceType)
+        lock.unlock()
+        return service
+    }
+
+    func resolve<Service>(_ serviceType: Service.Type, name: String?) -> Service? {
+        lock.lock()
+        let service = resolver.resolve(serviceType, name: name)
+        lock.unlock()
+        return service
+    }
+}

+ 6 - 5
Trio/Sources/Application/TrioApp.swift

@@ -52,15 +52,16 @@ extension Notification.Name {
         SecurityAssembly()
     ], parent: nil, defaultObjectScope: .container)
 
+    // Simple thread-safe wrapper
+    private static let resolverLock = NSRecursiveLock()
+
     var resolver: Resolver {
-        TrioApp.assembler.resolver
+        TrioApp.resolver
     }
 
-    // Temp static var
-    // Use to backward compatibility with old Dependencies logic on Logger
-    // TODO: Remove var after update "Use Dependencies" logic in Logger
     static var resolver: Resolver {
-        TrioApp.assembler.resolver
+        // Return a simple wrapper that adds locking
+        LockedResolver(resolver: assembler.resolver, lock: resolverLock)
     }
 
     private func loadServices() {

+ 42 - 0
Trio/Sources/Helpers/TimeAgoFormatter.swift

@@ -0,0 +1,42 @@
+//
+//  TimeAgoFormatter.swift
+//  Trio
+//
+//  Created by Cengiz Deniz on 21.05.25.
+//
+import Foundation
+
+enum TimeAgoFormatter {
+    /// Returns a user-facing string for how many minutes ago the given date occurred,
+    /// formatted with non-breaking spaces and localized abbreviation.
+    ///
+    /// - Parameter date: The past `Date` to calculate elapsed time from.
+    /// - Returns: A formatted string like `"< 1 m"` or `"2 m"`. Returns `"--"` if the date is `nil`.
+    static func minutesAgo(from date: Date?) -> String {
+        guard let date = date else {
+            return "--"
+        }
+
+        let secondsAgo = -date.timeIntervalSinceNow
+        let minutesAgo = Int(floor(secondsAgo / 60))
+
+        if minutesAgo >= 1 {
+            let minuteString = Formatter.timaAgoFormatter.string(for: Double(minutesAgo)) ?? "\(minutesAgo)"
+            return minuteString + "\u{00A0}" + String(localized: "m", comment: "Abbreviation for Minutes")
+        } else {
+            return "<" + "\u{00A0}" + "1" + "\u{00A0}" + String(localized: "m", comment: "Abbreviation for Minutes")
+        }
+    }
+
+    // Calculates the floored integer value of how many full minutes ago the given date occurred.
+    ///
+    /// - Parameter date: The past `Date` to compare against the current time.
+    /// - Returns: An integer representing the number of full minutes since the given date.
+    ///            Returns `Int.max` if the date is `nil`.
+    static func minutesAgoValue(from date: Date?) -> Int {
+        guard let date = date else {
+            return Int.max
+        }
+        return Int(floor(-date.timeIntervalSinceNow / 60))
+    }
+}

Plik diff jest za duży
+ 14590 - 1002
Trio/Sources/Localizations/Main/Localizable.xcstrings


+ 35 - 1
Trio/Sources/Logger/Logger.swift

@@ -40,6 +40,29 @@ func info(
     }.perform()
 }
 
+func info(
+    _ category: Logger.Category,
+    _ message: String,
+    notificationText: String,
+    type: MessageType = .info,
+    file: String = #file,
+    function: String = #function,
+    line: UInt = #line
+) {
+    DispatchWorkItem(qos: .background, flags: .enforceQoS) {
+        loggerLock.perform {
+            category.logger.info(
+                message,
+                notificationText: notificationText,
+                type: type,
+                file: file,
+                function: function,
+                line: line
+            )
+        }
+    }.perform()
+}
+
 func warning(
     _ category: Logger.Category,
     _ message: String,
@@ -246,11 +269,22 @@ final class Logger {
         function: String = #function,
         line: UInt = #line
     ) {
+        info(message, notificationText: message, type: type, file: file, function: function, line: line)
+    }
+
+    func info(
+        _ message: String,
+        notificationText: String,
+        type: MessageType = .info,
+        file: String = #file,
+        function: String = #function,
+        line: UInt = #line
+    ) {
         let printedMessage = "INFO: \(message)"
         os_log("%@ - %@ - %d %{public}@", log: log, type: .info, file.file, function, line, printedMessage)
         reporter.log(category.name, printedMessage, file: file, function: function, line: line)
 
-        showAlert(message, type: type)
+        showAlert(notificationText, type: type)
     }
 
     func warning(

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

@@ -131,7 +131,7 @@ struct DecimalPickerSettings {
     )
     var threshold_setting = PickerSetting(value: 60, step: 1, min: 60, max: 120, type: PickerSetting.PickerSettingType.glucose)
     var updateInterval = PickerSetting(value: 20, step: 5, min: 1, max: 60, type: PickerSetting.PickerSettingType.minute)
-    var delay = PickerSetting(value: 60, step: 10, min: 60, max: 120, type: PickerSetting.PickerSettingType.minute)
+    var delay = PickerSetting(value: 60, step: 5, min: 15, max: 120, type: PickerSetting.PickerSettingType.minute)
     var minuteInterval = PickerSetting(value: 30, step: 5, min: 10, max: 60, type: PickerSetting.PickerSettingType.minute)
     var timeCap = PickerSetting(value: 8, step: 1, min: 5, max: 12, type: PickerSetting.PickerSettingType.hour)
     var hours = PickerSetting(value: 6, step: 0.5, min: 2, max: 24, type: PickerSetting.PickerSettingType.hour)

+ 6 - 11
Trio/Sources/Models/TrioSettings.swift

@@ -42,15 +42,14 @@ struct TrioSettings: JSON, Equatable {
     var showCarbsRequiredBadge: Bool = true
     var useFPUconversion: Bool = true
     var individualAdjustmentFactor: Decimal = 0.5
-    var timeCap: Int = 8
-    var minuteInterval: Int = 30
-    var delay: Int = 60
+    var timeCap: Decimal = 8
+    var minuteInterval: Decimal = 30
+    var delay: Decimal = 60
     var useAppleHealth: Bool = false
     var smoothGlucose: Bool = false
     var eA1cDisplayUnit: EstimatedA1cDisplayUnit = .percent
     var high: Decimal = 180
     var low: Decimal = 70
-    var hours: Int = 6
     var glucoseColorScheme: GlucoseColorScheme = .staticColor
     var xGridLines: Bool = true
     var yGridLines: Bool = true
@@ -167,15 +166,15 @@ extension TrioSettings: Decodable {
             settings.overrideFactor = overrideFactor
         }
 
-        if let timeCap = try? container.decode(Int.self, forKey: .timeCap) {
+        if let timeCap = try? container.decode(Decimal.self, forKey: .timeCap) {
             settings.timeCap = timeCap
         }
 
-        if let minuteInterval = try? container.decode(Int.self, forKey: .minuteInterval) {
+        if let minuteInterval = try? container.decode(Decimal.self, forKey: .minuteInterval) {
             settings.minuteInterval = minuteInterval
         }
 
-        if let delay = try? container.decode(Int.self, forKey: .delay) {
+        if let delay = try? container.decode(Decimal.self, forKey: .delay) {
             settings.delay = delay
         }
 
@@ -237,10 +236,6 @@ extension TrioSettings: Decodable {
             settings.high = high
         }
 
-        if let hours = try? container.decode(Int.self, forKey: .hours) {
-            settings.hours = hours
-        }
-
         if let glucoseColorScheme = try? container.decode(GlucoseColorScheme.self, forKey: .glucoseColorScheme) {
             settings.glucoseColorScheme = glucoseColorScheme
         }

+ 9 - 7
Trio/Sources/Modules/BolusCalculatorConfig/View/BolusCalculatorConfigRootView.swift

@@ -93,13 +93,13 @@ extension BolusCalculatorConfig {
                         get: { selectedVerboseHint },
                         set: {
                             selectedVerboseHint = $0.map { AnyView($0) }
-                            hintLabel = String(localized: "Fatty Meal")
+                            hintLabel = String(localized: "Reduced Bolus")
                         }
                     ),
                     units: state.units,
                     type: .conditionalDecimal("fattyMealFactor"),
-                    label: String(localized: "Enable Fatty Meal Option"),
-                    conditionalLabel: String(localized: "Fatty Meal Bolus Percentage"),
+                    label: String(localized: "Enable Reduced Bolus Option"),
+                    conditionalLabel: String(localized: "Reduced Bolus Percentage"),
                     miniHint: String(localized: "Add and set a bolus option for meals that absorb slowly."),
                     verboseHint:
                     VStack(alignment: .leading, spacing: 10) {
@@ -107,15 +107,17 @@ extension BolusCalculatorConfig {
                         Text("Default Percent: 70%").bold()
                         Text("Do not enable this feature until you have optimized your CR (carb ratio) setting.").bold()
                         Text(
-                            "Enabling this setting adds a \"Fatty Meal\" option to the bolus calculator. Once this feature is enabled, a percentage setting will appear for you to select."
+                            "Enabling this setting adds a \"Reduced Bolus\" option to the bolus calculator. Once this feature is enabled, a percentage setting will appear for you to select."
                         )
                         Text(
-                            "When \"Fatty Meal\" is selected in the bolus calculator, the recommended bolus will be multiplied by the \"Fatty Meal Bolus Percentage\" as well as the \"Recommended Bolus Percentage\"."
+                            "When \"Reduced Bolus\" is selected in the bolus calculator, the recommended bolus will be multiplied by the \"Reduced Bolus Percentage\" as well as the \"Recommended Bolus Percentage\"."
                         )
                         Text(
-                            "If you have a \"Recommended Bolus Percentage\" of 80%, and a \"Fatty Meal Bolus Percentage\" of 70%, your recommended bolus will be multiplied by: (80 × 70) / 100 = 56%."
+                            "If you have a \"Recommended Bolus Percentage\" of 80%, and a \"Reduced Bolus Percentage\" of 70%, your recommended bolus will be multiplied by: (80 × 70) / 100 = 56%."
+                        )
+                        Text(
+                            "This is useful for slow-absorbing meals where high fat, protein, or fiber can delay carb absorption and cause a slower rise in blood sugar, so a reduced bolus helps match insulin to the delayed effect."
                         )
-                        Text("This could be useful for slow absorbing meals like pizza.")
                     }
                 )
 

+ 3 - 1
Trio/Sources/Modules/ContactImage/View/AddContactImageSheet.swift

@@ -189,7 +189,9 @@ struct AddContactImageSheet: View {
             Button(action: {
                 saveNewEntry()
             }, label: {
-                Text("Save").padding(10)
+                Text("Save")
+                    .frame(maxWidth: .infinity, alignment: .center)
+                    .padding(10)
             })
                 .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
                 .background(Color(.systemBlue))

+ 3 - 1
Trio/Sources/Modules/ContactImage/View/ContactImageDetailView.swift

@@ -164,7 +164,9 @@ struct ContactImageDetailView: View {
             Button(action: {
                 saveChanges()
             }, label: {
-                Text("Save").padding(10)
+                Text("Save")
+                    .frame(maxWidth: .infinity, alignment: .center)
+                    .padding(10)
             })
                 .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
                 .background(isUnchanged ? Color(.systemGray4) : Color(.systemBlue))

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

@@ -62,7 +62,7 @@ extension DynamicSettings {
                                         localized: "Dynamically adjust insulin sensitivity using Dynamic Ratio rather than Autosens Ratio."
                                     ) :
                                     String(
-                                        localized: "Trio has only been actively used and looping for less than seven days. Cannot enable dynamic ISF."
+                                        localized: "Trio does not have enough closed-loop data to enable Dynamic ISF. This data collection can take up to 7 days."
                                     )
                                 let miniHintTextColorForDisabled: Color = colorScheme == .dark ? .orange :
                                     .accentColor

+ 1 - 1
Trio/Sources/Modules/Home/HomeStateModel.swift

@@ -335,7 +335,7 @@ extension Home {
                 .map { [weak self] error in
                     self?.errorDate = error == nil ? nil : Date()
                     if let error = error {
-                        info(.default, String(describing: error))
+                        info(.default, String(describing: error), notificationText: error.localizedDescription)
                     }
                     return error?.localizedDescription
                 }

+ 1 - 11
Trio/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift

@@ -81,17 +81,7 @@ struct CurrentGlucoseView: View {
                         }
                     }
                     HStack {
-                        let minutesAgo = -1 * (glucose.last?.date?.timeIntervalSinceNow ?? 0) / 60
-                        var minutesAgoString: String {
-                            if minutesAgo > 1 {
-                                let minuteString = Formatter.timaAgoFormatter.string(for: Double(minutesAgo)) ?? ""
-                                return minuteString + "\u{00A0}" + String(localized: "m", comment: "Abbreviation for Minutes")
-                            } else {
-                                return "<" + "\u{00A0}" + "1" + "\u{00A0}" +
-                                    String(localized: "m", comment: "Abbreviation for Minutes")
-                            }
-                        }
-
+                        let minutesAgoString = TimeAgoFormatter.minutesAgo(from: glucose.last?.date)
                         Group {
                             Text(minutesAgoString)
                             Text(delta)

+ 2 - 6
Trio/Sources/Modules/Home/View/Header/LoopView.swift

@@ -57,15 +57,11 @@ struct LoopView: View {
     }
 
     private var timeString: String {
-        let minutesAgo = -1 * lastLoopDate.timeIntervalSinceNow / 60
-        let minuteString = Formatter.timaAgoFormatter.string(for: Double(minutesAgo)) ?? ""
-
+        let minutesAgo = TimeAgoFormatter.minutesAgoValue(from: lastLoopDate)
         if minutesAgo > 1440 {
             return "--"
-        } else if minutesAgo <= 1 {
-            return "<" + "\u{00A0}" + "1" + "\u{00A0}" + String(localized: "m", comment: "Abbreviation for Minutes")
         } else {
-            return minuteString + "\u{00A0}" + String(localized: "m", comment: "Abbreviation for Minutes")
+            return TimeAgoFormatter.minutesAgo(from: lastLoopDate)
         }
     }
 

+ 20 - 2
Trio/Sources/Modules/Home/View/Header/PumpView.swift

@@ -16,6 +16,23 @@ struct PumpView: View {
         return formatter
     }
 
+    private var hourglassIcon: String {
+        guard let expiration = expiresAtDate else { return "hourglass" }
+
+        let hoursRemaining = expiration.timeIntervalSince(timerDate) / 3600
+
+        switch hoursRemaining {
+        case 60 ... 72:
+            return "hourglass.bottomhalf.filled"
+        case 12 ..< 60:
+            return "hourglass"
+        case -8 ..< 12:
+            return "hourglass.tophalf.filled"
+        default:
+            return "hourglass"
+        }
+    }
+
     var body: some View {
         if let pumpStatusHighlightMessage = pumpStatusHighlightMessage { // display message instead pump info
             VStack(alignment: .center) {
@@ -80,9 +97,10 @@ struct PumpView: View {
 
                 if let date = expiresAtDate {
                     HStack {
-                        Image(systemName: "stopwatch.fill")
+                        Image(systemName: hourglassIcon)
                             .font(.callout)
-                            .foregroundStyle(timerColor)
+                            .foregroundStyle(timerColor, Color.yellow)
+                            .symbolRenderingMode(.palette)
 
                         let remainingTimeString = remainingTimeString(time: date.timeIntervalSince(timerDate))
 

+ 9 - 11
Trio/Sources/Modules/Home/View/HomeRootView.swift

@@ -177,7 +177,7 @@ extension Home {
                 )
             }
 
-            return rateString + " " + String(localized: " U/hr", comment: "Unit per hour with space") + manualBasalString
+            return rateString + String(localized: " U/hr", comment: "Unit per hour with space") + manualBasalString
         }
 
         var overrideString: String? {
@@ -399,18 +399,16 @@ extension Home {
                 /// eventualBG string at bottomTrailing
 
                 if let eventualBG = state.enactedAndNonEnactedDeterminations.first?.eventualBG {
-                    let bg = eventualBG as Decimal
+                    let eventualGlucose = eventualBG as Decimal
                     HStack {
                         Image(systemName: "arrow.right.circle")
-                            .font(.callout).fontWeight(.bold)
-                        Text(
-                            Formatter.decimalFormatterWithTwoFractionDigits.string(
-                                from: (
-                                    state.units == .mmolL ? bg
-                                        .asMmolL : bg
-                                ) as NSNumber
-                            )!
-                        ).font(.callout).fontWeight(.bold).fontDesign(.rounded)
+                            .font(.callout)
+                            .fontWeight(.bold)
+
+                        Text(state.units == .mgdL ? eventualGlucose.description : eventualGlucose.formattedAsMmolL)
+                            .font(.callout)
+                            .fontWeight(.bold)
+                            .fontDesign(.rounded)
                     }
                     // aligns the evBG icon exactly with the first pixel of loop status icon
                     .padding(.leading, 12)

+ 13 - 24
Trio/Sources/Modules/MealSettings/MealSettingsStateModel.swift

@@ -16,36 +16,25 @@ extension MealSettings {
         override func subscribe() {
             units = settingsManager.settings.units
 
-            subscribeSetting(\.useFPUconversion, on: $useFPUconversion) { useFPUconversion = $0 }
             subscribeSetting(\.maxCarbs, on: $maxCarbs) { maxCarbs = $0 }
             subscribeSetting(\.maxFat, on: $maxFat) { maxFat = $0 }
             subscribeSetting(\.maxProtein, on: $maxProtein) { maxProtein = $0 }
 
-            subscribeSetting(\.timeCap, on: $timeCap.map(Int.init), initial: {
-                timeCap = Decimal($0)
-            }, map: {
-                $0
-            })
-
             subscribePreferencesSetting(\.maxMealAbsorptionTime, on: $maxMealAbsorptionTime) { maxMealAbsorptionTime = $0 }
 
-            subscribeSetting(\.minuteInterval, on: $minuteInterval.map(Int.init), initial: {
-                minuteInterval = Decimal($0)
-            }, map: {
-                $0
-            })
-
-            subscribeSetting(\.delay, on: $delay.map(Int.init), initial: {
-                delay = Decimal($0)
-            }, map: {
-                $0
-            })
-
-            subscribeSetting(\.individualAdjustmentFactor, on: $individualAdjustmentFactor, initial: {
-                individualAdjustmentFactor = $0
-            }, map: {
-                $0
-            })
+            subscribeSetting(\.useFPUconversion, on: $useFPUconversion) { useFPUconversion = $0 }
+
+            // "Fat and Protein Delay"
+            subscribeSetting(\.delay, on: $delay) { delay = $0 }
+
+            // "Maximum Duration"
+            subscribeSetting(\.timeCap, on: $timeCap) { timeCap = $0 }
+
+            // "Spread Interval"
+            subscribeSetting(\.minuteInterval, on: $minuteInterval) { minuteInterval = $0 }
+
+            // "Fat and Protein Percentage"
+            subscribeSetting(\.individualAdjustmentFactor, on: $individualAdjustmentFactor) { individualAdjustmentFactor = $0 }
         }
     }
 }

+ 47 - 14
Trio/Sources/Modules/Onboarding/OnboardingStateModel.swift

@@ -61,16 +61,12 @@ extension Onboarding {
                 return false
             }
 
-            debug(.default, "Checking for fresh install in \(documentsURL.path)...")
-
             let expectedLogsFolder = "logs"
             let expectedPreferencesFile = OpenAPS.Settings.preferences
 
             do {
                 let contents = try fileManager.contentsOfDirectory(atPath: documentsURL.path)
 
-                debug(.default, "Found \(contents) in \(documentsURL.path)...")
-
                 // Expect exactly 2 entries: "logs" and the preferences file
                 guard contents.count == 2 else {
                     debug(.default, "Trio install is not fresh; returning user.")
@@ -81,8 +77,6 @@ extension Onboarding {
                 let expectedSet = Set([expectedLogsFolder, expectedPreferencesFile])
                 let actualSet = Set(contents)
 
-                debug(.default, "Expected: \(expectedSet), Actual: \(actualSet)")
-
                 let isFreshInstall = expectedSet == actualSet
                 debug(.default, "Trio install is fresh; new user.")
 
@@ -258,10 +252,12 @@ extension Onboarding {
             isConnectingToNS = false
             isValidNightscoutURL = false
 
-            // Attempt to fetch existing units, therapy settings and delivery limits from file
-            units = settingsManager.settings.units
-            fetchExistingTherapySettingsFromFile()
-            fetchExistingDeliveryLimtisFromFile()
+            if !isFreshTrioInstall {
+                // Attempt to fetch existing units, therapy settings and delivery limits from file
+                units = settingsManager.settings.units
+                fetchExistingTherapySettingsFromFile()
+                fetchExistingDeliveryLimtisFromFile()
+            }
         }
 
         // MARK: - Helpers
@@ -417,15 +413,16 @@ extension Onboarding {
         ///   - `units` from app settings.
         func fetchExistingDeliveryLimtisFromFile() {
             let pumpSettingsFromFile = provider.pumpSettingsFromFile
+            let providedSettings = settingsProvider.settings
 
             if let pumpSettingsFromFile = pumpSettingsFromFile {
-                maxBolus = pumpSettingsFromFile.maxBolus
-                maxBasal = pumpSettingsFromFile.maxBasal
+                maxBolus = pumpSettingsFromFile.maxBolus.clamp(to: providedSettings.maxBolus)
+                maxBasal = pumpSettingsFromFile.maxBasal.clamp(to: providedSettings.maxBasal)
             }
 
             let preferences = settingsManager.preferences
-            maxIOB = preferences.maxIOB
-            maxCOB = preferences.maxCOB
+            maxIOB = preferences.maxIOB.clamp(to: providedSettings.maxIOB)
+            maxCOB = preferences.maxCOB.clamp(to: providedSettings.maxCOB)
             minimumSafetyThreshold = preferences.threshold_setting
         }
 
@@ -702,6 +699,30 @@ extension Onboarding {
         func applyToSettings() {
             var settingsCopy = settingsManager.settings
             settingsCopy.units = units
+
+            // ensure existing values cannot exceed new guardrails
+            if !isFreshTrioInstall {
+                let providedSettings = settingsProvider.settings
+
+                settingsCopy.lowGlucose = settingsCopy.lowGlucose.clamp(to: providedSettings.lowGlucose)
+                settingsCopy.highGlucose = settingsCopy.highGlucose.clamp(to: providedSettings.highGlucose)
+                settingsCopy.carbsRequiredThreshold = settingsCopy.carbsRequiredThreshold
+                    .clamp(to: providedSettings.carbsRequiredThreshold)
+                settingsCopy.individualAdjustmentFactor = settingsCopy.individualAdjustmentFactor
+                    .clamp(to: providedSettings.individualAdjustmentFactor)
+                settingsCopy.timeCap = settingsCopy.timeCap.clamp(to: providedSettings.timeCap)
+                settingsCopy.minuteInterval = settingsCopy.minuteInterval.clamp(to: providedSettings.minuteInterval)
+                settingsCopy.delay = settingsCopy.delay.clamp(to: providedSettings.delay)
+                settingsCopy.high = settingsCopy.high.clamp(to: providedSettings.high)
+                settingsCopy.low = settingsCopy.low.clamp(to: providedSettings.low)
+                settingsCopy.maxCarbs = settingsCopy.maxCarbs.clamp(to: providedSettings.maxCarbs)
+                settingsCopy.maxFat = settingsCopy.maxFat.clamp(to: providedSettings.maxFat)
+                settingsCopy.maxProtein = settingsCopy.maxProtein.clamp(to: providedSettings.maxProtein)
+                settingsCopy.overrideFactor = settingsCopy.overrideFactor.clamp(to: providedSettings.overrideFactor)
+                settingsCopy.fattyMealFactor = settingsCopy.fattyMealFactor.clamp(to: providedSettings.fattyMealFactor)
+                settingsCopy.sweetMealFactor = settingsCopy.sweetMealFactor.clamp(to: providedSettings.sweetMealFactor)
+            }
+
             settingsManager.settings = settingsCopy
         }
 
@@ -744,6 +765,18 @@ extension Onboarding {
                 preferences.suspendZerosIOB = true
             }
 
+            // ensure correct bolusIncrement is set, if user is onboarding with paired pump
+            if let pumpManager = apsManager?.pumpManager {
+                let bolusIncrement = Decimal(
+                    pumpManager.supportedBolusVolumes.first ??
+                        Double(
+                            settingsManager.preferences
+                                .bolusIncrement
+                        )
+                )
+                preferences.bolusIncrement = bolusIncrement != 0.025 ? bolusIncrement : 0.1
+            }
+
             settingsManager.preferences = preferences
         }
 

+ 1 - 1
Trio/Sources/Modules/Onboarding/View/OnboardingView+AlgorithmUtil.swift

@@ -356,7 +356,7 @@ enum AlgorithmSettingsSubstep: Int, CaseIterable, Identifiable {
             return VStack(alignment: .leading, spacing: 8) {
                 Text("Default: 20% increase").bold().foregroundStyle(Color.primary)
                 Text(
-                    "Maximum allowed positive percent change in glucose level to permit SMBs. If the difference in glucose is greater than this, Trio will disable SMBs."
+                    "Maximum allowed positive percent change in glucose level to permit SMBs. If the difference in glucose is greater than this, Trio will only adjust Temp Basal Rate and not deliver an SMB that loop cycle."
                 )
                 Text(
                     "This is a safety limitation to avoid high SMB doses when glucose is rising abnormally fast, such as after a meal or with a very jumpy CGM sensor."

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

@@ -347,7 +347,7 @@ extension SMBSettings {
                     VStack(alignment: .leading, spacing: 10) {
                         Text("Default: 20% increase").bold()
                         Text(
-                            "Maximum allowed positive percent change in glucose level to permit SMBs. If the difference in glucose is greater than this, Trio will disable SMBs."
+                            "Maximum allowed positive percent change in glucose level to permit SMBs. If the difference in glucose is greater than this, Trio will only adjust Temp Basal Rate and not deliver an SMB that loop cycle."
                         )
                         Text(
                             "This is a safety limitation to avoid high SMB doses when glucose is rising abnormally fast, such as after a meal or with a very jumpy CGM sensor."

+ 6 - 7
Trio/Sources/Modules/Settings/SettingItems.swift

@@ -128,11 +128,10 @@ enum SettingItems {
             title: "Dynamic Settings",
             view: .dynamicISF,
             searchContents: [
-                "Activate Dynamic Sensitivity (ISF)",
-                "Activate Dynamic Carb Ratio (CR)",
-                "Use Sigmoid Formula",
-                "Adjustment Factor",
-                "AF",
+                "Dynamic ISF",
+                "Sigmoid",
+                "Logarithmic",
+                "Adjustment Factor (AF)",
                 "Sigmoid Adjustment Factor",
                 "Weighted Average of TDD",
                 "Adjust Basal"
@@ -181,8 +180,8 @@ enum SettingItems {
             searchContents: [
                 "Display Meal Presets",
                 "Recommended Bolus Percentage",
-                "Enable Fatty Meal Factor",
-                "Fatty Meal Factor",
+                "Enable Reduced Bolus Factor",
+                "Reduced Bolus Factor",
                 "Enable Super Bolus",
                 "Super Bolus Factor",
                 "Very Low Glucose Warning"

+ 112 - 10
Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift

@@ -1,6 +1,7 @@
 import Combine
 import CoreData
 import Foundation
+import LocalAuthentication
 import LoopKit
 import Observation
 import SwiftUI
@@ -88,6 +89,7 @@ extension Treatments {
         var note: String = ""
 
         var date = Date()
+        let defaultDate = Date()
 
         var carbsRequired: Decimal?
         var useFPUconversion: Bool = false
@@ -380,12 +382,26 @@ extension Treatments {
                 minPredBG
             }
 
+            // Use the cob value of the simulation if we have a simulated determination
+            var simulatedCOB: Int16?
+            if let simulatedCobValue = simulatedDetermination?.cob {
+                // Convert Decimal to Int16 and cap at maxCOB
+                let cobInt16 = Int16(truncating: NSDecimalNumber(decimal: simulatedCobValue))
+                let maxCobInt16 = Int16(truncating: NSDecimalNumber(decimal: maxCOB))
+                simulatedCOB = min(maxCobInt16, cobInt16)
+            }
+
+            // Check if this is a backdated entry by comparing with the default date
+            let isBackdated = date != defaultDate
+
             let result = await bolusCalculationManager.handleBolusCalculation(
                 carbs: carbs,
                 useFattyMealCorrection: useFattyMealCorrectionFactor,
                 useSuperBolus: useSuperBolus,
                 lastLoopDate: apsManager.lastLoopDate,
-                minPredBG: localMinPredBG
+                minPredBG: localMinPredBG,
+                simulatedCOB: simulatedCOB,
+                isBackdated: isBackdated
             )
 
             // Update state properties with calculation results on main thread
@@ -457,6 +473,91 @@ extension Treatments {
             }
         }
 
+        /// Returns a user-facing localized error message for a given authentication error.
+        ///
+        /// This function inspects the provided `Error` to determine whether it is an `LAError`,
+        /// and maps its error code to a human-readable, localized string describing the reason
+        /// for the failure. If the error is not an `LAError`, a generic fallback message is returned.
+        ///
+        /// - Parameter error: The `Error` returned from an authentication attempt (e.g., via `LAContext.evaluatePolicy`).
+        /// - Returns: A localized `String` describing the cause of the authentication failure.
+        private func parseAuthenticationError(from error: Error) -> String {
+            guard let laError = error as? LAError else {
+                return String(
+                    localized: "An unknown authentication error occurred. Please try again."
+                )
+            }
+
+            switch laError.code {
+            case .authenticationFailed:
+                return String(
+                    localized: "Authentication failed. Please try again."
+                )
+
+            case .userCancel:
+                return String(
+                    localized: "Authentication was canceled by you."
+                )
+
+            case .userFallback:
+                return String(
+                    localized: "You tapped the fallback option, but no fallback method is configured."
+                )
+
+            case .systemCancel:
+                return String(
+                    localized: "Authentication was canceled by the system. Try again."
+                )
+
+            case .appCancel:
+                return String(
+                    localized: "Authentication was canceled by the app."
+                )
+
+            case .invalidContext:
+                return String(
+                    localized: "Authentication context is invalid. Please try again."
+                )
+
+            case .notInteractive:
+                return String(
+                    localized: "Authentication UI cannot be displayed. Try restarting the app."
+                )
+
+            case .passcodeNotSet:
+                return String(
+                    localized: "Authentication requires a device passcode. Please set one in iOS Settings > Face ID & Passcode."
+                )
+
+            case .biometryNotAvailable:
+                return String(
+                    localized: "Biometric authentication is not available on this device."
+                )
+
+            case .biometryNotEnrolled:
+                return String(
+                    localized: "No biometric identities are enrolled. Please set up Face ID or Touch ID."
+                )
+
+            case .biometryLockout,
+                 .touchIDLockout:
+                return String(
+                    localized: "Biometric authentication is locked due to multiple failed attempts. Please unlock your device using your passcode."
+                )
+
+            case .biometryDisconnected,
+                 .biometryNotPaired:
+                return String(
+                    localized: "Biometric accessory is missing or not connected. Please reconnect it and try again."
+                )
+
+            default:
+                return String(
+                    localized: "An unknown biometric authentication error occurred. Please try again."
+                )
+            }
+        }
+
         func addPumpInsulin() async {
             guard amount > 0 else {
                 showModal(for: nil)
@@ -473,15 +574,14 @@ extension Treatments {
                         self.isAwaitingDeterminationResult = true
                     }
                     await apsManager.enactBolus(amount: maxAmount, isSMB: false, callback: nil)
-                } else {
-                    print("authentication failed")
                 }
             } catch {
-                print("authentication error for pump bolus: \(error.localizedDescription)")
+                debug(.bolusState, "Authentication error for pump bolus: \(error)")
+
                 await MainActor.run {
                     self.isAwaitingDeterminationResult = false
                     self.showDeterminationFailureAlert = true
-                    self.determinationFailureMessage = error.localizedDescription
+                    self.determinationFailureMessage = parseAuthenticationError(from: error)
                 }
             }
         }
@@ -509,15 +609,13 @@ extension Treatments {
                     await pumpHistoryStorage.storeExternalInsulinEvent(amount: amount, timestamp: date)
                     // perform determine basal sync
                     try await apsManager.determineBasalSync()
-                } else {
-                    print("authentication failed")
                 }
             } catch {
-                print("authentication error for external insulin: \(error.localizedDescription)")
+                debug(.bolusState, "authentication error for external insulin: \(error)")
                 await MainActor.run {
                     self.isAwaitingDeterminationResult = false
                     self.showDeterminationFailureAlert = true
-                    self.determinationFailureMessage = error.localizedDescription
+                    self.determinationFailureMessage = parseAuthenticationError(from: error)
                 }
             }
         }
@@ -834,7 +932,11 @@ extension Treatments.StateModel {
         } else {
             simulatedDetermination = await Task { [self] in
                 debug(.bolusState, "calling simulateDetermineBasal to get forecast data")
-                return await apsManager.simulateDetermineBasal(simulatedCarbsAmount: carbs, simulatedBolusAmount: amount)
+                return await apsManager.simulateDetermineBasal(
+                    simulatedCarbsAmount: carbs,
+                    simulatedBolusAmount: amount,
+                    simulatedCarbsDate: date
+                )
             }.value
 
             // Update evBG and minPredBG from simulated determination

+ 16 - 3
Trio/Sources/Modules/Treatments/View/ForecastChart.swift

@@ -50,10 +50,18 @@ struct ForecastChart: View {
     }
 
     private var forecastChartLabels: some View {
-        HStack {
+        // Check if carbs are actually backdated (more than 15 minutes in the past)
+        // This ensures we only consider it backdated if the user has deliberately changed the date
+        let minutesThreshold = 15.0 // 15 minutes threshold
+        let isBackdated = state.date.timeIntervalSinceNow < -minutesThreshold * 60 && state.simulatedDetermination != nil
+
+        // When backdated, display no carbs as this label is only supposed to show current entered carbs
+        let displayedCarbs = isBackdated ? 0 : state.carbs
+
+        return HStack {
             HStack {
                 Image(systemName: "fork.knife")
-                Text("\(state.carbs.description) g")
+                Text("\(displayedCarbs.description) g")
             }
             .font(.footnote)
             .foregroundStyle(.orange)
@@ -118,6 +126,11 @@ struct ForecastChart: View {
         }
     }
 
+    private var maxGlucoseMgDl: Decimal {
+        let maxGlucose = state.glucoseFromPersistence.map({ Decimal($0.glucose) }).max() ?? 300
+        return maxGlucose > 300 ? 400 : 300
+    }
+
     private var forecastChart: some View {
         Chart {
             drawGlucose()
@@ -168,7 +181,7 @@ struct ForecastChart: View {
         .chartXAxis { forecastChartXAxis }
         .chartXScale(domain: startMarker ... endMarker)
         .chartYAxis { forecastChartYAxis }
-        .chartYScale(domain: state.units == .mgdL ? 0 ... 300 : 0.asMmolL ... 300.asMmolL)
+        .chartYScale(domain: state.units == .mgdL ? 0 ... maxGlucoseMgDl : 0.asMmolL ... maxGlucoseMgDl.asMmolL)
         .chartLegend {
             if state.forecastDisplayType == ForecastDisplayType.lines {
                 HStack(spacing: 10) {

+ 22 - 6
Trio/Sources/Modules/Treatments/View/PopupView.swift

@@ -312,7 +312,16 @@ struct PopupView: View {
     /// Don't allow total carbs to exceed Max IOB setting.
     /// Formula: (Current COB + New Carbs) / Carb Ratio = COB Correction Dose
     private var cobCardContent: some View {
-        let hasExceededMaxCOB: Bool = Decimal(state.cob) + state.carbs > state.maxCOB
+        // Check if carbs are actually backdated (more than 15 minutes in the past)
+        // This ensures we only consider it backdated if the user has deliberately changed the date
+        let minutesThreshold = 15.0 // 15 minutes threshold
+        let isBackdated = state.date.timeIntervalSinceNow < -minutesThreshold * 60 && state.simulatedDetermination != nil
+
+        // Determine COB and carbs to display based on backdating status
+        let displayedCOB = isBackdated ? (state.simulatedDetermination?.cob ?? Decimal(state.cob)) : Decimal(state.cob)
+        let displayedCarbs = isBackdated ? 0 : state.carbs
+
+        let hasExceededMaxCOB: Bool = displayedCOB + displayedCarbs > state.maxCOB
         return Group {
             Grid(alignment: .center) {
                 // Row 1: Column headers for the COB calculation
@@ -333,11 +342,11 @@ struct PopupView: View {
                 GridRow {
                     Text("(")
                         .operatorStyle()
-                    Text(Int(state.cob).description)
+                    Text(Int(displayedCOB).description)
                         .valueStyle()
                     Text("+")
                         .operatorStyle()
-                    Text(Int(state.carbs).description)
+                    Text(Int(displayedCarbs).description)
                         .valueStyle()
                     Text(")")
                         .operatorStyle()
@@ -378,6 +387,13 @@ struct PopupView: View {
             }
             .multilineTextAlignment(.center)
 
+            if isBackdated {
+                Text("Backdated carbs (\(Int(state.carbs)) g) included in COB calculation")
+                    .font(.caption)
+                    .foregroundStyle(.orange)
+                    .padding(.top, 4)
+            }
+
             // Additional grid only displayed when Max COB limit has been exceeded
             if hasExceededMaxCOB {
                 Grid(alignment: .center) {
@@ -588,7 +604,7 @@ struct PopupView: View {
 
     /// Card showing applied factors to the final insulin calculation.
     /// Dynamically changes card based on user's selection in the Treatment view.
-    /// User can choose Fatty Meal, Super Bolus, or neither, but not both.
+    /// User can choose Reduced Bolus, Super Bolus, or neither, but not both.
     private var factorsCardContent: some View {
         Grid(alignment: .center) {
             // Choose the layout based on which options are selected
@@ -630,7 +646,7 @@ struct PopupView: View {
                 }
                 .unitStyle()
 
-            // Case: Full Bolus × Rec. Bolus % × Fatty Meal %
+            // Case: Full Bolus × Rec. Bolus % × Reduced Bolus %
             case (false, true):
                 // Row 1: Header.
                 GridRow(alignment: .lastTextBaseline) {
@@ -640,7 +656,7 @@ struct PopupView: View {
                     Text("Rec. Bolus %")
                     Text("")
                         .layoutPriority(-15)
-                    Text("Fatty %")
+                    Text("Red. Bolus %")
                 }
                 .secondaryStyle()
 

+ 11 - 3
Trio/Sources/Modules/Treatments/View/TreatmentsRootView.swift

@@ -224,6 +224,14 @@ extension Treatments {
                                         displayedComponents: [.hourAndMinute]
                                     ).controlSize(.mini)
                                         .labelsHidden()
+                                        .onChange(of: state.date) { _, _ in
+                                            // Trigger simulation when date changes to update forecasts for backdated carbs
+                                            Task {
+                                                // `updateForecasts()` does update the `simulatedDetermination` of type `Determination?` var on the main thread, so I can use this to pass its cob value into the bolus calc manager
+                                                await state.updateForecasts()
+                                                state.insulinCalculated = await state.calculateInsulin()
+                                            }
+                                        }
                                     Button {
                                         state.date = state.date.addingTimeInterval(15.minutes.timeInterval)
                                     }
@@ -247,7 +255,7 @@ extension Treatments {
                                 HStack(spacing: 10) {
                                     if state.fattyMeals {
                                         Toggle(isOn: $state.useFattyMealCorrectionFactor) {
-                                            Text("Fatty Meal")
+                                            Text("Reduced Bolus")
                                         }
                                         .toggleStyle(RadioButtonToggleStyle())
                                         .font(.footnote)
@@ -400,12 +408,12 @@ extension Treatments {
             }) {
                 MealPresetView(state: state)
             }
-            .alert("Determination Failed", isPresented: $state.showDeterminationFailureAlert) {
+            .alert("Error while processing Treatment", isPresented: $state.showDeterminationFailureAlert) {
                 Button("OK", role: .cancel) {
                     state.hideModal()
                 }
             } message: {
-                Text("Failed to update COB/IOB: \(state.determinationFailureMessage)")
+                Text("\(state.determinationFailureMessage)")
             }
         }
 

+ 28 - 17
Trio/Sources/Services/BolusCalculator/BolusCalculationManager.swift

@@ -9,9 +9,10 @@ protocol BolusCalculationManager {
         useFattyMealCorrection: Bool,
         useSuperBolus: Bool,
         lastLoopDate: Date,
-        minPredBG: Decimal?
-    ) async
-        -> CalculationResult
+        minPredBG: Decimal?,
+        simulatedCOB: Int16?,
+        isBackdated: Bool
+    ) async -> CalculationResult
 }
 
 final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
@@ -289,7 +290,9 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
         useFattyMealCorrection: Bool,
         useSuperBolus: Bool,
         lastLoopDate: Date,
-        minPredBG: Decimal?
+        minPredBG: Decimal?,
+        simulatedCOB: Int16?,
+        isBackdated: Bool
     ) async throws -> CalculationInput {
         do {
             // Get settings
@@ -337,15 +340,20 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
                 )
             }
 
+            // If the entry is backdated (user explicitly changed the date), set carbs to 0
+            // This prevents double-counting of carbs (entered carbs + COB from backdated entry)
+            let effectiveCarbs = isBackdated ? 0 : carbs
+            let effectiveCob = isBackdated ? simulatedCOB : bolusVars.cob
+
             return CalculationInput(
-                carbs: carbs,
+                carbs: effectiveCarbs,
                 currentBG: glucoseVars.currentBG,
                 deltaBG: glucoseVars.deltaBG,
                 target: bolusVars.target,
                 isf: bolusVars.isf,
                 carbRatio: bolusVars.carbRatio,
                 iob: bolusVars.iob,
-                cob: bolusVars.cob,
+                cob: effectiveCob ?? bolusVars.cob,
                 useFattyMealCorrectionFactor: useFattyMealCorrection,
                 fattyMealFactor: settings.fattyMealFactor,
                 useSuperBolus: useSuperBolus,
@@ -384,6 +392,7 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
         debug(.default, "15min insulin: \(fifteenMinutesInsulin)")
 
         // determine whole COB for which we want to dose insulin for and then determine insulin for wholeCOB
+        // we need to take backdated carbs into account - so we are using a freshly created (simulated) COB if carbs are backdated, otherwise it will default to the mostRecentDeterminations' COB value (as before)
         let wholeCob = min(Decimal(input.cob) + input.carbs, input.maxCOB)
         let wholeCobInsulin = wholeCob / input.carbRatio
         debug(.default, "Whole COB: \(wholeCob), COB insulin: \(wholeCobInsulin)")
@@ -411,11 +420,11 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
         }
 
         // apply custom factor at the end of the calculations
-        // apply custom factor if fatty meal toggle in bolus calc config settings is on and the box for fatty meals is checked (in RootView)
+        // apply custom factor if reduced bolus toggle in bolus calc config settings is on and the box for reduced bolus is checked (in RootView)
         var factoredInsulin = wholeCalc
         debug(.default, "Initial factored insulin: \(factoredInsulin)")
 
-        // Apply Recommended Bolus Percentage (input.fraction) and if selected apply Fatty Meal Bolus Percentage (input.fattyMealFactor)
+        // Apply Recommended Bolus Percentage (input.fraction) and if selected apply Reduced Bolus Percentage (input.fattyMealFactor)
         // If factoredInsulin is negative, though, don't apply either
         if factoredInsulin > 0 {
             factoredInsulin *= input.fraction
@@ -423,7 +432,7 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
 
             if input.useFattyMealCorrectionFactor {
                 factoredInsulin *= input.fattyMealFactor
-                debug(.default, "After fatty meal factor (\(input.fattyMealFactor)): \(factoredInsulin)")
+                debug(.default, "After reduced bolus factor (\(input.fattyMealFactor)): \(factoredInsulin)")
             }
         }
 
@@ -466,7 +475,6 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
             insulinCalculated: insulinCalculated,
             factoredInsulin: factoredInsulin,
             wholeCalc: wholeCalc,
-            correctionInsulin: targetDifferenceInsulin,
             iobInsulinReduction: iobInsulinReduction,
             superBolusInsulin: superBolusInsulin,
             targetDifference: targetDifference,
@@ -480,16 +488,19 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
     /// Handles the complete bolus calculation process
     /// - Parameters:
     ///   - carbs: Amount of carbohydrates to be consumed
-    ///   - useFattyMealCorrection: Whether to apply fatty meal correction
+    ///   - useFattyMealCorrection: Whether to apply reduced bolus correction
     ///   - useSuperBolus: Whether to use super bolus calculation
     ///   - minPredBG: Minimum Predicted Glucose determined by Oref
+    ///   - simulatedCOB: Optional simulated COB from backdated entries (if available)
     /// - Returns: CalculationResult containing the calculated insulin dose and details
     func handleBolusCalculation(
         carbs: Decimal,
         useFattyMealCorrection: Bool,
         useSuperBolus: Bool,
         lastLoopDate: Date,
-        minPredBG: Decimal? = nil
+        minPredBG: Decimal? = nil,
+        simulatedCOB: Int16? = nil,
+        isBackdated: Bool = false
     ) async -> CalculationResult {
         do {
             let input = try await prepareCalculationInput(
@@ -497,7 +508,9 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
                 useFattyMealCorrection: useFattyMealCorrection,
                 useSuperBolus: useSuperBolus,
                 lastLoopDate: lastLoopDate,
-                minPredBG: minPredBG
+                minPredBG: minPredBG,
+                simulatedCOB: simulatedCOB,
+                isBackdated: isBackdated
             )
             let result = await calculateInsulin(input: input)
             return result
@@ -511,7 +524,6 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
                 insulinCalculated: 0,
                 factoredInsulin: 0,
                 wholeCalc: 0,
-                correctionInsulin: 0,
                 iobInsulinReduction: 0,
                 superBolusInsulin: 0,
                 targetDifference: 0,
@@ -534,8 +546,8 @@ struct CalculationInput: Sendable {
     let carbRatio: Decimal // Carb to insulin ratio
     let iob: Decimal // Insulin on Board
     let cob: Int16 // Carbs on Board
-    let useFattyMealCorrectionFactor: Bool // Whether to apply fatty meal correction
-    let fattyMealFactor: Decimal // Factor for fatty meal adjustment
+    let useFattyMealCorrectionFactor: Bool // Whether to apply reduced bolus correction
+    let fattyMealFactor: Decimal // Factor for reduced bolus adjustment
     let useSuperBolus: Bool // Whether to use super bolus calculation
     let sweetMealFactor: Decimal // Factor for sweet meal adjustment
     let basal: Decimal // Current basal rate
@@ -552,7 +564,6 @@ struct CalculationResult: Sendable {
     let insulinCalculated: Decimal // Final calculated insulin amount which respects limits
     let factoredInsulin: Decimal // Total calculation after adjustments
     let wholeCalc: Decimal // Total calculation before adjustments
-    let correctionInsulin: Decimal // Insulin for BG correction
     let iobInsulinReduction: Decimal // IOB reduction amount
     let superBolusInsulin: Decimal // Additional insulin for super bolus
     let targetDifference: Decimal // Difference from target BG

+ 30 - 20
Trio/Sources/Services/Network/Nightscout/NightscoutAPI.swift

@@ -20,6 +20,14 @@ class NightscoutAPI {
         static let timeout: TimeInterval = 60
     }
 
+    private let excludedEnteredBy: [String] = [
+        NightscoutTreatment.local,
+        "AndroidAPS",
+        "openaps://AndroidAPS",
+        "iAPS",
+        "loop://iPhone"
+    ]
+
     enum Error: LocalizedError {
         case badStatusCode
         case missingURL
@@ -98,23 +106,29 @@ extension NightscoutAPI {
         }
     }
 
+    private func makeNeQueryItems() -> [URLQueryItem] {
+        excludedEnteredBy.enumerated().map { idx, value in
+            URLQueryItem(
+                name: "find[$and][\(idx)][enteredBy][$ne]",
+                value: value
+            )
+        }
+    }
+
     func fetchCarbs(sinceDate: Date? = nil) async throws -> [CarbsEntry] {
         var components = URLComponents()
         components.scheme = url.scheme
         components.host = url.host
         components.port = url.port
         components.path = Config.treatmentsPath
-        components.queryItems = [
-            URLQueryItem(name: "find[carbs][$exists]", value: "true"),
-            URLQueryItem(
-                name: "find[enteredBy][$ne]",
-                value: CarbsEntry.local.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
-            ),
-            URLQueryItem(
-                name: "find[enteredBy][$ne]",
-                value: NightscoutTreatment.local.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
-            )
+
+        var items: [URLQueryItem] = [
+            URLQueryItem(name: "find[carbs][$exists]", value: "true")
         ]
+
+        items.append(contentsOf: makeNeQueryItems())
+        components.queryItems = items
+
         if let date = sinceDate {
             let dateItem = URLQueryItem(
                 name: "find[created_at][$gt]",
@@ -137,7 +151,6 @@ extension NightscoutAPI {
             guard let httpResponse = response as? HTTPURLResponse, (200 ... 299).contains(httpResponse.statusCode) else {
                 throw URLError(.badServerResponse)
             }
-
             let carbs = try JSONCoding.decoder.decode([CarbsEntry].self, from: data)
             return carbs
         } catch {
@@ -243,18 +256,15 @@ extension NightscoutAPI {
         components.host = url.host
         components.port = url.port
         components.path = Config.treatmentsPath
-        components.queryItems = [
+
+        var items: [URLQueryItem] = [
             URLQueryItem(name: "find[eventType]", value: "Temporary+Target"),
-            URLQueryItem(
-                name: "find[enteredBy][$ne]",
-                value: TempTarget.local.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
-            ),
-            URLQueryItem(
-                name: "find[enteredBy][$ne]",
-                value: NightscoutTreatment.local.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)
-            ),
             URLQueryItem(name: "find[duration][$exists]", value: "true")
         ]
+
+        items.append(contentsOf: makeNeQueryItems())
+        components.queryItems = items
+
         if let date = sinceDate {
             let dateItem = URLQueryItem(
                 name: "find[created_at][$gt]",

+ 1 - 5
Trio/Sources/Services/UnlockManager/UnlockManager.swift

@@ -5,10 +5,6 @@ protocol UnlockManager {
     func unlock() async throws -> Bool
 }
 
-struct UnlockError: Error {
-    let error: Error?
-}
-
 final class BaseUnlockManager: UnlockManager {
     @MainActor func unlock() async throws -> Bool {
         let context = LAContext()
@@ -18,7 +14,7 @@ final class BaseUnlockManager: UnlockManager {
             _ = try await context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason)
             return true
         } catch {
-            throw UnlockError(error: error)
+            throw error
         }
     }
 }

+ 3 - 1
Trio/Sources/Services/WatchManager/AppleWatchManager.swift

@@ -622,7 +622,9 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                         useFattyMealCorrection: false,
                         useSuperBolus: false,
                         lastLoopDate: apsManager.lastLoopDate,
-                        minPredBG: minPredBG
+                        minPredBG: minPredBG,
+                        simulatedCOB: nil,
+                        isBackdated: false // we cannot backdate carbs via watch
                     )
 
                     // Send recommendation back to watch

+ 2 - 2
Trio/Sources/Shortcuts/Carbs/AddCarbPresetIntent.swift

@@ -76,13 +76,13 @@ import Swinject
             if let cq = carbQuantity {
                 quantityCarbs = cq
             } else {
-                quantityCarbs = try await $carbQuantity.requestValue("How many carbs ?")
+                quantityCarbs = try await $carbQuantity.requestValue("How many carbs do you want to add?")
             }
 
             let quantityCarbsName = quantityCarbs.toString()
             if confirmBeforeApplying {
                 try await requestConfirmation(
-                    result: .result(dialog: "Are you sure to add \(quantityCarbsName) g of carbs ?")
+                    result: .result(dialog: "Do you want to add \(quantityCarbsName) grams of carbs?")
                 )
             }
 

+ 183 - 18
TrioTests/BolusCalculatorTests/BolusCalculatorTests.swift

@@ -84,14 +84,14 @@ import Testing
         // correctionInsulin = targetDifferenceInsulin = 2U
         // iobInsulinReduction = 1U
         // superBolusInsulin = 0U (disabled)
-        // no adjustment for fatty meals (disabled)
+        // no adjustment for reduced bolus (disabled)
         // wholeCalc = round(wholeCobInsulin + correctionInsulin + fifteenMinutesInsulin - iobInsulinReduction, 3) = 11.125U
         // insulinCalculated = round(wholeCalc × fraction, 3) = 8.9U
 
-        // Calculate expected values with proper rounding using roundBolus method from the apsManager
-        let wholeCobInsulin = apsManager.roundBolus(amount: Decimal(100) / Decimal(10)) // 10U
-        let targetDifferenceInsulin = apsManager.roundBolus(amount: Decimal(80) / Decimal(40)) // 2U
-        let fifteenMinutesInsulin = apsManager.roundBolus(amount: Decimal(5) / Decimal(40)) // 0.125U
+        // Calculate expected values
+        let wholeCobInsulin = Decimal(100) / Decimal(10) // 10U
+        let targetDifferenceInsulin = Decimal(80) / Decimal(40) // 2U
+        let fifteenMinutesInsulin = Decimal(5) / Decimal(40)
         let wholeCalc = wholeCobInsulin + targetDifferenceInsulin + fifteenMinutesInsulin - Decimal(1) // 11.125U
         let expectedInsulinCalculated = apsManager.roundBolus(amount: wholeCalc * fraction) // 8.9U
 
@@ -104,7 +104,6 @@ import Testing
             Components from CalculationResult:
             - insulinCalculated: \(result.insulinCalculated)U (expected: \(expectedInsulinCalculated)U)
             - wholeCalc: \(result.wholeCalc)U (expected: \(wholeCalc)U)
-            - correctionInsulin: \(result.correctionInsulin)U (expected: \(targetDifferenceInsulin)U)
             - iobInsulinReduction: \(result.iobInsulinReduction)U (expected: 1U)
             - superBolusInsulin: \(result.superBolusInsulin)U (expected: 0U)
             - targetDifference: \(result.targetDifference) mg/dL (expected: 80 mg/dL)
@@ -121,10 +120,6 @@ import Testing
             "Final calculated insulin amount should be \(expectedInsulinCalculated)U"
         )
         #expect(result.wholeCalc == wholeCalc, "Total calculation before fraction should be \(wholeCalc)U")
-        #expect(
-            result.correctionInsulin == targetDifferenceInsulin,
-            "Insulin for BG correction should be \(targetDifferenceInsulin)U"
-        )
         #expect(result.iobInsulinReduction == -1.0, "Absolute IOB reduction amount should be 1U, hence -1U")
         #expect(result.superBolusInsulin == 0, "Additional insulin for super bolus should be 0U")
         #expect(result.targetDifference == 80, "Difference from target BG should be 80 mg/dL")
@@ -140,7 +135,7 @@ import Testing
         #expect(result.wholeCobInsulin == wholeCobInsulin, "Insulin for total carbs should be \(wholeCobInsulin)U")
     }
 
-    @Test("Calculate insulin for fatty meal") func testFattyMealCalculation() async throws {
+    @Test("Calculate insulin for reduced bolus") func testFattyMealCalculation() async throws {
         // STEP 1: Setup test scenario
         // We need to provide a CalculationInput struct
         let carbs: Decimal = 80
@@ -185,10 +180,10 @@ import Testing
             lastLoopDate: Date()
         )
 
-        // STEP 3: Calculate insulin with fatty meal enabled
+        // STEP 3: Calculate insulin with reduced bolus enabled
         let fattyMealResult = await calculator.calculateInsulin(input: input)
 
-        // STEP 4: Calculate insulin with fatty meal disabled for comparison
+        // STEP 4: Calculate insulin with reduced bolus disabled for comparison
         let standardInput = CalculationInput(
             carbs: carbs,
             currentBG: currentBG,
@@ -213,7 +208,7 @@ import Testing
         let standardResult = await calculator.calculateInsulin(input: standardInput)
 
         // STEP 5: Verify results
-        // Fatty meal should reduce the insulin amount by the fatty meal factor (0.8)
+        // Reduced bolus should reduce the insulin amount by the reduced bolus factor (0.8)
         let expectedReduction = fattyMealFactor
         let actualReduction = Decimal(
             (Double(fattyMealResult.insulinCalculated) / Double(standardResult.insulinCalculated) * 10.0).rounded() / 10.0
@@ -222,11 +217,11 @@ import Testing
         #expect(
             actualReduction == expectedReduction,
             """
-            Fatty meal calculation incorrect
+            Reduced bolus calculation incorrect
             Expected reduction factor: \(expectedReduction)
             Actual reduction factor: \(actualReduction)
             Standard calculation: \(standardResult.insulinCalculated)U
-            Fatty meal calculation: \(fattyMealResult.insulinCalculated)U
+            Reduced bolus calculation: \(fattyMealResult.insulinCalculated)U
             """
         )
     }
@@ -491,7 +486,9 @@ import Testing
             useFattyMealCorrection: false,
             useSuperBolus: false,
             lastLoopDate: Date(),
-            minPredBG: nil
+            minPredBG: nil,
+            simulatedCOB: nil,
+            isBackdated: false
         )
 
         // Then
@@ -525,7 +522,7 @@ import Testing
         // Then
         #expect(units == expectedUnits, "Units should match settings")
         #expect(fraction == expectedFraction, "Override factor should match settings")
-        #expect(fattyMealFactor == expectedFattyMealFactor, "Fatty meal factor should match settings")
+        #expect(fattyMealFactor == expectedFattyMealFactor, "Reduced bolus factor should match settings")
         #expect(sweetMealFactor == expectedSweetMealFactor, "Sweet meal factor should match settings")
         #expect(maxCarbs == expectedMaxCarbs, "Max carbs should match settings")
 
@@ -664,6 +661,174 @@ import Testing
             fileStorage.save(originalISFValues, as: OpenAPS.Settings.insulinSensitivities)
         }
     }
+
+    @Test("Calculate insulin with backdated carbs") func testHandleBolusCalculationFunction() async throws {
+        // STEP 1: Setup test scenario
+        let currentDate = Date()
+        let backdatedCarbsDate = currentDate.addingTimeInterval(-120 * 60) // 2 hours ago
+        let carbs: Decimal = 30 // 30g of carbs, backdated 2 hour
+        let cob: Int16 = 50
+
+        // Get the COB value for the backdated carbs
+        // Use the actual APS Manager to calculate simulated COB for more realistic test
+        let determination = await apsManager.simulateDetermineBasal(
+            simulatedCarbsAmount: carbs,
+            simulatedBolusAmount: 0,
+            simulatedCarbsDate: backdatedCarbsDate
+        )
+        let simulatedCOB = determination?.cob ?? Decimal(cob)
+
+        // STEP 2: Calculate results for normal and backdated carbs
+        let resultBackdated = await calculator.handleBolusCalculation(
+            carbs: carbs,
+            useFattyMealCorrection: false,
+            useSuperBolus: false,
+            lastLoopDate: Date.now.addingTimeInterval(-5.minutes.timeInterval),
+            minPredBG: 80,
+            simulatedCOB: Int16(truncating: NSDecimalNumber(decimal: simulatedCOB)),
+            isBackdated: true
+        )
+
+        let resultNormalEntry = await calculator.handleBolusCalculation(
+            carbs: carbs,
+            useFattyMealCorrection: false,
+            useSuperBolus: false,
+            lastLoopDate: Date.now.addingTimeInterval(-5.minutes.timeInterval),
+            minPredBG: 80,
+            simulatedCOB: Int16(truncating: NSDecimalNumber(decimal: simulatedCOB)),
+            isBackdated: false
+        )
+
+        // STEP 3: Compare
+        // The backdated scenario should recommend less insulin than the current time scenario
+        #expect(
+            resultBackdated.insulinCalculated < resultNormalEntry.insulinCalculated,
+            """
+            Backdated carbs should result in lower insulin recommendation
+            Current time: \(resultNormalEntry.insulinCalculated)U
+            Backdated: \(resultBackdated.insulinCalculated)U
+            Difference: \(resultNormalEntry.insulinCalculated - resultBackdated.insulinCalculated)U
+            """
+        )
+    }
+
+    @Test("Calculate insulin with backdated carbs") func testBackdatedCarbsCalculation() async throws {
+        // STEP 1: Setup test scenario
+        let currentDate = Date()
+        let backdatedCarbsDate = currentDate.addingTimeInterval(-60 * 60) // 1 hour ago
+
+        let currentBG: Decimal = 140
+        let target: Decimal = 100
+        let isf: Decimal = 40
+        let carbRatio: Decimal = 10
+        let iob: Decimal = 0.5
+        let cob: Int16 = 10 // Existing COB before adding backdated carbs
+        let carbs: Decimal = 30 // 30g of carbs, backdated 1 hour
+
+        // Get the COB value for the backdated carbs
+        // Use the actual APS Manager to calculate simulated COB for more realistic test
+        let determination = await apsManager.simulateDetermineBasal(
+            simulatedCarbsAmount: carbs,
+            simulatedBolusAmount: 0,
+            simulatedCarbsDate: backdatedCarbsDate
+        )
+
+        // Fallback to existing COB if determination is nil
+        let simulatedCOB = determination?.cob ?? Decimal(cob)
+
+        // For comparison - same scenario but with current time carbs
+        let currentTimeInput = CalculationInput(
+            carbs: carbs, // the newly entered carbs (30g)
+            currentBG: currentBG,
+            deltaBG: 0,
+            target: target,
+            isf: isf,
+            carbRatio: carbRatio,
+            iob: iob,
+            cob: cob, // the existing cob (10g)
+            useFattyMealCorrectionFactor: false,
+            fattyMealFactor: 0.8,
+            useSuperBolus: false,
+            sweetMealFactor: 1,
+            basal: 1.0,
+            fraction: 1.0,
+            maxBolus: 10,
+            maxIOB: 15,
+            maxCOB: 120,
+            minPredBG: 80,
+            lastLoopDate: currentDate
+        )
+
+        // Backdated scenario uses the same input but simulates date in the past
+        let backdatedInput = CalculationInput(
+            carbs: 0, // as the carbs are backdated we need to set the (newly entered) carbs to 0
+            currentBG: currentBG,
+            deltaBG: 0,
+            target: target,
+            isf: isf,
+            carbRatio: carbRatio,
+            iob: iob,
+            cob: Int16(truncating: NSDecimalNumber(decimal: simulatedCOB)), // current COB we got from the simulated Determination
+            useFattyMealCorrectionFactor: false,
+            fattyMealFactor: 0.8,
+            useSuperBolus: false,
+            sweetMealFactor: 1,
+            basal: 1.0,
+            fraction: 1.0,
+            maxBolus: 10,
+            maxIOB: 15,
+            maxCOB: 120,
+            minPredBG: 80,
+            lastLoopDate: currentDate
+        )
+
+        // STEP 2: Calculate insulin for both scenarios
+        let currentTimeResult = await calculator.calculateInsulin(input: currentTimeInput)
+        let backdatedResult = await calculator.calculateInsulin(input: backdatedInput)
+
+        // STEP 3: Verify results
+
+        // In the current time scenario, we expect COB to be old COB + current carbs
+        let expectedCurrentTimeCOB = Decimal(cob) + carbs
+        #expect(
+            currentTimeResult.wholeCob == expectedCurrentTimeCOB,
+            "Current time scenario should have \(expectedCurrentTimeCOB)g COB (\(cob)g existing + \(carbs)g new)"
+        )
+
+        // For backdated scenario, COB should be less than the current time scenario
+        // because some carbs have already been absorbed
+        #expect(
+            backdatedResult.wholeCob < currentTimeResult.wholeCob,
+            """
+            Backdated scenario should have less COB than current time scenario
+            Backdated: \(backdatedResult.wholeCob)g
+            Current time: \(currentTimeResult.wholeCob)g
+            Difference: \(currentTimeResult.wholeCob - backdatedResult.wholeCob)g
+            """
+        )
+
+        // The wholeCobInsulin should reflect the difference in COB
+        #expect(
+            backdatedResult.wholeCobInsulin < currentTimeResult.wholeCobInsulin,
+            """
+            Backdated scenario should require less insulin for carbs due to partial absorption
+            Backdated insulin: \(backdatedResult.wholeCobInsulin)U
+            Current time insulin: \(currentTimeResult.wholeCobInsulin)U
+            Difference: \(currentTimeResult.wholeCobInsulin - backdatedResult.wholeCobInsulin)U
+            """
+        )
+
+        // The backdated scenario should recommend less insulin than the current time scenario
+        #expect(
+            backdatedResult.insulinCalculated < currentTimeResult.insulinCalculated,
+            """
+            Backdated carbs should result in lower insulin recommendation
+            Current time: \(currentTimeResult.insulinCalculated)U
+            Backdated: \(backdatedResult.insulinCalculated)U
+            Difference: \(currentTimeResult.insulinCalculated - backdatedResult.insulinCalculated)U
+            """
+        )
+    }
 }
 
 // Copied over from BolusCalculationManager as they are not included in the protocol definition (and I don´t want them to be included)

+ 48 - 0
TrioTests/DynamicISFEnableTests.swift

@@ -0,0 +1,48 @@
+import CoreData
+import Foundation
+import Swinject
+import Testing
+
+@testable import Trio
+
+@Suite("Dynamic ISF Enable Logic Tests", .serialized) struct DynamicISFEnableTests {
+    var coreDataStack: CoreDataStack!
+    var context: NSManagedObjectContext!
+
+    init() async throws {
+        // In-memory Core Data for tests
+        coreDataStack = try await CoreDataStack.createForTests()
+        context = coreDataStack.newTaskContext()
+    }
+
+    func testEnableLogic(percentSamples: Double) async throws -> Bool {
+        let numberOfSamples = Int(288 * 7 * percentSamples)
+        let now = Date() // internal function uses Date()
+
+        try await context.perform {
+            for index in 0 ..< numberOfSamples {
+                let timeDelta = Double(index * 5 * 60)
+                let tdd = TDDStored(context: context)
+                tdd.date = now - timeDelta
+                tdd.total = 30
+                tdd.bolus = 15
+                tdd.tempBasal = 15
+                tdd.scheduledBasal = 0
+            }
+
+            try context.save()
+        }
+
+        return try await BaseTDDStorage.hasSufficientTDD(context: context)
+    }
+
+    @Test("Confirm samples from last 7 days enables Dynamic ISF") func testPercentSamplesEnablingLogic() async throws {
+        let enabled = try await testEnableLogic(percentSamples: 0.8)
+        #expect(enabled)
+    }
+
+    @Test("Confirm insufficient samples from last 7 days disables Dynamic ISF") func testPercentSamplesDisablesLogic() async throws {
+        let enabled = try await testEnableLogic(percentSamples: 0.7)
+        #expect(!enabled)
+    }
+}

+ 77 - 0
TrioTests/LocalizationTests.swift

@@ -0,0 +1,77 @@
+
+import Foundation
+import Testing
+
+private let bundle = Bundle.main
+
+@Suite("Localization Tests", .serialized) struct LocalizationTests {
+    @Test("No stray % inside format strings") func testNoStrayPercent() {
+        // Array to collect strings with issues
+        var offenders: [(lang: String, key: String, value: String, file: String)] = []
+
+        // Regular expression patterns
+        let placeholderPattern = "%[0-9]*\\$?[.,]?[0-9]*[a-zA-Z@]" // Matches placeholders like %@, %d, %1$@
+        let escapedPercentPattern = "%%" // Matches escaped percent signs
+        let percentPattern = "%" // Matches any percent sign
+
+        // Compile regexes (force-unwrapped since patterns are static and valid)
+        let placeholderRegex = try! NSRegularExpression(pattern: placeholderPattern)
+        let escapedPercentRegex = try! NSRegularExpression(pattern: escapedPercentPattern)
+        let percentRegex = try! NSRegularExpression(pattern: percentPattern)
+
+        // Assume 'bundle' is accessible, e.g., Bundle.main
+        for locale in bundle.localizations where locale != "Base" {
+            guard let lproj = bundle.path(forResource: locale, ofType: "lproj"),
+                  let files = FileManager.default.enumerator(atPath: lproj) else { continue }
+
+            // Iterate over .strings files in the localization directory
+            for case let f as String in files where f.hasSuffix(".strings") {
+                let path = (lproj as NSString).appendingPathComponent(f)
+                guard let table = NSDictionary(contentsOfFile: path) as? [String: String] else { continue }
+
+                // Check each key-value pair in the .strings file
+                for (key, value) in table {
+                    let nsValue = value as NSString
+                    let range = NSRange(location: 0, length: nsValue.length)
+
+                    // Determine if the value contains any placeholders
+                    let hasPlaceholders = placeholderRegex.firstMatch(in: value, range: range) != nil
+
+                    // Only check for stray % if the value has placeholders
+                    if hasPlaceholders {
+                        // Find all ranges covered by placeholders and escaped %%
+                        let placeholderMatches = placeholderRegex.matches(in: value, range: range)
+                        let escapedMatches = escapedPercentRegex.matches(in: value, range: range)
+                        let coveredRanges = (placeholderMatches + escapedMatches).map(\.range)
+
+                        // Find all % signs in the value
+                        let percentMatches = percentRegex.matches(in: value, range: range)
+
+                        // Check each % to see if it's stray (not covered by a placeholder or %%)
+                        for percentMatch in percentMatches {
+                            let percentLocation = percentMatch.range.location
+                            let isCovered = coveredRanges.contains { NSLocationInRange(percentLocation, $0) }
+                            if !isCovered {
+                                offenders.append((lang: locale, key: key, value: value, file: f))
+                                break // Stop checking this string after finding an issue
+                            }
+                        }
+                    }
+                    // If no placeholders, skip the check (single % is allowed)
+                }
+            }
+        }
+
+        // Assert that no offenders were found using Testing's #expect
+        #expect(
+            offenders.isEmpty,
+            """
+            Found \(offenders.count) string(s) that still have a single % although \
+            the value contains printf placeholders:
+
+            \(offenders.map { "\($0.lang) – \($0.file)\n⟨key⟩   \($0.key)\n⟨value⟩ \($0.value)" }
+                .joined(separator: "\n\n"))
+            """
+        )
+    }
+}