瀏覽代碼

Merge branch 'dev' into feature/dev-version-display

Signed-off-by: Sjoerd Bozon <sjoerd.bozon@gmail.com>
Sjoerd Bozon 10 月之前
父節點
當前提交
69d8b97d01
共有 100 個文件被更改,包括 7908 次插入8134 次删除
  1. 1 2
      .github/CODEOWNERS
  2. 29 14
      .github/workflows/build_trio.yml
  3. 132 0
      .github/workflows/unit_tests.yml
  4. 23 8
      Config.xcconfig
  5. 1 1
      DanaKit
  6. 4 1
      Gemfile
  7. 63 57
      Gemfile.lock
  8. 1 1
      Model/Helper/CarbEntryStored+helper.swift
  9. 6 3
      Model/Helper/GlucoseStored+helper.swift
  10. 6 4
      Model/Helper/OverrideStored+helper.swift
  11. 43 11
      Trio.xcodeproj/project.pbxproj
  12. 78 0
      Trio/Resources/Assets.xcassets/Colors/darkGreen.colorset/Contents.json
  13. 78 0
      Trio/Resources/Assets.xcassets/Colors/darkOrange.colorset/Contents.json
  14. 20 0
      Trio/Resources/Assets.xcassets/app_icons/trioCircledNoBackground.appiconset/Contents.json
  15. 二進制
      Trio/Resources/Assets.xcassets/app_icons/trioCircledNoBackground.appiconset/trioCircledNoBackground watch.png
  16. 二進制
      Trio/Resources/Assets.xcassets/app_icons/trioCircledNoBackground.appiconset/trioCircledNoBackground.png
  17. 二進制
      Trio/Resources/Assets.xcassets/app_icons/trioCircledNoBackground.imageset/ComplicationIcon.png
  18. 0 12
      Trio/Resources/Assets.xcassets/app_icons/trioCircledNoBackground.imageset/Contents.json
  19. 12 0
      Trio/Resources/InfoPlist.xcstrings
  20. 1 1
      Trio/Resources/javascript/bundle/autosens.js
  21. 1 1
      Trio/Resources/javascript/bundle/autotune-core.js
  22. 1 1
      Trio/Resources/javascript/bundle/autotune-prep.js
  23. 1 1
      Trio/Resources/javascript/bundle/basal-set-temp.js
  24. 1 1
      Trio/Resources/javascript/bundle/determine-basal.js
  25. 1 1
      Trio/Resources/javascript/bundle/glucose-get-last.js
  26. 1 1
      Trio/Resources/javascript/bundle/iob.js
  27. 1 1
      Trio/Resources/javascript/bundle/meal.js
  28. 1 1
      Trio/Resources/javascript/bundle/profile.js
  29. 2 2
      Trio/Resources/javascript/prepare/autosens.js
  30. 1 1
      Trio/Resources/javascript/prepare/autotune-core.js
  31. 1 1
      Trio/Resources/javascript/prepare/autotune-prep.js
  32. 7 7
      Trio/Resources/javascript/prepare/determine-basal.js
  33. 1 1
      Trio/Resources/javascript/prepare/iob.js
  34. 1 1
      Trio/Resources/javascript/prepare/meal.js
  35. 6 6
      Trio/Resources/javascript/prepare/profile.js
  36. 10 12
      Trio/Sources/APS/APSManager.swift
  37. 8 8
      Trio/Sources/APS/FetchTreatmentsManager.swift
  38. 1 1
      Trio/Sources/APS/OpenAPS/Constants.swift
  39. 45 19
      Trio/Sources/APS/OpenAPS/OpenAPS.swift
  40. 13 15
      Trio/Sources/APS/Storage/DeterminationStorage.swift
  41. 10 5
      Trio/Sources/APS/Storage/TDDStorage.swift
  42. 1 1
      Trio/Sources/Application/AppState.swift
  43. 273 0
      Trio/Sources/Application/LockedResolver.swift
  44. 6 5
      Trio/Sources/Application/TrioApp.swift
  45. 11 4
      Trio/Sources/Helpers/BackgroundTask+Helper.swift
  46. 2 0
      Trio/Sources/Helpers/Color+Extensions.swift
  47. 1 1
      Trio/Sources/Helpers/MainChartHelper.swift
  48. 4937 7323
      Trio/Sources/Localizations/Main/Localizable.xcstrings
  49. 32 0
      Trio/Sources/Models/BloodGlucose.swift
  50. 7 3
      Trio/Sources/Models/Oref2_variables.swift
  51. 1 1
      Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+Overrides.swift
  52. 30 13
      Trio/Sources/Modules/Adjustments/View/AdjustmentsRootView.swift
  53. 1 5
      Trio/Sources/Modules/Adjustments/View/Overrides/AdjustmentsRootView+Overrides.swift
  54. 1 7
      Trio/Sources/Modules/Adjustments/View/TempTargets/AdjustmentsRootView+TempTargets.swift
  55. 9 7
      Trio/Sources/Modules/BolusCalculatorConfig/View/BolusCalculatorConfigRootView.swift
  56. 1 1
      Trio/Sources/Modules/DynamicSettings/View/DynamicSettingsRootView.swift
  57. 25 7
      Trio/Sources/Modules/GeneralSettings/View/UnitsLimitsSettingsRootView.swift
  58. 1 1
      Trio/Sources/Modules/Home/View/Chart/ChartElements/SelectionPopoverView.swift
  59. 1 1
      Trio/Sources/Modules/Onboarding/View/OnboardingView+AlgorithmUtil.swift
  60. 25 8
      Trio/Sources/Modules/Onboarding/View/OnboardingView+Util.swift
  61. 1 1
      Trio/Sources/Modules/SMBSettings/View/SMBSettingsRootView.swift
  62. 2 3
      Trio/Sources/Modules/Settings/SettingItems.swift
  63. 350 0
      Trio/Sources/Modules/Stat/StatStateModel+Setup/GlucoseStatsSetup.swift
  64. 1 1
      Trio/Sources/Modules/Stat/StatStateModel+Setup/LoopChartSetup.swift
  65. 51 5
      Trio/Sources/Modules/Stat/StatStateModel.swift
  66. 43 9
      Trio/Sources/Modules/Stat/View/StatChartUtils.swift
  67. 80 20
      Trio/Sources/Modules/Stat/View/StatRootView.swift
  68. 253 0
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseDailyDistributionChart.swift
  69. 421 0
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseDailyPercentileChart.swift
  70. 14 14
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseDistributionChart.swift
  71. 32 32
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucosePercentileChart.swift
  72. 71 0
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucosePercentileDetailView.swift
  73. 189 121
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseSectorChart.swift
  74. 4 4
      Trio/Sources/Modules/Stat/View/ViewElements/Insulin/BolusStatsView.swift
  75. 2 1
      Trio/Sources/Modules/Stat/View/ViewElements/Insulin/TotalDailyDoseChart.swift
  76. 5 3
      Trio/Sources/Modules/Stat/View/ViewElements/Looping/LoopBarChartView.swift
  77. 3 2
      Trio/Sources/Modules/Stat/View/ViewElements/Meal/MealStatsView.swift
  78. 2 2
      Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift
  79. 8 5
      Trio/Sources/Modules/Treatments/View/ForecastChart.swift
  80. 5 7
      Trio/Sources/Modules/Treatments/View/PopupView.swift
  81. 1 1
      Trio/Sources/Modules/Treatments/View/TreatmentsRootView.swift
  82. 6 6
      Trio/Sources/Services/BolusCalculator/BolusCalculationManager.swift
  83. 1 1
      Trio/Sources/Services/LiveActivity/LiveActivityAttributes+Helper.swift
  84. 120 221
      Trio/Sources/Services/LiveActivity/LiveActivityManager.swift
  85. 30 20
      Trio/Sources/Services/Network/Nightscout/NightscoutAPI.swift
  86. 1 0
      Trio/Sources/Shortcuts/BaseIntentsRequest.swift
  87. 38 14
      Trio/Sources/Shortcuts/Bolus/BolusIntent.swift
  88. 28 4
      Trio/Sources/Shortcuts/Bolus/BolusIntentRequest.swift
  89. 26 14
      Trio/Sources/Shortcuts/Carbs/AddCarbPresetIntent.swift
  90. 43 6
      Trio/Sources/Shortcuts/Carbs/CarbPresetIntentRequest.swift
  91. 9 5
      Trio/Sources/Shortcuts/Override/ApplyOverridePresetIntent.swift
  92. 1 1
      Trio/Sources/Shortcuts/Override/CancelOverrideIntent.swift
  93. 14 4
      Trio/Sources/Shortcuts/TempPresets/ApplyTempPresetIntent.swift
  94. 1 1
      Trio/Sources/Shortcuts/TempPresets/CancelTempPresetIntent.swift
  95. 12 9
      TrioTests/BolusCalculatorTests/BolusCalculatorTests.swift
  96. 4 1
      TrioTests/CoreDataTests/GlucoseStorageTests.swift
  97. 48 0
      TrioTests/DynamicISFEnableTests.swift
  98. 9 4
      fastlane/Fastfile
  99. 2 2
      fastlane/testflight.md
  100. 0 0
      oref0_source_version.txt

+ 1 - 2
.github/CODEOWNERS

@@ -1,2 +1 @@
-*    @dnzxy @bjornoleh @MikePlante1 @aug0211 @AndreasStokholm @Sjoerd-Bo3 @t1dude
-*.js @dnzxy @bjornoleh @MikePlante1 @aug0211 @AndreasStokholm @Sjoerd-Bo3 @t1dude @jeremystorring
+*    @dnzxy @bjornoleh @MikePlante1 @AndreasStokholm @Sjoerd-Bo3 @t1dude @marv-out

+ 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:

+ 132 - 0
.github/workflows/unit_tests.yml

@@ -0,0 +1,132 @@
+name: zzz [DO NOT RUN] Automated unit tests
+
+on:
+  pull_request:
+    branches:
+      - dev
+    types: [opened, synchronize]
+    paths-ignore:
+      - '**.md'
+      - '**/README'
+      - '**.yml'
+      - '**.txt'
+
+  push:
+    branches:
+      - dev
+    paths-ignore:
+      - '**.md'
+      - '**/README'
+      - '**.yml'
+      - '**.txt'
+
+jobs:
+  test:
+    name: Run Unit Tests
+    runs-on: macos-15
+    if: github.repository_owner == 'nightscout'
+
+    steps:
+      - name: Select Xcode version
+        run: sudo xcode-select -s /Applications/Xcode_16.3.app/Contents/Developer
+
+      - name: Checkout code
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 1
+          submodules: recursive
+
+      - name: Restore cache
+        id: cache-restore
+        uses: actions/cache/restore@v4
+        with:
+          path: |
+            /Users/runner/Library/Developer/Xcode/DerivedData
+            .build
+          key: ${{ runner.os }}-trio-${{ hashFiles('**/*.swift', '**/*.xcodeproj', '**/*.xcworkspace') }}
+          restore-keys: |
+            ${{ runner.os }}-trio-
+
+      - name: Show cache contents before build
+        run: |
+          echo "📂 Contents of DerivedData:"
+          ls -lah /Users/runner/Library/Developer/Xcode/DerivedData || echo "Directory not found"
+          echo ""
+          echo "📂 Contents of .build:"
+          ls -lah .build || echo ".build directory not found"
+
+      - name: Build for testing
+        run: |
+          set -o pipefail && \
+          time xcodebuild build-for-testing \
+            -workspace Trio.xcworkspace \
+            -scheme "Trio Tests" \
+            -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.4' \
+
+      - name: Check for uncommitted changes
+        run: |
+          CHANGES=$(git status --porcelain)
+          if [ -n "$CHANGES" ]; then
+            echo "Uncommitted changes detected:"
+            echo "$CHANGES"
+            echo "$CHANGES" | while read -r line; do
+              FILE=$(echo $line | cut -c4-)
+              echo "::warning file=$FILE::Uncommitted change detected"
+            done
+            exit 0
+          else
+            echo "No uncommitted changes detected."
+          fi
+        shell: bash
+
+      - name: Show cache contents after build
+        run: |
+          echo "📂 Updated DerivedData contents:"
+          du -sh /Users/runner/Library/Developer/Xcode/DerivedData || echo "Directory not found"
+          ls -lah /Users/runner/Library/Developer/Xcode/DerivedData || echo "Directory not found"
+          echo ""
+          echo "📂 Updated .build contents:"
+          du -sh .build || echo ".build directory not found"
+          ls -lah .build || echo ".build directory not found"
+          
+      - name: Save cache
+        if: steps.cache-restore.outputs.cache-hit != 'true'
+        uses: actions/cache/save@v4
+        with:
+          path: |
+            /Users/runner/Library/Developer/Xcode/DerivedData
+            .build
+          key: ${{ runner.os }}-trio-${{ hashFiles('**/*.swift', '**/*.xcodeproj', '**/*.xcworkspace') }}  
+
+      - name: Run tests
+        run: |
+          set -o pipefail
+          time xcodebuild test-without-building \
+            -workspace Trio.xcworkspace \
+            -scheme "Trio Tests" \
+            -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.4' \
+            $([ "$ENABLE_PARALLEL_TESTING" = "true" ] && echo "-parallel-testing-enabled YES") \
+            2>&1 | tee xcodebuild.log
+
+      - name: Annotate test results
+        if: always()
+        run: |
+          if [ -f xcodebuild.log ]; then
+            if grep -q "Failing tests:" xcodebuild.log; then
+              echo "::error title=Unit Tests Failed::Some tests failed"
+              echo "## ❌ Some tests failed:" >> $GITHUB_STEP_SUMMARY
+              grep -A 20 "Failing tests:" xcodebuild.log | \
+                grep -E '^\s+[A-Za-z0-9]+\..+\(\)' | \
+                sed 's/^/  - /' >> $GITHUB_STEP_SUMMARY
+              echo "::group::Failed Test List"
+              grep -A 20 "Failing tests:" xcodebuild.log | \
+                grep -E '^\s+[A-Za-z0-9]+\..+\(\)' | \
+                sed 's/^/  - /'
+              echo "::endgroup::"
+            else
+              echo "::notice title=Unit Tests Passed::✅ All tests passed"
+              echo "✅ All tests passed" >> $GITHUB_STEP_SUMMARY
+            fi
+          else
+            echo "::warning::Test log (xcodebuild.log) not found"
+          fi

+ 23 - 8
Config.xcconfig

@@ -1,14 +1,29 @@
+// Some of the items can be modified to match the user's preference
 APP_DISPLAY_NAME = Trio
-APP_VERSION = 0.5.0
-APP_DEV_VERSION = 0.5.0.18
-APP_BUILD_NUMBER = 1
-COPYRIGHT_NOTICE =
-DEVELOPER_TEAM = ##TEAM_ID##
-BUNDLE_IDENTIFIER = org.nightscout.$(DEVELOPMENT_TEAM).trio
 APP_ICON = trioBlack
 APP_URL_SCHEME = Trio
 
-// Optional overrides
+// DEVELOPER_TEAM will be set to your Apple Developer ID - typically using ConfigOverride.xcconfig
+DEVELOPER_TEAM = ##TEAM_ID##
+
+// Typically this is not modified unless you want to create a separate (unique) app using your ID
+// It must include $(DEVELOPMENT_TEAM)
+// For example: myOwnApp.$(DEVELOPMENT_TEAM).trio
+BUNDLE_IDENTIFIER = org.nightscout.$(DEVELOPMENT_TEAM).trio
+
+// Danger zone - do not modify these unless you know what you are doing
+
+// The TRIO_APP_GROUP_ID should not be modified - it is required to have this exact format
+// to build with GitHub actions and to work with xDrip4iOS
+TRIO_APP_GROUP_ID = group.org.nightscout.$(DEVELOPMENT_TEAM).trio.trio-app-group
+
+// The developers set the version numbers, please leave them alone
+APP_VERSION = 0.5.1
+APP_DEV_VERSION = 0.5.1.4
+APP_BUILD_NUMBER = 1
+COPYRIGHT_NOTICE =
+
+// Optional overrides - these can be used to insert your TEAMID into the DEVELOPER_TEAM field
 #include? "../../ConfigOverride.xcconfig"
 #include? "../ConfigOverride.xcconfig"
-#include? "ConfigOverride.xcconfig"
+#include? "ConfigOverride.xcconfig"

+ 1 - 1
DanaKit

@@ -1 +1 @@
-Subproject commit ee9ebdd880fdcc9bc50885e60408b7c64f8834d1
+Subproject commit ca240f9df3cb5dbda9ad574161c9bbf9612908b2

+ 4 - 1
Gemfile

@@ -1,3 +1,6 @@
 source "https://rubygems.org"
 
-gem "fastlane"
+# gem "fastlane"
+
+# This branch uses fastlane 2.228.0 plus pr 29596
+gem "fastlane",  git: "https://github.com/loopandlearn/fastlane.git", ref: "a670d4b092b274d58ebb5497126e47fc6a84f533"

+ 63 - 57
Gemfile.lock

@@ -1,3 +1,51 @@
+GIT
+  remote: https://github.com/loopandlearn/fastlane.git
+  revision: a670d4b092b274d58ebb5497126e47fc6a84f533
+  ref: a670d4b092b274d58ebb5497126e47fc6a84f533
+  specs:
+    fastlane (2.228.0)
+      CFPropertyList (>= 2.3, < 4.0.0)
+      addressable (>= 2.8, < 3.0.0)
+      artifactory (~> 3.0)
+      aws-sdk-s3 (~> 1.0)
+      babosa (>= 1.0.3, < 2.0.0)
+      bundler (>= 1.12.0, < 3.0.0)
+      colored (~> 1.2)
+      commander (~> 4.6)
+      dotenv (>= 2.1.1, < 3.0.0)
+      emoji_regex (>= 0.1, < 4.0)
+      excon (>= 0.71.0, < 1.0.0)
+      faraday (~> 1.0)
+      faraday-cookie_jar (~> 0.0.6)
+      faraday_middleware (~> 1.0)
+      fastimage (>= 2.1.0, < 3.0.0)
+      fastlane-sirp (>= 1.0.0)
+      gh_inspector (>= 1.1.2, < 2.0.0)
+      google-apis-androidpublisher_v3 (~> 0.3)
+      google-apis-playcustomapp_v1 (~> 0.1)
+      google-cloud-env (>= 1.6.0, < 2.0.0)
+      google-cloud-storage (~> 1.31)
+      highline (~> 2.0)
+      http-cookie (~> 1.0.5)
+      json (< 3.0.0)
+      jwt (>= 2.1.0, < 3)
+      mini_magick (>= 4.9.4, < 5.0.0)
+      multipart-post (>= 2.0.0, < 3.0.0)
+      naturally (~> 2.2)
+      optparse (>= 0.1.1, < 1.0.0)
+      plist (>= 3.1.0, < 4.0.0)
+      rubyzip (>= 2.0.0, < 3.0.0)
+      security (= 0.1.5)
+      simctl (~> 1.6.3)
+      terminal-notifier (>= 2.0.0, < 3.0.0)
+      terminal-table (~> 3)
+      tty-screen (>= 0.6.3, < 1.0.0)
+      tty-spinner (>= 0.8.0, < 1.0.0)
+      word_wrap (~> 1.0.0)
+      xcodeproj (>= 1.13.0, < 2.0.0)
+      xcpretty (~> 0.4.1)
+      xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
+
 GEM
   remote: https://rubygems.org/
   specs:
@@ -9,26 +57,26 @@ GEM
       public_suffix (>= 2.0.2, < 7.0)
     artifactory (3.0.17)
     atomos (0.1.3)
-    aws-eventstream (1.3.2)
-    aws-partitions (1.1086.0)
-    aws-sdk-core (3.222.1)
+    aws-eventstream (1.4.0)
+    aws-partitions (1.1116.0)
+    aws-sdk-core (3.225.2)
       aws-eventstream (~> 1, >= 1.3.0)
       aws-partitions (~> 1, >= 1.992.0)
       aws-sigv4 (~> 1.9)
       base64
       jmespath (~> 1, >= 1.6.1)
       logger
-    aws-sdk-kms (1.99.0)
-      aws-sdk-core (~> 3, >= 3.216.0)
+    aws-sdk-kms (1.105.0)
+      aws-sdk-core (~> 3, >= 3.225.0)
       aws-sigv4 (~> 1.5)
-    aws-sdk-s3 (1.183.0)
-      aws-sdk-core (~> 3, >= 3.216.0)
+    aws-sdk-s3 (1.189.1)
+      aws-sdk-core (~> 3, >= 3.225.0)
       aws-sdk-kms (~> 1)
       aws-sigv4 (~> 1.5)
-    aws-sigv4 (1.11.0)
+    aws-sigv4 (1.12.1)
       aws-eventstream (~> 1, >= 1.0.2)
     babosa (1.0.4)
-    base64 (0.2.0)
+    base64 (0.3.0)
     claide (1.1.0)
     colored (1.2)
     colored2 (3.1.2)
@@ -70,48 +118,6 @@ GEM
     faraday_middleware (1.2.1)
       faraday (~> 1.0)
     fastimage (2.4.0)
-    fastlane (2.227.1)
-      CFPropertyList (>= 2.3, < 4.0.0)
-      addressable (>= 2.8, < 3.0.0)
-      artifactory (~> 3.0)
-      aws-sdk-s3 (~> 1.0)
-      babosa (>= 1.0.3, < 2.0.0)
-      bundler (>= 1.12.0, < 3.0.0)
-      colored (~> 1.2)
-      commander (~> 4.6)
-      dotenv (>= 2.1.1, < 3.0.0)
-      emoji_regex (>= 0.1, < 4.0)
-      excon (>= 0.71.0, < 1.0.0)
-      faraday (~> 1.0)
-      faraday-cookie_jar (~> 0.0.6)
-      faraday_middleware (~> 1.0)
-      fastimage (>= 2.1.0, < 3.0.0)
-      fastlane-sirp (>= 1.0.0)
-      gh_inspector (>= 1.1.2, < 2.0.0)
-      google-apis-androidpublisher_v3 (~> 0.3)
-      google-apis-playcustomapp_v1 (~> 0.1)
-      google-cloud-env (>= 1.6.0, < 2.0.0)
-      google-cloud-storage (~> 1.31)
-      highline (~> 2.0)
-      http-cookie (~> 1.0.5)
-      json (< 3.0.0)
-      jwt (>= 2.1.0, < 3)
-      mini_magick (>= 4.9.4, < 5.0.0)
-      multipart-post (>= 2.0.0, < 3.0.0)
-      naturally (~> 2.2)
-      optparse (>= 0.1.1, < 1.0.0)
-      plist (>= 3.1.0, < 4.0.0)
-      rubyzip (>= 2.0.0, < 3.0.0)
-      security (= 0.1.5)
-      simctl (~> 1.6.3)
-      terminal-notifier (>= 2.0.0, < 3.0.0)
-      terminal-table (~> 3)
-      tty-screen (>= 0.6.3, < 1.0.0)
-      tty-spinner (>= 0.8.0, < 1.0.0)
-      word_wrap (~> 1.0.0)
-      xcodeproj (>= 1.13.0, < 2.0.0)
-      xcpretty (~> 0.4.1)
-      xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
     fastlane-sirp (1.0.0)
       sysrandom (~> 1.0)
     gh_inspector (1.1.3)
@@ -157,7 +163,7 @@ GEM
     httpclient (2.9.0)
       mutex_m
     jmespath (1.6.2)
-    json (2.10.2)
+    json (2.12.2)
     jwt (2.10.1)
       base64
     logger (1.7.0)
@@ -167,13 +173,13 @@ GEM
     multipart-post (2.4.1)
     mutex_m (0.3.0)
     nanaimo (0.4.0)
-    naturally (2.2.1)
+    naturally (2.3.0)
     nkf (0.2.0)
     optparse (0.6.0)
     os (1.1.4)
     plist (3.7.2)
-    public_suffix (6.0.1)
-    rake (13.2.1)
+    public_suffix (6.0.2)
+    rake (13.3.0)
     representable (3.2.0)
       declarative (< 0.1.0)
       trailblazer-option (>= 0.1.1, < 0.2.0)
@@ -184,7 +190,7 @@ GEM
     ruby2_keywords (0.0.5)
     rubyzip (2.4.1)
     security (0.1.5)
-    signet (0.19.0)
+    signet (0.20.0)
       addressable (~> 2.8)
       faraday (>= 0.17.5, < 3.a)
       jwt (>= 1.5, < 3.0)
@@ -226,7 +232,7 @@ PLATFORMS
   x86_64-linux
 
 DEPENDENCIES
-  fastlane
+  fastlane!
 
 BUNDLED WITH
    2.6.2

+ 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 {

+ 6 - 3
Model/Helper/GlucoseStored+helper.swift

@@ -20,12 +20,15 @@ extension GlucoseStored {
         return request
     }
 
-    static func glucoseIsFlat(_ glucose: [GlucoseStored]) -> Bool {
-        guard glucose.count >= 6 else { return false }
+    static func glucoseIsHIGH(_ glucose: [GlucoseStored]) -> Bool {
+        guard glucose.count >= 4 else { return false }
 
         let firstValue = glucose.first?.glucose
 
-        return glucose.allSatisfy { $0.glucose == firstValue }
+        /// 400 mg/dL covers all Dexcom CGMs as well as European Libre 2 and most readings from xDrip4iOS.
+        /// U.S. / Canadian Libres can emit up to 500 mg/dL until it reads "HI"
+        /// Our condition considers both these values, 400 and 500, as possible "flat" readings when paired CGM reads HIGH.
+        return glucose.allSatisfy { $0.glucose == firstValue && ($0.glucose == 400 || $0.glucose == 500) }
     }
 
     // Preview

+ 6 - 4
Model/Helper/OverrideStored+helper.swift

@@ -7,10 +7,12 @@ extension NSPredicate {
     }
 
     static var lastActiveOverride: NSPredicate {
-        let date = Date.oneDayAgo
-        return NSPredicate(
-            format: "date >= %@ AND enabled == %@",
-            date as NSDate,
+        // For non-indefinite overrides, we still want to filter by date
+        // For indefinite overrides, we want them regardless of date
+        NSPredicate(
+            format: "(date >= %@ OR indefinite == %@) AND enabled == %@",
+            Date.oneDayAgo as NSDate,
+            true as NSNumber,
             true as NSNumber
         )
     }

+ 43 - 11
Trio.xcodeproj/project.pbxproj

@@ -28,7 +28,7 @@
 		190EBCC829FF13AA00BA767D /* UserInterfaceSettingsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 190EBCC729FF13AA00BA767D /* UserInterfaceSettingsStateModel.swift */; };
 		190EBCCB29FF13CB00BA767D /* UserInterfaceSettingsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 190EBCCA29FF13CB00BA767D /* UserInterfaceSettingsRootView.swift */; };
 		191F62682AD6B05A004D7911 /* NightscoutSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191F62672AD6B05A004D7911 /* NightscoutSettings.swift */; };
