Просмотр исходного кода

Merge latest dev, address PR comments

Marvin Polscheit 10 месяцев назад
Родитель
Сommit
f8d4e432c1
93 измененных файлов с 7154 добавлено и 8067 удалено
  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. 34 10
      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. BIN
      Trio/Resources/Assets.xcassets/app_icons/trioCircledNoBackground.appiconset/trioCircledNoBackground watch.png
  16. BIN
      Trio/Resources/Assets.xcassets/app_icons/trioCircledNoBackground.appiconset/trioCircledNoBackground.png
  17. BIN
      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. 6 6
      Trio/Sources/APS/APSManager.swift
  37. 1 1
      Trio/Sources/APS/OpenAPS/Constants.swift
  38. 45 19
      Trio/Sources/APS/OpenAPS/OpenAPS.swift
  39. 13 15
      Trio/Sources/APS/Storage/DeterminationStorage.swift
  40. 1 1
      Trio/Sources/APS/Storage/TDDStorage.swift
  41. 1 1
      Trio/Sources/Application/AppState.swift
  42. 11 4
      Trio/Sources/Helpers/BackgroundTask+Helper.swift
  43. 2 0
      Trio/Sources/Helpers/Color+Extensions.swift
  44. 1 1
      Trio/Sources/Helpers/MainChartHelper.swift
  45. 4590 7030
      Trio/Sources/Localizations/Main/Localizable.xcstrings
  46. 32 0
      Trio/Sources/Models/BloodGlucose.swift
  47. 7 3
      Trio/Sources/Models/Oref2_variables.swift
  48. 1 1
      Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+Overrides.swift
  49. 30 13
      Trio/Sources/Modules/Adjustments/View/AdjustmentsRootView.swift
  50. 1 5
      Trio/Sources/Modules/Adjustments/View/Overrides/AdjustmentsRootView+Overrides.swift
  51. 1 7
      Trio/Sources/Modules/Adjustments/View/TempTargets/AdjustmentsRootView+TempTargets.swift
  52. 25 7
      Trio/Sources/Modules/GeneralSettings/View/UnitsLimitsSettingsRootView.swift
  53. 1 1
      Trio/Sources/Modules/Home/View/Chart/ChartElements/SelectionPopoverView.swift
  54. 1 1
      Trio/Sources/Modules/Onboarding/View/OnboardingView+AlgorithmUtil.swift
  55. 25 8
      Trio/Sources/Modules/Onboarding/View/OnboardingView+Util.swift
  56. 1 1
      Trio/Sources/Modules/SMBSettings/View/SMBSettingsRootView.swift
  57. 0 1
      Trio/Sources/Modules/Settings/SettingItems.swift
  58. 350 0
      Trio/Sources/Modules/Stat/StatStateModel+Setup/GlucoseStatsSetup.swift
  59. 1 1
      Trio/Sources/Modules/Stat/StatStateModel+Setup/LoopChartSetup.swift
  60. 51 5
      Trio/Sources/Modules/Stat/StatStateModel.swift
  61. 43 9
      Trio/Sources/Modules/Stat/View/StatChartUtils.swift
  62. 80 20
      Trio/Sources/Modules/Stat/View/StatRootView.swift
  63. 253 0
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseDailyDistributionChart.swift
  64. 421 0
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseDailyPercentileChart.swift
  65. 14 14
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseDistributionChart.swift
  66. 32 32
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucosePercentileChart.swift
  67. 71 0
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucosePercentileDetailView.swift
  68. 189 121
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseSectorChart.swift
  69. 4 4
      Trio/Sources/Modules/Stat/View/ViewElements/Insulin/BolusStatsView.swift
  70. 2 1
      Trio/Sources/Modules/Stat/View/ViewElements/Insulin/TotalDailyDoseChart.swift
  71. 5 3
      Trio/Sources/Modules/Stat/View/ViewElements/Looping/LoopBarChartView.swift
  72. 3 2
      Trio/Sources/Modules/Stat/View/ViewElements/Meal/MealStatsView.swift
  73. 2 2
      Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift
  74. 2 4
      Trio/Sources/Modules/Treatments/View/ForecastChart.swift
  75. 2 4
      Trio/Sources/Modules/Treatments/View/PopupView.swift
  76. 1 1
      Trio/Sources/Services/LiveActivity/LiveActivityAttributes+Helper.swift
  77. 115 221
      Trio/Sources/Services/LiveActivity/LiveActivityManager.swift
  78. 6 4
      Trio/Sources/Shortcuts/Bolus/BolusIntent.swift
  79. 8 5
      Trio/Sources/Shortcuts/Bolus/BolusIntentRequest.swift
  80. 26 14
      Trio/Sources/Shortcuts/Carbs/AddCarbPresetIntent.swift
  81. 43 6
      Trio/Sources/Shortcuts/Carbs/CarbPresetIntentRequest.swift
  82. 9 5
      Trio/Sources/Shortcuts/Override/ApplyOverridePresetIntent.swift
  83. 1 1
      Trio/Sources/Shortcuts/Override/CancelOverrideIntent.swift
  84. 14 4
      Trio/Sources/Shortcuts/TempPresets/ApplyTempPresetIntent.swift
  85. 1 1
      Trio/Sources/Shortcuts/TempPresets/CancelTempPresetIntent.swift
  86. 4 1
      TrioTests/BolusCalculatorTests/BolusCalculatorTests.swift
  87. 4 1
      TrioTests/CoreDataTests/GlucoseStorageTests.swift
  88. 9 4
      fastlane/Fastfile
  89. 2 2
      fastlane/testflight.md
  90. 10 1
      oref0_source_version.txt
  91. 54 59
      trio-oref/lib/determine-basal/determine-basal.js
  92. 9 247
      trio-oref/lib/glucose-get-last.js
  93. 1 1
      trio-oref/oref_source_file_info.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.23