-		1935364028496F7D001E0B16 /* Oref2_variables.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1935363F28496F7D001E0B16 /* Oref2_variables.swift */; };
+		1935364028496F7D001E0B16 /* TrioCustomOrefVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1935363F28496F7D001E0B16 /* TrioCustomOrefVariables.swift */; };
 		193F6CDD2A512C8F001240FD /* Loops.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193F6CDC2A512C8F001240FD /* Loops.swift */; };
 		195D80B42AF6973A00D25097 /* DynamicSettingsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 195D80B32AF6973A00D25097 /* DynamicSettingsRootView.swift */; };
 		195D80B72AF697B800D25097 /* DynamicSettingsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 195D80B62AF697B800D25097 /* DynamicSettingsDataFlow.swift */; };
@@ -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 */; };
@@ -248,6 +249,7 @@
 		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 */; };
@@ -442,7 +444,11 @@
 		BDFF7A892D25F97D0016C40C /* TrioWatchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFF7A852D25F97D0016C40C /* TrioWatchApp.swift */; };
 		BDFF7A8B2D25F97D0016C40C /* Unit Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFF7A8A2D25F97D0016C40C /* Unit Tests.swift */; };
 		BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680C4420C9A345D46D90D06C /* ManualTempBasalProvider.swift */; };
+		C21FE1E72DA59C6B007D550B /* GlucoseDailyDistributionChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = C21FE1E62DA59C6B007D550B /* GlucoseDailyDistributionChart.swift */; };
+		C28DD7262DBA9A9E00EC02DD /* GlucosePercentileDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C28DD7252DBA9A9E00EC02DD /* GlucosePercentileDetailView.swift */; };
+		C29E268A2DADFD2A00F87E75 /* GlucoseDailyPercentileChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = C29E26892DADFD2A00F87E75 /* GlucoseDailyPercentileChart.swift */; };
 		C2A0A42F2CE03131003B98E8 /* ConstantValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2A0A42E2CE0312C003B98E8 /* ConstantValues.swift */; };
+		C2A6D1E42DB1581D0036DB66 /* GlucoseStatsSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2A6D1E32DB1581D0036DB66 /* GlucoseStatsSetup.swift */; };
 		C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.swift */; };
 		CA370FC152BC98B3D1832968 /* BasalProfileEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */; };
 		CC6C406E2ACDD69E009B8058 /* RawFetchedProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */; };
@@ -842,7 +848,7 @@
 		190EBCC729FF13AA00BA767D /* UserInterfaceSettingsStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserInterfaceSettingsStateModel.swift; sourceTree = "<group>"; };
 		190EBCCA29FF13CB00BA767D /* UserInterfaceSettingsRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserInterfaceSettingsRootView.swift; sourceTree = "<group>"; };
 		191F62672AD6B05A004D7911 /* NightscoutSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSettings.swift; sourceTree = "<group>"; };
-		1935363F28496F7D001E0B16 /* Oref2_variables.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Oref2_variables.swift; sourceTree = "<group>"; };
+		1935363F28496F7D001E0B16 /* TrioCustomOrefVariables.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioCustomOrefVariables.swift; sourceTree = "<group>"; };
 		193F6CDC2A512C8F001240FD /* Loops.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Loops.swift; sourceTree = "<group>"; };
 		195D80B32AF6973A00D25097 /* DynamicSettingsRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicSettingsRootView.swift; sourceTree = "<group>"; };
 		195D80B62AF697B800D25097 /* DynamicSettingsDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicSettingsDataFlow.swift; sourceTree = "<group>"; };
@@ -1035,6 +1041,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>"; };
@@ -1060,6 +1067,7 @@
 		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>"; };
@@ -1256,7 +1264,11 @@
 		BDFF7A922D25F97D0016C40C /* TrioWatchAppExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioWatchAppExtension.swift; sourceTree = "<group>"; };
 		BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorRootView.swift; sourceTree = "<group>"; };
 		C19984D62EFC0035A9E9644D /* TreatmentsProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TreatmentsProvider.swift; sourceTree = "<group>"; };
+		C21FE1E62DA59C6B007D550B /* GlucoseDailyDistributionChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseDailyDistributionChart.swift; sourceTree = "<group>"; };
+		C28DD7252DBA9A9E00EC02DD /* GlucosePercentileDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucosePercentileDetailView.swift; sourceTree = "<group>"; };
+		C29E26892DADFD2A00F87E75 /* GlucoseDailyPercentileChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseDailyPercentileChart.swift; sourceTree = "<group>"; };
 		C2A0A42E2CE0312C003B98E8 /* ConstantValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantValues.swift; sourceTree = "<group>"; };
+		C2A6D1E32DB1581D0036DB66 /* GlucoseStatsSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseStatsSetup.swift; sourceTree = "<group>"; };
 		C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalRootView.swift; sourceTree = "<group>"; };
 		C8D1A7CA8C10C4403D4BBFA7 /* TreatmentsDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TreatmentsDataFlow.swift; sourceTree = "<group>"; };
 		CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawFetchedProfile.swift; sourceTree = "<group>"; };
@@ -2104,8 +2116,9 @@
 			isa = PBXGroup;
 			children = (
 				38E4451D274DB04600EC9A94 /* AppDelegate.swift */,
-				388E595B25AD948C0019842D /* TrioApp.swift */,
 				BD4ED4FC2CF9D5E8000EDC9C /* AppState.swift */,
+				3B0B4E6B2DE1439A005C6627 /* LockedResolver.swift */,
+				388E595B25AD948C0019842D /* TrioApp.swift */,
 			);
 			path = Application;
 			sourceTree = "<group>";
@@ -2363,7 +2376,7 @@
 				3871F39B25ED892B0013ECB5 /* TempTarget.swift */,
 				3811DE8E25C9D80400A708ED /* User.swift */,
 				E0D4F80427513ECF00BDF1FE /* HealthKitSample.swift */,
-				1935363F28496F7D001E0B16 /* Oref2_variables.swift */,
+				1935363F28496F7D001E0B16 /* TrioCustomOrefVariables.swift */,
 				CE82E02628E869DF00473A9C /* AlertEntry.swift */,
 				19B0EF2028F6D66200069496 /* Statistics.swift */,
 				19012CDB291D2CB900FB8210 /* LoopStats.swift */,
@@ -2571,6 +2584,7 @@
 				BD8FC0552D66187700B95AED /* CoreDataTests */,
 				38FCF3F125E9028E0078B0D1 /* Info.plist */,
 				CEE9A65D2BBC9F6500EB5194 /* CalibrationsTests.swift */,
+				3BAAE60B2DE776630049589B /* DynamicISFEnableTests.swift */,
 				38FCF3F825E902C20078B0D1 /* FileStorageTests.swift */,
 				3B997DCE2DC00A3A006B6BB2 /* JSONImporterTests.swift */,
 				CE1F6DD82BADF4620064EB8D /* PluginManagerTests.swift */,
@@ -2799,6 +2813,7 @@
 		BD249D952D42FCA800412DEB /* StatStateModel+Setup */ = {
 			isa = PBXGroup;
 			children = (
+				C2A6D1E32DB1581D0036DB66 /* GlucoseStatsSetup.swift */,
 				BD249DA02D42FD1000412DEB /* TDDSetup.swift */,
 				BD249D9C2D42FCF300412DEB /* MealStatsSetup.swift */,
 				BD249D982D42FCCA00412DEB /* BolusStatsSetup.swift */,
@@ -3464,6 +3479,9 @@
 		DDCAE97A2D79F99B00B1BB51 /* Glucose */ = {
 			isa = PBXGroup;
 			children = (
+				C21FE1E62DA59C6B007D550B /* GlucoseDailyDistributionChart.swift */,
+				C29E26892DADFD2A00F87E75 /* GlucoseDailyPercentileChart.swift */,
+				C28DD7252DBA9A9E00EC02DD /* GlucosePercentileDetailView.swift */,
 				BD249D912D42FC5000412DEB /* GlucoseSectorChart.swift */,
 				BD249D8B2D42FC2500412DEB /* GlucoseDistributionChart.swift */,
 				BD249D892D42FC0E00412DEB /* GlucosePercentileChart.swift */,
@@ -4080,6 +4098,7 @@
 				DD3F1F8D2D9E0E0600DCE7B3 /* NightscoutSetupStepView.swift in Sources */,
 				F90692CF274B999A0037068D /* HealthKitDataFlow.swift in Sources */,
 				CE7CA3552A064973004BE681 /* ListStateIntent.swift in Sources */,
+				C28DD7262DBA9A9E00EC02DD /* GlucosePercentileDetailView.swift in Sources */,
 				BDF530D82B40F8AC002CAF43 /* LockScreenView.swift in Sources */,
 				195D80B72AF697B800D25097 /* DynamicSettingsDataFlow.swift in Sources */,
 				DD98ACC02D71013200C0778F /* StatChartUtils.swift in Sources */,
@@ -4164,6 +4183,7 @@
 				DD1745322C55AE6000211FAC /* TargetBehavoirStateModel.swift in Sources */,
 				38E44535274E411700EC9A94 /* Disk+Data.swift in Sources */,
 				BD8E6B232D9036F700ABF8FA /* OnboardingDataFlow.swift in Sources */,
+				C2A6D1E42DB1581D0036DB66 /* GlucoseStatsSetup.swift in Sources */,
 				3811DE3125C9D49500A708ED /* HomeProvider.swift in Sources */,
 				FE41E4D629463EE20047FD55 /* NightscoutPreferences.swift in Sources */,
 				E013D872273AC6FE0014109C /* GlucoseSimulatorSource.swift in Sources */,
@@ -4193,6 +4213,7 @@
 				BD249DA12D42FD1200412DEB /* TDDSetup.swift in Sources */,
 				CE7950262998056D00FA576E /* CGMSetupView.swift in Sources */,
 				582FAE432C05102C00D1C13F /* CoreDataError.swift in Sources */,
+				C29E268A2DADFD2A00F87E75 /* GlucoseDailyPercentileChart.swift in Sources */,
 				38A0363B25ECF07E00FCBB52 /* GlucoseStorage.swift in Sources */,
 				65070A332BFDCB83006F213F /* TidepoolStartView.swift in Sources */,
 				190EBCC629FF138000BA767D /* UserInterfaceSettingsProvider.swift in Sources */,
@@ -4237,6 +4258,7 @@
 				BD432CA12D2F4E3600D1EB79 /* WatchMessageKeys.swift in Sources */,
 				DD4C581F2D73C43D001BFF2C /* LoopStatsView.swift in Sources */,
 				58A3D53A2C96D4DE003F90FC /* AddTempTargetForm.swift in Sources */,
+				C21FE1E72DA59C6B007D550B /* GlucoseDailyDistributionChart.swift in Sources */,
 				DD1745302C55AE5300211FAC /* TargetBehaviorProvider.swift in Sources */,
 				58D08B382C8DFB6000AA37D3 /* BasalChart.swift in Sources */,
 				118DF76D2C5ECBC60067FEB7 /* OverridePresetEntity.swift in Sources */,
@@ -4258,6 +4280,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 */,
@@ -4343,7 +4366,7 @@
 				110AEDEB2C51A0AE00615CC9 /* ShortcutsConfigView.swift in Sources */,
 				38DF179027733EAD00B3528F /* SnowScene.swift in Sources */,
 				DD1DB7CC2BECCA1F0048B367 /* BuildDetails.swift in Sources */,
-				1935364028496F7D001E0B16 /* Oref2_variables.swift in Sources */,
+				1935364028496F7D001E0B16 /* TrioCustomOrefVariables.swift in Sources */,
 				CE2FAD3A297D93F0001A872C /* BloodGlucoseExtensions.swift in Sources */,
 				38E4453A274E411700EC9A94 /* Disk+[UIImage].swift in Sources */,
 				DD73FA0F2D74F58E00D19D1E /* BackgroundTask+Helper.swift in Sources */,
@@ -4647,6 +4670,7 @@
 				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 */,
@@ -4747,7 +4771,7 @@
 			baseConfigurationReference = 38F3783A2613555C009DB701 /* Config.xcconfig */;
 			buildSettings = {
 				ALWAYS_SEARCH_USER_PATHS = NO;
-				APP_GROUP_ID = "group.$(BUNDLE_IDENTIFIER).trio-app-group";
+				APP_GROUP_ID = "$(TRIO_APP_GROUP_ID)";
 				CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
 				CLANG_ANALYZER_NONNULL = YES;
 				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
@@ -4815,7 +4839,7 @@
 			baseConfigurationReference = 38F3783A2613555C009DB701 /* Config.xcconfig */;
 			buildSettings = {
 				ALWAYS_SEARCH_USER_PATHS = NO;
-				APP_GROUP_ID = "group.$(BUNDLE_IDENTIFIER).trio-app-group";
+				APP_GROUP_ID = "$(TRIO_APP_GROUP_ID)";
 				CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
 				CLANG_ANALYZER_NONNULL = YES;
 				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
@@ -4876,7 +4900,7 @@
 			isa = XCBuildConfiguration;
 			buildSettings = {
 				APP_DISPLAY_NAME = "$(APP_DISPLAY_NAME)";
-				APP_GROUP_ID = "$(APP_GROUP_ID)";
+				APP_GROUP_ID = "$(TRIO_APP_GROUP_ID)";
 				ASSETCATALOG_COMPILER_APPICON_NAME = "$(APP_ICON)";
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
@@ -4909,8 +4933,12 @@
 				PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER)";
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE_SPECIFIER = "";
+				SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+				SUPPORTS_MACCATALYST = NO;
+				SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
+				SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
 				SWIFT_VERSION = 5.0;
-				TARGETED_DEVICE_FAMILY = "1,2";
+				TARGETED_DEVICE_FAMILY = 1;
 			};
 			name = Debug;
 		};
@@ -4918,7 +4946,7 @@
 			isa = XCBuildConfiguration;
 			buildSettings = {
 				APP_DISPLAY_NAME = "$(APP_DISPLAY_NAME)";
-				APP_GROUP_ID = "$(APP_GROUP_ID)";
+				APP_GROUP_ID = "$(TRIO_APP_GROUP_ID)";
 				ASSETCATALOG_COMPILER_APPICON_NAME = "$(APP_ICON)";
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
@@ -4950,8 +4978,12 @@
 				PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER)";
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE_SPECIFIER = "";
+				SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+				SUPPORTS_MACCATALYST = NO;
+				SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
+				SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
 				SWIFT_VERSION = 5.0;
-				TARGETED_DEVICE_FAMILY = "1,2";
+				TARGETED_DEVICE_FAMILY = 1;
 			};
 			name = Release;
 		};

+ 78 - 0
Trio/Resources/Assets.xcassets/Colors/darkGreen.colorset/Contents.json

@@ -0,0 +1,78 @@
+{
+  "colors" : [
+    {
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0x47",
+          "green" : "0x9F",
+          "red" : "0x2A"
+        }
+      },
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "color" : {
+        "color-space" : "extended-srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0x46",
+          "green" : "0xA7",
+          "red" : "0x26"
+        }
+      },
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "contrast",
+          "value" : "high"
+        }
+      ],
+      "color" : {
+        "color-space" : "extended-srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0x30",
+          "green" : "0x6E",
+          "red" : "0x1D"
+        }
+      },
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        },
+        {
+          "appearance" : "contrast",
+          "value" : "high"
+        }
+      ],
+      "color" : {
+        "color-space" : "extended-srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0x49",
+          "green" : "0xAF",
+          "red" : "0x26"
+        }
+      },
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 78 - 0
Trio/Resources/Assets.xcassets/Colors/darkOrange.colorset/Contents.json

@@ -0,0 +1,78 @@
+{
+  "colors" : [
+    {
+      "color" : {
+        "color-space" : "extended-srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0x00",
+          "green" : "0x77",
+          "red" : "0xCC"
+        }
+      },
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "color" : {
+        "color-space" : "extended-srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0x08",
+          "green" : "0x7F",
+          "red" : "0xCC"
+        }
+      },
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "contrast",
+          "value" : "high"
+        }
+      ],
+      "color" : {
+        "color-space" : "extended-srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0x00",
+          "green" : "0x2A",
+          "red" : "0xA1"
+        }
+      },
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        },
+        {
+          "appearance" : "contrast",
+          "value" : "high"
+        }
+      ],
+      "color" : {
+        "color-space" : "extended-srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0x33",
+          "green" : "0x8F",
+          "red" : "0xCC"
+        }
+      },
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 20 - 0
Trio/Resources/Assets.xcassets/app_icons/trioCircledNoBackground.appiconset/Contents.json

@@ -0,0 +1,20 @@
+{
+  "images" : [
+    {
+      "filename" : "trioCircledNoBackground.png",
+      "idiom" : "universal",
+      "platform" : "ios",
+      "size" : "1024x1024"
+    },
+    {
+      "filename" : "trioCircledNoBackground watch.png",
+      "idiom" : "universal",
+      "platform" : "watchos",
+      "size" : "1024x1024"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

二進制
Trio/Resources/Assets.xcassets/app_icons/trioCircledNoBackground.appiconset/trioCircledNoBackground watch.png


二進制
Trio/Resources/Assets.xcassets/app_icons/trioCircledNoBackground.appiconset/trioCircledNoBackground.png


二進制
Trio/Resources/Assets.xcassets/app_icons/trioCircledNoBackground.imageset/ComplicationIcon.png


+ 0 - 12
Trio/Resources/Assets.xcassets/app_icons/trioCircledNoBackground.imageset/Contents.json

@@ -1,12 +0,0 @@
-{
-  "images" : [
-    {
-      "filename" : "ComplicationIcon.png",
-      "idiom" : "universal"
-    }
-  ],
-  "info" : {
-    "author" : "xcode",
-    "version" : 1
-  }
-}

+ 12 - 0
Trio/Resources/InfoPlist.xcstrings

@@ -457,6 +457,18 @@
         }
       }
     },
+    "NSCalendarsFullAccessUsageDescription" : {
+      "comment" : "Privacy - Calendars Full Access Usage Description",
+      "extractionState" : "extracted_with_value",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "new",
+            "value" : "To create events with BG reading values, so that they can be viewed on Apple Watch and CarPlay"
+          }
+        }
+      }
+    },
     "NSCalendarsUsageDescription" : {
       "comment" : "Privacy - Calendars Usage Description",
       "extractionState" : "extracted_with_value",

文件差異過大導致無法顯示
+ 1 - 1
Trio/Resources/javascript/bundle/autosens.js


文件差異過大導致無法顯示
+ 1 - 1
Trio/Resources/javascript/bundle/autotune-core.js


文件差異過大導致無法顯示
+ 1 - 1
Trio/Resources/javascript/bundle/autotune-prep.js


文件差異過大導致無法顯示
+ 1 - 1
Trio/Resources/javascript/bundle/basal-set-temp.js


文件差異過大導致無法顯示
+ 1 - 1
Trio/Resources/javascript/bundle/determine-basal.js


文件差異過大導致無法顯示
+ 1 - 1
Trio/Resources/javascript/bundle/glucose-get-last.js


文件差異過大導致無法顯示
+ 1 - 1
Trio/Resources/javascript/bundle/iob.js


文件差異過大導致無法顯示
+ 1 - 1
Trio/Resources/javascript/bundle/meal.js


文件差異過大導致無法顯示
+ 1 - 1
Trio/Resources/javascript/bundle/profile.js


+ 2 - 2
Trio/Resources/javascript/prepare/autosens.js

@@ -18,9 +18,9 @@ function generate(glucose_data, pumphistory_data, basalprofile, profile_data, ca
         temptargets: temptarget_data
     };
     detection_inputs.deviations = 96;
-    var ratio8h = freeaps_autosens(detection_inputs);
+    var ratio8h = trio_autosens(detection_inputs);
     detection_inputs.deviations = 288;
-    var ratio24h = freeaps_autosens(detection_inputs);
+    var ratio24h = trio_autosens(detection_inputs);
     var lowestRatio = ratio8h.ratio < ratio24h.ratio ? ratio8h : ratio24h;
     return lowestRatio;
 }

+ 1 - 1
Trio/Resources/javascript/prepare/autotune-core.js

@@ -13,5 +13,5 @@ function generate(prepped_glucose_data,previous_autotune_data,pumpprofile_data)
       , pumpProfile: pumpprofile_data
     };
 
-    return freeaps_autotuneCore(inputs);
+    return trio_autotuneCore(inputs);
 }

+ 1 - 1
Trio/Resources/javascript/prepare/autotune-prep.js

@@ -43,5 +43,5 @@ function generate(pumphistory_data, profile_data, glucose_data, pumpprofile_data
     , tune_insulin_curve: tune_insulin_curve
     };
 
-    return freeaps_autotunePrep(inputs);
+    return trio_autotunePrep(inputs);
 }

+ 7 - 7
Trio/Resources/javascript/prepare/determine-basal.js

@@ -1,19 +1,19 @@
 //для enact/smb-suggested.json параметры: monitor/iob.json monitor/temp_basal.json monitor/glucose.json settings/profile.json settings/autosens.json --meal monitor/meal.json --microbolus --reservoir monitor/reservoir.json
 