-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.2
+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
         )
     }

+ 34 - 10
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 */; };
@@ -444,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 */; };
@@ -844,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>"; };
@@ -1260,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>"; };
@@ -2368,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 */,
@@ -2805,6 +2813,7 @@
 		BD249D952D42FCA800412DEB /* StatStateModel+Setup */ = {
 			isa = PBXGroup;
 			children = (
+				C2A6D1E32DB1581D0036DB66 /* GlucoseStatsSetup.swift */,
 				BD249DA02D42FD1000412DEB /* TDDSetup.swift */,
 				BD249D9C2D42FCF300412DEB /* MealStatsSetup.swift */,
 				BD249D982D42FCCA00412DEB /* BolusStatsSetup.swift */,
@@ -3470,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 */,
@@ -4086,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 */,
@@ -4170,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 */,
@@ -4199,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 */,
@@ -4243,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 */,
@@ -4350,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 */,
@@ -4755,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;
@@ -4823,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;
@@ -4884,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;
@@ -4917,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;
 		};
@@ -4926,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;
@@ -4958,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
+  }
+}

BIN
Trio/Resources/Assets.xcassets/app_icons/trioCircledNoBackground.appiconset/trioCircledNoBackground watch.png


BIN
Trio/Resources/Assets.xcassets/app_icons/trioCircledNoBackground.appiconset/trioCircledNoBackground.png


BIN
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);
 }

+ 6 - 6
Trio/Sources/APS/APSManager.swift

@@ -440,12 +440,6 @@ 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
         }
 
@@ -668,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")

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

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

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

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

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

Разница между файлами не показана из-за своего большого размера
+ 4590 - 7030
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)

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

+ 0 - 1
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",

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

+ 2 - 4
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

+ 2 - 4
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)

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

+ 115 - 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,18 @@ 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 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 +238,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 +272,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 +285,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 +329,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)
     }
 }

+ 6 - 4
Trio/Sources/Shortcuts/Bolus/BolusIntent.swift

@@ -19,7 +19,8 @@ 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)?"
             )
         )
@@ -68,8 +69,9 @@ import Swinject
                 try await requestConfirmation(
                     result: .result(
                         dialog: IntentDialog(
-                            LocalizedStringResource(
-                                "Are you sure you want to bolus \(bolusFormatted) U of insulin?"
+                            stringLiteral: String(
+                                localized:
+                                "Are you sure to bolus \(bolusFormatted) U of insulin?"
                             )
                         )
                     )
@@ -83,7 +85,7 @@ import Swinject
             } else {
                 let finalBolusDisplay = try await BolusIntentRequest().bolus(amount)
                 return .result(
-                    dialog: IntentDialog(finalBolusDisplay)
+                    dialog: IntentDialog(stringLiteral: finalBolusDisplay)
                 )
             }
         } catch {

+ 8 - 5
Trio/Sources/Shortcuts/Bolus/BolusIntentRequest.swift

@@ -3,26 +3,29 @@ 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."
             )
         }
@@ -44,7 +47,7 @@ import Foundation
 
             return String(
                 localized:
-                "A external bolus of \(bolusQuantity.formatted()) U of insulin was recorded."
+                "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"))
         )
     }
 }

+ 4 - 1
TrioTests/BolusCalculatorTests/BolusCalculatorTests.swift

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

+ 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
 

+ 10 - 1
oref0_source_version.txt

@@ -1,6 +1,15 @@
-oref0 branch: tcd-fixes - git version: af8f79c
+oref0 branch: dev - git version: 37896e5
 
 Last commits:
+37896e5 Merge pull request #48 from nightscout/fix-bundle-naming
+f21a187 Rename output library to trio_[name]
+c0b46d3 Merge pull request #47 from nightscout/fix-400-guard
+1591b14 Remove leftover != 400 condition
+4204b12 Remove autoISF adjustments from glucose-get-last
+2596f3f Refactor 400 glucose guard: - Remove old 400 guard - Replace with check that only disables SMBs and keeps TBR at neutral == current basal rate, if shouldProtectDueToHIGH is not null/undefined and true - Remove all other 400 guards
+2ca5e20 Change folder name to Trio from iAPS
+6ad27e9 Merge pull request #46 from nightscout/tcd
+d98e4fc Merge pull request #45 from nightscout/tcd-fixes
 af8f79c Fix typo; remove enableDynamicCR setting from profile/index.js
 ffb9374 Always add minPredBG to rT object
 814b629 Remove dynamicCR

+ 54 - 59
trio-oref/lib/determine-basal/determine-basal.js

@@ -44,15 +44,15 @@ function convert_bg(value, profile)
         return Math.round(value);
     }
 }
-function enable_smb(profile, microBolusAllowed, meal_data, bg, target_bg, high_bg, oref_variables, time) {
-    if (oref_variables.smbIsScheduledOff){
+function enable_smb(profile, microBolusAllowed, meal_data, bg, target_bg, high_bg, trio_custom_variables, time) {
+    if (trio_custom_variables.smbIsScheduledOff){
         /* Below logic is related to profile overrides which can disable SMBs or disable them for a scheduled window.
          * SMBs will be disabled from [start, end), such that if an SMB is scheduled to be disabled from 10 AM to 2 PM,
          * an SMB will not be allowed from 10:00:00 until 1:59:59.
          */
         let currentHour = new Date(time.getHours());
-        let startTime = oref_variables.start;
-        let endTime = oref_variables.end;
+        let startTime = trio_custom_variables.start;
+        let endTime = trio_custom_variables.end;
 
         if (startTime < endTime && (currentHour >= startTime && currentHour < endTime)) {
             console.error("SMB disabled: current time is in SMB disabled scheduled")
@@ -79,8 +79,8 @@ function enable_smb(profile, microBolusAllowed, meal_data, bg, target_bg, high_b
         console.error("SMB disabled due to Bolus Wizard activity in the last 6 hours.");
         return false;
     // Disable if invalid CGM reading (HIGH)
-    } else if (bg == 400) {
-            console.error("Invalid CGM (HIGH). SMBs disabled.");
+    } else if (!!trio_custom_variables.shouldProtectDueToHIGH) {
+        console.error("Invalid CGM (HIGH). SMBs disabled.");
         return false;
     }
 
@@ -142,22 +142,22 @@ function enable_smb(profile, microBolusAllowed, meal_data, bg, target_bg, high_b
 }
 
 
-var determine_basal = function determine_basal(glucose_status, currenttemp, iob_data, profile, autosens_data, meal_data, tempBasalFunctions, microBolusAllowed, reservoir_data, currentTime, pumphistory, preferences, basalprofile, oref2_variables, middleWare) {
+var determine_basal = function determine_basal(glucose_status, currenttemp, iob_data, profile, autosens_data, meal_data, tempBasalFunctions, microBolusAllowed, reservoir_data, currentTime, pumphistory, preferences, basalprofile, trio_custom_variables, middleWare) {
 
     var profileTarget = profile.min_bg;
-    var overrideTarget = oref2_variables.overrideTarget;
-    if (overrideTarget != 0 && overrideTarget != 6 && oref2_variables.useOverride && !profile.temptargetSet) {
+    var overrideTarget = trio_custom_variables.overrideTarget;
+    if (overrideTarget != 0 && overrideTarget != 6 && trio_custom_variables.useOverride && !profile.temptargetSet) {
         profileTarget = overrideTarget;
     }
-    const smbIsOff = oref2_variables.smbIsOff;
-    const advancedSettings = oref2_variables.advancedSettings;
-    const isfAndCr = oref2_variables.isfAndCr;
-    const isf = oref2_variables.isf;
-    const cr_ = oref2_variables.cr;
-    const smbMinutes = oref2_variables.smbMinutes;
-    const uamMinutes = oref2_variables.uamMinutes;
+    const smbIsOff = trio_custom_variables.smbIsOff;
+    const advancedSettings = trio_custom_variables.advancedSettings;
+    const isfAndCr = trio_custom_variables.isfAndCr;
+    const isf = trio_custom_variables.isf;
+    const cr_ = trio_custom_variables.cr;
+    const smbMinutes = trio_custom_variables.smbMinutes;
+    const uamMinutes = trio_custom_variables.uamMinutes;
     // tdd past 24 hour
-    let tdd = oref2_variables.currentTDD;
+    let tdd = trio_custom_variables.currentTDD;
     var logOutPut = "";
     var tddReason = "";
 
@@ -174,12 +174,12 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_
 
 
 
-    const weightedAverage = oref2_variables.weightedAverage;
+    const weightedAverage = trio_custom_variables.weightedAverage;
     var overrideFactor = 1;
     var sensitivity = profile.sens;
     var carbRatio = profile.carb_ratio;
-    if (oref2_variables.useOverride) {
-        overrideFactor = oref2_variables.overridePercentage / 100;
+    if (trio_custom_variables.useOverride) {
+        overrideFactor = trio_custom_variables.overridePercentage / 100;
         if (isfAndCr) {
             sensitivity /= overrideFactor;
             carbRatio /= overrideFactor;
@@ -189,7 +189,7 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_
         }
     }
     const weightPercentage = profile.weightPercentage;
-    const average_total_data = oref2_variables.average_total_data;
+    const average_total_data = trio_custom_variables.average_total_data;
 
     // In case the autosens.min/max limits are reversed:
     const minLimitChris = Math.min(profile.autosens_min, profile.autosens_max);
@@ -390,11 +390,11 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_
     var basal = profile_current_basal;
 
     // Print Current Override factor, if any
-    if (oref2_variables.useOverride) {
-        if (oref2_variables.duration == 0) {
+    if (trio_custom_variables.useOverride) {
+        if (trio_custom_variables.duration == 0) {
             console.log("Profile Override is active. Override " + round(overrideFactor * 100, 0) + "%. Override Duration: " + "Enabled indefinitely");
         } else
-            console.log("Profile Override is active. Override " + round(overrideFactor * 100, 0) + "%. Override Expires in: " + oref2_variables.duration + " min.");
+            console.log("Profile Override is active. Override " + round(overrideFactor * 100, 0) + "%. Override Expires in: " + trio_custom_variables.duration + " min.");
     }
 
     var bgTime = new Date(glucose_status.date);
@@ -425,21 +425,18 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_
     if (bg <= 10 || bg === 38 || noise >= 3) {  //Dexcom is in ??? mode or calibrating, or xDrip reports high noise
         rT.reason = "CGM is calibrating, in ??? state, or noise is high";
     }
-    var tooflat=false;
-    if (bg > 60 && glucose_status.delta == 0 && glucose_status.short_avgdelta > -1 && glucose_status.short_avgdelta < 1 && glucose_status.long_avgdelta > -1 && glucose_status.long_avgdelta < 1 && bg != 400) {
+    if (bg > 60 && glucose_status.delta == 0 && glucose_status.short_avgdelta > -1 && glucose_status.short_avgdelta < 1 && glucose_status.long_avgdelta > -1 && glucose_status.long_avgdelta < 1) {
         if (glucose_status.device == "fakecgm") {
             console.error("CGM data is unchanged (" + convert_bg(bg,profile) + "+" + convert_bg(glucose_status.delta,profile)+ ") for 5m w/ " + convert_bg(glucose_status.short_avgdelta,profile) + " mg/dL ~15m change & " + convert_bg(glucose_status.long_avgdelta,2) + " mg/dL ~45m change");
             console.error("Simulator mode detected (" + glucose_status.device + "): continuing anyway");
-        } else if (bg != 400) {
-            tooflat=true;
-        }
+        } 
     }
 
     if (minAgo > 12 || minAgo < -5) { // Dexcom data is too old, or way in the future
         rT.reason = "If current system time " + systemTime + " is correct, then BG data is too old. The last BG data was read "+minAgo+"m ago at "+bgTime;
 
         // if BG is too old/noisy, or is completely unchanging, cancel any high temps and shorten any long zero temps
-    } else if ( glucose_status.short_avgdelta === 0 && glucose_status.long_avgdelta === 0 && bg != 400 ) {
+    } else if ( glucose_status.short_avgdelta === 0 && glucose_status.long_avgdelta === 0 ) {
         if ( glucose_status.last_cal && glucose_status.last_cal < 3 ) {
             rT.reason = "CGM was just calibrated";
         } else {
@@ -447,30 +444,28 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_
         }
     }
 
-    if (bg != 400) {
-        if (bg <= 10 || bg === 38 || noise >= 3 || minAgo > 12 || minAgo < -5 || ( glucose_status.short_avgdelta === 0 && glucose_status.long_avgdelta === 0 ) ) {
-            if (currenttemp.rate >= basal) { // high temp is running
-                rT.reason += ". Canceling high temp basal of " + currenttemp.rate;
-                rT.deliverAt = deliverAt;
-                rT.temp = 'absolute';
-                rT.duration = 0;
-                rT.rate = 0;
-                return rT;
-                // don't use setTempBasal(), as it has logic that allows <120% high temps to continue running
-                //return tempBasalFunctions.setTempBasal(basal, 30, profile, rT, currenttemp);
-            } else if ( currenttemp.rate === 0 && currenttemp.duration > 30 ) { //shorten long zero temps to 30m
-                rT.reason += ". Shortening " + currenttemp.duration + "m long zero temp to 30m. ";
-                rT.deliverAt = deliverAt;
-                rT.temp = 'absolute';
-                rT.duration = 30;
-                rT.rate = 0;
-                return rT;
-                // don't use setTempBasal(), as it has logic that allows long zero temps to continue running
-                //return tempBasalFunctions.setTempBasal(0, 30, profile, rT, currenttemp);
-            } else { //do nothing.
-                rT.reason += ". Temp " + currenttemp.rate + " <= current basal " + basal + "U/hr; doing nothing. ";
-                return rT;
-            }
+    if (bg <= 10 || bg === 38 || noise >= 3 || minAgo > 12 || minAgo < -5 || ( glucose_status.short_avgdelta === 0 && glucose_status.long_avgdelta === 0 ) ) {
+        if (currenttemp.rate >= basal) { // high temp is running
+            rT.reason += ". Canceling high temp basal of " + currenttemp.rate;
+            rT.deliverAt = deliverAt;
+            rT.temp = 'absolute';
+            rT.duration = 0;
+            rT.rate = 0;
+            return rT;
+            // don't use setTempBasal(), as it has logic that allows <120% high temps to continue running
+            //return tempBasalFunctions.setTempBasal(basal, 30, profile, rT, currenttemp);
+        } else if ( currenttemp.rate === 0 && currenttemp.duration > 30 ) { //shorten long zero temps to 30m
+            rT.reason += ". Shortening " + currenttemp.duration + "m long zero temp to 30m. ";
+            rT.deliverAt = deliverAt;
+            rT.temp = 'absolute';
+            rT.duration = 30;
+            rT.rate = 0;
+            return rT;
+            // don't use setTempBasal(), as it has logic that allows long zero temps to continue running
+            //return tempBasalFunctions.setTempBasal(0, 30, profile, rT, currenttemp);
+        } else { //do nothing.
+            rT.reason += ". Temp " + currenttemp.rate + " <= current basal " + basal + "U/hr; doing nothing. ";
+            return rT;
         }
     }
 
@@ -782,7 +777,7 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_
             bg,
             target_bg,
             high_bg,
-            oref2_variables,
+            trio_custom_variables,
             systemTime
         );
     }
@@ -1508,11 +1503,11 @@ var maxDelta_bg_threshold;
                 uamMinutesSetting = profile.maxUAMSMBBasalMinutes;
             }
 
-            if (oref2_variables.useOverride && advancedSettings && smbMinutes !== smbMinutesSetting) {
+            if (trio_custom_variables.useOverride && advancedSettings && smbMinutes !== smbMinutesSetting) {
                 console.error("SMB Max Minutes - setting overriden from " + smbMinutesSetting + " to " + smbMinutes);
                 smbMinutesSetting = smbMinutes;
             }
-            if (oref2_variables.useOverride && advancedSettings && uamMinutes !== uamMinutesSetting) {
+            if (trio_custom_variables.useOverride && advancedSettings && uamMinutes !== uamMinutesSetting) {
                 console.error("UAM Max Minutes - setting overriden from " + uamMinutesSetting + " to " + uamMinutes);
                 uamMinutesSetting = uamMinutes;
             }
@@ -1621,8 +1616,8 @@ var maxDelta_bg_threshold;
 
         var maxSafeBasal = tempBasalFunctions.getMaxSafeBasal(profile);
 
-
-        if (bg == 400) {
+        // set neutral TBR at current basal rate because glucose is considered as requiring dosing Protect due to HIGH (400 mg/dL)
+        if (!!trio_custom_variables.shouldProtectDueToHIGH) {
             return tempBasalFunctions.setTempBasal(profile.current_basal, 30, profile, rT, currenttemp);
         }
 

+ 9 - 247
trio-oref/lib/glucose-get-last.js

@@ -1,17 +1,10 @@
 function getDateFromEntry(entry) {
-return entry.date || Date.parse(entry.display_time) || Date.parse(entry.dateString);
-}
-
-function round(value, digits)
-{
-    if (! digits) { digits = 0; }
-    var scale = Math.pow(10, digits);
-    return Math.round(value * scale) / scale;
+  return entry.date || Date.parse(entry.display_time) || Date.parse(entry.dateString);
 }
 
 var getLastGlucose = function (data) {
     data = data.filter(function(obj) {
-    return obj.glucose || obj.sgv;
+      return obj.glucose || obj.sgv;
     }).map(function prepGlucose (obj) {
         //Support the NS sgv field to avoid having to convert in a custom way
         obj.glucose = obj.glucose || obj.sgv;
@@ -51,12 +44,12 @@ var getLastGlucose = function (data) {
                 //console.error(then.glucose, minutesago, avgdelta);
             //}
             // use the average of all data points in the last 2.5m for all further "now" calculations
-            if (-2 < minutesago && minutesago < 2.5) {
+            if (-2 < minutesago && minutesago <= 2.5) {
                 now.glucose = ( now.glucose + then.glucose ) / 2;
                 now_date = ( now_date + then_date ) / 2;
                 //console.error(then.glucose, now.glucose);
             // short_deltas are calculated from everything ~5-15 minutes ago
-            } else if (2.5 < minutesago && minutesago < 17.5) {
+            } else if (2.5 < minutesago && minutesago <= 17.5) {
                 //console.error(minutesago, avgdelta);
                 short_deltas.push(avgdelta);
                 // last_deltas are calculated from everything ~5 minutes ago
@@ -73,26 +66,6 @@ var getLastGlucose = function (data) {
     var last_delta = 0;
     var short_avgdelta = 0;
     var long_avgdelta = 0;
-
-    // start autoISF by https://github.com/ga-zelle/autoISF , relevant variables and functions
-    // mod 7: append 2 variables for 5% range
-    var autoISF_duration = 0;
-    var autoISF_average = 0;
-    // mod 8: append 3 variables for deltas based on regression analysis
-    var slope05 = 0;
-    var slope15 = 0;
-    var slope40 = 0;
-    // mod 14f: append results from best fitting parabola
-    var dura_p = 0;
-    var delta_pl = 0;
-    var delta_pn = 0;
-    var r_squ = 0;
-    var bg_acceleration = 0;
-    var a_0 = 0;
-    var a_1 = 0;
-    var a_2 = 0;
-    var pp_debug = "autoISF Mod14-Debug: ";
-
     if (last_deltas.length > 0) {
         last_delta = last_deltas.reduce(function(a, b) { return a + b; }) / last_deltas.length;
     }
@@ -102,228 +75,17 @@ var getLastGlucose = function (data) {
     if (long_deltas.length > 0) {
         long_avgdelta = long_deltas.reduce(function(a, b) { return a + b; }) / long_deltas.length;
     }
-    var bw = 0.05;
-    var sumBG = now.glucose;
-    var oldavg = now.glucose;
-    var minutesdur = 0;
-    for (var i = 1; i < data.length; i++) {
-        var then = data[i];
-        var then_date = getDateFromEntry(then);
-    //  mod 7c: stop the series if there was a CGM gap greater than 13 minutes, i.e. 2 regular readings
-            if (Math.round((now_date - then_date) / (1000 * 60)) - minutesdur > 13) {
-            break;
-            }
-            if (then.glucose > oldavg*(1-bw) && then.glucose < oldavg*(1+bw)) {
-            sumBG += then.glucose;
-            oldavg = sumBG / (i+1);
-            minutesdur = Math.round((now_date - then_date) / (1000 * 60));
-            } else {
-            break;
-        }
-    }
-            autoISF_average = oldavg;
-            autoISF_duration = minutesdur;
 
-            // mod 8: calculate 3 variables for deltas based on linear regression
-            // initially just test the handling of arguments
-            var slope05 = 1.05;
-            var slope15 = 1.15;
-            var slope40 = 1.40;
-
-            // mod 8a: now do the real maths based on
-            // http://www.carl-engler-schule.de/culm/culm/culm2/th_messdaten/mdv2/auszug_ausgleichsgerade.pdf
-            var sumBG  = 0;         // y
-            var sumt   = 0;         // x
-            var sumBG2 = 0;         // y^2
-            var sumt2  = 0;         // x^2
-            var sumxy  = 0;         // x*y
-            //double a;
-            var b;                   // y = a + b * x
-            var level = 7.5;
-            var minutesL;
-            // here, longer deltas include all values from 0 up the related limit
-            for (var i = 0; i < data.length; i++) {
-                var then = data[i];
-                var then_date = getDateFromEntry(then);
-                minutesL = (now_date - then_date) / (1000 * 60);
-                // watch out: the scan goes backwards in time, so delta has wrong sign
-                if(i * sumt2 == sumt * sumt) {
-                    b = 0.0;
-                }
-                else {
-                    b = (i * sumxy - sumt * sumBG) / (i * sumt2 - sumt * sumt);
-                }
-                if (minutesL > level && level == 7.5) {
-                    slope05 = -b * 5;
-                    level = 17.5;
-                }
-                if (minutesL > level && level == 17.5) {
-                    slope15 = -b * 5;
-                    level = 42.5;
-                }
-                if (minutesL > level && level == 42.5) {
-                    slope40 = -b * 5;
-                    break;
-                }
-
-                sumt   += minutesL;
-                sumt2  += minutesL * minutesL;
-                sumBG  += then.glucose;
-                sumBG2 += then.glucose * then.glucose;
-                sumxy  += then.glucose * minutesL;
-            }
-
-            // mod 14f: calculate best parabola and determine delta by extending it 5 minutes into the future
-            // nach https://www.codeproject.com/Articles/63170/Least-Squares-Regression-for-Quadratic-Curve-Fitti
-            //
-            //  y = a2*x^2 + a1*x + a0      or
-            //  y = a*x^2  + b*x  + c       respectively
-
-            // initially just test the handling of arguments
-            var dura_p  = 0;
-            var delta_pl = 0;
-            var delta_pn = 0;
-            var bg_acceleration = 0;
-            var r_squ   = 0;
-            var best_a = 0;
-            var best_b = 0;
-            var best_c = 0;
-            var a_0 = 0;
-            var a_1 = 0;
-            var a_2 = 0;
-
-            if (data.length <= 3) {                      // last 3 points make a trivial parabola
-                dura_p  = 0;
-                delta_pl = 0;
-                delta_pn = 0;
-                bg_acceleration = 0;
-                r_squ   = 0;
-                a_0 = 0;
-                a_1 = 0;
-                a_2 = 0;
-            } else {
-                //double corrMin = 0.90;                  // go backwards until the correlation coefficient goes below
-                var sy    = 0;                        // y
-                var sx    = 0;                        // x
-                var sx2   = 0;                        // x^2
-                var sx3   = 0;                        // x^3
-                var sx4   = 0;                        // x^4
-                var sxy   = 0;                        // x*y
-                var sx2y  = 0;                        // x^2*y
-                var corrMax = 0;
-                var iframe = data[0];
-                var time_0 = getDateFromEntry(iframe);
-                var ti_last = 0;
-                //# for best numerical accurarcy time and bg must be of same order of magnitude
-                var scaleTime = 300;                  //# in 5m; values are  0, 1, 2, 3, 4, ...
-                var scaleBg   =  50;                  //# TIR range is now 1.4 - 3.6
-
-                for (var i = 0; i < data.length; i++) {
-                    var then = data[i];
-                    var then_date = getDateFromEntry(then);
-                    // skip records older than 47.5 minutes
-                    var ti = (then_date - time_0) / 1000 / scaleTime;
-                    if (-ti *scaleTime > 47 * 60) {                        // skip records older than 47.5 minutes
-                        break;
-                    } else if (ti < ti_last - 7.5 * 60 / scaleTime) {       // stop scan if a CGM gap > 7.5 minutes is detected
-                        if ( i<3) {                             // history too short for fit
-                            dura_p =  -ti_last / 60;
-                            delta_pl = 0;
-                            delta_pn = 0;
-                            bg_acceleration= 0;
-                            r_squ = 0;
-                            a_0 = 0;
-                            a_1 = 0;
-                            a_2 = 0;
-                        }
-                        break;
-                    }
-                    ti_last = ti;
-                    var bg = then.glucose/scaleBg;
-                    sx += ti;
-                    sx2 += Math.pow(ti, 2);
-                    sx3 += Math.pow(ti, 3);
-                    sx4 += Math.pow(ti, 4);
-                    sy  += bg;
-                    sxy += ti * bg;
-                    sx2y += Math.pow(ti, 2) * bg;
-                    var n = i + 1;
-                    var D  = 0;
-                    var Da = 0;
-                    var Db = 0;
-                    var Dc = 0;
-                    if (n > 3) {
-                        D  = sx4 * (sx2 * n - sx * sx) - sx3 * (sx3 * n - sx * sx2) + sx2 * (sx3 * sx - sx2 * sx2);
-                        Da = sx2y* (sx2 * n - sx * sx) - sxy * (sx3 * n - sx * sx2) + sy  * (sx3 * sx - sx2 * sx2);
-                        Db = sx4 * (sxy * n - sy * sx) - sx3 * (sx2y* n - sy * sx2) + sx2 * (sx2y* sx - sxy * sx2);
-                        Dc = sx4 * (sx2 *sy - sx *sxy) - sx3 * (sx3 *sy - sx *sx2y) + sx2 * (sx3 *sxy - sx2 * sx2y);
-                    }
-                    if (D != 0) {
-                        var a = Da / D;
-                        b = Db / D;              // b initialised in linear fit !
-                        var c = Dc / D;
-                        var y_mean = sy / n;
-                        var s_squares = 0;
-                        var s_residual_squares = 0;
-                        for (var j = 0; j <= i; j++) {
-                            var before = data[j];
-                            var before_date = getDateFromEntry(before);
-                            s_squares += Math.pow(before.glucose / scaleBg - y_mean, 2);
-                            var delta_t = (before_date - time_0) / 1000 / scaleTime;
-                            var bg_j = a * Math.pow(delta_t, 2) + b * delta_t + c;
-                            s_residual_squares += Math.pow(before.glucose / scaleBg - bg_j, 2);
-                        }
-                        var r_squ = 0.64;
-                        if (s_squares != 0) {
-                            r_squ = 1 - s_residual_squares / s_squares;
-                        }
-                        if (n > 3) {
-                            if (r_squ >= corrMax) {
-                                corrMax = r_squ;
-                                // double delta_t = (then_date - time_0) / 1000;
-                                dura_p = -ti * scaleTime / 60;            // remember we are going backwards in time
-                                var delta5Min = 5 * 60 / scaleTime;
-                                delta_pl =-scaleBg * (a * Math.pow(- delta5Min, 2) - b * delta5Min);     // 5 minute slope from last fitted bg starting from last bg, i.e. t=0
-                                delta_pn = scaleBg * (a * Math.pow( delta5Min, 2) + b * delta5Min);     // 5 minute slope to next fitted bg starting from last bg, i.e. t=0
-                                bg_acceleration = 2 * a * scaleBg;             // 2nd derivative of parabola per (5min)^2
-                                a_0 = c * scaleBg;
-                                a_1 = b * scaleBg;
-                                a_2 = a * scaleBg;
-                                //r_squ = corrMax;
-                                best_a = a * scaleBg;
-                                best_b = b * scaleBg;
-                                best_c = c * scaleBg;
-                            }
-                        }
-                    }
-                }
-                pp_debug += "coeffs a/b/c=(" + round(best_a,2) + " / " + round(best_b,2) + " / " + round(best_c,2) + "); bg date=" + time_0 + "; ";
-                pp_debug += "Parabola Fits a0/a1/a2=(" + round(a_0,2) + " / " + round(a_1,2) + " / " + round(a_2,2) + "); ";
-            }
-           pp_debug += "Slopes 05/15/40=(" + round(slope05,2) + " / " + round(slope15,2) + " / " + round(slope40,2) + "); "
     return {
-        delta: Math.round( last_delta * 10000 ) / 10000
-        , glucose: Math.round( now.glucose * 10000 ) / 10000
+        delta: Math.round( last_delta * 100 ) / 100
+        , glucose: Math.round( now.glucose * 100 ) / 100
         , noise: Math.round(now.noise)
-        , short_avgdelta: Math.round( short_avgdelta * 10000 ) / 10000
-        , long_avgdelta: Math.round( long_avgdelta * 10000 ) / 10000
-        // autoISF values to return to determineBasal.js
-        , autoISF_average: Math.round( autoISF_average * 10000) / 10000
-        , autoISF_duration: Math.round(autoISF_duration * 10000) / 10000
-        , dura_p: Math.round( dura_p * 10000) / 10000
-        , delta_pl: Math.round( delta_pl * 10000) / 10000
-        , delta_pn: Math.round( delta_pn * 10000) / 10000
-        , bg_acceleration: bg_acceleration
-        , r_squ: Math.round( corrMax * 10000) / 10000
-        , parabola_fit_a0: Math.round( a_0 * 10000) / 10000
-        , parabola_fit_a1: Math.round( a_1 * 10000) / 10000
-        , parabola_fit_a2: Math.round( a_2 * 10000) / 10000
-        , pp_debug
-        // end autoISF values
+        , short_avgdelta: Math.round( short_avgdelta * 100 ) / 100
+        , long_avgdelta: Math.round( long_avgdelta * 100 ) / 100
         , date: now_date
         , last_cal: last_cal
         , device: now.device
     };
 };
 
-module.exports = getLastGlucose;
+module.exports = getLastGlucose;

+ 1 - 1
trio-oref/oref_source_file_info.txt

@@ -1,2 +1,2 @@
 These source files are copied from https://github.com/nightscout/trio-oref, and are for information purposes only.
-The algorithm is run based on minimised files in FreeAPS/Resources/javascript/bundle.
+The algorithm is run based on minimised files in Trio/Resources/javascript/bundle.