-function generate(iob, currenttemp, glucose, profile, autosens = null, meal = null, microbolusAllowed = false, reservoir = null, clock = new Date(), pump_history, preferences, basalProfile, oref2_variables) {
+function generate(iob, currenttemp, glucose, profile, autosens = null, meal = null, microbolusAllowed = false, reservoir = null, clock = new Date(), pump_history, preferences, basalProfile, trio_custom_oref_variables) {
 
     var clock = new Date();
     
     var middleware_was_used = "";
     try {
-        var middlewareReason = middleware(iob, currenttemp, glucose, profile, autosens, meal, reservoir, clock, pump_history, preferences, basalProfile, oref2_variables);
+        var middlewareReason = middleware(iob, currenttemp, glucose, profile, autosens, meal, reservoir, clock, pump_history, preferences, basalProfile, trio_custom_oref_variables);
         middleware_was_used = (middlewareReason || "Nothing changed");
         console.log("Middleware reason: " + middleware_was_used);
     } catch (error) {
         console.log("Invalid middleware: " + error);
     };
 
-    var glucose_status = freeaps_glucoseGetLast(glucose);
+    var glucose_status = trio_glucoseGetLast(glucose);
     var autosens_data = null;
 
     if (autosens) {
@@ -40,10 +40,10 @@ function generate(iob, currenttemp, glucose, profile, autosens = null, meal = nu
         basalprofile = basalProfile;
     }
     
-    var oref2_variables_ = {};
-    if (oref2_variables) {
-        oref2_variables_ = oref2_variables;
+    var trio_custom_oref_variables_temp = {};
+    if (trio_custom_oref_variables) {
+        trio_custom_oref_variables_temp = trio_custom_oref_variables;
     }
     
-    return freeaps_determineBasal(glucose_status, currenttemp, iob, profile, autosens_data, meal_data, freeaps_basalSetTemp, microbolusAllowed, reservoir_data, clock, pumphistory, preferences, basalprofile, oref2_variables_, middleware_was_used);
+    return trio_determineBasal(glucose_status, currenttemp, iob, profile, autosens_data, meal_data, trio_basalSetTemp, microbolusAllowed, reservoir_data, clock, pumphistory, preferences, basalprofile, trio_custom_oref_variables_temp, middleware_was_used);
 }

+ 1 - 1
Trio/Resources/javascript/prepare/iob.js

@@ -11,5 +11,5 @@ function generate(pumphistory_data, profile_data, clock_data, autosens_data = nu
       if (autosens_data) {
         inputs.autosens = autosens_data;
       }
-      return freeaps_iob(inputs);
+      return trio_iob(inputs);
 }

+ 1 - 1
Trio/Resources/javascript/prepare/meal.js

@@ -23,7 +23,7 @@ function generate(pumphistory_data, profile_data, clock_data, glucose_data, basa
     , glucose: glucose_data
     };
 
-    var recentCarbs = freeaps_meal(inputs);
+    var recentCarbs = trio_meal(inputs);
 
     if (glucose_data.length < 4) {
         console.error("Not enough glucose data to calculate carb absorption; found:", glucose_data.length);

+ 6 - 6
Trio/Resources/javascript/prepare/profile.js

@@ -1,7 +1,7 @@
 //для pumpprofile.json параметры: settings/settings.json settings/bg_targets.json settings/insulin_sensitivities.json settings/basal_profile.json preferences.json settings/carb_ratios.json settings/temptargets.json settings/model.json
 //для profile.json параметры: settings/settings.json settings/bg_targets.json settings/insulin_sensitivities.json settings/basal_profile.json preferences.json settings/carb_ratios.json settings/temptargets.json settings/model.json settings/autotune.json
 
-function generate(pumpsettings_data, bgtargets_data, isf_data, basalprofile_data, preferences_input = false, carbratio_input = false, temptargets_input = false, model_input = false, autotune_input = false, freeaps_data) {
+function generate(pumpsettings_data, bgtargets_data, isf_data, basalprofile_data, preferences_input = false, carbratio_input = false, temptargets_input = false, model_input = false, autotune_input = false, trio_data) {
     if (bgtargets_data.units !== 'mg/dL') {
         if (bgtargets_data.units === 'mmol/L') {
             for (var i = 0, len = bgtargets_data.targets.length; i < len; i++) {
@@ -35,9 +35,9 @@ function generate(pumpsettings_data, bgtargets_data, isf_data, basalprofile_data
         temptargets_data = temptargets_input;
     }
     
-    var freeaps = { };
-    if (freeaps_data) {
-        freeaps = freeaps_data;
+    var trioData = { };
+    if (trio_data) {
+        trioData = trio_data;
     }
 
     var model_data = { };
@@ -98,10 +98,10 @@ function generate(pumpsettings_data, bgtargets_data, isf_data, basalprofile_data
 
     if (autotune_data) {
         if (autotune_data.basalprofile) { inputs.basals = autotune_data.basalprofile; }
-        if (!freeaps.onlyAutotuneBasals) {
+        if (!trioData.onlyAutotuneBasals) {
             if (autotune_data.isfProfile) { inputs.isf = autotune_data.isfProfile; }
             if (autotune_data.carb_ratio) { inputs.carbratio.schedule[0].ratio = autotune_data.carb_ratio; }
         }
     }
-    return freeaps_profile(inputs);
+    return trio_profile(inputs);
 }

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

@@ -440,21 +440,9 @@ final class BaseAPSManager: APSManager, Injectable {
                 return false
             }
 
-            guard !GlucoseStored.glucoseIsFlat(glucose) else {
-                debug(.apsManager, "Glucose data is too flat")
-                self.processError(APSError.glucoseError(message: String(localized: "Glucose data is too flat")))
-                return false
-            }
-
             return true
         }
 
-        guard isValidGlucoseData else {
-            debug(.apsManager, "Glucose validation failed")
-            processError(APSError.glucoseError(message: "Glucose validation failed"))
-            return
-        }
-
         do {
             let now = Date()
 
@@ -466,6 +454,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
@@ -670,6 +662,12 @@ final class BaseAPSManager: APSManager, Injectable {
             throw APSError.apsError(message: "Pump not set")
         }
 
+        // Check if pump is suspended and abort if it is
+        if pump.status.pumpStatus.suspended {
+            info(.apsManager, "Skipping enactDetermination because pump is suspended")
+            return // return without throwing an error
+        }
+
         // Unable to do temp basal during manual temp basal 😁
         if isManualTempBasal {
             throw APSError.manualBasalTemp(message: "Loop not possible during the manual basal temp")

+ 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() {

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

@@ -53,7 +53,7 @@ extension OpenAPS {
         static let iob = "monitor/iob.json"
         static let cgmState = "monitor/cgm-state.json"
         static let podAge = "monitor/pod-age.json"
-        static let oref2_variables = "monitor/oref2_variables.json"
+        static let trio_custom_oref_variables = "monitor/trio_custom_oref_variables.json"
         static let alertHistory = "monitor/alerthistory.json"
         static let statistics = "monitor/statistics.json"
     }

+ 45 - 19
Trio/Sources/APS/OpenAPS/OpenAPS.swift

@@ -98,15 +98,15 @@ final class OpenAPS {
     }
 
     // fetch glucose to pass it to the meal function and to determine basal
-    private func fetchAndProcessGlucose() async throws -> String {
+    private func fetchAndProcessGlucose(fetchLimit: Int?) async throws -> String {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: context,
             predicate: NSPredicate.predicateForOneDayAgoInMinutes,
             key: "date",
             ascending: false,
-            fetchLimit: 72,
-            batchSize: 24
+            fetchLimit: fetchLimit,
+            batchSize: 48
         )
 
         return try await context.perform {
@@ -292,8 +292,8 @@ final class OpenAPS {
         // Perform asynchronous calls in parallel
         async let pumpHistoryObjectIDs = fetchPumpHistoryObjectIDs() ?? []
         async let carbs = fetchAndProcessCarbs(additionalCarbs: simulatedCarbsAmount ?? 0, carbsDate: simulatedCarbsDate)
-        async let glucose = fetchAndProcessGlucose()
-        async let oref2 = oref2()
+        async let glucose = fetchAndProcessGlucose(fetchLimit: 72)
+        async let prepareTrioCustomOrefVariables = prepareTrioCustomOrefVariables()
         async let profileAsync = loadFileFromStorageAsync(name: Settings.profile)
         async let basalAsync = loadFileFromStorageAsync(name: Settings.basalProfile)
         async let autosenseAsync = loadFileFromStorageAsync(name: Settings.autosense)
@@ -306,7 +306,7 @@ final class OpenAPS {
             pumpHistoryJSON,
             carbsAsJSON,
             glucoseAsJSON,
-            oref2_variables,
+            trioCustomOrefVariables,
             profile,
             basalProfile,
             autosens,
@@ -316,7 +316,7 @@ final class OpenAPS {
             try parsePumpHistory(await pumpHistoryObjectIDs, simulatedBolusAmount: simulatedBolusAmount),
             try carbs,
             try glucose,
-            try oref2,
+            try prepareTrioCustomOrefVariables,
             profileAsync,
             basalAsync,
             autosenseAsync,
@@ -368,7 +368,7 @@ final class OpenAPS {
             pumpHistory: pumpHistoryJSON,
             preferences: preferences,
             basalProfile: basalProfile,
-            oref2_variables: oref2_variables
+            trioCustomOrefVariables: trioCustomOrefVariables
         )
 
         debug(.openAPS, "\(simulation ? "[SIMULATION]" : "") OREF DETERMINATION: \(orefDetermination)")
@@ -385,11 +385,15 @@ final class OpenAPS {
 
             return determination
         } else {
+            debug(
+                .openAPS,
+                "\(DebuggingIdentifiers.failed) No determination data. orefDetermination: \(orefDetermination), Determination(from: orefDetermination): \(String(describing: Determination(from: orefDetermination))), deliverAt: \(String(describing: Determination(from: orefDetermination)?.deliverAt))"
+            )
             throw APSError.apsError(message: "No determination data.")
         }
     }
 
-    func oref2() async throws -> RawJSON {
+    func prepareTrioCustomOrefVariables() async throws -> RawJSON {
         try await context.perform {
             // Retrieve user preferences
             let userPreferences = self.storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self)
@@ -425,8 +429,10 @@ final class OpenAPS {
             let averageTDDLastTenDays = totalTDD / Decimal(totalDaysCount)
             let weightedTDD = weightPercentage * averageTDDLastTwoHours + (1 - weightPercentage) * averageTDDLastTenDays
 
-            // Prepare Oref2 variables
-            let oref2Data = Oref2_variables(
+            let glucose = try self.fetchGlucose()
+
+            // Prepare Trio's custom oref variables
+            let trioCustomOrefVariablesData = TrioCustomOrefVariables(
                 average_total_data: currentTDD > 0 ? averageTDDLastTenDays : 0,
                 weightedAverage: currentTDD > 0 ? weightedTDD : 1,
                 currentTDD: currentTDD,
@@ -446,12 +452,13 @@ final class OpenAPS {
                 start: (activeOverrides.first?.start ?? 0) as Decimal,
                 end: (activeOverrides.first?.end ?? 0) as Decimal,
                 smbMinutes: activeOverrides.first?.smbMinutes?.decimalValue ?? maxSMBBasalMinutes,
-                uamMinutes: activeOverrides.first?.uamMinutes?.decimalValue ?? maxUAMBasalMinutes
+                uamMinutes: activeOverrides.first?.uamMinutes?.decimalValue ?? maxUAMBasalMinutes,
+                shouldProtectDueToHIGH: GlucoseStored.glucoseIsHIGH(glucose)
             )
 
-            // Save and return the Oref2 variables
-            self.storage.save(oref2Data, as: OpenAPS.Monitor.oref2_variables)
-            return self.loadFileFromStorage(name: Monitor.oref2_variables)
+            // Save and return contents of Trio's custom oref variables
+            self.storage.save(trioCustomOrefVariablesData, as: OpenAPS.Monitor.trio_custom_oref_variables)
+            return self.loadFileFromStorage(name: Monitor.trio_custom_oref_variables)
         }
     }
 
@@ -461,7 +468,7 @@ final class OpenAPS {
         // Perform asynchronous calls in parallel
         async let pumpHistoryObjectIDs = fetchPumpHistoryObjectIDs() ?? []
         async let carbs = fetchAndProcessCarbs()
-        async let glucose = fetchAndProcessGlucose()
+        async let glucose = fetchAndProcessGlucose(fetchLimit: nil)
         async let getProfile = loadFileFromStorageAsync(name: Settings.profile)
         async let getBasalProfile = loadFileFromStorageAsync(name: Settings.basalProfile)
         async let getTempTargets = loadFileFromStorageAsync(name: Settings.tempTargets)
@@ -671,7 +678,7 @@ final class OpenAPS {
         pumpHistory: JSON,
         preferences: JSON,
         basalProfile: JSON,
-        oref2_variables: JSON
+        trioCustomOrefVariables: JSON
     ) async throws -> RawJSON {
         try await withCheckedThrowingContinuation { continuation in
             jsWorker.inCommonContext { worker in
@@ -700,7 +707,7 @@ final class OpenAPS {
                     pumpHistory,
                     preferences,
                     basalProfile,
-                    oref2_variables
+                    trioCustomOrefVariables
                 ])
 
                 continuation.resume(returning: result)
@@ -830,7 +837,7 @@ final class OpenAPS {
     }
 }
 
-// Non-Async fetch methods for oref2
+// Non-Async fetch methods for trio_custom_oref_variables
 extension OpenAPS {
     func fetchActiveTempTargets() throws -> [TempTargetStored] {
         try CoreDataStack.shared.fetchEntities(
@@ -864,4 +871,23 @@ extension OpenAPS {
             propertiesToFetch: ["date", "total"]
         ) as? [[String: Any]] ?? []
     }
+
+    func fetchGlucose() throws -> [GlucoseStored] {
+        let results = try CoreDataStack.shared.fetchEntities(
+            ofType: GlucoseStored.self,
+            onContext: context,
+            predicate: NSPredicate.predicateFor30MinAgo,
+            key: "date",
+            ascending: false,
+            fetchLimit: 4
+        )
+
+        return try context.perform {
+            guard let glucoseResults = results as? [GlucoseStored] else {
+                throw CoreDataError.fetchError(function: #function, file: #file)
+            }
+
+            return glucoseResults
+        }
+    }
 }

+ 13 - 15
Trio/Sources/APS/Storage/DeterminationStorage.swift

@@ -216,22 +216,20 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
             relationshipKeyPathsForPrefetching: ["forecastValues"]
         )
 
-        var result: [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])] = []
-
-        await context.perform {
-            if let forecasts = results as? [Forecast] {
-                for forecast in forecasts {
-                    // Use the helper property that already sorts by index
-                    let sortedValues = forecast.forecastValuesArray
-                    result.append((
-                        id: UUID(),
-                        forecastID: forecast.objectID,
-                        forecastValueIDs: sortedValues.map(\.objectID)
-                    ))
-                }
+        // Process results entirely within a single context.perform block to avoid data races
+        return await context.perform {
+            guard let forecasts = results as? [Forecast] else { return [] }
+
+            // Create and return the result array entirely within this block
+            return forecasts.map { forecast in
+                // Use the helper property that already sorts by index
+                let sortedValues = forecast.forecastValuesArray
+                return (
+                    id: UUID(),
+                    forecastID: forecast.objectID,
+                    forecastValueIDs: sortedValues.map(\.objectID)
+                )
             }
         }
-
-        return result
     }
 }

+ 10 - 5
Trio/Sources/APS/Storage/TDDStorage.swift

@@ -626,7 +626,7 @@ final class BaseTDDStorage: TDDStorage, Injectable {
 
             // Get weight percentage from preferences (default 0.65 if not set)
             let userPreferences = self.storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self)
-            let weightPercentage = userPreferences?.weightPercentage ?? Decimal(0.65) // why is this 1 as default in oref2??
+            let weightPercentage = userPreferences?.weightPercentage ?? Decimal(0.65) // why is this 1 as default in trio-oref??
 
             // Calculate weighted average using the formula:
             // weightedTDD = (weightPercentage × recent_average) + ((1 - weightPercentage) × historical_average)
@@ -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
         }
     }

+ 1 - 1
Trio/Sources/Application/AppState.swift

@@ -1,6 +1,6 @@
 import Foundation
 import Observation
-import SwiftUICore
+import SwiftUI
 import UIKit
 
 @Observable class AppState {

+ 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() {

+ 11 - 4
Trio/Sources/Helpers/BackgroundTask+Helper.swift

@@ -17,13 +17,20 @@ func endBackgroundTaskSafely(_ taskID: inout UIBackgroundTaskIdentifier, taskNam
 ///
 /// - Parameter name: The background task name.
 func startBackgroundTask(withName name: String) -> UIBackgroundTaskIdentifier {
-    var taskID = UIBackgroundTaskIdentifier.invalid
-
-    taskID = UIApplication.shared.beginBackgroundTask(withName: name) {
+    // Use a local copy of the taskID for the expiration handler
+    let taskID = UIApplication.shared.beginBackgroundTask(withName: name) { [taskID = UIBackgroundTaskIdentifier.invalid] in
+        // Create a new Task that takes the value of the taskID as a parameter
+        // and does not use the captured variable
         Task { @MainActor in
-            endBackgroundTaskSafely(&taskID, taskName: name)
+            // Since we can no longer change the original taskID,
+            // we simply end the Task with the given ID
+            if taskID != .invalid {
+                UIApplication.shared.endBackgroundTask(taskID)
+                debug(.default, "Background task '\(name)' ended in expiration handler.")
+            }
         }
     }
 
+    debug(.default, "Background task '\(name)' started with ID: \(taskID)")
     return taskID
 }

+ 2 - 0
Trio/Sources/Helpers/Color+Extensions.swift

@@ -72,4 +72,6 @@ extension Color {
     static let lemon = Color("Lemon")
     static let minus = Color("minus")
     static let darkGray = Color("darkGray")
+    static let darkGreen = Color("darkGreen")
+    static let darkOrange = Color("darkOrange")
 }

+ 1 - 1
Trio/Sources/Helpers/MainChartHelper.swift

@@ -1,7 +1,7 @@
 import Charts
 import CoreData
 import Foundation
-import SwiftUICore
+import SwiftUI
 
 enum MainChartHelper {
     // Calculates the glucose value thats the nearest to parameter 'time'

文件差異過大導致無法顯示
+ 4937 - 7323
Trio/Sources/Localizations/Main/Localizable.xcstrings


+ 32 - 0
Trio/Sources/Models/BloodGlucose.swift

@@ -176,9 +176,21 @@ extension Int {
     var formattedAsMmolL: String {
         NumberFormatter.glucoseFormatter.string(from: asMmolL as NSDecimalNumber) ?? "\(asMmolL)"
     }
+
+    func formatted(for units: GlucoseUnits) -> String {
+        units == .mgdL ? description : formattedAsMmolL
+    }
+
+    func formatted(withUnits units: GlucoseUnits) -> String {
+        formatted(for: units) + " \(units.rawValue)"
+    }
 }
 
 extension Decimal {
+    func asUnit(_ unit: GlucoseUnits) -> Decimal {
+        unit == .mgdL ? self : asMmolL
+    }
+
     var asMmolL: Decimal {
         Trio.rounded(self * GlucoseUnits.exchangeRate, scale: 1, roundingMode: .plain)
     }
@@ -190,9 +202,21 @@ extension Decimal {
     var formattedAsMmolL: String {
         NumberFormatter.glucoseFormatter.string(from: asMmolL as NSDecimalNumber) ?? "\(asMmolL)"
     }
+
+    func formatted(for units: GlucoseUnits) -> String {
+        units == .mgdL ? description : formattedAsMmolL
+    }
+
+    func formatted(withUnits units: GlucoseUnits) -> String {
+        formatted(for: units) + " \(units.rawValue)"
+    }
 }
 
 extension Double {
+    func asUnit(_ units: GlucoseUnits) -> Double {
+        units == .mgdL ? self : Double(truncating: asMmolL as NSNumber)
+    }
+
     var asMmolL: Decimal {
         Trio.rounded(Decimal(self) * GlucoseUnits.exchangeRate, scale: 1, roundingMode: .plain)
     }
@@ -204,6 +228,14 @@ extension Double {
     var formattedAsMmolL: String {
         NumberFormatter.glucoseFormatter.string(from: asMmolL as NSDecimalNumber) ?? "\(asMmolL)"
     }
+
+    func formatted(for units: GlucoseUnits) -> String {
+        units == .mgdL ? description : formattedAsMmolL
+    }
+
+    func formatted(withUnits units: GlucoseUnits) -> String {
+        formatted(for: units) + " \(units.rawValue)"
+    }
 }
 
 extension NumberFormatter {

+ 7 - 3
Trio/Sources/Models/Oref2_variables.swift

@@ -1,6 +1,6 @@
 import Foundation
 
-struct Oref2_variables: JSON, Equatable {
+struct TrioCustomOrefVariables: JSON, Equatable {
     var average_total_data: Decimal
     var currentTDD: Decimal
     var weightedAverage: Decimal
@@ -21,6 +21,7 @@ struct Oref2_variables: JSON, Equatable {
     var end: Decimal
     var smbMinutes: Decimal
     var uamMinutes: Decimal
+    var shouldProtectDueToHIGH: Bool
 
     init(
         average_total_data: Decimal,
@@ -42,7 +43,8 @@ struct Oref2_variables: JSON, Equatable {
         start: Decimal,
         end: Decimal,
         smbMinutes: Decimal,
-        uamMinutes: Decimal
+        uamMinutes: Decimal,
+        shouldProtectDueToHIGH: Bool
     ) {
         self.average_total_data = average_total_data
         self.weightedAverage = weightedAverage
@@ -64,10 +66,11 @@ struct Oref2_variables: JSON, Equatable {
         self.end = end
         self.smbMinutes = smbMinutes
         self.uamMinutes = uamMinutes
+        self.shouldProtectDueToHIGH = shouldProtectDueToHIGH
     }
 }
 
-extension Oref2_variables {
+extension TrioCustomOrefVariables {
     private enum CodingKeys: String, CodingKey {
         case average_total_data
         case weightedAverage
@@ -89,5 +92,6 @@ extension Oref2_variables {
         case end
         case smbMinutes
         case uamMinutes
+        case shouldProtectDueToHIGH
     }
 }

+ 1 - 1
Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+Overrides.swift

@@ -1,7 +1,7 @@
 import Combine
 import CoreData
 import Foundation
-import SwiftUICore
+import SwiftUI
 
 extension Adjustments.StateModel {
     // MARK: - Enact Overrides

+ 30 - 13
Trio/Sources/Modules/Adjustments/View/AdjustmentsRootView.swift

@@ -21,6 +21,8 @@ extension Adjustments {
         @State var isRemoveAlertPresented = false
         @State var removeAlert: Alert?
         @State var isEditingTT = false
+        @State var showCancelOverrideConfirmDialog = false
+        @State var showCancelTempTargetConfirmDialog = false
 
         private var shouldDisplayStickyOverrideStopButton: Bool {
             state.isOverrideEnabled && state.activeOverrideName.isNotEmpty
@@ -143,6 +145,32 @@ extension Adjustments {
                         EditTempTargetForm(tempTargetToEdit: tempTarget, state: state)
                     }
                 }
+                .confirmationDialog("Override to Stop", isPresented: $showCancelOverrideConfirmDialog) {
+                    Button("Stop", role: .destructive) {
+                        Task {
+                            // Save cancelled Override in OverrideRunStored Entity
+                            // Cancel ALL active Override
+                            await state.disableAllActiveOverrides(createOverrideRunEntry: true)
+                        }
+                    }
+                    Button("Cancel", role: .cancel) {}
+                } message: {
+                    Text("Stop the Override \"\(state.currentActiveOverride?.name ?? "")\"?")
+                }
+                .confirmationDialog("Temp Target to Stop", isPresented: $showCancelTempTargetConfirmDialog) {
+                    Button("Stop", role: .destructive) {
+                        Task {
+                            // Save cancelled Temp Targets in TempTargetRunStored Entity
+                            // Cancel ALL active Temp Targets
+                            await state.disableAllActiveTempTargets(createTempTargetRunEntry: true)
+                            // Update View
+                            state.updateLatestTempTargetConfiguration()
+                        }
+                    }
+                    Button("Cancel", role: .cancel) {}
+                } message: {
+                    Text("Stop the Temp Target \"\(state.currentActiveTempTarget?.name ?? "")\"?")
+                }
             }).background(appState.trioBackgroundColor(for: colorScheme))
         }
 
@@ -224,11 +252,7 @@ extension Adjustments {
             switch state.selectedTab {
             case .overrides:
                 Button(action: {
-                    Task {
-                        // Save cancelled Override in OverrideRunStored Entity
-                        // Cancel ALL active Override
-                        await state.disableAllActiveOverrides(createOverrideRunEntry: true)
-                    }
+                    showCancelOverrideConfirmDialog = true
                 }, label: {
                     Text("Stop Override")
 
@@ -239,14 +263,7 @@ extension Adjustments {
                     .tint(.white)
             case .tempTargets:
                 Button(action: {
-                    Task {
-                        // Save cancelled Temp Targets in TempTargetRunStored Entity
-                        // Cancel ALL active Temp Targets
-                        await state.disableAllActiveTempTargets(createTempTargetRunEntry: true)
-
-                        // Update View
-                        state.updateLatestTempTargetConfiguration()
-                    }
+                    showCancelTempTargetConfirmDialog = true
                 }, label: {
                     Text("Stop Temp Target")
 

+ 1 - 5
Trio/Sources/Modules/Adjustments/View/Overrides/AdjustmentsRootView+Overrides.swift

@@ -123,11 +123,7 @@ extension Adjustments.RootView {
                 .clipShape(Rectangle())
 
             Button(action: {
-                Task {
-                    // Save cancelled Override in OverrideRunStored Entity
-                    // Cancel ALL active Override
-                    await state.disableAllActiveOverrides(createOverrideRunEntry: true)
-                }
+                showCancelOverrideConfirmDialog = true
             }, label: {
                 Text("Stop Override")
                     .frame(maxWidth: .infinity, maxHeight: .infinity)

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

@@ -144,13 +144,7 @@ extension Adjustments.RootView {
                 .clipShape(Rectangle())
 
             Button(action: {
-                Task {
-                    // Save cancelled Temp Targets in TempTargetRunStored Entity
-                    // Cancel ALL active Temp Targets
-                    await state.disableAllActiveTempTargets(createTempTargetRunEntry: true)
-                    // Update View
-                    state.updateLatestTempTargetConfiguration()
-                }
+                showCancelTempTargetConfirmDialog = true
             }, label: {
                 Text("Stop Temp Target")
                     .frame(maxWidth: .infinity, maxHeight: .infinity)

+ 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.")
                     }
                 )
 

+ 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

+ 25 - 7
Trio/Sources/Modules/GeneralSettings/View/UnitsLimitsSettingsRootView.swift

@@ -46,22 +46,40 @@ extension UnitsLimitsSettings {
                     verboseHint:
                     VStack(alignment: .leading, spacing: 10) {
                         Text("Default: 0 units").bold()
+
                         Text(
-                            "Warning: This must be greater than 0 for any automatic temporary basal rates or SMBs to be given."
-                        ).bold()
-                        Text(
-                            "This setting helps prevent delivering too much insulin at once. It’s typically a value close to the amount you might need for a very high blood sugar and the biggest meal of your life combined."
+                            "Note: This setting must be greater than 0 for any automatic insulin dosing by Trio (unless you currently have negative IOB)."
                         )
+                        .bold()
+                        .foregroundStyle(Color.orange)
+
                         Text(
-                            "This is the maximum amount of Insulin On Board (IOB) above profile basal rates from all sources - positive temporary basal rates, manual or meal boluses, and SMBs - that Trio is allowed to accumulate to address an above target glucose."
+                            "Choose a value that covers your highest insulin needs — think about a correction for a very high glucose reading plus your biggest meal bolus. This gives Trio room to work while keeping you safe."
                         )
+
                         Text(
-                            "If a calculated amount exceeds this limit, the suggested and / or delivered amount will be reduced so that active insulin on board (IOB) will not exceed this safety limit."
+                            "Max IOB sets a safety limit on how much insulin Trio can automatically deliver above your scheduled basal rates. This prevents the system from giving too much insulin at once."
                         )
+
+                        VStack(alignment: .leading, spacing: 0) {
+                            Text("Trio calculates your current Insulin On Board (IOB) from:")
+                            Text("• Boluses (including SMBs)")
+                            Text("• Temporary Basal Rates (TBRs)")
+                            Text("  ◦ A TBR higher than your scheduled rate will increase IOB")
+                            Text("  ◦ A TBR lower than your scheduled rate will decrease IOB")
+                        }
+
                         Text(
-                            "Note: You can still manually bolus above this limit, but the suggested bolus amount will never exceed this in the bolus calculator."
+                            "If delivering more insulin would push your IOB above this limit, Trio will reduce or skip the dose to stay within the safety boundary. This applies to SMBs, TBRs, and the recommendation from the bolus calculator."
                         )
+
+                        VStack(alignment: .leading, spacing: 0) {
+                            Text("What's NOT limited:")
+                            Text("• Manual boluses you enter yoursef")
+                            Text("• Manual temporary basal rates you set yourself")
+                        }
                     }
+                    .fixedSize(horizontal: false, vertical: true)
                 )
 
                 SettingInputSection(

+ 1 - 1
Trio/Sources/Modules/Home/View/Chart/ChartElements/SelectionPopoverView.swift

@@ -1,6 +1,6 @@
 import Charts
 import Foundation
-import SwiftUICore
+import SwiftUI
 
 struct SelectionPopoverView: ChartContent {
     let selectedGlucose: GlucoseStored

+ 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."

+ 25 - 8
Trio/Sources/Modules/Onboarding/View/OnboardingView+Util.swift

@@ -386,25 +386,42 @@ enum DeliveryLimitSubstep: Int, CaseIterable, Identifiable {
     func description(units: GlucoseUnits) -> any View {
         switch self {
         case .maxIOB:
-            return VStack(alignment: .leading, spacing: 8) {
-                Text(
-                    "Note: This setting must be greater than 0 for any automatic insulin dosing by Trio."
-                ).bold().foregroundStyle(Color.orange)
+            return VStack(alignment: .leading, spacing: 10) {
+                Text("Default: 0 units").bold()
 
                 Text(
-                    "This setting helps prevent delivering too much insulin at once. It’s typically a value close to the amount you might need for a very high blood sugar and the biggest meal of your life combined."
+                    "Note: This setting must be greater than 0 for any automatic insulin dosing by Trio (unless you currently have negative IOB)."
                 )
+                .bold()
+                .foregroundStyle(Color.orange)
 
                 Text(
-                    "This is the maximum amount of Insulin On Board (IOB) above profile basal rates from all sources - positive temporary basal rates, manual or meal boluses, and SMBs - that Trio is allowed to accumulate to address an above target glucose."
+                    "Choose a value that covers your highest insulin needs — think about a correction for a very high glucose reading plus your biggest meal bolus. This gives Trio room to work while keeping you safe."
                 )
+
                 Text(
-                    "If a calculated amount exceeds this limit, the suggested and / or delivered amount will be reduced so that active insulin on board (IOB) will not exceed this safety limit."
+                    "Max IOB sets a safety limit on how much insulin Trio can automatically deliver above your scheduled basal rates. This prevents the system from giving too much insulin at once."
                 )
+
+                VStack(alignment: .leading, spacing: 0) {
+                    Text("Trio calculates your current Insulin On Board (IOB) from:")
+                    Text("• Boluses (including SMBs)")
+                    Text("• Temporary Basal Rates (TBRs)")
+                    Text("  ◦ A TBR higher than your scheduled rate will increase IOB")
+                    Text("  ◦ A TBR lower than your scheduled rate will decrease IOB")
+                }
+
                 Text(
-                    "Note: You can still manually bolus above this limit, but the suggested bolus amount will never exceed this in the bolus calculator."
+                    "If delivering more insulin would push your IOB above this limit, Trio will reduce or skip the dose to stay within the safety boundary. This applies to SMBs, TBRs, and the recommendation from the bolus calculator."
                 )
+
+                VStack(alignment: .leading, spacing: 0) {
+                    Text("What's NOT limited:")
+                    Text("• Manual boluses you enter yoursef")
+                    Text("• Manual temporary basal rates you set yourself")
+                }
             }
+            .fixedSize(horizontal: false, vertical: true)
         case .maxBolus:
             return VStack(alignment: .leading, spacing: 8) {
                 Text(

+ 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."

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

@@ -161,7 +161,6 @@ enum SettingItems {
                 "Insulin Peak Time",
                 "Skip Neutral Temps",
                 "Unsuspend If No Temp",
-                "Suspend Zeros IOB",
                 "SMB Delivery Ratio",
                 "SMB Interval",
                 "Min 5m Carbimpact",
@@ -180,8 +179,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"

+ 350 - 0
Trio/Sources/Modules/Stat/StatStateModel+Setup/GlucoseStatsSetup.swift

@@ -0,0 +1,350 @@
+import CoreData
+import Foundation
+
+/// A thread-safe value type to hold glucose data without Core Data dependencies
+struct GlucoseReading: Sendable {
+    let value: Int
+    let date: Date
+}
+
+/// Represents statistical data for daily glucose metrics by distribution ranges
+struct GlucoseDailyDistributionStats: Identifiable {
+    let id = UUID()
+    /// The date this data represents
+    let date: Date
+    /// The time-in-range type used for calculations
+    let timeInRangeType: TimeInRangeType
+    /// The original glucose readings
+    let readings: [GlucoseStored]
+    /// Percentage of glucose readings below 54 mg/dL
+    let veryLowPct: Double
+    /// Percentage of glucose readings in the [54 – lowLimit] mg/dL range
+    let lowPct: Double
+    /// Percentage of glucose readings within the tighter control range of [bottomThreshold – topThreshold] mg/dL
+    let inSmallRangePct: Double
+    /// Percentage of glucose readings within the target range of [bottomThreshold – highLimit] mg/dL
+    let inRangePct: Double
+    /// Percentage of glucose readings in the (highLimit – 250] mg/dL range
+    let highPct: Double
+    /// Percentage of glucose readings above 250 mg/dL
+    let veryHighPct: Double
+
+    init(
+        date: Date,
+        timeInRangeType: TimeInRangeType,
+        readings: [GlucoseStored] = [GlucoseStored](),
+        veryLowPct: Double = 0,
+        lowPct: Double = 0,
+        inSmallRangePct: Double = 0,
+        inRangePct: Double = 0,
+        highPct: Double = 0,
+        veryHighPct: Double = 0
+    ) {
+        self.date = date
+        self.timeInRangeType = timeInRangeType
+        self.readings = readings
+        self.veryLowPct = veryLowPct
+        self.lowPct = lowPct
+        self.inSmallRangePct = inSmallRangePct
+        self.inRangePct = inRangePct
+        self.highPct = highPct
+        self.veryHighPct = veryHighPct
+    }
+}
+
+/// Represents percentile-based statistical data for daily glucose metrics
+struct GlucoseDailyPercentileStats: Identifiable {
+    let id = UUID()
+    /// The date this data represents
+    let date: Date
+    /// The original glucose readings
+    let readings: [GlucoseStored]
+    /// Minimum glucose value
+    let minimum: Double
+    /// 10th percentile glucose value
+    let percentile10: Double
+    /// 25th percentile glucose value (lower quartile)
+    let percentile25: Double
+    /// Median (50th percentile) glucose value
+    let median: Double
+    /// 75th percentile glucose value (upper quartile)
+    let percentile75: Double
+    /// 90th percentile glucose value
+    let percentile90: Double
+    /// Maximum glucose value
+    let maximum: Double
+
+    init(
+        date: Date,
+        readings: [GlucoseStored] = [GlucoseStored](),
+        minimum: Double = 0,
+        percentile10: Double = 0,
+        percentile25: Double = 0,
+        median: Double = 0,
+        percentile75: Double = 0,
+        percentile90: Double = 0,
+        maximum: Double = 0
+    ) {
+        self.date = date
+        self.readings = readings
+        self.minimum = minimum
+        self.percentile10 = percentile10
+        self.percentile25 = percentile25
+        self.median = median
+        self.percentile75 = percentile75
+        self.percentile90 = percentile90
+        self.maximum = maximum
+    }
+}
+
+extension Stat.StateModel {
+    /// Performs setup for both percentile and distribution glucose statistics from provided IDs
+    ///
+    /// This method optimizes performance by:
+    /// 1. Computing both percentile and distribution statistics concurrently
+    /// 2. Creating lookup caches for both stat types simultaneously
+    ///
+    /// - Parameter ids: Array of NSManagedObjectIDs for glucose readings
+    func setupGlucoseStats(with ids: [NSManagedObjectID]) async {
+        // Get dates for the past 90 days
+        let dates = getDates()
+
+        // Calculate both types of statistics concurrently
+        async let percentileStats = calculateDailyPercentileStats(
+            for: dates,
+            glucoseIDs: ids
+        )
+
+        async let distributionStats = calculateDailyDistributionStats(
+            for: dates,
+            glucoseIDs: ids,
+            highLimit: highLimit,
+            timeInRangeType: timeInRangeType
+        )
+
+        let (pStats, dStats) = await (percentileStats, distributionStats)
+
+        dailyGlucosePercentileStats = pStats
+        glucosePercentileCache = Dictionary(
+            uniqueKeysWithValues: pStats.map {
+                (Calendar.current.startOfDay(for: $0.date), $0)
+            }
+        )
+
+        dailyGlucoseDistributionStats = dStats
+        glucoseDistributionCache = Dictionary(
+            uniqueKeysWithValues: dStats.map {
+                (Calendar.current.startOfDay(for: $0.date), $0)
+            }
+        )
+    }
+
+    /// Generates an array of dates for the specified number of days
+    /// - Parameter daysCount: Number of days to generate
+    /// - Returns: Array of dates starting from (today - daysCount) to today
+    func getDates() -> [Date] {
+        let calendar = Calendar.current
+        let today = calendar.startOfDay(for: Date())
+
+        return (0 ..< 90).map { dayOffset -> Date in
+            calendar.startOfDay(for: calendar.date(byAdding: .day, value: -(89 - dayOffset), to: today)!)
+        }
+    }
+
+    /// Processes glucose readings for a set of dates in a thread-safe manner
+    /// - Parameters:
+    ///   - dates: Array of dates to process data for
+    ///   - glucoseIDs: Array of NSManagedObjectIDs for glucose readings
+    /// - Returns: Array of (date, readings) tuples containing filtered readings for each date
+    private func processGlucoseReadingsForDates(
+        _ dates: [Date],
+        glucoseIDs: [NSManagedObjectID]
+    ) async -> [(date: Date, readings: [GlucoseReading])] {
+        let calendar = Calendar.current
+
+        // Handle cancellation early
+        if Task.isCancelled {
+            return []
+        }
+
+        // Extract the thread-safe glucose readings
+        let privateContext = CoreDataStack.shared.newTaskContext()
+
+        // Map into Sendable struct
+        let glucoseReadings: [GlucoseReading] = await privateContext.perform {
+            // Get NSManagedObject on private context and map into GlucoseReading struct
+            glucoseIDs.compactMap { id -> GlucoseReading? in
+                guard let reading = privateContext.object(with: id) as? GlucoseStored,
+                      let date = reading.date else { return nil }
+                return GlucoseReading(value: Int(reading.glucose), date: date)
+            }
+        }
+
+        return await withTaskGroup(of: (date: Date, readings: [GlucoseReading]).self) { group in
+            for date in dates {
+                group.addTask {
+                    let dayStart = calendar.startOfDay(for: date)
+                    let dayEnd = calendar.isDateInToday(date) ?
+                        Date.now :
+                        calendar.date(byAdding: .day, value: 1, to: dayStart)!
+
+                    let filteredReadings = glucoseReadings.filter {
+                        $0.date >= dayStart && $0.date < dayEnd
+                    }
+                    return (date: date, readings: filteredReadings)
+                }
+            }
+
+            // Collect results
+            var results: [(date: Date, readings: [GlucoseReading])] = []
+            for await result in group {
+                results.append(result)
+            }
+            return results.sorted { $0.date < $1.date }
+        }
+    }
+
+    /// Creates a GlucoseDailyDistributionStats object from thread-safe reading values
+    /// - Parameters:
+    ///   - date: Date for the day
+    ///   - readings: Array of thread-safe glucose readings
+    ///   - highLimit: Upper limit for target glucose range
+    ///   - timeInRangeType: The time-in-range type to use for calculations
+    /// - Returns: GlucoseDailyDistributionStats object with calculated statistics
+    private func createGlucoseDailyDistributionStatsFromReadings(
+        date: Date,
+        readings: [GlucoseReading],
+        highLimit: Decimal,
+        timeInRangeType: TimeInRangeType
+    ) -> GlucoseDailyDistributionStats {
+        let totalReadings = Double(readings.count)
+
+        // Count readings in each range
+        let veryHighReadings = readings.filter { $0.value > 250 }.count
+        let highReadings = readings.filter { $0.value > Int(highLimit) && $0.value <= 250 }.count
+        let inRangeReadings = readings.filter { $0.value >= timeInRangeType.bottomThreshold && $0.value <= Int(highLimit) }
+            .count
+        let inSmallRangeReadings = readings
+            .filter { $0.value >= timeInRangeType.bottomThreshold && $0.value <= timeInRangeType.topThreshold }.count
+        let lowReadings = readings.filter { $0.value < timeInRangeType.bottomThreshold && $0.value >= 54 }.count
+        let veryLowReadings = readings.filter { $0.value < 54 }.count
+
+        // Calculate percentages
+        let veryLowPct = totalReadings > 0 ? Double(veryLowReadings) / totalReadings * 100 : 0
+        let lowPct = totalReadings > 0 ? Double(lowReadings) / totalReadings * 100 : 0
+        let inSmallRangePct = totalReadings > 0 ? Double(inSmallRangeReadings) / totalReadings * 100 : 0
+        let inRangePct = totalReadings > 0 ? Double(inRangeReadings) / totalReadings * 100 : 0
+        let highPct = totalReadings > 0 ? Double(highReadings) / totalReadings * 100 : 0
+        let veryHighPct = totalReadings > 0 ? Double(veryHighReadings) / totalReadings * 100 : 0
+
+        // Create empty managed object array since we don't need the actual Core Data objects
+        let emptyStoredArray: [GlucoseStored] = []
+
+        return GlucoseDailyDistributionStats(
+            date: date,
+            timeInRangeType: timeInRangeType,
+            readings: emptyStoredArray,
+            veryLowPct: veryLowPct,
+            lowPct: lowPct,
+            inSmallRangePct: inSmallRangePct,
+            inRangePct: inRangePct,
+            highPct: highPct,
+            veryHighPct: veryHighPct
+        )
+    }
+
+    /// Creates a GlucoseDailyPercentileStats object from thread-safe reading values
+    /// - Parameters:
+    ///   - date: Date for the day
+    ///   - readings: Array of thread-safe glucose readings
+    /// - Returns: GlucoseDailyPercentileStats object with calculated statistics
+    private func createGlucoseDailyPercentileStatsFromReadings(
+        date: Date,
+        readings: [GlucoseReading]
+    ) -> GlucoseDailyPercentileStats {
+        let glucoseValues = readings.map { Double($0.value) }.sorted()
+
+        // If no data, return empty data
+        guard !glucoseValues.isEmpty else {
+            return GlucoseDailyPercentileStats(date: date)
+        }
+
+        let count = glucoseValues.count
+
+        let calculatePercentile = { (p: Double) -> Double in
+            let position = Double(count - 1) * p
+            let lower = Int(floor(position))
+            let upper = Int(ceil(position))
+
+            if lower == upper {
+                return glucoseValues[lower]
+            }
+
+            let weight = position - Double(lower)
+            return glucoseValues[lower] * (1 - weight) + glucoseValues[upper] * weight
+        }
+
+        // Calculate all percentiles concurrently
+        return GlucoseDailyPercentileStats(
+            date: date,
+            readings: [],
+            minimum: glucoseValues.first ?? 0,
+            percentile10: calculatePercentile(0.10),
+            percentile25: calculatePercentile(0.25),
+            median: calculatePercentile(0.5),
+            percentile75: calculatePercentile(0.75),
+            percentile90: calculatePercentile(0.90),
+            maximum: glucoseValues.last ?? 0
+        )
+    }
+
+    func calculateDailyDistributionStats(
+        for dates: [Date],
+        glucoseIDs: [NSManagedObjectID],
+        highLimit: Decimal,
+        timeInRangeType: TimeInRangeType
+    ) async -> [GlucoseDailyDistributionStats] {
+        // Process readings for each date
+        let processedData = await processGlucoseReadingsForDates(
+            dates,
+            glucoseIDs: glucoseIDs
+        )
+
+        // Transform into distribution stats
+        return processedData.map { date, readings in
+            if readings.isEmpty {
+                return GlucoseDailyDistributionStats(date: date, timeInRangeType: timeInRangeType)
+            } else {
+                return createGlucoseDailyDistributionStatsFromReadings(
+                    date: date,
+                    readings: readings,
+                    highLimit: highLimit,
+                    timeInRangeType: timeInRangeType
+                )
+            }
+        }
+    }
+
+    func calculateDailyPercentileStats(
+        for dates: [Date],
+        glucoseIDs: [NSManagedObjectID]
+    ) async -> [GlucoseDailyPercentileStats] {
+        // Process readings for each date
+        let processedData = await processGlucoseReadingsForDates(
+            dates,
+            glucoseIDs: glucoseIDs
+        )
+
+        // Transform into percentile stats
+        return processedData.map { date, readings in
+            if readings.isEmpty {
+                return GlucoseDailyPercentileStats(date: date)
+            } else {
+                return createGlucoseDailyPercentileStatsFromReadings(
+                    date: date,
+                    readings: readings
+                )
+            }
+        }
+    }
+}

+ 1 - 1
Trio/Sources/Modules/Stat/StatStateModel+Setup/LoopChartSetup.swift

@@ -39,7 +39,7 @@ enum LoopStatsDataType: String {
 
     var displayName: String {
         switch self {
-        case .successfulLoop: return String(localized: "Successful Loop")
+        case .successfulLoop: return String(localized: "Successful Loops")
         case .glucoseCount: return String(localized: "Glucose Count")
         }
     }

+ 51 - 5
Trio/Sources/Modules/Stat/StatStateModel.swift

@@ -37,6 +37,13 @@ extension Stat {
         var bolusAveragesCache: [Date: (manual: Double, smb: Double, external: Double)] = [:]
         var bolusTotalsCache: [(Date, total: Double)] = []
 
+        // Cache for Glucose Daily Stats
+        var dailyGlucosePercentileStats: [GlucoseDailyPercentileStats] = []
+        var glucosePercentileCache: [Date: GlucoseDailyPercentileStats] = [:]
+        var dailyGlucoseDistributionStats: [GlucoseDailyDistributionStats] = []
+        var glucoseDistributionCache: [Date: GlucoseDailyDistributionStats] = [:]
+        var glucoseReadings: [GlucoseStored] = []
+
         // Selected Duration for Glucose Stats
         var selectedIntervalForGlucoseStats: StatsTimeIntervalWithToday = .today {
             didSet {
@@ -58,7 +65,7 @@ extension Stat {
         }
 
         // Selected Glucose Chart Type
-        var selectedGlucoseChartType: GlucoseChartType = .percentile
+        var selectedGlucoseChartType: GlucoseChartType = .percentileByTime
 
         // Selected Insulin Chart Type
         var selectedInsulinChartType: InsulinChartType = .totalDailyDose
@@ -83,6 +90,7 @@ extension Stat {
             setupBolusStats()
             setupLoopStatRecords()
             setupMealStats()
+            setupGlucoseDailyStats()
             units = settingsManager.settings.units
             eA1cDisplayUnit = settingsManager.settings.eA1cDisplayUnit
             useFPUconversion = settingsManager.settings.useFPUconversion
@@ -91,9 +99,16 @@ extension Stat {
 
         func setupGlucoseArray(for interval: StatsTimeIntervalWithToday) {
             Task {
+                // Load data for current interval (existing code)
                 let ids = await fetchGlucose(for: interval)
                 await updateGlucoseArray(with: ids)
 
+                // Also ensure we have the full dataset loaded
+                if glucoseReadings.isEmpty {
+                    let allIds = await fetchGlucose(for: .total)
+                    await updateAllGlucoseArray(with: allIds)
+                }
+
                 // Calculate hourly stats and glucose range stats asynchronously with fetched glucose IDs
                 async let hourlyStats: () = calculateHourlyStatsForGlucoseAreaChart(from: ids)
                 async let glucoseRangeStats: () = calculateGlucoseRangeStatsForStackedChart(from: ids)
@@ -101,6 +116,16 @@ extension Stat {
             }
         }
 
+        func setupGlucoseDailyStats() {
+            Task {
+                // Get glucose IDs once (using the private fetchGlucose method)
+                let allIds = await fetchGlucose(for: .total)
+
+                // Pass the IDs to the implementation in GlucoseStatsSetup.swift
+                await setupGlucoseStats(with: allIds)
+            }
+        }
+
         private func fetchGlucose(for interval: StatsTimeIntervalWithToday) async -> [NSManagedObjectID] {
             do {
                 let predicate: NSPredicate
@@ -152,6 +177,19 @@ extension Stat {
                 )
             }
         }
+
+        @MainActor private func updateAllGlucoseArray(with IDs: [NSManagedObjectID]) {
+            do {
+                let glucoseObjects = try IDs.compactMap { id in
+                    try viewContext.existingObject(with: id) as? GlucoseStored
+                }
+                glucoseReadings = glucoseObjects
+            } catch {
+                debugPrint(
+                    "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the all glucose array: \(error.localizedDescription)"
+                )
+            }
+        }
     }
 
     @Observable final class UpdateTimer {
@@ -179,16 +217,24 @@ extension Stat.StateModel {
     /// Defines the available types of glucose charts
     enum GlucoseChartType: String, CaseIterable {
         /// Ambulatory Glucose Profile showing percentile ranges
-        case percentile = "Percentile"
+        case percentileByTime = "Percentile"
         /// Time-based distribution of glucose ranges
-        case distribution = "Distribution"
+        case distributionByTime = "Distribution"
+        /// Day-based box plot of glucose percentile ranges
+        case percentileByDay = "Percentile (by day)"
+        /// Day-based distribution of glucose ranges
+        case distributionByDay = "Distribution (by day)"
 
         var displayName: String {
             switch self {
-            case .percentile:
+            case .percentileByTime:
                 return String(localized: "Percentile")
-            case .distribution:
+            case .distributionByTime:
                 return String(localized: "Distribution")
+            case .percentileByDay:
+                return String(localized: "Percentile (by day)")
+            case .distributionByDay:
+                return String(localized: "Distribution (by day)")
             }
         }
     }

+ 43 - 9
Trio/Sources/Modules/Stat/View/StatChartUtils.swift

@@ -24,8 +24,29 @@ struct StatChartUtils {
         from scrollPosition: Date,
         for selectedInterval: Stat.StateModel.StatsTimeInterval
     ) -> (start: Date, end: Date) {
-        let end = scrollPosition.addingTimeInterval(visibleDomainLength(for: selectedInterval))
-        return (scrollPosition, end)
+        let calendar = Calendar.current
+
+        if selectedInterval == .day {
+            // For day view, don't modify the scroll position
+            let end = scrollPosition.addingTimeInterval(visibleDomainLength(for: selectedInterval) - 1)
+            return (scrollPosition, end)
+        } else {
+            // For week and longer intervals, we need smart alignment
+            // Find the nearest day boundary
+            let startOfDay = calendar.startOfDay(for: scrollPosition)
+            let components = calendar.dateComponents([.hour, .minute, .second], from: scrollPosition)
+            let totalSeconds = Double(components.hour ?? 0) * 3600 + Double(components.minute ?? 0) * 60 +
+                Double(components.second ?? 0)
+
+            // Align start end to midnight
+            let alignedStart = totalSeconds > 12 * 3600 ?
+                calendar.date(byAdding: .day, value: 1, to: startOfDay)! : startOfDay
+            let intervalLength = visibleDomainLength(for: selectedInterval)
+            let end = alignedStart.addingTimeInterval(intervalLength + (2 * 3600))
+            let alignedEnd = calendar.startOfDay(for: end).addingTimeInterval(-1)
+
+            return (alignedStart, alignedEnd)
+        }
     }
 
     /// Returns the appropriate date format style based on the selected time interval.
@@ -46,7 +67,9 @@ struct StatChartUtils {
     static func alignmentComponents(for selectedInterval: Stat.StateModel.StatsTimeInterval) -> DateComponents {
         switch selectedInterval {
         case .day: return DateComponents(hour: 0)
-        case .week: return DateComponents(weekday: 2)
+        case .week:
+            let calendar = Calendar.current
+            return DateComponents(weekday: calendar.firstWeekday)
         case .month,
              .total: return DateComponents(day: 1)
         }
@@ -58,14 +81,21 @@ struct StatChartUtils {
     static func getInitialScrollPosition(for selectedInterval: Stat.StateModel.StatsTimeInterval) -> Date {
         let calendar = Calendar.current
         let now = Date()
+        let today = calendar.startOfDay(for: now)
 
+        let baseDate: Date
         switch selectedInterval {
-//        case .day: return calendar.date(byAdding: .day, value: -1, to: now)!
-        case .day: return calendar.startOfDay(for: now)
-        case .week: return calendar.date(byAdding: .day, value: -7, to: now)!
-        case .month: return calendar.date(byAdding: .month, value: -1, to: now)!
-        case .total: return calendar.date(byAdding: .month, value: -3, to: now)!
+        case .day:
+            baseDate = today
+        case .week:
+            baseDate = calendar.date(byAdding: .day, value: -6, to: today)!
+        case .month:
+            baseDate = calendar.date(byAdding: .day, value: -29, to: today)!
+        case .total:
+            baseDate = calendar.date(byAdding: .day, value: -89, to: today)!
         }
+
+        return calendar.date(byAdding: .second, value: 1, to: baseDate)!
     }
 
     /// Checks if two dates belong to the same time unit based on the selected duration.
@@ -74,7 +104,11 @@ struct StatChartUtils {
     ///   - date2: The second date.
     ///   - selectedInterval: The selected time interval for statistics.
     /// - Returns: A Boolean indicating whether the two dates are in the same time unit.
-    static func isSameTimeUnit(_ date1: Date, _ date2: Date, for selectedInterval: Stat.StateModel.StatsTimeInterval) -> Bool {
+    static func isSameTimeUnit(
+        _ date1: Date,
+        _ date2: Date,
+        for selectedInterval: Stat.StateModel.StatsTimeInterval
+    ) -> Bool {
         let calendar = Calendar.current
         switch selectedInterval {
         case .day:

+ 80 - 20
Trio/Sources/Modules/Stat/View/StatRootView.swift

@@ -17,6 +17,12 @@ extension Stat {
 
         @State var state = StateModel()
         @State private var selectedView: StateModel.StatisticViewType = .glucose
+        @State private var isGlucoseDaySelected: Bool = false
+
+        private var intervalOptions: [Stat.StateModel.StatsTimeIntervalWithToday] {
+            state.selectedGlucoseChartType == .percentileByDay || state.selectedGlucoseChartType == .distributionByDay
+                ? [.week, .month, .total] : Stat.StateModel.StatsTimeIntervalWithToday.allCases
+        }
 
         var body: some View {
             VStack {
@@ -73,10 +79,18 @@ extension Stat {
                     }
                 }
                 .pickerStyle(.menu)
+                .onChange(of: state.selectedGlucoseChartType) { _, newValue in
+                    // If switching to daily chart and day/today is selected, switch to week
+                    if newValue == .percentileByDay || newValue == .distributionByDay,
+                       state.selectedIntervalForGlucoseStats == .day || state.selectedIntervalForGlucoseStats == .today
+                    {
+                        state.selectedIntervalForGlucoseStats = .week
+                    }
+                }
             }.padding(.horizontal)
 
             Picker("Duration", selection: $state.selectedIntervalForGlucoseStats) {
-                ForEach(StateModel.StatsTimeIntervalWithToday.allCases, id: \.self) { timeInterval in
+                ForEach(intervalOptions, id: \.self) { timeInterval in
                     Text(timeInterval.displayName)
                 }
             }
@@ -90,15 +104,28 @@ extension Stat {
                 )
             } else {
                 timeInRangeCard
-                glucoseStatsCard
+
+                if !isGlucoseDaySelected && state.selectedGlucoseChartType != .percentileByDay && state
+                    .selectedGlucoseChartType != .distributionByDay
+                {
+                    glucoseStatsCard
+                }
 
                 HStack {
                     var hintText: String {
                         switch state.selectedGlucoseChartType {
-                        case .percentile:
+                        case .percentileByTime:
                             String(localized: "Tap and hold the AGP graph or Time-in-Range ring to reveal more details.")
-                        case .distribution:
+                        case .distributionByTime:
                             String(localized: "Tap and hold the Time-in-Range ring to reveal more details.")
+                        case .percentileByDay:
+                            String(
+                                localized: "Tap a percentile or tap and hold a bar to reveal more details. Swipe to scroll through time."
+                            )
+                        case .distributionByDay:
+                            String(
+                                localized: "Tap and hold a bar in the chart to reveal more details. Swipe to scroll through time."
+                            )
                         }
                     }
                     Image(systemName: "hand.draw.fill")
@@ -115,18 +142,56 @@ extension Stat {
             StatCard {
                 VStack(spacing: Constants.spacing) {
                     switch state.selectedGlucoseChartType {
-                    case .percentile:
+                    case .distributionByDay,
+                         .percentileByDay:
+                        let interval: Stat.StateModel.StatsTimeInterval = {
+                            switch state.selectedIntervalForGlucoseStats {
+                            case .month,
+                                 .total:
+                                return Stat.StateModel.StatsTimeInterval(
+                                    rawValue: state.selectedIntervalForGlucoseStats.rawValue
+                                )!
+                            default:
+                                return .week
+                            }
+                        }()
+
+                        if state.selectedGlucoseChartType == .percentileByDay {
+                            GlucoseDailyPercentileChart(
+                                glucose: state.glucoseFromPersistence,
+                                highLimit: state.highLimit,
+                                units: state.units,
+                                timeInRangeType: state.timeInRangeType,
+                                selectedInterval: interval,
+                                isDaySelected: $isGlucoseDaySelected,
+                                state: state
+                            )
+                        } else { // if state.selectedGlucoseChartType == .distributionByDay
+                            GlucoseDailyDistributionChart(
+                                glucose: state.glucoseReadings,
+                                highLimit: state.highLimit,
+                                units: state.units,
+                                timeInRangeType: state.timeInRangeType,
+                                selectedInterval: interval,
+                                eA1cDisplayUnit: state.eA1cDisplayUnit,
+                                isDaySelected: $isGlucoseDaySelected,
+                                state: state
+                            )
+                        }
+
+                    case .percentileByTime:
                         GlucosePercentileChart(
                             glucose: state.glucoseFromPersistence,
                             highLimit: state.highLimit,
-                            lowLimit: state.lowLimit,
+                            timeInRangeType: state.timeInRangeType,
                             units: state.units,
                             hourlyStats: state.hourlyStats,
                             isToday: state.selectedIntervalForGlucoseStats == .today
                         )
-                    case .distribution:
+
+                    case .distributionByTime:
                         GlucoseDistributionChart(
-                            glucose: state.glucoseFromPersistence,
+                            glucose: state.glucoseReadings,
                             highLimit: state.highLimit,
                             lowLimit: state.lowLimit,
                             units: state.units,
@@ -145,7 +210,8 @@ extension Stat {
                         highLimit: state.highLimit,
                         units: state.units,
                         glucose: state.glucoseFromPersistence,
-                        timeInRangeType: state.timeInRangeType
+                        timeInRangeType: state.timeInRangeType,
+                        showChart: true
                     )
 
                     Divider()
@@ -263,19 +329,13 @@ extension Stat {
                         loopingChartView
                         loopStats
                     }
-                case .trioUpTime:
-                    // TODO: Trio Up-Time Chart
-                    ContentUnavailableView(
-                        String(localized: "Coming soon."),
-                        systemImage: "hourglass",
-                        description: Text("Trio Up-Time Chart")
-                    )
-                case .cgmConnectionTrace:
-                    // TODO: CGM Connection Trace Chart
+                case .cgmConnectionTrace,
+                     .trioUpTime:
+                    // TODO: Trio Up-Time Chart & CGM Connection Trace Chart
                     ContentUnavailableView(
                         String(localized: "Coming soon."),
                         systemImage: "hourglass",
-                        description: Text("CGM Connection Trace Chart")
+                        description: Text(state.selectedLoopingChartType.displayName)
                     )
                 }
             }
@@ -346,7 +406,7 @@ extension Stat {
                     ContentUnavailableView(
                         String(localized: "Coming soon."),
                         systemImage: "hourglass",
-                        description: Text("Meal to Hypoglycemia/Hyperglycemia Distribution Chart")
+                        description: Text(state.selectedMealChartType.displayName)
                     )
                 }
             }

+ 253 - 0
Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseDailyDistributionChart.swift

@@ -0,0 +1,253 @@
+import Charts
+import SwiftUI
+
+struct GlucoseDailyDistributionChart: View {
+    let glucose: [GlucoseStored]
+    let highLimit: Decimal
+    let units: GlucoseUnits
+    let timeInRangeType: TimeInRangeType
+    let selectedInterval: Stat.StateModel.StatsTimeInterval
+    let eA1cDisplayUnit: EstimatedA1cDisplayUnit
+
+    @Binding var isDaySelected: Bool
+
+    // Scrolling and selection states
+    @State private var scrollPosition = Date()
+    @State private var selectedDate: Date?
+    @State private var updateTimer = Stat.UpdateTimer()
+    @State private var visibleGlucose: [GlucoseStored] = []
+
+    // State model for accessing the shared data
+    let state: Stat.StateModel
+
+    // Computes the visible date range based on the current scroll position
+    @State private var visibleDateRange: (start: Date, end: Date) = (Date(), Date())
+
+    // Gets daily distribution stats for the visible date range
+    private var visibleDailyStats: [GlucoseDailyDistributionStats] {
+        let calendar = Calendar.current
+        return state.dailyGlucoseDistributionStats.filter { stat in
+            let statDate = calendar.startOfDay(for: stat.date)
+            return statDate >= calendar.startOfDay(for: visibleDateRange.start) &&
+                statDate <= calendar.startOfDay(for: visibleDateRange.end)
+        }
+    }
+
+    private func calculateVisibleDateRange() {
+        visibleDateRange = StatChartUtils.visibleDateRange(from: scrollPosition, for: selectedInterval)
+    }
+
+    // Gets selected day stats
+    private var selectedDateStats: GlucoseDailyDistributionStats? {
+        guard let selectedDate = selectedDate else { return nil }
+        let calendar = Calendar.current
+        let startOfSelectedDate = calendar.startOfDay(for: selectedDate)
+        return state.glucoseDistributionCache[startOfSelectedDate]
+    }
+
+    private func calculateVisibleGlucose() {
+        let calendar = Calendar.current
+        visibleGlucose = glucose.filter { reading in
+            guard let date = reading.date else { return false }
+            return date >= calendar.startOfDay(for: visibleDateRange.start) &&
+                date <= calendar.date(byAdding: .day, value: 1, to: calendar.startOfDay(for: visibleDateRange.end))!
+        }
+    }
+
+    // Compute selected day glucose readings
+    private var selectedDateGlucose: [GlucoseStored] {
+        guard let selectedDate = selectedDate else { return [] }
+        let calendar = Calendar.current
+        let dayStart = calendar.startOfDay(for: selectedDate)
+        let dayEnd = calendar.date(byAdding: .day, value: 1, to: dayStart) ?? dayStart
+
+        return glucose.filter { reading in
+            guard let date = reading.date else { return false }
+            return date >= dayStart && date < dayEnd
+        }
+    }
+
+    // Active glucose data - either selected day or visible range
+    private var activeGlucoseData: [GlucoseStored] {
+        selectedDate != nil ? selectedDateGlucose : visibleGlucose
+    }
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 8) {
+            chartView
+                .frame(height: 200)
+
+            // Date label with transition
+            Text(selectedDate.map { formattedDate(for: $0) } ?? StatChartUtils.formatVisibleDateRange(
+                from: visibleDateRange.start,
+                to: visibleDateRange.end,
+                for: selectedInterval
+            ))
+                .font(.subheadline)
+                .frame(maxWidth: .infinity, alignment: .center)
+                .padding(.top, 8)
+                .animation(.easeInOut, value: selectedDate)
+
+            // Single sector chart with data switching
+            GlucoseSectorChart(
+                highLimit: highLimit,
+                units: units,
+                glucose: activeGlucoseData,
+                timeInRangeType: timeInRangeType,
+                showChart: false
+            )
+            .animation(.easeInOut, value: selectedDate)
+
+            Divider().padding(.vertical, 4)
+
+            // Single metrics view with data switching
+            GlucoseMetricsView(
+                units: units,
+                eA1cDisplayUnit: eA1cDisplayUnit,
+                glucose: activeGlucoseData
+            )
+            .animation(.easeInOut, value: selectedDate)
+        }
+        .onAppear {
+            scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedInterval)
+            calculateVisibleDateRange()
+            calculateVisibleGlucose()
+        }
+        .onChange(of: scrollPosition) {
+            updateTimer.scheduleUpdate {
+                calculateVisibleDateRange()
+                calculateVisibleGlucose()
+            }
+        }
+        .onChange(of: selectedInterval) { _, _ in
+            selectedDate = nil
+            isDaySelected = false
+            scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedInterval)
+        }
+    }
+
+    /// Formatted date string for display
+    private func formattedDate(for date: Date) -> String {
+        let dateFormatter = DateFormatter()
+        dateFormatter.dateFormat = "EEEE, MMMM d, yyyy"
+        return dateFormatter.string(from: date)
+    }
+
+    /// The main chart visualization showing glucose distribution by day
+    private var chartView: some View {
+        Chart {
+            ForEach(state.dailyGlucoseDistributionStats) { day in
+                barMark(x: day, y: day.veryLowPct, rangeName: "veryLow")
+                barMark(x: day, y: day.lowPct, rangeName: "low")
+                barMark(x: day, y: day.inSmallRangePct, rangeName: "inSmallRange")
+                barMark(x: day, y: day.inRangePct - day.inSmallRangePct, rangeName: "inRange")
+                barMark(x: day, y: day.highPct, rangeName: "high")
+                barMark(x: day, y: day.veryHighPct, rangeName: "veryHigh")
+            }
+        }
+        .chartForegroundStyleScale([
+            legend("veryLow"): .purple,
+            legend("low"): .red,
+            legend("inSmallRange"): .green,
+            legend("inRange"): .darkGreen,
+            legend("high"): .loopYellow,
+            legend("veryHigh"): .orange
+        ])
+        .chartXSelection(value: $selectedDate.animation(.easeInOut))
+        .onChange(of: selectedDate) { _, newValue in
+            withAnimation(.easeInOut) {
+                isDaySelected = newValue != nil
+            }
+        }
+        .chartYScale(domain: 0 ... 100)
+        .chartXAxis {
+            AxisMarks(preset: .aligned, values: .stride(by: .day)) { value in
+                if let date = value.as(Date.self) {
+                    let calendar = Calendar.current
+
+                    switch selectedInterval {
+                    case .month:
+                        // Mark the first day of the week
+                        let weekday = calendar.component(.weekday, from: date)
+                        if weekday == calendar.firstWeekday {
+                            AxisValueLabel(format: .dateTime.day(), centered: true)
+                                .font(.footnote)
+                            AxisGridLine()
+                        }
+                    case .total:
+                        // Mark the start of the month
+                        let day = calendar.component(.day, from: date)
+                        if day == 1 {
+                            AxisValueLabel(format: .dateTime.month(.abbreviated), centered: true)
+                                .font(.footnote)
+                            AxisGridLine()
+                        }
+                    default:
+                        // Mark every day
+                        AxisValueLabel(format: .dateTime.weekday(.abbreviated), centered: true)
+                            .font(.footnote)
+                        AxisGridLine()
+                    }
+                }
+            }
+        }
+        .chartYAxis {
+            AxisMarks(position: .trailing, values: [4, 25, 50, 75, 100]) { value in
+                if let percentage = value.as(Double.self) {
+                    AxisValueLabel {
+                        Text((percentage / 100).formatted(.percent.precision(.fractionLength(0))))
+                            .font(.footnote)
+                    }
+                    AxisGridLine()
+                }
+            }
+        }
+        .chartYAxisLabel(alignment: .trailing) {
+            Text("Percentage")
+                .foregroundStyle(.primary)
+                .font(.footnote)
+                .padding(.vertical, 3)
+        }
+        .chartScrollableAxes(.horizontal)
+        .chartScrollPosition(x: $scrollPosition.animation(.easeInOut))
+        .chartScrollTargetBehavior(
+            .valueAligned(
+                matching: DateComponents(hour: 0),
+                majorAlignment: .matching(
+                    StatChartUtils.alignmentComponents(for: selectedInterval)
+                )
+            )
+        )
+        .chartXVisibleDomain(length: StatChartUtils.visibleDomainLength(for: selectedInterval))
+    }
+
+    /// Formats a short string with the glucose values of the requested range.
+    private func legend(_ rangeName: String) -> String {
+        switch rangeName {
+        case "veryLow":
+            return "<\(Decimal(54).formatted(for: units))"
+        case "low":
+            return "\(Decimal(54).formatted(for: units))-\(Decimal(timeInRangeType.bottomThreshold - 1).formatted(for: units))"
+        case "inSmallRange":
+            return "\(Decimal(timeInRangeType.bottomThreshold).formatted(for: units))-\(Decimal(timeInRangeType.topThreshold).formatted(for: units))"
+        case "inRange":
+            return "\(Decimal(timeInRangeType.topThreshold + 1).formatted(for: units))-\(highLimit.formatted(for: units))"
+        case "high":
+            return "\((highLimit + 1).formatted(for: units))-\(Decimal(250).formatted(for: units))"
+        case "veryHigh":
+            return ">\(Decimal(250).formatted(for: units))"
+        default:
+            return "error"
+        }
+    }
+
+    /// Creates a bar mark for the requested date and range
+    private func barMark(x: GlucoseDailyDistributionStats, y: Double, rangeName: String) -> some ChartContent {
+        BarMark(
+            x: .value("Date", x.date, unit: .day),
+            y: .value("Percentage", y)
+        )
+        .foregroundStyle(by: .value("Range", legend(rangeName)))
+        .opacity(selectedDate == nil || Calendar.current.isDate(selectedDate!, inSameDayAs: x.date) ? 1 : 0.3)
+    }
+}

+ 421 - 0
Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseDailyPercentileChart.swift

@@ -0,0 +1,421 @@
+import Charts
+import SwiftUI
+
+enum GlucosePercentileType: String, Identifiable {
+    case minimum = "Min"
+    case percentile10 = "10th"
+    case percentile25 = "25th"
+    case median = "Median"
+    case percentile75 = "75th"
+    case percentile90 = "90th"
+    case maximum = "Max"
+
+    var id: String { rawValue }
+
+    // Function to get the percentile value from a stats object
+    func getValue(from stats: GlucoseDailyPercentileStats) -> Double {
+        switch self {
+        case .minimum: return stats.minimum
+        case .percentile10: return stats.percentile10
+        case .percentile25: return stats.percentile25
+        case .median: return stats.median
+        case .percentile75: return stats.percentile75
+        case .percentile90: return stats.percentile90
+        case .maximum: return stats.maximum
+        }
+    }
+}
+
+struct GlucoseDailyPercentileChart: View {
+    let glucose: [GlucoseStored]
+    let highLimit: Decimal
+    let units: GlucoseUnits
+    let timeInRangeType: TimeInRangeType
+    let selectedInterval: Stat.StateModel.StatsTimeInterval
+
+    @Binding var isDaySelected: Bool
+
+    // Scrolling and selection states
+    @State private var scrollPosition = Date()
+    @State private var selectedDate: Date?
+    @State private var updateTimer = Stat.UpdateTimer()
+    @State private var visibleDailyStats: [GlucoseDailyPercentileStats] = []
+
+    // State for selected percentile
+    @State private var selectedPercentile: GlucosePercentileType?
+
+    // State model for accessing the shared calculations
+    let state: Stat.StateModel
+
+    // Computes the visible date range based on the current scroll position
+    @State private var visibleDateRange: (start: Date, end: Date) = (Date(), Date())
+
+    private func calculateVisibleDailyStats() {
+        let calendar = Calendar.current
+        visibleDailyStats = state.dailyGlucosePercentileStats.filter { stat in
+            let statDate = calendar.startOfDay(for: stat.date)
+            return statDate >= calendar.startOfDay(for: visibleDateRange.start) &&
+                statDate <= calendar.startOfDay(for: visibleDateRange.end)
+        }
+    }
+
+    private func calculateVisibleDateRange() {
+        visibleDateRange = StatChartUtils.visibleDateRange(from: scrollPosition, for: selectedInterval)
+    }
+
+    // Gets selected day stats
+    private var selectedDateStats: GlucoseDailyPercentileStats? {
+        selectedDate.flatMap { day in
+            state.glucosePercentileCache[Calendar.current.startOfDay(for: day)]
+        }
+    }
+
+    // Aggregates data from all visible days
+    private var aggregatedVisibleStats: GlucoseDailyPercentileStats? {
+        guard !visibleDailyStats.isEmpty else { return nil }
+
+        // Collect all glucose values from visible days
+        var allMinimums: [Double] = []
+        var allMaximums: [Double] = []
+        var all10thPercentiles: [Double] = []
+        var all25thPercentiles: [Double] = []
+        var allMedians: [Double] = []
+        var all75thPercentiles: [Double] = []
+        var all90thPercentiles: [Double] = []
+
+        // Collect data from all visible days
+        for stats in visibleDailyStats where stats.median > 0 {
+            allMinimums.append(stats.minimum)
+            allMaximums.append(stats.maximum)
+            all10thPercentiles.append(stats.percentile10)
+            all25thPercentiles.append(stats.percentile25)
+            allMedians.append(stats.median)
+            all75thPercentiles.append(stats.percentile75)
+            all90thPercentiles.append(stats.percentile90)
+        }
+
+        // Calculate aggregated values
+        let aggMinimum = allMinimums.min() ?? 0
+        let aggMaximum = allMaximums.max() ?? 0
+        let aggP10 = StatChartUtils.medianCalculationDouble(array: all10thPercentiles)
+        let aggP25 = StatChartUtils.medianCalculationDouble(array: all25thPercentiles)
+        let aggMedian = StatChartUtils.medianCalculationDouble(array: allMedians)
+        let aggP75 = StatChartUtils.medianCalculationDouble(array: all75thPercentiles)
+        let aggP90 = StatChartUtils.medianCalculationDouble(array: all90thPercentiles)
+
+        // Create a new stats object with the visible date range and aggregated values
+        return GlucoseDailyPercentileStats(
+            date: visibleDateRange.start,
+            readings: [], // Empty array since this is aggregated data
+            minimum: aggMinimum,
+            percentile10: aggP10,
+            percentile25: aggP25,
+            median: aggMedian,
+            percentile75: aggP75,
+            percentile90: aggP90,
+            maximum: aggMaximum
+        )
+    }
+
+    // Format a single date for display
+    private func formatDate(_ date: Date) -> String {
+        date.formatted(.dateTime.weekday(.wide).month(.wide).day().year())
+    }
+
+    // Get the appropriate detail view data
+    private var detailViewData: (data: GlucoseDailyPercentileStats, dateText: String)? {
+        if let selectedData = selectedDateStats {
+            // Case 1: Selected specific day
+            return (selectedData, selectedData.date.formatted(.dateTime.weekday(.wide).month(.wide).day().year()))
+        } else if let aggregatedData = aggregatedVisibleStats {
+            // Case 2: Using aggregated data
+            return (aggregatedData, StatChartUtils.formatVisibleDateRange(
+                from: visibleDateRange.start,
+                to: visibleDateRange.end,
+                for: selectedInterval
+            ))
+        }
+        return nil
+    }
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 8) {
+            boxplotChart
+                .frame(height: 300)
+
+            // Display detail view if we have data
+            if let viewData = detailViewData {
+                GlucoseDailyPercentileDetailView(
+                    dayData: viewData.data,
+                    units: units,
+                    dateRangeText: viewData.dateText,
+                    selectedPercentile: $selectedPercentile
+                )
+                .padding(.top, 4)
+            }
+        }
+        .onAppear {
+            calculateVisibleDateRange()
+            scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedInterval)
+            calculateVisibleDailyStats()
+        }
+        .onChange(of: scrollPosition) {
+            updateTimer.scheduleUpdate {
+                calculateVisibleDateRange()
+                calculateVisibleDailyStats()
+            }
+        }
+        .onChange(of: selectedInterval) { _, _ in
+            selectedDate = nil
+            selectedPercentile = nil
+            isDaySelected = false
+            scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedInterval)
+        }
+    }
+
+    // Simple boxplot chart with improved visuals - broken down into components
+    private var boxplotChart: some View {
+        Chart {
+            // First draw all the non-interactive elements
+            ForEach(state.dailyGlucosePercentileStats) { day in
+                if day.maximum > 0 { // Check if we have valid data
+                    // Add background components for each day
+                    spacerBarMark(for: day)
+                    percentileBarMark(
+                        for: day,
+                        startValue: day.minimum.asUnit(units),
+                        endValue: day.percentile10.asUnit(units),
+                        rangeName: "0-100%"
+                    )
+                    percentileBarMark(
+                        for: day,
+                        startValue: day.percentile10.asUnit(units),
+                        endValue: day.percentile25.asUnit(units),
+                        rangeName: "10-90%"
+                    )
+                    percentileBarMark(
+                        for: day,
+                        startValue: day.percentile25.asUnit(units),
+                        endValue: day.percentile75.asUnit(units),
+                        rangeName: "25-75%"
+                    )
+                    percentileBarMark(
+                        for: day,
+                        startValue: day.percentile75.asUnit(units),
+                        endValue: day.percentile90.asUnit(units),
+                        rangeName: "10-90%"
+                    )
+                    percentileBarMark(
+                        for: day,
+                        startValue: day.percentile90.asUnit(units),
+                        endValue: day.maximum.asUnit(units),
+                        rangeName: "0-100%"
+                    )
+                }
+            }
+
+            // Draw median marks - these should appear above the percentile bars but below the selected percentile
+            ForEach(state.dailyGlucosePercentileStats) { day in
+                if day.maximum > 0 {
+                    medianMark(for: day)
+                }
+            }
+
+            // Draw the selected percentile elements LAST so they're on top
+            if let selectedPercentile = selectedPercentile {
+                ForEach(state.dailyGlucosePercentileStats) { day in
+                    if day.maximum > 0 {
+                        // Line connecting points
+                        LineMark(
+                            x: .value("SelectedDate", day.date, unit: .day),
+                            y: .value("SelectedValue", selectedPercentile.getValue(from: day).asUnit(units))
+                        )
+                        .foregroundStyle(Color.purple)
+                        .lineStyle(StrokeStyle(lineWidth: selectedInterval == .total ? 1 : 2))
+                        .zIndex(200) // Set very high z-index
+
+                        // Point marks
+                        PointMark(
+                            x: .value("SelectedDate", day.date, unit: .day),
+                            y: .value("SelectedValue", selectedPercentile.getValue(from: day).asUnit(units))
+                        )
+                        .symbolSize(selectedInterval == .total ? 10 : 30)
+                        .foregroundStyle(Color.purple)
+                        .zIndex(300) // Even higher z-index for points
+                    }
+                }
+            }
+
+            // Threshold lines
+            RuleMark(
+                y: .value("Low Limit", Double(timeInRangeType.bottomThreshold).asUnit(units))
+            )
+            .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 5]))
+            .foregroundStyle(by: .value("Range", "\(timeInRangeType.bottomThreshold.formatted(withUnits: units))"))
+            .zIndex(100)
+
+            RuleMark(
+                y: .value("Mid Limit", Double(timeInRangeType.topThreshold).asUnit(units))
+            )
+            .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 5]))
+            .foregroundStyle(by: .value("Range", "\(timeInRangeType.topThreshold.formatted(withUnits: units))"))
+            .zIndex(100)
+
+            RuleMark(
+                y: .value("High Limit", Double(highLimit.asUnit(units)))
+            )
+            .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 5]))
+            .foregroundStyle(by: .value("Range", "\(highLimit.formatted(withUnits: units))"))
+            .zIndex(100)
+        }
+        .chartYAxis {
+            AxisMarks(values: .automatic) { value in
+                AxisGridLine()
+                AxisTick()
+                AxisValueLabel {
+                    if let glucoseValue = value.as(Double.self) {
+                        Text(
+                            units == .mmolL ?
+                                glucoseValue.formatted(.number.precision(.fractionLength(1))) :
+                                glucoseValue.formatted(.number.precision(.fractionLength(0)))
+                        )
+                        .font(.caption)
+                    }
+                }
+            }
+        }
+        .chartXAxis {
+            AxisMarks(preset: .aligned, values: .stride(by: .day)) { value in
+                if let date = value.as(Date.self) {
+                    let calendar = Calendar.current
+
+                    switch selectedInterval {
+                    case .month:
+                        // Mark the first day of the week
+                        let weekday = calendar.component(.weekday, from: date)
+                        if weekday == calendar.firstWeekday {
+                            AxisValueLabel(format: .dateTime.day(), centered: true)
+                                .font(.footnote)
+                            AxisGridLine()
+                        }
+                    case .total:
+                        // Mark the start of the month
+                        let day = calendar.component(.day, from: date)
+                        if day == 1 {
+                            AxisValueLabel(format: .dateTime.month(.abbreviated), centered: true)
+                                .font(.footnote)
+                            AxisGridLine()
+                        }
+                    default:
+                        // Mark every day
+                        AxisValueLabel(format: .dateTime.weekday(.abbreviated), centered: true)
+                            .font(.footnote)
+                        AxisGridLine()
+                    }
+                }
+            }
+        }
+        .chartYScale(domain: glucoseYScaleDomain())
+        .chartXSelection(value: $selectedDate.animation(.easeInOut))
+        .onChange(of: selectedDate) { _, newValue in
+            isDaySelected = newValue != nil
+            // Clear percentile selection when a day is selected
+            if newValue != nil {
+                selectedPercentile = nil
+            }
+        }
+        .chartForegroundStyleScale([
+            "0-100%": .blue.opacity(0.15),
+            "10-90%": .blue.opacity(0.3),
+            "25-75%": .blue.opacity(0.5),
+            "Median": .blue,
+            "\(timeInRangeType.bottomThreshold.formatted(withUnits: units))": .red,
+            "\(timeInRangeType.topThreshold.formatted(withUnits: units))": .mint,
+            "\(highLimit.formatted(withUnits: units))": .orange
+        ])
+        .chartScrollableAxes(.horizontal)
+        .chartScrollPosition(x: $scrollPosition)
+        .chartScrollTargetBehavior(
+            .valueAligned(
+                matching: DateComponents(hour: 0),
+                majorAlignment: .matching(
+                    StatChartUtils.alignmentComponents(for: selectedInterval)
+                )
+            )
+        )
+        .chartXVisibleDomain(length: StatChartUtils.visibleDomainLength(for: selectedInterval))
+    }
+
+    // MARK: - Chart Components
+
+    private func percentileBarMark(
+        for day: GlucoseDailyPercentileStats,
+        startValue: Double,
+        endValue: Double,
+        rangeName: String
+    ) -> some ChartContent {
+        BarMark(
+            x: .value("Day", day.date, unit: .day),
+            y: .value("Percentage", endValue - startValue)
+        )
+        .foregroundStyle(by: .value("Range", rangeName))
+        .opacity(getOpacity(for: day))
+    }
+
+    // Median mark - a horizontal line at the median point
+    private func medianMark(for day: GlucoseDailyPercentileStats) -> some ChartContent {
+        let baseDate = Calendar.current.startOfDay(for: day.date)
+        let startOffset = Int(0.15 * 24 * 60) // 15% of minutes in a day
+        let endOffset = Int(0.85 * 24 * 60) // 85% of minutes in a day
+
+        return RuleMark(
+            xStart: .value("DayStart", Calendar.current.date(byAdding: .minute, value: startOffset, to: baseDate)!),
+            xEnd: .value("DayEnd", Calendar.current.date(byAdding: .minute, value: endOffset, to: baseDate)!),
+            y: .value("Median", day.median.asUnit(units))
+        )
+        .lineStyle(StrokeStyle(lineWidth: 2))
+        .foregroundStyle(by: .value("Range", "Median"))
+        .opacity(getOpacity(for: day))
+    }
+
+    // Helper function to determine opacity based on selections
+    private func getOpacity(for day: GlucoseDailyPercentileStats) -> Double {
+        selectedDate.map { date in
+            StatChartUtils.isSameTimeUnit(day.date, date, for: .total) ? 1 : 0.3
+        } ?? 1
+    }
+
+    // Spacer box for each day
+    private func spacerBarMark(for day: GlucoseDailyPercentileStats) -> some ChartContent {
+        BarMark(
+            x: .value("Day", day.date, unit: .day),
+            y: .value("Percentage", day.minimum.asUnit(units))
+        )
+        .foregroundStyle(Color.clear)
+    }
+
+    // Calculate an appropriate Y axis domain for the chart
+    private func glucoseYScaleDomain() -> ClosedRange<Double> {
+        // Find actual min/max from data
+        if visibleDailyStats.isEmpty {
+            return 0 ... (units == .mgdL ? 250 : 14.0)
+        }
+
+        var allValues: [Double] = []
+        for day in visibleDailyStats where day.minimum > 0 {
+            allValues.append(day.minimum.asUnit(units))
+            allValues.append(day.maximum.asUnit(units))
+        }
+
+        guard !allValues.isEmpty else {
+            return 0 ... (units == .mgdL ? 250 : 14.0)
+        }
+
+        let minValue = allValues.min() ?? 0
+        let maxValue = allValues.max() ?? (units == .mgdL ? 250 : 14.0)
+
+        // Add some padding
+        let padding = units == .mgdL ? 20.0 : 1.0
+        return max(0, minValue - padding) ... maxValue + padding
+    }
+}

+ 14 - 14
Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseDistributionChart.swift

@@ -25,38 +25,38 @@ struct GlucoseDistributionChart: View {
                 }
             }
             .chartForegroundStyleScale([
-                "<54": .purple.opacity(0.7),
-                "54-\(timeInRangeType.bottomThreshold)": .red.opacity(0.7),
-                "\(timeInRangeType.bottomThreshold)-\(timeInRangeType.topThreshold)": .green,
-                "\(timeInRangeType.topThreshold)-180": .green.opacity(0.7),
-                "180-200": .yellow.opacity(0.7),
-                "200-220": .orange.opacity(0.7),
-                ">220": .orange.opacity(0.8)
+                "<54": .purple.opacity(0.8),
+                "54-\(timeInRangeType.bottomThreshold)": .red.opacity(0.8),
+                "\(timeInRangeType.bottomThreshold)-\(timeInRangeType.topThreshold)": .green.opacity(0.8),
+                "\(timeInRangeType.topThreshold)-180": .darkGreen.opacity(0.8),
+                "180-200": .yellow.opacity(0.8),
+                "200-220": .orange.opacity(0.8),
+                ">220": .darkOrange.opacity(0.8)
             ])
             .chartLegend(position: .bottom, alignment: .leading, spacing: 12) {
                 let legendItems: [(String, Color)] = [
-                    ("<\(units == .mgdL ? Decimal(54) : 54.asMmolL)", .purple.opacity(0.7)),
+                    ("<\(units == .mgdL ? Decimal(54) : 54.asMmolL)", .purple.opacity(0.8)),
                     (
                         "\(units == .mgdL ? Decimal(54) : 54.asMmolL)-\(units == .mgdL ? Decimal(timeInRangeType.bottomThreshold) : timeInRangeType.bottomThreshold.asMmolL)",
-                        .red.opacity(0.7)
+                        .red.opacity(0.8)
                     ),
                     (
                         "\(units == .mgdL ? Decimal(timeInRangeType.bottomThreshold) : timeInRangeType.bottomThreshold.asMmolL)-\(units == .mgdL ? Decimal(timeInRangeType.topThreshold) : timeInRangeType.topThreshold.asMmolL)",
-                        .green
+                        .green.opacity(0.8)
                     ),
                     (
                         "\(units == .mgdL ? Decimal(timeInRangeType.topThreshold) : timeInRangeType.topThreshold.asMmolL)-\(units == .mgdL ? Decimal(180) : 180.asMmolL)",
-                        .green.opacity(0.7)
+                        .darkGreen.opacity(0.8)
                     ),
                     (
                         "\(units == .mgdL ? Decimal(180) : 180.asMmolL)-\(units == .mgdL ? Decimal(200) : 200.asMmolL)",
-                        .yellow.opacity(0.7)
+                        .yellow.opacity(0.8)
                     ),
                     (
                         "\(units == .mgdL ? Decimal(200) : 200.asMmolL)-\(units == .mgdL ? Decimal(220) : 220.asMmolL)",
-                        .orange.opacity(0.7)
+                        .orange.opacity(0.8)
                     ),
-                    (">\(units == .mgdL ? Decimal(220) : 220.asMmolL)", .orange.opacity(0.8))
+                    (">\(units == .mgdL ? Decimal(220) : 220.asMmolL)", .darkOrange.opacity(0.8))
                 ]
 
                 let columns = [GridItem(.adaptive(minimum: 65), spacing: 4)]

+ 32 - 32
Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucosePercentileChart.swift

@@ -11,8 +11,8 @@ struct GlucosePercentileChart: View {
     let glucose: [GlucoseStored]
     /// The upper glucose limit for the chart.
     let highLimit: Decimal
-    /// The lower glucose limit for the chart.
-    let lowLimit: Decimal
+    /// TITR or TING
+    let timeInRangeType: TimeInRangeType
     /// The units used for glucose measurement (mg/dL or mmol/L).
     let units: GlucoseUnits
     /// The hourly glucose statistics.
@@ -47,28 +47,28 @@ struct GlucosePercentileChart: View {
                     // 10-90 percentile area
                     AreaMark(
                         x: .value("Hour", Calendar.current.dateForChartHour(stats.hour)),
-                        yStart: .value("10th Percentile", stats.percentile10),
-                        yEnd: .value("90th Percentile", stats.percentile90),
+                        yStart: .value("10th Percentile", stats.percentile10.asUnit(units)),
+                        yEnd: .value("90th Percentile", stats.percentile90.asUnit(units)),
                         series: .value("10-90", "10-90")
                     )
-                    .foregroundStyle(by: .value("Series", "10-90"))
+                    .foregroundStyle(by: .value("Series", "10-90%"))
                     .opacity(stats.median > 0 ? 0.3 : 0)
 
                     // 25-75 percentile area
                     AreaMark(
                         x: .value("Hour", Calendar.current.dateForChartHour(stats.hour)),
-                        yStart: .value("25th Percentile", stats.percentile25),
-                        yEnd: .value("75th Percentile", stats.percentile75),
+                        yStart: .value("25th Percentile", stats.percentile25.asUnit(units)),
+                        yEnd: .value("75th Percentile", stats.percentile75.asUnit(units)),
                         series: .value("25-75", "25-75")
                     )
-                    .foregroundStyle(by: .value("Series", "25-75"))
+                    .foregroundStyle(by: .value("Series", "25-75%"))
                     .opacity(stats.median > 0 ? 0.5 : 0)
 
                     // Median line
                     if stats.median > 0 {
                         LineMark(
                             x: .value("Hour", Calendar.current.dateForChartHour(stats.hour)),
-                            y: .value("Median", stats.median),
+                            y: .value("Median", stats.median.asUnit(units)),
                             series: .value("Median", "Median")
                         )
                         .lineStyle(StrokeStyle(lineWidth: 2))
@@ -77,13 +77,17 @@ struct GlucosePercentileChart: View {
                 }
 
                 // High/Low limit lines
-                RuleMark(y: .value("High Limit", Double(highLimit)))
+                RuleMark(y: .value("Low Limit", Double(timeInRangeType.bottomThreshold).asUnit(units)))
                     .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 5]))
-                    .foregroundStyle(by: .value("Series", "High"))
+                    .foregroundStyle(by: .value("Series", "\(timeInRangeType.bottomThreshold.formatted(withUnits: units))"))
 
-                RuleMark(y: .value("Low Limit", Double(lowLimit)))
+                RuleMark(y: .value("Mid Limit", Double(timeInRangeType.topThreshold).asUnit(units)))
                     .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 5]))
-                    .foregroundStyle(by: .value("Series", "Low"))
+                    .foregroundStyle(by: .value("Series", "\(timeInRangeType.topThreshold.formatted(withUnits: units))"))
+
+                RuleMark(y: .value("High Limit", Double(highLimit.asUnit(units))))
+                    .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 5]))
+                    .foregroundStyle(by: .value("Series", "\(highLimit.formatted(withUnits: units))"))
 
                 if let selectedStats, let selection {
                     RuleMark(x: .value("Selection", selection))
@@ -102,19 +106,21 @@ struct GlucosePercentileChart: View {
                 }
             }
             .chartForegroundStyleScale([
-                "10-90": Color.blue.opacity(0.3),
-                "25-75": Color.blue.opacity(0.5),
+                "10-90%": Color.blue.opacity(0.3),
+                "25-75%": Color.blue.opacity(0.5),
                 "Median": Color.blue,
-                "High": Color.orange,
-                "Low": Color.red
+                "\(timeInRangeType.bottomThreshold.formatted(withUnits: units))": Color.red,
+                "\(timeInRangeType.topThreshold.formatted(withUnits: units))": Color.mint,
+                "\(highLimit.formatted(withUnits: units))": Color.orange
             ])
             .chartLegend(position: .bottom, alignment: .leading, spacing: 12) {
                 let legendItems: [(String, Color)] = [
                     ("10-90%", Color.blue.opacity(0.3)),
-                    ("20-75%", Color.blue.opacity(0.5)),
+                    ("25-75%", Color.blue.opacity(0.5)),
                     (String(localized: "Median"), Color.blue),
-                    (String(localized: "High Threshold"), Color.orange),
-                    (String(localized: "Low Threshold"), Color.red)
+                    (String(localized: "\(timeInRangeType.bottomThreshold.formatted(withUnits: units))"), Color.red),
+                    (String(localized: "\(timeInRangeType.topThreshold.formatted(withUnits: units))"), Color.mint),
+                    (String(localized: "\(highLimit.formatted(withUnits: units))"), Color.orange)
                 ]
 
                 let columns = [GridItem(.adaptive(minimum: 100), spacing: 4)]
@@ -130,7 +136,7 @@ struct GlucosePercentileChart: View {
                     if let glucose = value.as(Double.self) {
                         AxisValueLabel {
                             Text(
-                                units == .mmolL ? glucose.asMmolL.formatted(.number.precision(.fractionLength(0))) : glucose
+                                units == .mmolL ? glucose.formatted(.number.precision(.fractionLength(1))) : glucose
                                     .formatted(.number.precision(.fractionLength(0)))
                             )
                             .font(.footnote)
@@ -183,12 +189,6 @@ struct AGPSelectionPopover: View {
         }
     }
 
-    /// A helper function to format glucose values based on the selected unit.
-    private func formattedGlucoseValue(_ value: Double) -> String {
-        units == .mmolL ? value.formattedAsMmolL :
-            value.formatted()
-    }
-
     var body: some View {
         VStack(alignment: .leading, spacing: 4) {
             Text(timeText).bold().font(.subheadline)
@@ -196,27 +196,27 @@ struct AGPSelectionPopover: View {
             Grid(alignment: .leading, horizontalSpacing: 8, verticalSpacing: 4) {
                 GridRow {
                     Text("Median:").bold()
-                    Text(formattedGlucoseValue(stats.median))
+                    Text(stats.median.formatted(for: units))
                     Text(units.rawValue).foregroundStyle(.secondary)
                 }
                 GridRow {
                     Text("90%:").bold()
-                    Text(formattedGlucoseValue(stats.percentile90))
+                    Text(stats.percentile90.formatted(for: units))
                     Text(units.rawValue).foregroundStyle(.secondary)
                 }
                 GridRow {
                     Text("75%:").bold()
-                    Text(formattedGlucoseValue(stats.percentile75))
+                    Text(stats.percentile75.formatted(for: units))
                     Text(units.rawValue).foregroundStyle(.secondary)
                 }
                 GridRow {
                     Text("25%:").bold()
-                    Text(formattedGlucoseValue(stats.percentile25))
+                    Text(stats.percentile25.formatted(for: units))
                     Text(units.rawValue).foregroundStyle(.secondary)
                 }
                 GridRow {
                     Text("10%:").bold()
-                    Text(formattedGlucoseValue(stats.percentile10))
+                    Text(stats.percentile10.formatted(for: units))
                     Text(units.rawValue).foregroundStyle(.secondary)
                 }
             }.font(.headline)

+ 71 - 0
Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucosePercentileDetailView.swift

@@ -0,0 +1,71 @@
+import SwiftUI
+
+struct GlucoseDailyPercentileDetailView: View {
+    let dayData: GlucoseDailyPercentileStats
+    let units: GlucoseUnits
+    let dateRangeText: String
+
+    // Binding to the parent's selectedPercentile
+    @Binding var selectedPercentile: GlucosePercentileType?
+
+    var body: some View {
+        VStack(alignment: .center, spacing: 8) {
+            Text(dateRangeText)
+                .font(.subheadline.weight(.medium))
+                .padding(.bottom, 4)
+
+            // Only show percentile details if we have valid data
+            if dayData.median > 0 {
+                // Improved percentile display
+                HStack(spacing: 0) {
+                    percentileItem(label: "Min", value: round(dayData.minimum), type: .minimum)
+                    percentileItem(label: "10%", value: round(dayData.percentile10), type: .percentile10)
+                    percentileItem(label: "25%", value: round(dayData.percentile25), type: .percentile25)
+                    percentileItem(label: "Median", value: round(dayData.median), type: .median)
+                    percentileItem(label: "75%", value: round(dayData.percentile75), type: .percentile75)
+                    percentileItem(label: "90%", value: round(dayData.percentile90), type: .percentile90)
+                    percentileItem(label: "Max", value: round(dayData.maximum), type: .maximum)
+                }
+                .padding(.vertical, 8)
+            } else {
+                Text("No glucose data available for this day")
+                    .foregroundStyle(.secondary)
+                    .padding()
+            }
+        }
+    }
+
+    /// Creates a single percentile item for the detail view
+    private func percentileItem(
+        label: String,
+        value: Double,
+        type: GlucosePercentileType
+    ) -> some View {
+        VStack(spacing: 2) {
+            Text(Decimal(value).formatted(for: units))
+                .font(.callout.monospacedDigit())
+                .foregroundStyle(type == selectedPercentile ? Color.purple : .primary)
+
+            Text(label)
+                .font(.caption2)
+                .foregroundStyle(type == selectedPercentile ? Color.purple : .secondary)
+        }
+        .frame(maxWidth: .infinity)
+        .padding(4)
+        .background(
+            RoundedRectangle(cornerRadius: 4)
+                .fill(type == selectedPercentile ? Color.purple.opacity(0.1) : Color.clear)
+                .overlay(
+                    RoundedRectangle(cornerRadius: 4)
+                        .strokeBorder(type == selectedPercentile ? Color.purple : Color.clear, lineWidth: 1)
+                )
+        )
+        .contentShape(Rectangle())
+        .onTapGesture {
+            withAnimation {
+                // Toggle selection on tap
+                selectedPercentile = (selectedPercentile == type) ? nil : type
+            }
+        }
+    }
+}

+ 189 - 121
Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseSectorChart.swift

@@ -8,6 +8,7 @@ struct GlucoseSectorChart: View {
     let units: GlucoseUnits
     let glucose: [GlucoseStored]
     let timeInRangeType: TimeInRangeType
+    let showChart: Bool
 
     @State private var selectedCount: Int?
     @State private var selectedRange: GlucoseRange?
@@ -23,119 +24,181 @@ struct GlucoseSectorChart: View {
     }
 
     var body: some View {
-        HStack(alignment: .center, spacing: 20) {
-            // Calculate total number of glucose readings
-            let total = Decimal(glucose.count)
-            // Count readings greater than high limit (180 mg/dL)
-            let high = glucose.filter { $0.glucose > Int(highLimit) }.count
-            // Count readings between low limit (TITR: 70 mg/dL, TING 63 mg/dL) and 140 mg/dL (tight control)
-            let tight = glucose
-                .filter { $0.glucose >= timeInRangeType.bottomThreshold && $0.glucose <= timeInRangeType.topThreshold }.count
-            // Count readings between 140 and high limit (normal range)
-            let normal = glucose.filter { $0.glucose >= timeInRangeType.bottomThreshold && $0.glucose <= Int(highLimit) }.count
-            // Count readings less than low limit (low)
-            let low = glucose.filter { $0.glucose < timeInRangeType.bottomThreshold }.count
-
-            let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
-            let sumReadings = justGlucoseArray.reduce(0, +)
-
-            let glucoseAverage = Decimal(sumReadings) / total
-            let medianGlucose = StatChartUtils.medianCalculation(array: justGlucoseArray)
-
-            let lowPercentage = Decimal(low) / total * 100
-            let tightPercentage = Decimal(tight) / total * 100
-            let inRangePercentage = Decimal(normal) / total * 100
-            let highPercentage = Decimal(high) / total * 100
-
-            VStack(alignment: .leading, spacing: 10) {
-                VStack(alignment: .leading, spacing: 5) {
-                    Text("\(formatValue(Decimal(timeInRangeType.bottomThreshold)))-\(formatValue(highLimit))").font(.subheadline)
+        if glucose.count < 1 {
+            Text("No glucose readings found.")
+        } else {
+            HStack(alignment: .center, spacing: 20) {
+                // Calculate total number of glucose readings
+                let total = Decimal(glucose.count)
+                // Count readings greater than high limit (180 mg/dL)
+                let high = glucose.filter { $0.glucose > Int(highLimit) }.count
+                // Count readings between low limit (TITR: 70 mg/dL, TING 63 mg/dL) and 140 mg/dL (tight control)
+                let tight = glucose
+                    .filter { $0.glucose >= timeInRangeType.bottomThreshold && $0.glucose <= timeInRangeType.topThreshold }.count
+                // Count readings between 140 and high limit (normal range)
+                let normal = glucose.filter { $0.glucose >= timeInRangeType.bottomThreshold && $0.glucose <= Int(highLimit) }
+                    .count
+                // Count readings less than low limit (low) (70 mg/dL if not showing chart, otherwise 70 for TITR and 63 for TING)
+                let low = glucose.filter { $0.glucose < (showChart ? Int(timeInRangeType.bottomThreshold) : 70) }.count
+                // Count readings less than moderately low limit (63 mg/dL)
+                let moderatelyLow = glucose.filter { $0.glucose < 63 }.count
+                // Count readings less than moderately high limit (220 mg/dL)
+                let moderatelyHigh = glucose.filter { $0.glucose > 220 }.count
+                // Count readings less than very low limit (54 mg/dL)
+                let veryLow = glucose.filter { $0.glucose < 54 }.count
+                // Count readings less than very high limit (250 mg/dL)
+                let veryHigh = glucose.filter { $0.glucose > 250 }.count
+
+                let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
+                let sumReadings = justGlucoseArray.reduce(0, +)
+
+                let glucoseAverage = Decimal(sumReadings) / total
+                let medianGlucose = StatChartUtils.medianCalculation(array: justGlucoseArray)
+
+                let lowPercentage = Decimal(low) / total * 100
+                let tightPercentage = Decimal(tight) / total * 100
+                let inRangePercentage = Decimal(normal) / total * 100
+                let highPercentage = Decimal(high) / total * 100
+                let moderatelyLowPercentage = Decimal(moderatelyLow) / total * 100
+                let moderatelyHighPercentage = Decimal(moderatelyHigh) / total * 100
+                let veryLowPercentage = Decimal(veryLow) / total * 100
+                let veryHighPercentage = Decimal(veryHigh) / total * 100
+
+                VStack(alignment: .leading, spacing: 10) {
+                    VStack(alignment: .leading, spacing: 5) {
+                        Text(
+                            "\(Decimal(timeInRangeType.bottomThreshold).formatted(for: units))-\(highLimit.formatted(for: units))"
+                        )
+                        .font(.subheadline)
                         .foregroundStyle(Color.secondary)
-                    Text(inRangePercentage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "%")
-                        .foregroundStyle(Color.loopGreen)
-                }
+                        Text(formatPercentage(inRangePercentage, tight: true))
+                            .foregroundStyle(Color.loopGreen)
+                    }
 
-                VStack(alignment: .leading, spacing: 5) {
-                    Text(
-                        "\(formatValue(Decimal(timeInRangeType.bottomThreshold)))-\(formatValue(Decimal(timeInRangeType.topThreshold)))"
-                    )
-                    .font(.subheadline)
-                    .foregroundStyle(Color.secondary)
-                    Text(tightPercentage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "%")
-                        .foregroundStyle(Color.green)
-                }
-            }.padding(.leading, 5)
+                    VStack(alignment: .leading, spacing: 5) {
+                        Text(
+                            "\(Decimal(timeInRangeType.bottomThreshold).formatted(for: units))-\(Decimal(timeInRangeType.topThreshold).formatted(for: units))"
+                        )
+                        .font(.subheadline)
+                        .foregroundStyle(Color.secondary)
+                        Text(formatPercentage(tightPercentage, tight: true))
+                            .foregroundStyle(Color.green)
+                    }
+                }.padding(.leading, 5)
+
+                VStack(alignment: .leading, spacing: 10) {
+                    VStack(alignment: .leading, spacing: 5) {
+                        Text("> \(highLimit.formatted(for: units))").font(.subheadline)
+                            .foregroundStyle(Color.secondary)
+                        Text(formatPercentage(highPercentage, tight: true))
+                            .foregroundStyle(Color.loopYellow)
+                    }
 
-            VStack(alignment: .leading, spacing: 10) {
-                VStack(alignment: .leading, spacing: 5) {
-                    Text("> \(formatValue(highLimit))").font(.subheadline)
+                    VStack(alignment: .leading, spacing: 5) {
+                        Text(
+                            "< \(Decimal(showChart ? timeInRangeType.bottomThreshold : 70).formatted(for: units))"
+                        )
+                        .font(.subheadline)
                         .foregroundStyle(Color.secondary)
-                    Text(highPercentage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "%")
-                        .foregroundStyle(Color.orange)
+                        Text(formatPercentage(lowPercentage, tight: true))
+                            .foregroundStyle(Color.red)
+                    }
                 }
+                // If not showing chart, show extra stats
+                if !showChart {
+                    VStack(alignment: .leading, spacing: 10) {
+                        VStack(alignment: .leading, spacing: 5) {
+                            Text("> \(Decimal(220).formatted(for: units))").font(.subheadline)
+                                .foregroundStyle(Color.secondary)
+                            Text(formatPercentage(moderatelyHighPercentage, tight: true))
+                                .foregroundStyle(Color.loopYellow)
+                        }
 
-                VStack(alignment: .leading, spacing: 5) {
-                    Text(
-                        "< \(formatValue(Decimal(timeInRangeType.bottomThreshold)))"
-                    )
-                    .font(.subheadline)
-                    .foregroundStyle(Color.secondary)
-                    Text(lowPercentage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "%")
-                        .foregroundStyle(Color.loopRed)
-                }
-            }
+                        VStack(alignment: .leading, spacing: 5) {
+                            Text(
+                                "< \(Decimal(63).formatted(for: units))"
+                            )
+                            .font(.subheadline)
+                            .foregroundStyle(Color.secondary)
+                            Text(formatPercentage(moderatelyLowPercentage, tight: true))
+                                .foregroundStyle(Color.red)
+                        }
+                    }
+                    VStack(alignment: .leading, spacing: 10) {
+                        VStack(alignment: .leading, spacing: 5) {
+                            Text("> \(Decimal(250).formatted(for: units))").font(.subheadline)
+                                .foregroundStyle(Color.secondary)
+                            Text(formatPercentage(veryHighPercentage, tight: true))
+                                .foregroundStyle(Color.orange)
+                        }
 
-            VStack(alignment: .leading, spacing: 10) {
-                VStack(alignment: .leading, spacing: 5) {
-                    Text("Average").font(.subheadline).foregroundStyle(Color.secondary)
-                    Text(
-                        units == .mgdL ? glucoseAverage
-                            .formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))) : glucoseAverage.asMmolL
-                            .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))
-                    )
+                        VStack(alignment: .leading, spacing: 5) {
+                            Text(
+                                "< \(Decimal(54).formatted(for: units))"
+                            )
+                            .font(.subheadline)
+                            .foregroundStyle(Color.secondary)
+                            Text(formatPercentage(veryLowPercentage, tight: true))
+                                .foregroundStyle(Color.purple)
+                        }
+                    }
                 }
 
-                VStack(alignment: .leading, spacing: 5) {
-                    Text("Median").font(.subheadline).foregroundStyle(Color.secondary)
-                    Text(
-                        units == .mgdL ? medianGlucose
-                            .formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))) : medianGlucose.asMmolL
-                            .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))
-                    )
+                VStack(alignment: .leading, spacing: 10) {
+                    VStack(alignment: .leading, spacing: 5) {
+                        Text(showChart ? "Average" : "Avg").font(.subheadline).foregroundStyle(Color.secondary)
+                        Text(
+                            units == .mgdL ? glucoseAverage
+                                .formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))) : glucoseAverage
+                                .asMmolL
+                                .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))
+                        )
+                    }
+
+                    VStack(alignment: .leading, spacing: 5) {
+                        Text(showChart ? "Median" : "Med").font(.subheadline).foregroundStyle(Color.secondary)
+                        Text(
+                            units == .mgdL ? medianGlucose
+                                .formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))) : medianGlucose
+                                .asMmolL
+                                .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))
+                        )
+                    }
                 }
-            }
 
-            Chart {
-                ForEach(rangeData, id: \.range) { data in
-                    SectorMark(
-                        angle: .value("Percentage", data.count),
-                        innerRadius: .ratio(0.618),
-                        outerRadius: selectedRange == data.range ? 100 : 80,
-                        angularInset: 1.5
-                    )
-                    .foregroundStyle(data.color)
+                if showChart {
+                    Chart {
+                        ForEach(rangeData, id: \.range) { data in
+                            SectorMark(
+                                angle: .value("Percentage", data.count),
+                                innerRadius: .ratio(0.618),
+                                outerRadius: selectedRange == data.range ? 100 : 80
+                            )
+                            .foregroundStyle(data.color)
+                        }
+                    }
+                    .chartAngleSelection(value: $selectedCount)
+                    .frame(height: 100)
                 }
             }
-            .chartAngleSelection(value: $selectedCount)
-            .frame(height: 100)
-        }
-        .onChange(of: selectedCount) { _, newValue in
-            if let newValue {
-                withAnimation {
-                    getSelectedRange(value: newValue)
-                }
-            } else {
-                withAnimation {
-                    selectedRange = nil
+            .onChange(of: selectedCount) { _, newValue in
+                if let newValue {
+                    withAnimation {
+                        getSelectedRange(value: newValue)
+                    }
+                } else {
+                    withAnimation {
+                        selectedRange = nil
+                    }
                 }
             }
-        }
-        .overlay(alignment: .top) {
-            if let selectedRange {
-                let data = getDetailedData(for: selectedRange)
-                RangeDetailPopover(data: data)
-                    .transition(.scale.combined(with: .opacity))
-                    .offset(y: -150) // TODO: make this dynamic
+            .overlay(alignment: .top) {
+                if let selectedRange {
+                    let data = getDetailedData(for: selectedRange)
+                    RangeDetailPopover(data: data)
+                        .transition(.scale.combined(with: .opacity))
+                        .offset(y: -150) // TODO: make this dynamic
+                }
             }
         }
     }
@@ -167,7 +230,7 @@ struct GlucoseSectorChart: View {
 
         // Return array of tuples with range data
         return [
-            (.high, highCount, Decimal(highCount) / Decimal(total) * 100, .orange),
+            (.high, highCount, Decimal(highCount) / Decimal(total) * 100, .loopYellow),
             (.inRange, inRangeCount, Decimal(inRangeCount) / Decimal(total) * 100, .green),
             (.low, lowCount, Decimal(lowCount) / Decimal(total) * 100, .red)
         ]
@@ -216,15 +279,18 @@ struct GlucoseSectorChart: View {
 
             return RangeDetail(
                 title: String(localized: "High Glucose"),
-                color: .orange,
+                color: .loopYellow,
                 items: [
-                    (String(localized: "Very High (>\(formatValue(250)))"), formatPercentage(Decimal(veryHigh) / total * 100)),
                     (
-                        String(localized: "High (\(formatValue(highLimit))-\(formatValue(250)))"),
+                        String(localized: "Very High (>\(Decimal(250).formatted(for: units)))"),
+                        formatPercentage(Decimal(veryHigh) / total * 100)
+                    ),
+                    (
+                        String(localized: "High (\(highLimit.formatted(for: units))-\(Decimal(250).formatted(for: units)))"),
                         formatPercentage(Decimal(high) / total * 100)
                     ),
-                    (String(localized: "Average"), formatValue(average)),
-                    (String(localized: "Median"), formatValue(median)),
+                    (String(localized: "Average"), average.formatted(for: units)),
+                    (String(localized: "Median"), median.formatted(for: units)),
                     (String(localized: "SD"), formatSD(standardDeviation))
                 ]
             )
@@ -242,18 +308,18 @@ struct GlucoseSectorChart: View {
                 items: [
                     (
                         String(
-                            localized: "Normal (\(formatValue(Decimal(timeInRangeType.bottomThreshold)))-\(formatValue(highLimit)))"
+                            localized: "Normal (\(Decimal(timeInRangeType.bottomThreshold).formatted(for: units))-\(highLimit.formatted(for: units)))"
                         ),
                         formatPercentage(Decimal(glucoseValues.count) / total * 100)
                     ),
                     (
                         String(
-                            localized: "\(timeInRangeType == .timeInTightRange ? "TITR" : "TING") (\(formatValue(Decimal(timeInRangeType.bottomThreshold)))-\(formatValue(Decimal(timeInRangeType.topThreshold))))"
+                            localized: "\(timeInRangeType == .timeInTightRange ? "TITR" : "TING") (\(Decimal(timeInRangeType.bottomThreshold).formatted(for: units))-\(Decimal(timeInRangeType.topThreshold).formatted(for: units)))"
                         ),
                         formatPercentage(Decimal(tight) / total * 100)
                     ),
-                    (String(localized: "Average"), formatValue(average)),
-                    (String(localized: "Median"), formatValue(median)),
+                    (String(localized: "Average"), average.formatted(for: units)),
+                    (String(localized: "Median"), median.formatted(for: units)),
                     (String(localized: "SD"), formatSD(standardDeviation))
                 ]
             )
@@ -271,12 +337,17 @@ struct GlucoseSectorChart: View {
                 color: .red,
                 items: [
                     (
-                        String(localized: "Low (\(formatValue(54))-\(formatValue(Decimal(timeInRangeType.bottomThreshold))))"),
+                        String(
+                            localized: "Low (\(Decimal(54).formatted(for: units))-\(Decimal(timeInRangeType.bottomThreshold).formatted(for: units)))"
+                        ),
                         formatPercentage(Decimal(low) / total * 100)
                     ),
-                    (String(localized: "Very Low (<\(formatValue(54)))"), formatPercentage(Decimal(veryLow) / total * 100)),
-                    (String(localized: "Average"), formatValue(average)),
-                    (String(localized: "Median"), formatValue(median)),
+                    (
+                        String(localized: "Very Low (<\(Decimal(54).formatted(for: units))"),
+                        formatPercentage(Decimal(veryLow) / total * 100)
+                    ),
+                    (String(localized: "Average"), average.formatted(for: units)),
+                    (String(localized: "Median"), median.formatted(for: units)),
                     (String(localized: "SD"), formatSD(standardDeviation))
                 ]
             )
@@ -286,10 +357,14 @@ struct GlucoseSectorChart: View {
     /// Formats a percentage value to a string with one decimal place.
     /// - Parameter value: A decimal value representing the percentage.
     /// - Returns: A formatted percentage string
-    private func formatPercentage(_ value: Decimal) -> String {
+    private func formatPercentage(_ value: Decimal, tight: Bool = false) -> String {
         let formatter = NumberFormatter()
         formatter.numberStyle = .percent
-        formatter.maximumFractionDigits = 1
+        formatter.minimumFractionDigits = value == 100 ? 0 : 1
+        formatter.maximumFractionDigits = value == 100 ? 0 : 1
+        if tight {
+            formatter.positiveSuffix = "%"
+        }
         return formatter.string(from: NSDecimalNumber(decimal: value / 100)) ?? "0%"
     }
 
@@ -319,13 +394,6 @@ struct GlucoseSectorChart: View {
             .number.grouping(.never).rounded().precision(.fractionLength(0))
         ) : sd.formattedAsMmolL
     }
-
-    /// Formats a glucose value based on the current units.
-    /// - Parameter value: A decimal value representing the glucose level.
-    /// - Returns: A formatted string of the glucose value.
-    private func formatValue(_ value: Decimal) -> String {
-        units == .mgdL ? value.description : value.formattedAsMmolL
-    }
 }
 
 /// Represents details about a specific glucose range category including title, color and percentage breakdowns

+ 4 - 4
Trio/Sources/Modules/Stat/View/ViewElements/Insulin/BolusStatsView.swift

@@ -250,24 +250,24 @@ struct BolusStatsView: View {
         .chartXAxis {
             AxisMarks(preset: .aligned, values: .stride(by: selectedInterval == .day ? .hour : .day)) { value in
                 if let date = value.as(Date.self) {
-                    let day = Calendar.current.component(.day, from: date)
-                    let hour = Calendar.current.component(.hour, from: date)
-
                     switch selectedInterval {
                     case .day:
+                        let hour = Calendar.current.component(.hour, from: date)
                         if hour % 6 == 0 { // Show only every 6 hours
                             AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
                                 .font(.footnote)
                             AxisGridLine()
                         }
                     case .month:
-                        if day % 3 == 0 { // Only show every 3rd day
+                        let weekday = calendar.component(.weekday, from: date)
+                        if weekday == calendar.firstWeekday { // Only show the first day of the week
                             AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
                                 .font(.footnote)
                             AxisGridLine()
                         }
                     case .total:
                         // Only show every other month
+                        let day = Calendar.current.component(.day, from: date)
                         if day == 1 && Calendar.current.component(.month, from: date) % 2 == 1 {
                             AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
                                 .font(.footnote)

+ 2 - 1
Trio/Sources/Modules/Stat/View/ViewElements/Insulin/TotalDailyDoseChart.swift

@@ -211,7 +211,8 @@ struct TotalDailyDoseChart: View {
                             AxisGridLine()
                         }
                     case .month:
-                        if day % 3 == 0 { // Only show every 3rd day
+                        let weekday = calendar.component(.weekday, from: date)
+                        if weekday == calendar.firstWeekday { // Only show the first day of the week
                             AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
                                 .font(.footnote)
                             AxisGridLine()

+ 5 - 3
Trio/Sources/Modules/Stat/View/ViewElements/Looping/LoopBarChartView.swift

@@ -36,9 +36,11 @@ struct LoopBarChartView: View {
             .chartXAxis {
                 AxisMarks(position: .bottom) { value in
                     if let percentage = value.as(Double.self) {
-                        AxisValueLabel {
-                            Text("\(Int(percentage))%")
-                                .font(.footnote)
+                        if selectedInterval != .today {
+                            AxisValueLabel {
+                                Text("\(Int(percentage))%")
+                                    .font(.footnote)
+                            }
                         }
                         AxisGridLine()
                     }

+ 3 - 2
Trio/Sources/Modules/Stat/View/ViewElements/Meal/MealStatsView.swift

@@ -231,7 +231,6 @@ struct MealStatsView: View {
         .chartXAxis {
             AxisMarks(preset: .aligned, values: .stride(by: selectedInterval == .day ? .hour : .day)) { value in
                 if let date = value.as(Date.self) {
-                    let day = Calendar.current.component(.day, from: date)
                     let hour = Calendar.current.component(.hour, from: date)
 
                     switch selectedInterval {
@@ -242,13 +241,15 @@ struct MealStatsView: View {
                             AxisGridLine()
                         }
                     case .month:
-                        if day % 3 == 0 { // Only show every 3rd day
+                        let weekday = calendar.component(.weekday, from: date)
+                        if weekday == calendar.firstWeekday { // Only show the first day of the week
                             AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
                                 .font(.footnote)
                             AxisGridLine()
                         }
                     case .total:
                         // Only show every other month
+                        let day = Calendar.current.component(.day, from: date)
                         if day == 1 && Calendar.current.component(.month, from: date) % 2 == 1 {
                             AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
                                 .font(.footnote)

+ 2 - 2
Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift

@@ -391,8 +391,8 @@ extension Treatments {
                 simulatedCOB = min(maxCobInt16, cobInt16)
             }
 
-            // Check if this is a backdated entry by comparing with the default date
-            let isBackdated = date != defaultDate
+            // Check if this is a backdated entry by comparing with the default date using a tolerance
+            let isBackdated = abs(date.timeIntervalSince(defaultDate)) > 1.0
 
             let result = await bolusCalculationManager.handleBolusCalculation(
                 carbs: carbs,

+ 8 - 5
Trio/Sources/Modules/Treatments/View/ForecastChart.swift

@@ -50,10 +50,8 @@ struct ForecastChart: View {
     }
 
     private var forecastChartLabels: some View {
-        // 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
+        // Check if this is a backdated entry by comparing with the default date using a tolerance
+        let isBackdated = abs(state.date.timeIntervalSince(state.defaultDate)) > 1.0
 
         // When backdated, display no carbs as this label is only supposed to show current entered carbs
         let displayedCarbs = isBackdated ? 0 : state.carbs
@@ -126,6 +124,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()
@@ -176,7 +179,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) {

+ 5 - 7
Trio/Sources/Modules/Treatments/View/PopupView.swift

@@ -312,10 +312,8 @@ 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 {
-        // 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
+        // Check if this is a backdated entry by comparing with the default date using a tolerance
+        let isBackdated = abs(state.date.timeIntervalSince(state.defaultDate)) > 1.0
 
         // Determine COB and carbs to display based on backdating status
         let displayedCOB = isBackdated ? (state.simulatedDetermination?.cob ?? Decimal(state.cob)) : Decimal(state.cob)
@@ -604,7 +602,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
@@ -646,7 +644,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) {
@@ -656,7 +654,7 @@ struct PopupView: View {
                     Text("Rec. Bolus %")
                     Text("")
                         .layoutPriority(-15)
-                    Text("Fatty %")
+                    Text("Red. Bolus %")
                 }
                 .secondaryStyle()
 

+ 1 - 1
Trio/Sources/Modules/Treatments/View/TreatmentsRootView.swift

@@ -255,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)

+ 6 - 6
Trio/Sources/Services/BolusCalculator/BolusCalculationManager.swift

@@ -420,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
@@ -432,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)")
             }
         }
 
@@ -488,7 +488,7 @@ 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)
@@ -546,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

+ 1 - 1
Trio/Sources/Services/LiveActivity/LiveActivityAttributes+Helper.swift

@@ -55,7 +55,7 @@ extension LiveActivityAttributes.ContentState {
         return formatter.string(from: deltaAsDecimal as NSNumber) ?? "--"
     }
 
-    init?(
+    init(
         new bg: GlucoseData,
         prev _: GlucoseData?,
         units: GlucoseUnits,

+ 120 - 221
Trio/Sources/Services/LiveActivity/LiveActivityManager.swift

@@ -7,7 +7,6 @@ import UIKit
 
 @available(iOS 16.2, *) private struct ActiveActivity {
     let activity: Activity<LiveActivityAttributes>
-    let startDate: Date
 
     /// Determines if the current activity needs to be recreated.
     ///
@@ -23,10 +22,21 @@ import UIKit
         @unknown default:
             return true
         }
-        return -startDate.timeIntervalSinceNow > TimeInterval(60 * 60)
+        return -activity.attributes.startDate.timeIntervalSinceNow > TimeInterval(60 * 60)
     }
 }
 
+final class LiveActivityData: ObservableObject {
+    /// Determination data used to update live activity state.
+    @Published var determination: DeterminationData?
+    /// Array of glucose readings fetched from persistent storage.
+    @Published var glucoseFromPersistence: [GlucoseData]?
+    /// The current override data (if any).
+    @Published var override: OverrideData?
+    /// The widget items displayed within the live activity.
+    @Published var widgetItems: [LiveActivityAttributes.LiveActivityItem]?
+}
+
 /// A service managing live activity updates and state management.
 ///
 /// This class handles the creation, update, and termination of live activities based on various data sources
@@ -51,18 +61,10 @@ final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver
         settingsManager.settings
     }
 
-    /// Determination data used to update live activity state.
-    var determination: DeterminationData?
     /// The current active live activity.
     private var currentActivity: ActiveActivity?
-    /// The most recent glucose reading.
-    private var latestGlucose: GlucoseData?
-    /// Array of glucose readings fetched from persistent storage.
-    var glucoseFromPersistence: [GlucoseData]?
-    /// The current override data (if any).
-    var override: OverrideData?
-    /// The widget items displayed within the live activity.
-    var widgetItems: [LiveActivityAttributes.LiveActivityItem]?
+
+    private var data = LiveActivityData()
 
     /// A Core Data task context.
     let context = CoreDataStack.shared.newTaskContext()
@@ -85,11 +87,16 @@ final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver
         systemEnabled = activityAuthorizationInfo.areActivitiesEnabled
         injectServices(resolver)
         setupNotifications()
-        registerSubscribers()
         registerHandler()
         monitorForLiveActivityAuthorizationChanges()
-        setupGlucoseArray()
         broadcaster.register(SettingsObserver.self, observer: self)
+        data.objectWillChange.sink { [weak self] in
+            Task { @MainActor in
+                // by the time this runs, the object change is done, so we see the new data here
+                await self?.pushCurrentContent()
+            }
+        }.store(in: &subscriptions)
+        loadInitialData()
     }
 
     /// Sets up application notifications that trigger live activity updates when the app state changes.
@@ -98,18 +105,18 @@ final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver
         notificationCenter
             .addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil) { [weak self] _ in
                 Task { @MainActor in
-                    self?.forceActivityUpdate()
+                    await self?.pushCurrentContent()
                 }
             }
         notificationCenter
             .addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: nil) { [weak self] _ in
                 Task { @MainActor in
-                    self?.forceActivityUpdate()
+                    await self?.pushCurrentContent()
                 }
             }
         notificationCenter.addObserver(
             self,
-            selector: #selector(handleLiveActivityOrderChange),
+            selector: #selector(loadWidgetItems),
             name: .liveActivityOrderDidChange,
             object: nil
         )
@@ -120,143 +127,75 @@ final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver
     /// This method triggers an update to the live activity content state based on the new settings.
     /// - Parameter _: The updated `TrioSettings`.
     func settingsDidChange(_: TrioSettings) {
-        Task {
-            await updateContentState(determination)
+        Task { @MainActor in
+            await self.pushCurrentContent()
         }
     }
 
     /// Registers handlers for Core Data changes related to overrides, glucose readings, and determinations.
     private func registerHandler() {
         coreDataPublisher?.filteredByEntityName("OverrideStored").sink { [weak self] _ in
-            guard let self = self else { return }
-            self.overridesDidUpdate()
+            Task { await self?.loadOverrides() }
         }.store(in: &subscriptions)
 
         coreDataPublisher?.filteredByEntityName("GlucoseStored").sink { [weak self] _ in
-            guard let self = self else { return }
-            self.setupGlucoseArray()
+            Task { await self?.loadGlucose() }
         }.store(in: &subscriptions)
 
         coreDataPublisher?.filteredByEntityName("OrefDetermination")
             .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .utility))
             .sink { [weak self] _ in
-                guard let self = self else { return }
-                self.cobOrIobDidUpdate()
+                Task { await self?.loadDetermination() }
             }.store(in: &subscriptions)
     }
 
-    /// Registers subscribers for updates from the glucose storage.
-    private func registerSubscribers() {
-        glucoseStorage.updatePublisher
-            .receive(on: queue)
-            .sink { [weak self] _ in
-                guard let self = self else { return }
-                self.setupGlucoseArray()
-            }
-            .store(in: &subscriptions)
-    }
-
     /// Fetches and maps new determination data and updates the live activity content state.
-    private func cobOrIobDidUpdate() {
-        Task { @MainActor in
-            do {
-                self.determination = try await fetchAndMapDetermination()
-                if let determination = determination {
-                    await self.updateContentState(determination)
-                }
-            } catch {
-                debug(
-                    .default,
-                    "\(DebuggingIdentifiers.failed) failed to fetch and map determination: \(error)"
-                )
-            }
+    private func loadDetermination() async {
+        do {
+            data.determination = try await fetchAndMapDetermination()
+        } catch {
+            debug(
+                .default,
+                "[LiveActivityManager] \(DebuggingIdentifiers.failed) failed to fetch and map determination: \(error)"
+            )
         }
     }
 
     /// Fetches and maps override data and updates the live activity content state.
-    private func overridesDidUpdate() {
-        Task { @MainActor in
-            do {
-                self.override = try await fetchAndMapOverride()
-                if let determination = determination {
-                    await self.updateContentState(determination)
-                }
-            } catch {
-                debug(.default, "\(DebuggingIdentifiers.failed) failed to fetch and map override: \(error)")
-            }
+    private func loadOverrides() async {
+        do {
+            data.override = try await fetchAndMapOverride()
+        } catch {
+            debug(.default, "[LiveActivityManager] \(DebuggingIdentifiers.failed) failed to fetch and map override: \(error)")
         }
     }
 
     /// Handles changes to the live activity order.
     ///
     /// Loads widget items from user defaults and triggers an update to the live activity order.
-    @objc private func handleLiveActivityOrderChange() {
-        Task {
-            self.widgetItems = UserDefaults.standard.loadLiveActivityOrderFromUserDefaults() ?? LiveActivityAttributes
-                .LiveActivityItem.defaultItems
-            await self.updateLiveActivityOrder()
-        }
-    }
-
-    /// Updates the live activity content state based on new determination or override data.
-    ///
-    /// - Parameter update: An object representing new `DeterminationData` or `OverrideData`.
-    @MainActor private func updateContentState<T>(_ update: T) async {
-        guard let latestGlucose = latestGlucose else {
-            return
-        }
-        var content: LiveActivityAttributes.ContentState?
-
-        widgetItems = UserDefaults.standard.loadLiveActivityOrderFromUserDefaults() ?? LiveActivityAttributes
+    @objc private func loadWidgetItems() {
+        data.widgetItems = UserDefaults.standard.loadLiveActivityOrderFromUserDefaults() ?? LiveActivityAttributes
             .LiveActivityItem.defaultItems
+    }
 
-        if let determination = update as? DeterminationData {
-            content = LiveActivityAttributes.ContentState(
-                new: latestGlucose,
-                prev: latestGlucose,
-                units: settings.units,
-                chart: glucoseFromPersistence ?? [],
-                settings: settings,
-                determination: determination,
-                override: override,
-                widgetItems: widgetItems
-            )
-        } else if let override = update as? OverrideData {
-            content = LiveActivityAttributes.ContentState(
-                new: latestGlucose,
-                prev: latestGlucose,
-                units: settings.units,
-                chart: glucoseFromPersistence ?? [],
-                settings: settings,
-                determination: determination,
-                override: override,
-                widgetItems: widgetItems
+    /// Sets up the array of glucose data from persistent storage and triggers an update to the live activity.
+    private func loadGlucose() async {
+        do {
+            data.glucoseFromPersistence = try await fetchAndMapGlucose()
+        } catch {
+            debug(
+                .default,
+                "[LiveActivityManager] \(DebuggingIdentifiers.failed) failed to fetch glucose with error: \(error)"
             )
         }
-
-        if let content = content {
-            await pushUpdate(content)
-        }
     }
 
-    /// Triggers an update of the live activity order.
-    ///
-    /// This method refreshes the activity's content state to reflect any changes in the widget order.
-    @MainActor private func updateLiveActivityOrder() async {
+    private func loadInitialData() {
         Task {
-            await updateContentState(determination)
-        }
-    }
-
-    /// Sets up the array of glucose data from persistent storage and triggers an update to the live activity.
-    private func setupGlucoseArray() {
-        Task { @MainActor in
-            do {
-                self.glucoseFromPersistence = try await fetchAndMapGlucose()
-                glucoseDidUpdate(glucoseFromPersistence ?? [])
-            } catch {
-                debug(.default, "\(DebuggingIdentifiers.failed) failed to fetch glucose with error: \(error)")
-            }
+            await self.loadGlucose()
+            await self.loadOverrides()
+            await self.loadDetermination()
+            self.loadWidgetItems()
         }
     }
 
@@ -273,22 +212,6 @@ final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver
         }
     }
 
-    /// Forces an update to the live activity.
-    ///
-    /// If live activities are enabled and the current activity requires recreation, this method triggers a new glucose update.
-    /// Otherwise, it ends the current live activity.
-    @MainActor private func forceActivityUpdate() {
-        if settings.useLiveActivity {
-            if currentActivity?.needsRecreation() ?? true {
-                glucoseDidUpdate(glucoseFromPersistence ?? [])
-            }
-        } else {
-            Task {
-                await self.endActivity()
-            }
-        }
-    }
-
     /// Pushes an update to the live activity with the specified content state.
     ///
     /// If an existing activity requires recreation or is outdated, this method ends it and starts a new one.
@@ -296,6 +219,23 @@ final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver
     ///
     /// - Parameter state: The new content state to push to the live activity.
     @MainActor private func pushUpdate(_ state: LiveActivityAttributes.ContentState) async {
+        if !settings.useLiveActivity || !systemEnabled {
+            await endActivity()
+            return
+        }
+
+        if currentActivity == nil {
+            // try to restore an existing activity
+            currentActivity = Activity<LiveActivityAttributes>.activities
+                .max { $0.attributes.startDate < $1.attributes.startDate }.map {
+                    ActiveActivity(activity: $0)
+                }
+
+            if let currentActivity {
+                debug(.default, "[LiveActivityManager] Restored live activity: \(currentActivity.activity.id)")
+            }
+        }
+
         // End all unknown activities except the current one
         for unknownActivity in Activity<LiveActivityAttributes>.activities
             .filter({ self.currentActivity?.activity.id != $0.id })
@@ -303,22 +243,14 @@ final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver
             await unknownActivity.end(nil, dismissalPolicy: .immediate)
         }
 
-        // Defensive: capture the current activity at function start
-        let activityAtStart = currentActivity
-
-        if let currentActivity = activityAtStart {
+        if let currentActivity {
             if currentActivity.needsRecreation(), UIApplication.shared.applicationState == .active {
                 debug(.default, "[LiveActivityManager] Ending current activity for recreation: \(currentActivity.activity.id)")
                 await endActivity()
                 // After endActivity(), currentActivity is guaranteed to be nil
                 // No recursive task, but explicitly restart
-                if self.currentActivity == nil {
-                    debug(.default, "[LiveActivityManager] Re-pushing update after recreation.")
-                    await pushUpdate(state)
-                } else {
-                    debug(.default, "[LiveActivityManager] Warning: currentActivity was not nil after endActivity!")
-                }
-                return
+                debug(.default, "[LiveActivityManager] Re-pushing update after recreation.")
+                await pushUpdate(state)
             } else {
                 let content = ActivityContent(
                     state: state,
@@ -345,7 +277,7 @@ final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver
                             date: Date.now,
                             highGlucose: settings.high,
                             lowGlucose: settings.low,
-                            target: determination?.target ?? 100 as Decimal,
+                            target: data.determination?.target ?? 100 as Decimal,
                             glucoseColorScheme: settings.glucoseColorScheme.rawValue,
                             detailedViewState: nil,
                             isInitialState: true
@@ -358,39 +290,43 @@ final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver
                     content: expired,
                     pushType: nil
                 )
-                currentActivity = ActiveActivity(activity: activity, startDate: Date.now)
+                currentActivity = ActiveActivity(activity: activity)
                 debug(.default, "[LiveActivityManager] Created new activity: \(activity.id)")
-                await pushUpdate(state)
+
+                // Update the newly created activity with actual data
+                let updateContent = ActivityContent(
+                    state: state,
+                    staleDate: Date.now.addingTimeInterval(5 * 60)
+                )
+                await activity.update(updateContent)
+                debug(.default, "[LiveActivityManager] Set initial content for new activity: \(activity.id)")
             } catch {
                 debug(
                     .default,
-                    "\(#file): Error creating new activity: \(error)"
+                    "[LiveActivityManager]: Error creating new activity: \(error)"
                 )
+                // Reset currentActivity on error to allow retry on next update
+                currentActivity = nil
             }
         }
     }
 
     /// Ends the current live activity and ensures that all unknown activities are terminated.
     private func endActivity() async {
-        debug(.default, "Ending all live activities...")
+        debug(.default, "[LiveActivityManager] Ending all live activities...")
 
         if let currentActivity {
-            debug(.default, "Ending current activity: \(currentActivity.activity.id)")
+            debug(.default, "[LiveActivityManager] Ending current activity: \(currentActivity.activity.id)")
             await currentActivity.activity.end(nil, dismissalPolicy: .immediate)
             self.currentActivity = nil
         }
 
-        for activity in Activity<LiveActivityAttributes>.activities {
-            debug(.default, "Ending lingering activity: \(activity.id)")
-            await activity.end(nil, dismissalPolicy: .immediate)
-        }
-
         for unknownActivity in Activity<LiveActivityAttributes>.activities {
-            debug(.default, "Ending unknown activity: \(unknownActivity.id)")
+            debug(.default, "[LiveActivityManager] Ending unknown activity: \(unknownActivity.id)")
             await unknownActivity.end(nil, dismissalPolicy: .immediate)
         }
 
-        debug(.default, "All live activities ended.")
+        debug(.default, "[LiveActivityManager] All live activities ended.")
     }
 
     /// Restarts the live activity from a Live Activity Intent.
@@ -398,87 +334,50 @@ final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver
     /// This method mimics xdrip's `restartActivityFromLiveActivityIntent()` behavior by verifying that a valid content state exists,
     /// ending the current live activity, and starting a new one using the current state.
     @MainActor func restartActivityFromLiveActivityIntent() async {
-        guard let latestGlucose = latestGlucose,
-              let determination = determination
-        else {
-            debug(.default, "Cannot restart live activity because required persistent state is not available. Fetching data...")
-            return
-        }
-
-        guard let contentState = LiveActivityAttributes.ContentState(
-            new: latestGlucose,
-            prev: latestGlucose,
-            units: settings.units,
-            chart: glucoseFromPersistence ?? [],
-            settings: settings,
-            determination: determination,
-            override: override,
-            widgetItems: widgetItems
-        ) else {
-            debug(.default, "Cannot restart live activity because content state cannot be created")
-            return
-        }
-
         await endActivity()
 
         while (currentActivity != nil && currentActivity!.activity.activityState != .ended) || Activity<LiveActivityAttributes>
             .activities.contains(where: { $0.activityState != .ended })
         {
-            debug(.default, "Waiting for Live Activity to end...")
+            debug(.default, "[LiveActivityManager] Waiting for Live Activity to end...")
             try? await Task.sleep(nanoseconds: 200_000_000) // 0.2s sleep
         }
 
-        Task { @MainActor in
-            await self.pushUpdate(contentState)
-        }
-        debug(.default, "Restarted Live Activity from LiveActivityIntent (via iOS Shortcut)")
+        // Add additional delay to ensure iOS has fully cleaned up the previous activity
+        debug(.default, "[LiveActivityManager] Waiting additional time for iOS to clean up...")
+        try? await Task.sleep(nanoseconds: 1_000_000_000) // 1s additional delay
+
+        await pushCurrentContent()
+
+        debug(.default, "[LiveActivityManager] Restarted Live Activity from LiveActivityIntent (via iOS Shortcut)")
     }
 }
 
 @available(iOS 16.2, *)
 extension LiveActivityManager {
-    /// Updates the live activity when new glucose data is available.
-    ///
-    /// This function adjusts the live activity content based on new glucose readings and triggers an update to the live activity.
-    /// - Parameter glucose: An array of `GlucoseData` objects.
-    @MainActor func glucoseDidUpdate(_ glucose: [GlucoseData]) {
-        guard settings.useLiveActivity else {
-            if currentActivity != nil {
-                Task {
-                    await self.endActivity()
-                }
-            }
+    @MainActor func pushCurrentContent() async {
+        guard let glucose = data.glucoseFromPersistence, let bg = glucose.first else {
+            debug(.default, "[LiveActivityManager] pushCurrentContent: no current glucose data available")
             return
         }
+        let prevGlucose = data.glucoseFromPersistence?.dropFirst().first
 
-        if glucose.count > 1 {
-            latestGlucose = glucose.dropFirst().first
-        }
-        defer {
-            self.latestGlucose = glucose.first
-        }
-
-        guard let bg = glucose.first else {
+        guard let determination = data.determination else {
+            debug(.default, "[LiveActivityManager] pushCurrentContent: no determination available")
             return
         }
 
-        if let determination = determination {
-            let content = LiveActivityAttributes.ContentState(
-                new: bg,
-                prev: latestGlucose,
-                units: settings.units,
-                chart: glucose,
-                settings: settings,
-                determination: determination,
-                override: override,
-                widgetItems: widgetItems
-            )
+        let content = LiveActivityAttributes.ContentState(
+            new: bg,
+            prev: prevGlucose,
+            units: settings.units,
+            chart: glucose,
+            settings: settings,
+            determination: determination,
+            override: data.override,
+            widgetItems: data.widgetItems
+        )
 
-            if let content = content {
-                Task {
-                    await self.pushUpdate(content)
-                }
-            }
-        }
+        await pushUpdate(content)
     }
 }

+ 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 - 0
Trio/Sources/Shortcuts/BaseIntentsRequest.swift

@@ -15,6 +15,7 @@ import Swinject
     @Injected() var apsManager: APSManager!
     @Injected() var overrideStorage: OverrideStorage!
     @Injected() var liveActivityManager: LiveActivityManager!
+    @Injected() var pumpHistoryStorage: PumpHistoryStorage!
 
     let resolver: Resolver
 

+ 38 - 14
Trio/Sources/Shortcuts/Bolus/BolusIntent.swift

@@ -19,27 +19,44 @@ import Swinject
         /// A preferred approach would be to just block negatives and not specify an upperBound here, since it is implemented elsewhere
         inclusiveRange: (lowerBound: 0, upperBound: 200),
         requestValueDialog: IntentDialog(
-            LocalizedStringResource(
+            stringLiteral: String(
+                localized:
                 "Bolus amount (units of insulin)?"
             )
         )
     ) var bolusQuantity: Double
 
     @Parameter(
+        title: LocalizedStringResource("External Insulin"),
+        description: LocalizedStringResource("If toggled, Insulin will be added to IOB but it will not be delivered"),
+        default: false,
+        requestValueDialog: IntentDialog(stringLiteral: String(localized: "External Insulin?"))
+    ) var externalInsulin: Bool
+
+    @Parameter(
         title: LocalizedStringResource("Confirm Before applying"),
         description: LocalizedStringResource("If toggled, you will need to confirm before applying."),
         default: true
     ) var confirmBeforeApplying: Bool
 
     static var parameterSummary: some ParameterSummary {
-        When(\.$confirmBeforeApplying, .equalTo, true, {
-            Summary("Applying \(\.$bolusQuantity) U") {
+        When(\.$externalInsulin, .equalTo, true, {
+            Summary("Log external insulin bolus \(\.$bolusQuantity) U") {
+                \.$externalInsulin
                 \.$confirmBeforeApplying
             }
         }, otherwise: {
-            Summary("Immediately applying \(\.$bolusQuantity) U") {
-                \.$confirmBeforeApplying
-            }
+            When(\.$confirmBeforeApplying, .equalTo, true, {
+                Summary("Applying \(\.$bolusQuantity) U") {
+                    \.$externalInsulin
+                    \.$confirmBeforeApplying
+                }
+            }, otherwise: {
+                Summary("Immediately applying \(\.$bolusQuantity) U") {
+                    \.$externalInsulin
+                    \.$confirmBeforeApplying
+                }
+            })
         })
     }
 
@@ -52,19 +69,26 @@ import Swinject
                 try await requestConfirmation(
                     result: .result(
                         dialog: IntentDialog(
-                            LocalizedStringResource(
-                                "Are you sure you want to bolus \(bolusFormatted) U of insulin?"
+                            stringLiteral: String(
+                                localized:
+                                externalInsulin ? "Are you sure to log \(bolusFormatted) U of external insulin?" :
+                                    "Are you sure to bolus \(bolusFormatted) U of insulin?"
                             )
                         )
                     )
                 )
             }
-
-            let finalBolusDisplay = try await BolusIntentRequest().bolus(amount)
-            return .result(
-                dialog: IntentDialog(finalBolusDisplay)
-            )
-
+            if externalInsulin {
+                let finalExternalBolusDisplay = try await BolusIntentRequest().bolusExternal(amount)
+                return .result(
+                    dialog: IntentDialog(stringLiteral: finalExternalBolusDisplay)
+                )
+            } else {
+                let finalBolusDisplay = try await BolusIntentRequest().bolus(amount)
+                return .result(
+                    dialog: IntentDialog(stringLiteral: finalBolusDisplay)
+                )
+            }
         } catch {
             throw error
         }

+ 28 - 4
Trio/Sources/Shortcuts/Bolus/BolusIntentRequest.swift

@@ -3,28 +3,52 @@ import CoreData
 import Foundation
 
 @available(iOS 16.0,*) final class BolusIntentRequest: BaseIntentsRequest {
-    func bolus(_ bolusAmount: Double) async throws -> LocalizedStringResource {
+    func bolus(_ bolusAmount: Double) async throws -> String {
         var bolusQuantity: Decimal = 0
         switch settingsManager.settings.bolusShortcut {
         // Block boluses if they are disabled
         case .notAllowed:
-            return LocalizedStringResource(
+            return String(
+                localized:
                 "Bolusing via Shortcuts is disabled in Trio settings."
             )
 
         // Block any bolus attempted if it is larger than the max bolus in settings
         case .limitBolusMax:
             if Decimal(bolusAmount) > settingsManager.pumpSettings.maxBolus {
-                return LocalizedStringResource(
+                return String(
+                    localized:
                     "The bolus cannot be larger than the pump setting max bolus (\(settingsManager.pumpSettings.maxBolus.description))."
                 )
             } else {
                 bolusQuantity = apsManager.roundBolus(amount: Decimal(bolusAmount))
             }
             await apsManager.enactBolus(amount: Double(bolusQuantity), isSMB: false, callback: nil)
-            return LocalizedStringResource(
+            return String(
+                localized:
                 "A bolus command of \(bolusQuantity.formatted()) U of insulin was sent."
             )
         }
     }
+
+    func bolusExternal(_ bolusAmount: Double) async throws -> String {
+        var bolusQuantity: Decimal = 0
+        var maxExternal: Decimal { settingsManager.pumpSettings.maxBolus * 3 }
+        if Decimal(bolusAmount) > maxExternal {
+            return String(
+                localized:
+                "The external bolus cannot be larger than 3 x the pump setting max bolus (\(settingsManager.pumpSettings.maxBolus.description))."
+            )
+        } else {
+            bolusQuantity = apsManager.roundBolus(amount: Decimal(bolusAmount))
+            await pumpHistoryStorage.storeExternalInsulinEvent(amount: bolusQuantity, timestamp: Date())
+            // perform determine basal sync
+            try await apsManager.determineBasalSync()
+
+            return String(
+                localized:
+                "An external bolus of \(bolusQuantity.formatted()) U of insulin was recorded."
+            )
+        }
+    }
 }

+ 26 - 14
Trio/Sources/Shortcuts/Carbs/AddCarbPresetIntent.swift

@@ -8,38 +8,37 @@ import Swinject
     static var title: LocalizedStringResource = "Add carbs"
 
     // Description of the action in the Shortcuts app
-    static var description = IntentDescription("Allow to add carbs in Trio.")
-
-    init() {
-        dateAdded = Date()
-    }
+    static var description = IntentDescription(LocalizedStringResource("Allow to add carbs in Trio."))
 
     @Parameter(
         title: "Quantity Carbs",
         description: "Quantity of carbs in g",
         controlStyle: .field,
         inclusiveRange: (lowerBound: 0, upperBound: 200),
-        requestValueDialog: IntentDialog("What is the numeric value of the carb to add")
+        requestValueDialog: IntentDialog(stringLiteral: String(localized: "How many grams of carbs did you eat?"))
     ) var carbQuantity: Double?
 
     @Parameter(
         title: "Quantity fat",
         description: "Quantity of fat in g",
         default: 0.0,
-        inclusiveRange: (0, 200)
+        inclusiveRange: (0, 200),
+        requestValueDialog: IntentDialog(stringLiteral: String(localized: "How many grams of fat did you eat?"))
     ) var fatQuantity: Double
 
     @Parameter(
         title: "Quantity Protein",
         description: "Quantity of Protein in g",
         default: 0.0,
-        inclusiveRange: (0, 200)
+        inclusiveRange: (0, 200),
+        requestValueDialog: IntentDialog(stringLiteral: String(localized: "How many grams of protein did you eat?"))
     ) var proteinQuantity: Double
 
     @Parameter(
         title: "Date",
-        description: "Date of adding"
-    ) var dateAdded: Date
+        description: "Date of adding",
+        requestValueDialog: IntentDialog(stringLiteral: String(localized: "When did you eat ?"))
+    ) var dateAdded: Date?
 
     @Parameter(
         title: "Notes",
@@ -76,13 +75,25 @@ import Swinject
             if let cq = carbQuantity {
                 quantityCarbs = cq
             } else {
-                quantityCarbs = try await $carbQuantity.requestValue("How many carbs do you want to add?")
+                quantityCarbs = try await $carbQuantity.requestValue("How many grams of carbs?")
+            }
+
+            let dateCarbsAdded: Date
+            let dateDefinedByUser: Bool
+            if let da = dateAdded {
+                dateCarbsAdded = da
+                dateDefinedByUser = true
+            } else {
+                dateCarbsAdded = Date()
+                dateDefinedByUser = false
             }
 
             let quantityCarbsName = quantityCarbs.toString()
             if confirmBeforeApplying {
                 try await requestConfirmation(
-                    result: .result(dialog: "Do you want to add \(quantityCarbsName) grams of carbs?")
+                    result: .result(
+                        dialog: IntentDialog(stringLiteral: String(localized: "Add \(quantityCarbsName) grams of carbs?"))
+                    )
                 )
             }
 
@@ -90,8 +101,9 @@ import Swinject
                 quantityCarbs,
                 fatQuantity,
                 proteinQuantity,
-                dateAdded,
-                note
+                dateCarbsAdded,
+                note,
+                dateDefinedByUser
             )
             return .result(
                 dialog: IntentDialog(stringLiteral: finalQuantityCarbsDisplay)

+ 43 - 6
Trio/Sources/Shortcuts/Carbs/CarbPresetIntentRequest.swift

@@ -7,7 +7,8 @@ import Foundation
         _ quantityFat: Double,
         _ quantityProtein: Double,
         _ dateAdded: Date,
-        _ note: String?
+        _ note: String?,
+        _ dateDefinedByUser: Bool
     ) async throws -> String {
         guard quantityCarbs >= 0.0 || quantityFat >= 0.0 || quantityProtein >= 0.0 else {
             return "not adding carbs in Trio"
@@ -30,15 +31,51 @@ import Foundation
             areFetchedFromRemote: false
         )
         var resultDisplay: String
-        resultDisplay = "\(carbs) g carbs"
+        resultDisplay = String(localized: "Added \(String(format: "%.0f", Double(carbs))) g carbs")
         if quantityFat > 0.0 {
-            resultDisplay = "\(resultDisplay) and \(quantityFat) g fats"
+            resultDisplay = String(localized: "\(resultDisplay) and \(String(format: "%.0f", Double(quantityFat))) g fat")
         }
         if quantityProtein > 0.0 {
-            resultDisplay = "\(resultDisplay) and \(quantityProtein) g protein"
+            resultDisplay = String(localized: "\(resultDisplay) and \(String(format: "%.0f", Double(quantityProtein))) g protein")
         }
-        let dateName = dateAdded.formatted()
-        resultDisplay = "\(resultDisplay) added at \(dateName)"
+        if dateDefinedByUser {
+            let dateFormatter = DateFormatter()
+            dateFormatter.dateStyle = .none
+            dateFormatter.timeStyle = .short
+
+            let hourName = dateFormatter.string(from: dateAdded)
+            resultDisplay = String(localized: "\(resultDisplay) at \(hourName)")
+
+            let dayStatus = determineDateStatus(dateAdded)
+            if let dayStatus = dayStatus {
+                resultDisplay = String(localized: "\(resultDisplay)  \(dayStatus)")
+            }
+        }
+
         return resultDisplay
     }
+
+    func determineDateStatus(_ date: Date) -> LocalizedStringResource? {
+        let calendar = Calendar.current
+        let now = Date()
+
+        let dateStartOfDay = calendar.startOfDay(for: date)
+        let nowStartOfDay = calendar.startOfDay(for: now)
+
+        let components = calendar.dateComponents([.day], from: nowStartOfDay, to: dateStartOfDay)
+
+        if let dayDifference = components.day {
+            switch dayDifference {
+            case -1:
+                return LocalizedStringResource(stringLiteral: "Yesterday")
+            case 0:
+                return nil
+            case 1:
+                return LocalizedStringResource(stringLiteral: "Tomorrow")
+            default:
+                return nil
+            }
+        }
+        return nil
+    }
 }

+ 9 - 5
Trio/Sources/Shortcuts/Override/ApplyOverridePresetIntent.swift

@@ -12,7 +12,8 @@ struct ApplyOverridePresetIntent: AppIntent {
     /// The override preset to be applied.
     @Parameter(
         title: LocalizedStringResource("Override"),
-        description: LocalizedStringResource("Override choice")
+        description: LocalizedStringResource("Override choice"),
+        requestValueDialog: IntentDialog(stringLiteral: String(localized: "Which override do you want to apply?"))
     ) var preset: OverridePreset?
 
     /// A boolean parameter that determines whether confirmation is required before applying the override.
@@ -49,7 +50,7 @@ struct ApplyOverridePresetIntent: AppIntent {
                 // Request user selection if no preset is provided
                 presetToApply = try await $preset.requestDisambiguation(
                     among: await OverridePresetsIntentRequest().fetchAndProcessOverrides(),
-                    dialog: IntentDialog(LocalizedStringResource("Select override"))
+                    dialog: IntentDialog(stringLiteral: String(localized: "Select override"))
                 )
             }
 
@@ -60,7 +61,8 @@ struct ApplyOverridePresetIntent: AppIntent {
                 try await requestConfirmation(
                     result: .result(
                         dialog: IntentDialog(
-                            LocalizedStringResource(
+                            stringLiteral: String(
+                                localized:
                                 "Confirm to apply override '\(displayName)'"
                             )
                         )
@@ -72,7 +74,8 @@ struct ApplyOverridePresetIntent: AppIntent {
             if await OverridePresetsIntentRequest().enactOverride(presetToApply) {
                 return .result(
                     dialog: IntentDialog(
-                        LocalizedStringResource(
+                        stringLiteral: String(
+                            localized:
                             "Override '\(presetToApply.name)' applied"
                         )
                     )
@@ -80,7 +83,8 @@ struct ApplyOverridePresetIntent: AppIntent {
             } else {
                 return .result(
                     dialog: IntentDialog(
-                        LocalizedStringResource(
+                        stringLiteral: String(
+                            localized:
                             "Override '\(presetToApply.name)' failed"
                         )
                     )

+ 1 - 1
Trio/Sources/Shortcuts/Override/CancelOverrideIntent.swift

@@ -16,7 +16,7 @@ struct CancelOverrideIntent: AppIntent {
     @MainActor func perform() async throws -> some ProvidesDialog {
         await OverridePresetsIntentRequest().cancelOverride()
         return .result(
-            dialog: IntentDialog(LocalizedStringResource("Override canceled"))
+            dialog: IntentDialog(stringLiteral: String(localized: "Override canceled"))
         )
     }
 }

+ 14 - 4
Trio/Sources/Shortcuts/TempPresets/ApplyTempPresetIntent.swift

@@ -10,7 +10,11 @@ struct ApplyTempPresetIntent: AppIntent {
     static var description = IntentDescription("Enable a Temporary Target")
 
     /// The temporary target preset to be applied.
-    @Parameter(title: "Preset") var preset: TempPreset?
+    @Parameter(
+        title: "Preset",
+        description: "the preset to apply",
+        requestValueDialog: IntentDialog(stringLiteral: String(localized: "Which preset to apply?"))
+    ) var preset: TempPreset?
 
     /// A boolean parameter that determines whether confirmation is required before applying the temporary target.
     @Parameter(
@@ -71,7 +75,11 @@ struct ApplyTempPresetIntent: AppIntent {
             // Request confirmation before applying if required
             if confirmBeforeApplying {
                 try await requestConfirmation(
-                    result: .result(dialog: "Confirm to apply Temporary Target '\(displayName)'")
+                    result: .result(
+                        dialog: IntentDialog(
+                            stringLiteral: String(localized: "Confirm to apply Temporary Target '\(displayName)'")
+                        )
+                    )
                 )
             }
 
@@ -79,7 +87,8 @@ struct ApplyTempPresetIntent: AppIntent {
             if await intentRequest.enactTempTarget(presetToApply) {
                 return .result(
                     dialog: IntentDialog(
-                        LocalizedStringResource(
+                        stringLiteral: String(
+                            localized:
                             "Temporary Target '\(presetToApply.name)' applied"
                         )
                     )
@@ -87,7 +96,8 @@ struct ApplyTempPresetIntent: AppIntent {
             } else {
                 return .result(
                     dialog: IntentDialog(
-                        LocalizedStringResource(
+                        stringLiteral: String(
+                            localized:
                             "Temporary Target '\(presetToApply.name)' failed"
                         )
                     )

+ 1 - 1
Trio/Sources/Shortcuts/TempPresets/CancelTempPresetIntent.swift

@@ -16,7 +16,7 @@ struct CancelTempPresetIntent: AppIntent {
     @MainActor func perform() async throws -> some ProvidesDialog {
         await TempPresetsIntentRequest().cancelTempTarget()
         return .result(
-            dialog: IntentDialog(stringLiteral: "Temporary Target canceled")
+            dialog: IntentDialog(stringLiteral: String(localized: "Temporary Target canceled"))
         )
     }
 }

+ 12 - 9
TrioTests/BolusCalculatorTests/BolusCalculatorTests.swift

@@ -84,7 +84,7 @@ 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
 
@@ -135,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
@@ -180,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,
@@ -208,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
@@ -217,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
             """
         )
     }
@@ -522,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")
 
@@ -662,7 +662,10 @@ import Testing
         }
     }
 
-    @Test("Calculate insulin with backdated carbs") func testHandleBolusCalculationFunction() async throws {
+    @Test(
+        "Calculate insulin with backdated carbs",
+        .enabled(if: false, "Flaky test, disabled while investigating")
+    ) func testHandleBolusCalculationFunction() async throws {
         // STEP 1: Setup test scenario
         let currentDate = Date()
         let backdatedCarbsDate = currentDate.addingTimeInterval(-120 * 60) // 2 hours ago

+ 4 - 1
TrioTests/CoreDataTests/GlucoseStorageTests.swift

@@ -131,7 +131,10 @@ import Testing
         #expect(entry.eventType == .capillaryGlucose, "Type should be capillaryGlucose")
     }
 
-    @Test("Test glucose alarms") func testGlucoseAlarms() async throws {
+    @Test(
+        "Test glucose alarms",
+        .enabled(if: false, "Flaky test, disabled while investigating")
+    ) func testGlucoseAlarms() async throws {
         // Given
         let lowGlucose = [
             BloodGlucose(direction: BloodGlucose.Direction.flat, date: 123, dateString: Date(), glucose: 55)

+ 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)
+    }
+}

+ 9 - 4
fastlane/Fastfile

@@ -180,10 +180,15 @@ platform :ios do
     )
 
     def configure_bundle_id(name, identifier, capabilities)
-      bundle_id = Spaceship::ConnectAPI::BundleId.find(identifier) || Spaceship::ConnectAPI::BundleId.create(name: name, identifier: identifier)
-      capabilities.each { |capability|
-        bundle_id.create_capability(capability)
-      }
+      bundle_id = Spaceship::ConnectAPI::BundleId.find(identifier) || Spaceship::ConnectAPI::BundleId.create(
+        name:       name,
+        identifier: identifier,
+        platform:   "IOS"
+      )
+      existing = bundle_id.get_capabilities.map(&:capability_type)
+      capabilities.reject { |c| existing.include?(c) }.each do |cap|
+        bundle_id.create_capability(cap)
+      end
     end
 
     configure_bundle_id("Trio", "#{BUNDLE_ID}", [

+ 2 - 2
fastlane/testflight.md

@@ -166,8 +166,8 @@ _Referring to the table below, tap on each **IDENTIFIER** that has a different *
 |:--|:--|:--|
 | Trio | XC org nightscout TEAMID trio | org.nightscout.TEAMID.trio |
 | Trio LiveActivity | - | org.nightscout.TEAMID.trio.LiveActivity |
-| Trio Watch | XC IDENTIFIER | org.nightscout.TEAMID.trio.watchkitapp |
-| Trio WatchKit Extension | XC IDENTIFIER | org.nightscout.TEAMID.trio.watchkitapp.watchkitextension |
+| Trio Watch App | XC IDENTIFIER | org.nightscout.TEAMID.trio.watchkitapp |
+| Trio Watch Complication | XC IDENTIFIER | org.nightscout.TEAMID.trio.watchkitapp.TrioWatchComplication |
 
 ## Add App Group to Bundle Identifiers
 

+ 0 - 0
oref0_source_version.txt


部分文件因文件數量過多而無法顯示