ソースを参照

Merge branch 'dev' of github.com:nightscout/Trio into oref-swift

Deniz Cengiz 3 ヶ月 前
コミット
e8f08441ef
85 ファイル変更905 行追加1619 行削除
  1. 100 202
      .github/workflows/build_trio.yml
  2. 4 11
      .github/workflows/validate_secrets.yml
  3. 1 1
      CGMBLEKit
  4. 1 1
      Config.xcconfig
  5. 1 1
      DanaKit
  6. 1 1
      G7SensorKit
  7. 1 5
      Gemfile
  8. 52 51
      Gemfile.lock
  9. 1 1
      LibreTransmitter
  10. 1 1
      LoopKit
  11. 1 1
      MinimedKit
  12. 1 0
      Model/Classes+Properties/ContactTrickEntryStored+CoreDataProperties.swift
  13. 2 1
      Model/TrioCoreDataPersistentContainer.xcdatamodeld/TrioCoreDataPersistentContainer.xcdatamodel/contents
  14. 1 1
      OmniBLE
  15. 1 1
      OmniKit
  16. 1 1
      RileyLinkKit
  17. 1 1
      TidepoolService
  18. 0 18
      Trio Watch App Extension/Assets.xcassets/Colors/Background_DarkBlue.colorset/Contents.json
  19. 0 18
      Trio Watch App Extension/Assets.xcassets/Colors/Background_DarkerDarkBlue.colorset/Contents.json
  20. 0 18
      Trio Watch App Extension/Assets.xcassets/Colors/Basal.colorset/Contents.json
  21. 0 18
      Trio Watch App Extension/Assets.xcassets/Colors/Insulin.colorset/Contents.json
  22. 0 18
      Trio Watch App Extension/Assets.xcassets/Colors/LoopGray.colorset/Contents.json
  23. 0 18
      Trio Watch App Extension/Assets.xcassets/Colors/LoopGreen.colorset/Contents.json
  24. 0 38
      Trio Watch App Extension/Assets.xcassets/Colors/LoopPink.colorset/Contents.json
  25. 0 18
      Trio Watch App Extension/Assets.xcassets/Colors/LoopRed.colorset/Contents.json
  26. 0 18
      Trio Watch App Extension/Assets.xcassets/Colors/TabBar.colorset/Contents.json
  27. 3 21
      Trio Watch App Extension/Assets.xcassets/Colors/UAM.colorset/Contents.json
  28. 0 18
      Trio Watch App Extension/Assets.xcassets/Colors/ZT.colorset/Contents.json
  29. 46 42
      Trio.xcodeproj/project.pbxproj
  30. 0 18
      Trio/Resources/Assets.xcassets/Colors/Background_DarkBlue.colorset/Contents.json
  31. 0 18
      Trio/Resources/Assets.xcassets/Colors/Background_DarkerDarkBlue.colorset/Contents.json
  32. 0 18
      Trio/Resources/Assets.xcassets/Colors/Basal.colorset/Contents.json
  33. 0 18
      Trio/Resources/Assets.xcassets/Colors/DarkerBlue.colorset/Contents.json
  34. 0 18
      Trio/Resources/Assets.xcassets/Colors/Insulin.colorset/Contents.json
  35. 0 38
      Trio/Resources/Assets.xcassets/Colors/Lemon.colorset/Contents.json
  36. 0 18
      Trio/Resources/Assets.xcassets/Colors/LoopGray.colorset/Contents.json
  37. 0 18
      Trio/Resources/Assets.xcassets/Colors/LoopGreen.colorset/Contents.json
  38. 0 38
      Trio/Resources/Assets.xcassets/Colors/LoopPink.colorset/Contents.json
  39. 0 18
      Trio/Resources/Assets.xcassets/Colors/LoopRed.colorset/Contents.json
  40. 0 18
      Trio/Resources/Assets.xcassets/Colors/ManualTempBasal.colorset/Contents.json
  41. 0 18
      Trio/Resources/Assets.xcassets/Colors/TabBar.colorset/Contents.json
  42. 3 21
      Trio/Resources/Assets.xcassets/Colors/UAM.colorset/Contents.json
  43. 0 18
      Trio/Resources/Assets.xcassets/Colors/ZT.colorset/Contents.json
  44. 0 18
      Trio/Resources/Assets.xcassets/Colors/minus.colorset/Contents.json
  45. 0 184
      Trio/Sources/APS/CGM/DexcomSourceG5.swift
  46. 0 195
      Trio/Sources/APS/CGM/DexcomSourceG6.swift
  47. 0 103
      Trio/Sources/APS/CGM/LibreTransmitterSource.swift
  48. 2 10
      Trio/Sources/APS/CGM/PluginSource.swift
  49. 4 19
      Trio/Sources/APS/DeviceDataManager.swift
  50. 22 6
      Trio/Sources/APS/OpenAPS/OpenAPS.swift
  51. 203 47
      Trio/Sources/APS/Storage/AlertStorage.swift
  52. 5 2
      Trio/Sources/APS/Storage/ContactImageStorage.swift
  53. 9 9
      Trio/Sources/APS/Storage/GlucoseStorage.swift
  54. 0 2
      Trio/Sources/Helpers/Color+Extensions.swift
  55. 104 0
      Trio/Sources/Helpers/TempTargetCalculations.swift
  56. 34 1
      Trio/Sources/Localizations/Main/Localizable.xcstrings
  57. 17 0
      Trio/Sources/Models/ContactTrickEntry.swift
  58. 1 1
      Trio/Sources/Models/DecimalPickerSettings.swift
  59. 20 37
      Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+TempTargets.swift
  60. 10 3
      Trio/Sources/Modules/Adjustments/AdjustmentsStateModel.swift
  61. 25 5
      Trio/Sources/Modules/Adjustments/View/TempTargets/AddTempTargetForm.swift
  62. 5 1
      Trio/Sources/Modules/Adjustments/View/TempTargets/AdjustmentsRootView+TempTargets.swift
  63. 41 12
      Trio/Sources/Modules/Adjustments/View/TempTargets/EditTempTargetForm.swift
  64. 39 14
      Trio/Sources/Modules/Adjustments/View/TempTargets/TempTargetHelpView.swift
  65. 11 0
      Trio/Sources/Modules/ContactImage/View/AddContactImageSheet.swift
  66. 9 0
      Trio/Sources/Modules/ContactImage/View/ContactImageDetailView.swift
  67. 3 3
      Trio/Sources/Modules/DataTable/DataTableDataFlow.swift
  68. 2 2
      Trio/Sources/Modules/DataTable/DataTableProvider.swift
  69. 2 2
      Trio/Sources/Modules/DataTable/DataTableStateModel.swift
  70. 2 2
      Trio/Sources/Modules/DataTable/View/CarbEntryEditorView.swift
  71. 14 11
      Trio/Sources/Modules/DataTable/View/DataTableRootView.swift
  72. 0 11
      Trio/Sources/Modules/Home/HomeStateModel+Setup/TempTargetSetup.swift
  73. 37 9
      Trio/Sources/Modules/Home/View/HomeRootView.swift
  74. 8 3
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/TherapySettings/InsulinSensitivityStepView.swift
  75. 4 4
      Trio/Sources/Modules/PumpConfig/PumpConfigProvider.swift
  76. 5 5
      Trio/Sources/Modules/PumpConfig/PumpConfigStateModel.swift
  77. 1 1
      Trio/Sources/Modules/PumpConfig/View/PumpConfigRootView.swift
  78. 8 9
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseDailyPercentileChart.swift
  79. 1 1
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucosePercentileChart.swift
  80. 6 6
      Trio/Sources/Modules/Treatments/View/MealPreset/MealPresetView.swift
  81. 3 3
      Trio/Sources/Router/Screen.swift
  82. 9 1
      Trio/Sources/Services/ContactImage/ContactPicture.swift
  83. 0 12
      fastlane/Fastfile
  84. 15 25
      fastlane/testflight.md
  85. 1 1
      scripts/swiftformat.sh

+ 100 - 202
.github/workflows/build_trio.yml

@@ -8,172 +8,101 @@ on:
     - cron: "43 6 * * 0" # Sunday at UTC 06:43
 
 env:
+  GH_PAT: ${{ secrets.GH_PAT }}
   UPSTREAM_REPO: nightscout/Trio
   UPSTREAM_BRANCH: ${{ github.ref_name }} # branch on upstream repository to sync from (replace with specific branch name if needed)
-  TARGET_BRANCH: ${{ github.ref_name }} # target branch on fork to be kept in sync, and target branch on upstream to be kept alive (replace with specific branch name if needed)
-  ALIVE_BRANCH_MAIN: alive-main
-  ALIVE_BRANCH_DEV: alive-dev
+  TARGET_BRANCH: ${{ github.ref_name }} # target branch on fork to be kept in sync
 
 jobs:
-
-  # Set a logic flag if this is the second instance of this day-of-week in this month
-  day_in_month:
+  # use a single runner for these sequential steps
+  check_status:
     runs-on: ubuntu-latest
-    name: Check day in month
-    outputs:
-      IS_SECOND_IN_MONTH: ${{ steps.date-check.outputs.is_second_instance }}
-
-    steps:
-      - id: date-check
-        name: Check if this is the second time this day-of-week happens this month
-        run: |
-          DAY_OF_MONTH=$(date +%-d)
-          WEEK_OF_MONTH=$(( ($(date +%-d) - 1) / 7 + 1 ))
-          if [[ $WEEK_OF_MONTH -eq 2 ]]; then
-            echo "is_second_instance=true" >> "$GITHUB_OUTPUT"
-          else
-            echo "is_second_instance=false" >> "$GITHUB_OUTPUT"
-          fi
-
-  # Checks if Distribution certificate is present and valid, optionally nukes and
-  # creates new certs if the repository variable ENABLE_NUKE_CERTS == 'true'
-  check_certs:
-    name: Check certificates
-    uses: ./.github/workflows/create_certs.yml
-    secrets: inherit
-
-  # Checks if GH_PAT holds workflow permissions
-  # Checks for existence of alive branch; if non-existent creates it
-  check_alive_and_permissions:
-    needs: check_certs
-    runs-on: ubuntu-latest
-    name: Check alive branch and permissions
+    name: Check status to decide whether to build
     permissions:
       contents: write
     outputs:
-      WORKFLOW_PERMISSION: ${{ steps.workflow-permission.outputs.has_permission }}
+      NEW_COMMITS: ${{ steps.sync.outputs.has_new_commits }}
+      IS_SECOND_IN_MONTH: ${{ steps.date-check.outputs.is_second_instance }}
 
+    # Check GH_PAT, sync repository, check day in month
     steps:
-      - name: Check for workflow permissions
-        id: workflow-permission
-        env:
-          TOKEN_TO_CHECK: ${{ secrets.GH_PAT }}
-        run: |
-          PERMISSIONS=$(curl -sS -f -I -H "Authorization: token ${{ env.TOKEN_TO_CHECK }}" https://api.github.com | grep ^x-oauth-scopes: | cut -d' ' -f2-);
 
-          if [[ $PERMISSIONS =~ "workflow" || $PERMISSIONS == "" ]]; then
-            echo "GH_PAT holds workflow permissions or is fine-grained PAT."
-            echo "has_permission=true" >> $GITHUB_OUTPUT # Set WORKFLOW_PERMISSION to false.
-          else 
-            echo "GH_PAT lacks workflow permissions."
-            echo "Automated build features will be skipped!"
-            echo "has_permission=false" >> $GITHUB_OUTPUT # Set WORKFLOW_PERMISSION to false.
-          fi
-
-      - name: Check for alive branches
-        if: steps.workflow-permission.outputs.has_permission == 'true'
-        id: check-alive
-        env:
-          GITHUB_TOKEN: ${{ secrets.GH_PAT }}
+      - name: Access
+        id: workflow-permission
         run: |
-          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
+          # Validate Access Token
+          
+          # Ensure that gh exit codes are handled when output is piped.
+          set -o pipefail
+          
+          # Define patterns to validate the access token (GH_PAT) and distinguish between classic and fine-grained tokens.
+          GH_PAT_CLASSIC_PATTERN='^ghp_[a-zA-Z0-9]{36}$'
+          GH_PAT_FINE_GRAINED_PATTERN='^github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}$'
+          
+          # Validate Access Token (GH_PAT)
+          if [ -z "$GH_PAT" ]; then
+            failed=true
+            echo "::error::The GH_PAT secret is unset or empty. Set it and try again."
           else
-            echo "alive-dev missing"
-            echo "ALIVE_DEV_EXISTS=false" >> $GITHUB_ENV
+            if [[ $GH_PAT =~ $GH_PAT_CLASSIC_PATTERN ]]; then
+              provides_scopes=true
+              echo "The GH_PAT secret is a structurally valid classic token."
+            elif [[ $GH_PAT =~ $GH_PAT_FINE_GRAINED_PATTERN ]]; then
+              echo "The GH_PAT secret is a structurally valid fine-grained token."
+            else
+              unknown_format=true
+              echo "The GH_PAT secret does not have a known token format."
+            fi
+            
+            # Attempt to capture the x-oauth-scopes scopes of the token.
+            if ! scopes=$(curl -sS -f -I -H "Authorization: token $GH_PAT" https://api.github.com | { grep -i '^x-oauth-scopes:' || true; } | cut -d ' ' -f2- | tr -d '\r'); then
+              failed=true
+              if [ $unknown_format ]; then
+                echo "::error::Unable to connect to GitHub using the GH_PAT secret. Verify that it is set correctly (including the 'ghp_' or 'github_pat_' prefix) and try again."
+              else
+                echo "::error::Unable to connect to GitHub using the GH_PAT secret. Verify that the token exists and has not expired at https://github.com/settings/tokens. If necessary, regenerate or create a new token (and update the secret), then try again."
+              fi
+            elif [[ $scopes =~ workflow ]]; then
+              echo "The GH_PAT secret has repo and workflow permissions."
+              echo "has_permission=true" >> $GITHUB_OUTPUT
+            elif [[ $scopes =~ repo ]]; then
+              echo "The GH_PAT secret has repo (but not workflow) permissions."
+            elif [ $provides_scopes ]; then
+              failed=true
+              if [ -z "$scopes" ]; then
+                echo "The GH_PAT secret is valid and can be used to connect to GitHub, but it does not provide any permission scopes."
+              else
+                echo "The GH_PAT secret is valid and can be used to connect to GitHub, but it only provides the following permission scopes: $scopes"
+              fi
+              echo "::error::The GH_PAT secret is lacking at least the 'repo' permission scope required to access the Match-Secrets repository. Update the token permissions at https://github.com/settings/tokens (to include the 'repo' and 'workflow' scopes) and try again."
+            else
+              echo "The GH_PAT secret is valid and can be used to connect to GitHub, but it does not provide inspectable scopes. Assuming that the 'repo' and 'workflow' permission scopes required to access the Match-Secrets repository and perform automations are present."
+              echo "has_permission=true" >> $GITHUB_OUTPUT
+            fi
           fi
-
-      - name: Create alive-main branch if missing
-        if: env.ALIVE_MAIN_EXISTS == 'false'
-        env:
-          GITHUB_TOKEN: ${{ secrets.GH_PAT }}
-        run: |
-          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')
-      
-          echo "Creating alive-main from upstream main"
-          gh api \
-            --method POST \
-            -H "Authorization: token $GITHUB_TOKEN" \
-            -H "Accept: application/vnd.github.v3+json" \
-            /repos/${{ github.repository_owner }}/Trio/git/refs \
-            -f ref='refs/heads/alive-main' \
-            -f sha=$SHA_MAIN
-
-      - 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" \
-            -H "Accept: application/vnd.github.v3+json" \
-            /repos/${{ github.repository_owner }}/Trio/git/refs \
-            -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:
-    needs: [check_certs, check_alive_and_permissions]
-    runs-on: ubuntu-latest
-    name: Check upstream and keep alive
-    outputs:
-      NEW_COMMITS: ${{ steps.sync.outputs.has_new_commits }}
-      ABORT_SYNC: ${{ steps.check_branch.outputs.ABORT_SYNC }}
-
-    steps:
-      - name: Check if running on main or dev branch
-        if: |
-          needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
-          (vars.SCHEDULED_BUILD != 'false' || vars.SCHEDULED_SYNC != 'false')
-        id: check_branch
-        run: |
-          if [ "${GITHUB_REF##*/}" = "main" ]; then
-            echo "Running on main branch"
-            echo "ALIVE_BRANCH=${ALIVE_BRANCH_MAIN}" >> $GITHUB_OUTPUT
-            echo "ABORT_SYNC=false" >> $GITHUB_OUTPUT
-          elif [ "${GITHUB_REF##*/}" = "dev" ]; then
-            echo "Running on dev branch"
-            echo "ALIVE_BRANCH=${ALIVE_BRANCH_DEV}" >> $GITHUB_OUTPUT
-            echo "ABORT_SYNC=false" >> $GITHUB_OUTPUT
-          else
-            echo "Not running on main or dev branch"
-            echo "ABORT_SYNC=true" >> $GITHUB_OUTPUT
+          
+          # Exit unsuccessfully if secret validation failed.
+          if [ $failed ]; then
+            exit 2
           fi
 
       - name: Checkout target repo
         if: |
-          needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
+          steps.workflow-permission.outputs.has_permission == 'true' &&
           (vars.SCHEDULED_BUILD != 'false' || vars.SCHEDULED_SYNC != 'false')
         uses: actions/checkout@v4
         with:
           token: ${{ secrets.GH_PAT }}
-          ref: ${{ steps.check_branch.outputs.ALIVE_BRANCH }}
 
+      # This syncs any target branch to upstream branch of the same name
       - name: Sync upstream changes
         if: | # do not run the upstream sync action on the upstream repository
-          needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
-          vars.SCHEDULED_SYNC != 'false' && github.repository_owner != 'nightscout' && steps.check_branch.outputs.ABORT_SYNC == 'false'
+          steps.workflow-permission.outputs.has_permission == 'true' &&
+          vars.SCHEDULED_SYNC != 'false' && github.repository_owner != 'nightscout'
         id: sync
         uses: aormsby/Fork-Sync-With-Upstream-action@v3.4.1
         with:
-          target_sync_branch: ${{ steps.check_branch.outputs.ALIVE_BRANCH }}
+          target_sync_branch: ${{ env.TARGET_BRANCH }}
           shallow_since: 6 months ago
           target_repo_token: ${{ secrets.GH_PAT }}
           upstream_sync_branch: ${{ env.UPSTREAM_BRANCH }}
@@ -182,35 +111,24 @@ jobs:
       # Display a sample message based on the sync output var 'has_new_commits'
       - name: New commits found
         if: |
-          needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
+          steps.workflow-permission.outputs.has_permission == 'true' &&
           vars.SCHEDULED_SYNC != 'false' && steps.sync.outputs.has_new_commits == 'true'
         run: echo "New commits were found to sync."
 
       - name: No new commits
         if: |
-          needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' && 
+          steps.workflow-permission.outputs.has_permission == 'true' &&
           vars.SCHEDULED_SYNC != 'false' && steps.sync.outputs.has_new_commits == 'false'
         run: echo "There were no new commits."
 
       - name: Show value of 'has_new_commits'
-        if: needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' && vars.SCHEDULED_SYNC != 'false' && steps.check_branch.outputs.ABORT_SYNC == 'false'
+        if: steps.workflow-permission.outputs.has_permission == 'true' && vars.SCHEDULED_SYNC != 'false'
         run: |
           echo ${{ steps.sync.outputs.has_new_commits }}
           echo "NEW_COMMITS=${{ steps.sync.outputs.has_new_commits }}" >> $GITHUB_OUTPUT
 
-      # Keep repository "alive": add empty commits to ALIVE_BRANCH after "time_elapsed" days of inactivity to avoid inactivation of scheduled workflows
-      - name: Keep alive
-        run: |
-          echo "Keep Alive temporarily removed while gautamkrishnar/keepalive-workflow is not available"
-      #  if: |
-      #    needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
-      #    (vars.SCHEDULED_BUILD != 'false' || vars.SCHEDULED_SYNC != 'false')
-      #  uses: gautamkrishnar/keepalive-workflow@v1 # using the workflow with default settings
-      #  with:
-      #    time_elapsed: 20 # Time elapsed from the previous commit to trigger a new automated commit (in days)
-
       - name: Show scheduled build configuration message
-        if: needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION != 'true'
+        if: steps.workflow-permission.outputs.has_permission != 'true'
         run: |
           echo "### :calendar: Scheduled Sync and Build Disabled :mobile_phone_off:" >> $GITHUB_STEP_SUMMARY
           echo "You have not yet configured the scheduled sync and build for Trio's browser build." >> $GITHUB_STEP_SUMMARY
@@ -218,67 +136,47 @@ jobs:
           echo "If you want to enable automatic builds and updates for your Trio, please follow the instructions \
               under the following path <code>Trio/fastlane/testflight.md</code>." >> $GITHUB_STEP_SUMMARY
 
+      # Set a logic flag if this is the second instance of this day-of-week in this month
+      - name: Check if this is the second time this day-of-week happens this month
+        id: date-check
+        run: |
+          DAY_OF_MONTH=$(date +%-d)
+          WEEK_OF_MONTH=$(( ($(date +%-d) - 1) / 7 + 1 ))
+          if [[ $WEEK_OF_MONTH -eq 2 ]]; then
+            echo "is_second_instance=true" >> "$GITHUB_OUTPUT"
+          else
+            echo "is_second_instance=false" >> "$GITHUB_OUTPUT"
+          fi
+
+  # Checks if Distribution certificate is present and valid, optionally nukes and
+  # creates new certs if the repository variable ENABLE_NUKE_CERTS == 'true'
+  # only run if a build is planned
+  check_certs:
+      needs: [check_status]
+      name: Check certificates
+      uses: ./.github/workflows/create_certs.yml
+      secrets: inherit
+      if: |
+        github.event_name == 'workflow_dispatch' ||
+          (vars.SCHEDULED_BUILD != 'false' && needs.check_status.outputs.IS_SECOND_IN_MONTH == 'true') ||
+          (vars.SCHEDULED_SYNC != 'false' && needs.check_status.outputs.NEW_COMMITS == 'true' )
+
   # Builds Trio
   build:
     name: Build
-    needs: [check_certs, check_alive_and_permissions, check_latest_from_upstream, day_in_month]
+    needs: [check_certs, check_status]
     runs-on: macos-15
     permissions:
       contents: write
     if:
-      | # builds with manual start; if automatic: once a month or when new commits are found
+      | # builds with manual start; if scheduled: once a month or when new commits are found
       github.event_name == 'workflow_dispatch' ||
-      (needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
-        (vars.SCHEDULED_BUILD != 'false' && needs.day_in_month.outputs.IS_SECOND_IN_MONTH == 'true') ||
-        (vars.SCHEDULED_SYNC != 'false' && needs.check_latest_from_upstream.outputs.NEW_COMMITS == 'true' )
-      )
+        (vars.SCHEDULED_BUILD != 'false' && needs.check_status.outputs.IS_SECOND_IN_MONTH == 'true') ||
+        (vars.SCHEDULED_SYNC != 'false' && needs.check_status.outputs.NEW_COMMITS == 'true' )
     steps:
       - name: Select Xcode version
         run: "sudo xcode-select --switch /Applications/Xcode_16.4.app/Contents/Developer"
       
-      - name: Checkout Repo for syncing
-        if: |
-          needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
-          vars.SCHEDULED_SYNC != 'false'
-        uses: actions/checkout@v4
-        with:
-          token: ${{ secrets.GH_PAT }}
-          ref: ${{ env.TARGET_BRANCH }}
-
-      - name: Sync upstream changes
-        if: | # do not run the upstream sync action on the upstream repository
-          needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
-          vars.SCHEDULED_SYNC != 'false' && github.repository_owner != 'nightscout' && needs.check_latest_from_upstream.outputs.ABORT_SYNC == 'false'
-        id: sync
-        uses: aormsby/Fork-Sync-With-Upstream-action@v3.4.1
-        with:
-          target_sync_branch: ${{ env.TARGET_BRANCH }}
-          shallow_since: 6 months ago
-          target_repo_token: ${{ secrets.GH_PAT }}
-          upstream_sync_branch: ${{ env.UPSTREAM_BRANCH }}
-          upstream_sync_repo: ${{ env.UPSTREAM_REPO }}
-
-      # Display a sample message based on the sync output var 'has_new_commits'
-      - name: New commits found
-        if: |
-          needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
-          vars.SCHEDULED_SYNC != 'false' && steps.sync.outputs.has_new_commits == 'true' && needs.check_latest_from_upstream.outputs.ABORT_SYNC == 'false'
-        run: echo "New commits were found to sync."
-
-      - name: No new commits
-        if: |
-          needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' && 
-          vars.SCHEDULED_SYNC != 'false' && steps.sync.outputs.has_new_commits == 'false' && needs.check_latest_from_upstream.outputs.ABORT_SYNC == 'false'
-        run: echo "There were no new commits."
-
-      - name: Show value of 'has_new_commits'
-        if: |
-          needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true'
-          && vars.SCHEDULED_SYNC != 'false' && needs.check_latest_from_upstream.outputs.ABORT_SYNC == 'false'
-        run: |
-          echo ${{ steps.sync.outputs.has_new_commits }}
-          echo "NEW_COMMITS=${{ steps.sync.outputs.has_new_commits }}" >> $GITHUB_OUTPUT
-
       - name: Checkout Repo for building
         uses: actions/checkout@v4
         with:

+ 4 - 11
.github/workflows/validate_secrets.yml

@@ -5,7 +5,7 @@ on: [workflow_call, workflow_dispatch]
 jobs:
   validate-access-token:
     name: Access
-    runs-on: macos-15
+    runs-on: ubuntu-latest
     env:
       GH_PAT: ${{ secrets.GH_PAT }}
       GH_TOKEN: ${{ secrets.GH_PAT }}
@@ -71,13 +71,6 @@ jobs:
             exit 2
           fi
 
-  validate-match-secrets:
-    name: Match-Secrets
-    needs: validate-access-token
-    runs-on: macos-15
-    env:
-      GH_TOKEN: ${{ secrets.GH_PAT }}
-    steps:
       - name: Validate Match-Secrets
         run: |
           # Validate Match-Secrets
@@ -111,7 +104,7 @@ jobs:
 
   validate-fastlane-secrets:
     name: Fastlane
-    needs: [validate-access-token, validate-match-secrets]
+    needs: [validate-access-token]
     runs-on: macos-15
     env:
       GH_PAT: ${{ secrets.GH_PAT }}
@@ -178,8 +171,8 @@ jobs:
           elif ! echo "$FASTLANE_KEY" | openssl pkcs8 -nocrypt >/dev/null; then
             failed=true
             echo "::error::The FASTLANE_KEY secret is set but invalid. Verify that you copied it correctly from the API Key file (*.p8) you downloaded and try again."
-          elif ! (bundle exec fastlane validate_secrets 2>&1 || true) | tee fastlane.log; then # ignore "fastlane validate_secrets" errors and continue on errors without annotating an exit code
-            if grep -q "bad decrypt" fastlane.log; then
+          elif ! bundle exec fastlane validate_secrets 2>&1 | tee fastlane.log; then
+            if grep -q "Couldn't decrypt the repo" fastlane.log; then
               failed=true
               echo "::error::Unable to decrypt the Match-Secrets repository using the MATCH_PASSWORD secret. Verify that it is set correctly and try again."
             elif grep -q -e "required agreement" -e "license agreement" fastlane.log; then

+ 1 - 1
CGMBLEKit

@@ -1 +1 @@
-Subproject commit 26fa00bed8c2f5e4b52ecb3241b422d058117c2c
+Subproject commit a442ea0a21078e82264176a89617d2f9a3a6f36d

+ 1 - 1
Config.xcconfig

@@ -19,7 +19,7 @@ TRIO_APP_GROUP_ID = group.org.nightscout.$(DEVELOPMENT_TEAM).trio.trio-app-group
 
 // The developers set the version numbers, please leave them alone
 APP_VERSION = 0.6.0
-APP_DEV_VERSION = 0.6.0.26
+APP_DEV_VERSION = 0.6.0.43
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 

+ 1 - 1
DanaKit

@@ -1 +1 @@
-Subproject commit 299331d4e540a0e7d1a74c30ddbb5be1d68892e8
+Subproject commit bad8fad9ccf980f4a3384b2454a7cd41abe69464

+ 1 - 1
G7SensorKit

@@ -1 +1 @@
-Subproject commit 43f55ad8e1227fa6b4bec25d152726c56c0ffb0c
+Subproject commit ee064ddcc1c13e0050ee56d0eec38a6bdc0d3c76

+ 1 - 5
Gemfile

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

+ 52 - 51
Gemfile.lock

@@ -1,51 +1,3 @@
-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:
@@ -53,6 +5,7 @@ GEM
       base64
       nkf
       rexml
+    abbrev (0.1.2)
     addressable (2.8.7)
       public_suffix (>= 2.0.2, < 7.0)
     artifactory (3.0.17)
@@ -77,13 +30,14 @@ GEM
     aws-sigv4 (1.12.1)
       aws-eventstream (~> 1, >= 1.0.2)
     babosa (1.0.4)
-    base64 (0.3.0)
+    base64 (0.2.0)
     bigdecimal (3.2.3)
     claide (1.1.0)
     colored (1.2)
     colored2 (3.1.2)
     commander (4.6.0)
       highline (~> 2.0.0)
+    csv (3.3.5)
     declarative (0.0.20)
     digest-crc (0.7.0)
       rake (>= 12.0.0, < 14.0.0)
@@ -120,6 +74,54 @@ GEM
     faraday_middleware (1.2.1)
       faraday (~> 1.0)
     fastimage (2.4.0)
+    fastlane (2.230.0)
+      CFPropertyList (>= 2.3, < 4.0.0)
+      abbrev (~> 0.1.2)
+      addressable (>= 2.8, < 3.0.0)
+      artifactory (~> 3.0)
+      aws-sdk-s3 (~> 1.0)
+      babosa (>= 1.0.3, < 2.0.0)
+      base64 (~> 0.2.0)
+      bundler (>= 1.12.0, < 3.0.0)
+      colored (~> 1.2)
+      commander (~> 4.6)
+      csv (~> 3.3)
+      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)
+      logger (>= 1.6, < 2.0)
+      mini_magick (>= 4.9.4, < 5.0.0)
+      multipart-post (>= 2.0.0, < 3.0.0)
+      mutex_m (~> 0.3.0)
+      naturally (~> 2.2)
+      nkf (~> 0.2.0)
+      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)
@@ -234,8 +236,7 @@ PLATFORMS
   x86_64-linux
 
 DEPENDENCIES
-  fastlane!
-  rexml (>= 3.4.2)
+  fastlane (= 2.230.0)
 
 BUNDLED WITH
    2.6.2

+ 1 - 1
LibreTransmitter

@@ -1 +1 @@
-Subproject commit 25c31bae22082caaa6823179010129912d6c8f8f
+Subproject commit 38cc483f3d7716735ceee6e57b6ed4dd68eaf1d0

+ 1 - 1
LoopKit

@@ -1 +1 @@
-Subproject commit ce07c0993b1038f6f60ea5b6db7c23da0be3fee6
+Subproject commit edd4e6037d263ef32dd8dd4c0d699c5429097373

+ 1 - 1
MinimedKit

@@ -1 +1 @@
-Subproject commit a1888623f398994e07ad970a0164be1117e9bec1
+Subproject commit d52c0f8f1fe615760794fdac233ba78657449870

+ 1 - 0
Model/Classes+Properties/ContactTrickEntryStored+CoreDataProperties.swift

@@ -17,6 +17,7 @@ public extension ContactImageEntryStored {
     @NSManaged var ringWidth: Int16
     @NSManaged var ringGap: Int16
     @NSManaged var id: UUID?
+    @NSManaged var colorMode: String?
     @NSManaged var fontSize: Int16
     @NSManaged var fontSizeSecondary: Int16
     @NSManaged var fontWidth: String?

+ 2 - 1
Model/TrioCoreDataPersistentContainer.xcdatamodeld/TrioCoreDataPersistentContainer.xcdatamodel/contents

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23788" systemVersion="24G84" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
+<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="24512" systemVersion="25B78" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
     <entity name="BolusStored" representedClassName="BolusStored" syncable="YES">
         <attribute name="amount" optional="YES" attributeType="Decimal" defaultValueString="0"/>
         <attribute name="isExternal" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
@@ -27,6 +27,7 @@
     </entity>
     <entity name="ContactImageEntryStored" representedClassName="ContactImageEntryStored" syncable="YES" codeGenerationType="class">
         <attribute name="bottom" optional="YES" attributeType="String"/>
+        <attribute name="colorMode" optional="YES" attributeType="String"/>
         <attribute name="contactId" optional="YES" attributeType="String"/>
         <attribute name="fontSize" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
         <attribute name="fontSizeSecondary" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>

+ 1 - 1
OmniBLE

@@ -1 +1 @@
-Subproject commit d8375ebf242e0d0e02ace7a03d9e1632557de38e
+Subproject commit ffec85de22d979e4bee6535c374ab72c692e101b

+ 1 - 1
OmniKit

@@ -1 +1 @@
-Subproject commit 1a73635568750289ac4d2f702b6bf191efbdda9f
+Subproject commit 64731f0b31d61cae14d00528a9c2bf78ea6da9a6

+ 1 - 1
RileyLinkKit

@@ -1 +1 @@
-Subproject commit c818fa8c90c0c98a4ba26cd18dacfeed01cc2bd5
+Subproject commit 83b211a442672612e1790c2f0d393aeb23600b5f

+ 1 - 1
TidepoolService

@@ -1 +1 @@
-Subproject commit 84cab9b60e65b4aa814b0e12024a5e068ca65bfd
+Subproject commit b4fb9a0672f6e4a7bfed619fc3193b03a8a2ab79

+ 0 - 18
Trio Watch App Extension/Assets.xcassets/Colors/Background_DarkBlue.colorset/Contents.json

@@ -11,24 +11,6 @@
         }
       },
       "idiom" : "universal"
-    },
-    {
-      "appearances" : [
-        {
-          "appearance" : "luminosity",
-          "value" : "dark"
-        }
-      ],
-      "color" : {
-        "color-space" : "srgb",
-        "components" : {
-          "alpha" : "1.000",
-          "blue" : "0.216",
-          "green" : "0.133",
-          "red" : "0.039"
-        }
-      },
-      "idiom" : "universal"
     }
   ],
   "info" : {

+ 0 - 18
Trio Watch App Extension/Assets.xcassets/Colors/Background_DarkerDarkBlue.colorset/Contents.json

@@ -11,24 +11,6 @@
         }
       },
       "idiom" : "universal"
-    },
-    {
-      "appearances" : [
-        {
-          "appearance" : "luminosity",
-          "value" : "dark"
-        }
-      ],
-      "color" : {
-        "color-space" : "srgb",
-        "components" : {
-          "alpha" : "1.000",
-          "blue" : "0.109",
-          "green" : "0.058",
-          "red" : "0.011"
-        }
-      },
-      "idiom" : "universal"
     }
   ],
   "info" : {

+ 0 - 18
Trio Watch App Extension/Assets.xcassets/Colors/Basal.colorset/Contents.json

@@ -11,24 +11,6 @@
         }
       },
       "idiom" : "universal"
-    },
-    {
-      "appearances" : [
-        {
-          "appearance" : "luminosity",
-          "value" : "dark"
-        }
-      ],
-      "color" : {
-        "color-space" : "srgb",
-        "components" : {
-          "alpha" : "0.500",
-          "blue" : "0.988",
-          "green" : "0.588",
-          "red" : "0.118"
-        }
-      },
-      "idiom" : "universal"
     }
   ],
   "info" : {

+ 0 - 18
Trio Watch App Extension/Assets.xcassets/Colors/Insulin.colorset/Contents.json

@@ -11,24 +11,6 @@
         }
       },
       "idiom" : "universal"
-    },
-    {
-      "appearances" : [
-        {
-          "appearance" : "luminosity",
-          "value" : "dark"
-        }
-      ],
-      "color" : {
-        "color-space" : "srgb",
-        "components" : {
-          "alpha" : "1.000",
-          "blue" : "0.988",
-          "green" : "0.588",
-          "red" : "0.118"
-        }
-      },
-      "idiom" : "universal"
     }
   ],
   "info" : {

+ 0 - 18
Trio Watch App Extension/Assets.xcassets/Colors/LoopGray.colorset/Contents.json

@@ -11,24 +11,6 @@
         }
       },
       "idiom" : "universal"
-    },
-    {
-      "appearances" : [
-        {
-          "appearance" : "luminosity",
-          "value" : "dark"
-        }
-      ],
-      "color" : {
-        "color-space" : "srgb",
-        "components" : {
-          "alpha" : "1.000",
-          "blue" : "0.741",
-          "green" : "0.741",
-          "red" : "0.741"
-        }
-      },
-      "idiom" : "universal"
     }
   ],
   "info" : {

+ 0 - 18
Trio Watch App Extension/Assets.xcassets/Colors/LoopGreen.colorset/Contents.json

@@ -11,24 +11,6 @@
         }
       },
       "idiom" : "universal"
-    },
-    {
-      "appearances" : [
-        {
-          "appearance" : "luminosity",
-          "value" : "dark"
-        }
-      ],
-      "color" : {
-        "color-space" : "srgb",
-        "components" : {
-          "alpha" : "1.000",
-          "blue" : "0.592",
-          "green" : "0.812",
-          "red" : "0.435"
-        }
-      },
-      "idiom" : "universal"
     }
   ],
   "info" : {

+ 0 - 38
Trio Watch App Extension/Assets.xcassets/Colors/LoopPink.colorset/Contents.json

@@ -1,38 +0,0 @@
-{
-  "colors" : [
-    {
-      "color" : {
-        "color-space" : "srgb",
-        "components" : {
-          "alpha" : "1.000",
-          "blue" : "0.796",
-          "green" : "0.750",
-          "red" : "1.000"
-        }
-      },
-      "idiom" : "universal"
-    },
-    {
-      "appearances" : [
-        {
-          "appearance" : "luminosity",
-          "value" : "dark"
-        }
-      ],
-      "color" : {
-        "color-space" : "srgb",
-        "components" : {
-          "alpha" : "1.000",
-          "blue" : "0.796",
-          "green" : "0.750",
-          "red" : "1.000"
-        }
-      },
-      "idiom" : "universal"
-    }
-  ],
-  "info" : {
-    "author" : "xcode",
-    "version" : 1
-  }
-}

+ 0 - 18
Trio Watch App Extension/Assets.xcassets/Colors/LoopRed.colorset/Contents.json

@@ -11,24 +11,6 @@
         }
       },
       "idiom" : "universal"
-    },
-    {
-      "appearances" : [
-        {
-          "appearance" : "luminosity",
-          "value" : "dark"
-        }
-      ],
-      "color" : {
-        "color-space" : "srgb",
-        "components" : {
-          "alpha" : "1.000",
-          "blue" : "0.341",
-          "green" : "0.341",
-          "red" : "0.922"
-        }
-      },
-      "idiom" : "universal"
     }
   ],
   "info" : {

+ 0 - 18
Trio Watch App Extension/Assets.xcassets/Colors/TabBar.colorset/Contents.json

@@ -11,24 +11,6 @@
         }
       },
       "idiom" : "universal"
-    },
-    {
-      "appearances" : [
-        {
-          "appearance" : "luminosity",
-          "value" : "dark"
-        }
-      ],
-      "color" : {
-        "color-space" : "srgb",
-        "components" : {
-          "alpha" : "1.000",
-          "blue" : "0.950",
-          "green" : "0.550",
-          "red" : "0.490"
-        }
-      },
-      "idiom" : "universal"
     }
   ],
   "info" : {

+ 3 - 21
Trio Watch App Extension/Assets.xcassets/Colors/UAM.colorset/Contents.json

@@ -5,27 +5,9 @@
         "color-space" : "srgb",
         "components" : {
           "alpha" : "1.000",
-          "blue" : "0.271",
-          "green" : "0.518",
-          "red" : "1.000"
-        }
-      },
-      "idiom" : "universal"
-    },
-    {
-      "appearances" : [
-        {
-          "appearance" : "luminosity",
-          "value" : "dark"
-        }
-      ],
-      "color" : {
-        "color-space" : "srgb",
-        "components" : {
-          "alpha" : "1.000",
-          "blue" : "0.271",
-          "green" : "0.518",
-          "red" : "1.000"
+          "blue" : "0.969",
+          "green" : "0.169",
+          "red" : "0.820"
         }
       },
       "idiom" : "universal"

+ 0 - 18
Trio Watch App Extension/Assets.xcassets/Colors/ZT.colorset/Contents.json

@@ -11,24 +11,6 @@
         }
       },
       "idiom" : "universal"
-    },
-    {
-      "appearances" : [
-        {
-          "appearance" : "luminosity",
-          "value" : "dark"
-        }
-      ],
-      "color" : {
-        "color-space" : "srgb",
-        "components" : {
-          "alpha" : "1.000",
-          "blue" : "0.937",
-          "green" : "0.380",
-          "red" : "0.443"
-        }
-      },
-      "idiom" : "universal"
     }
   ],
   "info" : {

+ 46 - 42
Trio.xcodeproj/project.pbxproj

@@ -9,7 +9,7 @@
 /* Begin PBXBuildFile section */
 		041D1E995A6AE92E9289DC49 /* TreatmentsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8D1A7CA8C10C4403D4BBFA7 /* TreatmentsDataFlow.swift */; };
 		0437CE46C12535A56504EC19 /* SnoozeRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5822B15939E719628E9FF7C /* SnoozeRootView.swift */; };
-		0D9A5E34A899219C5C4CDFAF /* DataTableStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9455FA2D92E77A6C4AFED8A3 /* DataTableStateModel.swift */; };
+		0D9A5E34A899219C5C4CDFAF /* HistoryStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9455FA2D92E77A6C4AFED8A3 /* HistoryStateModel.swift */; };
 		0F7A65FBD2CD8D6477ED4539 /* GlucoseNotificationSettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E625985B47742D498CB1681A /* GlucoseNotificationSettingsProvider.swift */; };
 		110AEDE32C5193D200615CC9 /* BolusIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 110AEDE02C5193D100615CC9 /* BolusIntent.swift */; };
 		110AEDE42C5193D200615CC9 /* BolusIntentRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 110AEDE12C5193D100615CC9 /* BolusIntentRequest.swift */; };
@@ -54,7 +54,7 @@
 		19F95FF729F10FEE00314DDC /* StatStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19F95FF629F10FEE00314DDC /* StatStateModel.swift */; };
 		19F95FFA29F1102A00314DDC /* StatRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19F95FF929F1102A00314DDC /* StatRootView.swift */; };
 		1BBB001DAD60F3B8CEA4B1C7 /* ISFEditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505E09DC17A0C3D0AF4B66FE /* ISFEditorStateModel.swift */; };
-		1D845DF2E3324130E1D95E67 /* DataTableProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60744C3E9BB3652895C908CC /* DataTableProvider.swift */; };
+		1D845DF2E3324130E1D95E67 /* HistoryProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60744C3E9BB3652895C908CC /* HistoryProvider.swift */; };
 		23888883D4EA091C88480FF2 /* TreatmentsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C19984D62EFC0035A9E9644D /* TreatmentsProvider.swift */; };
 		3171D2818C7C72CD1584BB5E /* GlucoseNotificationSettingsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC2C6489D29ECCCAD78E0721 /* GlucoseNotificationSettingsStateModel.swift */; };
 		320D030F724170A637F06D50 /* (null) in Sources */ = {isa = PBXBuildFile; };
@@ -359,6 +359,7 @@
 		491D6FBE2D56741C00C49F67 /* TempTargetRunStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FB92D56741C00C49F67 /* TempTargetRunStored+CoreDataClass.swift */; };
 		491D6FBF2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FBA2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift */; };
 		491D6FC02D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FBB2D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift */; };
+		49239B432EEA27AD00469145 /* TempTargetCalculations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49239B422EEA27AD00469145 /* TempTargetCalculations.swift */; };
 		49B9B57F2D5768D2009C6B59 /* AdjustmentStored+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49B9B57E2D5768D2009C6B59 /* AdjustmentStored+Helper.swift */; };
 		5075C1608E6249A51495C422 /* TargetsEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BDEA2DC60EDE0A3CA54DC73 /* TargetsEditorProvider.swift */; };
 		53F2382465BF74DB1A967C8B /* PumpConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8630D58BDAD6D9C650B9B39 /* PumpConfigProvider.swift */; };
@@ -422,7 +423,7 @@
 		71D44AAB2CA5F5EA0036EE9E /* AlertPermissionsChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D44AAA2CA5F5EA0036EE9E /* AlertPermissionsChecker.swift */; };
 		72F1BD388F42FCA6C52E4500 /* ConfigEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44080E4709E3AE4B73054563 /* ConfigEditorProvider.swift */; };
 		7BCFACB97C821041BA43A114 /* ManualTempBasalRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */; };
-		7F7B756BE8543965D9FDF1A2 /* DataTableDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A401509D21F7F35D4E109EDA /* DataTableDataFlow.swift */; };
+		7F7B756BE8543965D9FDF1A2 /* HistoryDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A401509D21F7F35D4E109EDA /* HistoryDataFlow.swift */; };
 		8194B80890CDD6A3C13B0FEE /* SnoozeStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E26904AACA8D9C15D229D675 /* SnoozeStateModel.swift */; };
 		88AB39B23C9552BD6E0C9461 /* ISFEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBB3BAE7494CB771ABAC7B8B /* ISFEditorRootView.swift */; };
 		8A91342A2D63D9A1007F8874 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
@@ -605,7 +606,7 @@
 		CEE9A65C2BBB41C800EB5194 /* CalibrationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9A65B2BBB41C800EB5194 /* CalibrationService.swift */; };
 		CEE9A65E2BBC9F6500EB5194 /* CalibrationsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9A65D2BBC9F6500EB5194 /* CalibrationsTests.swift */; };
 		CEF1ED6B2D58FB5800FAF41E /* CGMOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF1ED6A2D58FB4600FAF41E /* CGMOptions.swift */; };
-		D6D02515BBFBE64FEBE89856 /* DataTableRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 881E04BA5E0A003DE8E0A9C6 /* DataTableRootView.swift */; };
+		D6D02515BBFBE64FEBE89856 /* HistoryRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 881E04BA5E0A003DE8E0A9C6 /* HistoryRootView.swift */; };
 		D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F2A13DF0EDEEEDC4106AA2A /* NightscoutConfigDataFlow.swift */; };
 		DBA5254DBB2586C98F61220C /* ISFEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9F137F126D9F8DEB799F26 /* ISFEditorProvider.swift */; };
 		DD09D47B2C5986D1003FEA5D /* CalendarEventSettingsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD09D47A2C5986D1003FEA5D /* CalendarEventSettingsDataFlow.swift */; };
@@ -1295,6 +1296,7 @@
 		491D6FBA2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetRunStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		491D6FBB2D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		491D6FBC2D56741C00C49F67 /* TempTargetStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
+		49239B422EEA27AD00469145 /* TempTargetCalculations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetCalculations.swift; sourceTree = "<group>"; };
 		49B9B57E2D5768D2009C6B59 /* AdjustmentStored+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentStored+Helper.swift"; sourceTree = "<group>"; };
 		4DD795BA46B193644D48138C /* TargetsEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorRootView.swift; sourceTree = "<group>"; };
 		505E09DC17A0C3D0AF4B66FE /* ISFEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorStateModel.swift; sourceTree = "<group>"; };
@@ -1336,7 +1338,7 @@
 		5A2325572BFCC168003518CA /* NightscoutConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutConnectView.swift; sourceTree = "<group>"; };
 		5C018D1680307A31C9ED7120 /* CGMSettingsStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CGMSettingsStateModel.swift; sourceTree = "<group>"; };
 		5D5B4F8B4194BB7E260EF251 /* ConfigEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConfigEditorStateModel.swift; sourceTree = "<group>"; };
-		60744C3E9BB3652895C908CC /* DataTableProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DataTableProvider.swift; sourceTree = "<group>"; };
+		60744C3E9BB3652895C908CC /* HistoryProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HistoryProvider.swift; sourceTree = "<group>"; };
 		64AA5E04A2761F6EEA6568E1 /* CarbRatioEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CarbRatioEditorStateModel.swift; sourceTree = "<group>"; };
 		65070A322BFDCB83006F213F /* TidepoolStartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TidepoolStartView.swift; sourceTree = "<group>"; };
 		67F94DD2853CF42BA4E30616 /* BasalProfileEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorDataFlow.swift; sourceTree = "<group>"; };
@@ -1356,16 +1358,16 @@
 		79BDA519C9B890FD9A5DFCF3 /* ISFEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorDataFlow.swift; sourceTree = "<group>"; };
 		7E22146D3DF4853786C78132 /* CarbRatioEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CarbRatioEditorDataFlow.swift; sourceTree = "<group>"; };
 		8782B44544F38F2B2D82C38E /* NightscoutConfigRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NightscoutConfigRootView.swift; sourceTree = "<group>"; };
-		881E04BA5E0A003DE8E0A9C6 /* DataTableRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DataTableRootView.swift; sourceTree = "<group>"; };
+		881E04BA5E0A003DE8E0A9C6 /* HistoryRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HistoryRootView.swift; sourceTree = "<group>"; };
 		8A9134292D63D9A1007F8874 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
 		8A91342B2D63D9A2007F8874 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = "<group>"; };
 		920DDB21E5D0EB813197500D /* ConfigEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConfigEditorRootView.swift; sourceTree = "<group>"; };
-		9455FA2D92E77A6C4AFED8A3 /* DataTableStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DataTableStateModel.swift; sourceTree = "<group>"; };
+		9455FA2D92E77A6C4AFED8A3 /* HistoryStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HistoryStateModel.swift; sourceTree = "<group>"; };
 		96653287EDB276A111288305 /* ManualTempBasalDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalDataFlow.swift; sourceTree = "<group>"; };
 		9C8D5F457B5AFF763F8CF3DF /* CarbRatioEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CarbRatioEditorProvider.swift; sourceTree = "<group>"; };
 		9F9F137F126D9F8DEB799F26 /* ISFEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorProvider.swift; sourceTree = "<group>"; };
 		A0A48AE3AC813A49A517846A /* NightscoutConfigStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NightscoutConfigStateModel.swift; sourceTree = "<group>"; };
-		A401509D21F7F35D4E109EDA /* DataTableDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DataTableDataFlow.swift; sourceTree = "<group>"; };
+		A401509D21F7F35D4E109EDA /* HistoryDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HistoryDataFlow.swift; sourceTree = "<group>"; };
 		A8630D58BDAD6D9C650B9B39 /* PumpConfigProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpConfigProvider.swift; sourceTree = "<group>"; };
 		AAFF91130F2FCCC7EBBA11AD /* BasalProfileEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorStateModel.swift; sourceTree = "<group>"; };
 		AF65DA88F972B56090AD6AC3 /* PumpConfigDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpConfigDataFlow.swift; sourceTree = "<group>"; };
@@ -1903,7 +1905,7 @@
 			isa = PBXGroup;
 			children = (
 				BDA7593D2D37CFC000E649A4 /* CarbEntryEditorView.swift */,
-				881E04BA5E0A003DE8E0A9C6 /* DataTableRootView.swift */,
+				881E04BA5E0A003DE8E0A9C6 /* HistoryRootView.swift */,
 			);
 			path = View;
 			sourceTree = "<group>";
@@ -2114,7 +2116,7 @@
 				F75CB57ED6971B46F8756083 /* CGMSettings */,
 				0610F7D6D2EC00E3BA1569F0 /* ConfigEditor */,
 				E592A3762CEEC038009A472C /* ContactImage */,
-				9E56E3626FAD933385101B76 /* DataTable */,
+				9E56E3626FAD933385101B76 /* History */,
 				195D80B22AF696EE00D25097 /* DynamicSettings */,
 				DD17454C2C55CA0200211FAC /* GeneralSettings */,
 				F66B236E00924A05D6A9F9DF /* GlucoseNotificationSettings */,
@@ -2525,18 +2527,18 @@
 				CE1F6DE62BAF1A180064EB8D /* BuildDetails.plist */,
 				38F3783A2613555C009DB701 /* Config.xcconfig */,
 				BD1CF8B72C1A4A8400CB930A /* ConfigOverride.xcconfig */,
-				388E595A25AD948C0019842D /* Trio */,
-				587A54C82BCDCE0F009D38E2 /* Model */,
-				38FCF3EE25E9028E0078B0D1 /* TrioTests */,
+				3818AA48274C267000843DB3 /* Frameworks */,
 				6B1A8D1C2B14D91600E76752 /* LiveActivity */,
+				587A54C82BCDCE0F009D38E2 /* Model */,
+				3818AA44274C229000843DB3 /* Packages */,
+				388E595925AD948C0019842D /* Products */,
+				192F0FF5276AC36D0085BE4D /* Recovered References */,
+				388E595A25AD948C0019842D /* Trio */,
 				BDFF7AA12D25FAC70016C40C /* Trio Watch App */,
 				BDFF7A9C2D25FA730016C40C /* Trio Watch App Extension */,
 				BDFF7AA02D25FAA80016C40C /* Trio Watch App Tests */,
 				DD09D6492D2B6253000D82C9 /* Trio Watch Complication */,
-				3818AA48274C267000843DB3 /* Frameworks */,
-				3818AA44274C229000843DB3 /* Packages */,
-				388E595925AD948C0019842D /* Products */,
-				192F0FF5276AC36D0085BE4D /* Recovered References */,
+				38FCF3EE25E9028E0078B0D1 /* TrioTests */,
 			);
 			sourceTree = "<group>";
 		};
@@ -2639,44 +2641,45 @@
 		388E5A5A25B6F05F0019842D /* Helpers */ = {
 			isa = PBXGroup;
 			children = (
-				DDD5889C2DDDC9A900C8848D /* TimeAgoFormatter.swift */,
-				DD82D4B72DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift */,
-				DDCAE8322D78D49C00B1BB51 /* TherapySettingsUtil.swift */,
-				BD249DA62D42FE3800412DEB /* Calendar+GlucoseStatsChart.swift */,
+				E06B9119275B5EEA003C04B6 /* Array+Extension.swift */,
 				DD73FA0E2D74F57300D19D1E /* BackgroundTask+Helper.swift */,
+				DD1DB7CB2BECCA1F0048B367 /* BuildDetails.swift */,
+				FE66D16A291F74F8005D6F77 /* Bundle+Extensions.swift */,
+				BD249DA62D42FE3800412DEB /* Calendar+GlucoseStatsChart.swift */,
 				CEF1ED6A2D58FB4600FAF41E /* CGMOptions.swift */,
-				C2A0A42E2CE0312C003B98E8 /* ConstantValues.swift */,
-				DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */,
 				38F37827261260DC009DB701 /* Color+Extensions.swift */,
 				389ECE042601144100D86C4F /* ConcurrentMap.swift */,
+				C2A0A42E2CE0312C003B98E8 /* ConstantValues.swift */,
 				38192E0C261BAF980094D973 /* ConvenienceExtensions.swift */,
+				BD1661302B82ADAB00256551 /* CustomProgressView.swift */,
+				581516A32BCED84A00BF67D7 /* DebuggingIdentifiers.swift */,
 				3871F39E25ED895A0013ECB5 /* Decimal+Extensions.swift */,
 				38C4D33625E9A1A200D30B77 /* DispatchQueue+Extensions.swift */,
 				389487392614928B004DF424 /* DispatchTimer.swift */,
+				DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */,
 				3811DE5425C9D4D500A708ED /* Formatters.swift */,
 				38FEF412273B317A00574A46 /* HKUnit.swift */,
 				38B4F3AE25E2979F00E76A18 /* IndexedCollection.swift */,
 				389A571F26079BAA00BC102F /* Interpolation.swift */,
 				388E5A5B25B6F0770019842D /* JSON.swift */,
 				38A00B2225FC2B55006BC0B0 /* LRUCache.swift */,
+				582DF9782C8CE1E5001F516D /* MainChartHelper.swift */,
 				38FCF3D525E8FDF40078B0D1 /* MD5.swift */,
 				38E98A2C25F52DC400C0CED0 /* NSLocking+Extensions.swift */,
 				38C4D33925E9A1ED00D30B77 /* NSObject+AssociatedValues.swift */,
 				3811DE5725C9D4D500A708ED /* ProgressBar.swift */,
+				DD82D4B72DCAB2BA00BAFC77 /* PropertyPersistentFlags.swift */,
+				3811DEE325CA063400A708ED /* PropertyWrappers */,
 				3811DE5525C9D4D500A708ED /* Publisher.swift */,
+				DD6B7CB12C7B6F0800B75029 /* Rounding.swift */,
+				CEA4F62229BE10F70011ADF7 /* SavitzkyGolayFilter.swift */,
 				38E98A3625F5509500C0CED0 /* String+Extensions.swift */,
-				3811DEE325CA063400A708ED /* PropertyWrappers */,
-				E06B9119275B5EEA003C04B6 /* Array+Extension.swift */,
+				49239B422EEA27AD00469145 /* TempTargetCalculations.swift */,
+				DDCAE8322D78D49C00B1BB51 /* TherapySettingsUtil.swift */,
+				DDD5889C2DDDC9A900C8848D /* TimeAgoFormatter.swift */,
+				BD2FF19F2AE29D43005D1C5D /* ToggleStyles.swift */,
 				CEB434E428B8FF5D00B70274 /* UIColor.swift */,
-				FE66D16A291F74F8005D6F77 /* Bundle+Extensions.swift */,
 				FEFFA7A12929FE49007B8193 /* UIDevice+Extensions.swift */,
-				CEA4F62229BE10F70011ADF7 /* SavitzkyGolayFilter.swift */,
-				BD2FF19F2AE29D43005D1C5D /* ToggleStyles.swift */,
-				BD1661302B82ADAB00256551 /* CustomProgressView.swift */,
-				581516A32BCED84A00BF67D7 /* DebuggingIdentifiers.swift */,
-				DD1DB7CB2BECCA1F0048B367 /* BuildDetails.swift */,
-				DD6B7CB12C7B6F0800B75029 /* Rounding.swift */,
-				582DF9782C8CE1E5001F516D /* MainChartHelper.swift */,
 			);
 			path = Helpers;
 			sourceTree = "<group>";
@@ -3220,15 +3223,15 @@
 			path = PumpConfig;
 			sourceTree = "<group>";
 		};
-		9E56E3626FAD933385101B76 /* DataTable */ = {
+		9E56E3626FAD933385101B76 /* History */ = {
 			isa = PBXGroup;
 			children = (
-				A401509D21F7F35D4E109EDA /* DataTableDataFlow.swift */,
-				60744C3E9BB3652895C908CC /* DataTableProvider.swift */,
-				9455FA2D92E77A6C4AFED8A3 /* DataTableStateModel.swift */,
+				A401509D21F7F35D4E109EDA /* HistoryDataFlow.swift */,
+				60744C3E9BB3652895C908CC /* HistoryProvider.swift */,
+				9455FA2D92E77A6C4AFED8A3 /* HistoryStateModel.swift */,
 				0EE66DD474AFFD4FD787D5B9 /* View */,
 			);
-			path = DataTable;
+			path = History;
 			sourceTree = "<group>";
 		};
 		A42F1FEDFFD0DDE00AAD54D3 /* BasalProfileEditor */ = {
@@ -4776,6 +4779,7 @@
 				3B5CD29C2D4AEA3C00CE213C /* ProfileGenerator.swift in Sources */,
 				3B5CD29D2D4AEA3C00CE213C /* Targets.swift in Sources */,
 				DDA6E2502D22187500C2988C /* ChartLegendView.swift in Sources */,
+				49239B432EEA27AD00469145 /* TempTargetCalculations.swift in Sources */,
 				3811DEAF25C9D88300A708ED /* KeyValueStorage.swift in Sources */,
 				DDD6D4D32CDE90720029439A /* EstimatedA1cDisplayUnit.swift in Sources */,
 				DDA6E3572D25988500C2988C /* ContactImageHelpView.swift in Sources */,
@@ -5143,18 +5147,18 @@
 				3BA8D1B32DDB87150006191F /* DecimalExtensions.swift in Sources */,
 				DD3F1F892D9E078D00DCE7B3 /* TherapySettingEditorView.swift in Sources */,
 				DD1745132C54169400211FAC /* DevicesView.swift in Sources */,
-				7F7B756BE8543965D9FDF1A2 /* DataTableDataFlow.swift in Sources */,
-				1D845DF2E3324130E1D95E67 /* DataTableProvider.swift in Sources */,
+				7F7B756BE8543965D9FDF1A2 /* HistoryDataFlow.swift in Sources */,
+				1D845DF2E3324130E1D95E67 /* HistoryProvider.swift in Sources */,
 				DDCE790F2D6F97FC000A4D7A /* SubmodulesView.swift in Sources */,
 				19F95FFA29F1102A00314DDC /* StatRootView.swift in Sources */,
-				0D9A5E34A899219C5C4CDFAF /* DataTableStateModel.swift in Sources */,
+				0D9A5E34A899219C5C4CDFAF /* HistoryStateModel.swift in Sources */,
 				6BCF84DD2B16843A003AD46E /* LiveActitiyAttributes.swift in Sources */,
 				195D80B92AF697F700D25097 /* DynamicSettingsProvider.swift in Sources */,
 				DD3F1F832D9DC78800DCE7B3 /* UnitSelectionStepView.swift in Sources */,
 				DD09D47D2C5986DA003FEA5D /* CalendarEventSettingsProvider.swift in Sources */,
 				DD09D47B2C5986D1003FEA5D /* CalendarEventSettingsDataFlow.swift in Sources */,
 				DD1745202C55523E00211FAC /* SMBSettingsDataFlow.swift in Sources */,
-				D6D02515BBFBE64FEBE89856 /* DataTableRootView.swift in Sources */,
+				D6D02515BBFBE64FEBE89856 /* HistoryRootView.swift in Sources */,
 				DD1745292C55642100211FAC /* SettingInputSection.swift in Sources */,
 				38569349270B5DFB0002C50D /* AppGroupSource.swift in Sources */,
 				3B5CD2A12D4AEA5100CE213C /* JavascriptOptional.swift in Sources */,

+ 0 - 18
Trio/Resources/Assets.xcassets/Colors/Background_DarkBlue.colorset/Contents.json

@@ -11,24 +11,6 @@
         }
       },
       "idiom" : "universal"
-    },
-    {
-      "appearances" : [
-        {
-          "appearance" : "luminosity",
-          "value" : "dark"
-        }
-      ],
-      "color" : {
-        "color-space" : "srgb",
-        "components" : {
-          "alpha" : "1.000",
-          "blue" : "0.216",
-          "green" : "0.133",
-          "red" : "0.039"
-        }
-      },
-      "idiom" : "universal"
     }
   ],
   "info" : {

+ 0 - 18
Trio/Resources/Assets.xcassets/Colors/Background_DarkerDarkBlue.colorset/Contents.json

@@ -11,24 +11,6 @@
         }
       },
       "idiom" : "universal"
-    },
-    {
-      "appearances" : [
-        {
-          "appearance" : "luminosity",
-          "value" : "dark"
-        }
-      ],
-      "color" : {
-        "color-space" : "srgb",
-        "components" : {
-          "alpha" : "1.000",
-          "blue" : "0.109",
-          "green" : "0.058",
-          "red" : "0.011"
-        }
-      },
-      "idiom" : "universal"
     }
   ],
   "info" : {

+ 0 - 18
Trio/Resources/Assets.xcassets/Colors/Basal.colorset/Contents.json

@@ -11,24 +11,6 @@
         }
       },
       "idiom" : "universal"
-    },
-    {
-      "appearances" : [
-        {
-          "appearance" : "luminosity",
-          "value" : "dark"
-        }
-      ],
-      "color" : {
-        "color-space" : "srgb",
-        "components" : {
-          "alpha" : "0.500",
-          "blue" : "0.988",
-          "green" : "0.588",
-          "red" : "0.118"
-        }
-      },
-      "idiom" : "universal"
     }
   ],
   "info" : {

+ 0 - 18
Trio/Resources/Assets.xcassets/Colors/DarkerBlue.colorset/Contents.json

@@ -11,24 +11,6 @@
         }
       },
       "idiom" : "universal"
-    },
-    {
-      "appearances" : [
-        {
-          "appearance" : "luminosity",
-          "value" : "dark"
-        }
-      ],
-      "color" : {
-        "color-space" : "srgb",
-        "components" : {
-          "alpha" : "1.000",
-          "blue" : "1.000",
-          "green" : "0.288",
-          "red" : "0.118"
-        }
-      },
-      "idiom" : "universal"
     }
   ],
   "info" : {

+ 0 - 18
Trio/Resources/Assets.xcassets/Colors/Insulin.colorset/Contents.json

@@ -11,24 +11,6 @@
         }
       },
       "idiom" : "universal"
-    },
-    {
-      "appearances" : [
-        {
-          "appearance" : "luminosity",
-          "value" : "dark"
-        }
-      ],
-      "color" : {
-        "color-space" : "srgb",
-        "components" : {
-          "alpha" : "1.000",
-          "blue" : "0.988",
-          "green" : "0.588",
-          "red" : "0.118"
-        }
-      },
-      "idiom" : "universal"
     }
   ],
   "info" : {

+ 0 - 38
Trio/Resources/Assets.xcassets/Colors/Lemon.colorset/Contents.json

@@ -1,38 +0,0 @@
-{
-  "colors" : [
-    {
-      "color" : {
-        "color-space" : "srgb",
-        "components" : {
-          "alpha" : "1.000",
-          "blue" : "0.089",
-          "green" : "0.940",
-          "red" : "1.000"
-        }
-      },
-      "idiom" : "universal"
-    },
-    {
-      "appearances" : [
-        {
-          "appearance" : "luminosity",
-          "value" : "dark"
-        }
-      ],
-      "color" : {
-        "color-space" : "srgb",
-        "components" : {
-          "alpha" : "1.000",
-          "blue" : "0.089",
-          "green" : "0.940",
-          "red" : "1.000"
-        }
-      },
-      "idiom" : "universal"
-    }
-  ],
-  "info" : {
-    "author" : "xcode",
-    "version" : 1
-  }
-}

+ 0 - 18
Trio/Resources/Assets.xcassets/Colors/LoopGray.colorset/Contents.json

@@ -11,24 +11,6 @@
         }
       },
       "idiom" : "universal"
-    },
-    {
-      "appearances" : [
-        {
-          "appearance" : "luminosity",
-          "value" : "dark"
-        }
-      ],
-      "color" : {
-        "color-space" : "srgb",
-        "components" : {
-          "alpha" : "1.000",
-          "blue" : "0.741",
-          "green" : "0.741",
-          "red" : "0.741"
-        }
-      },
-      "idiom" : "universal"
     }
   ],
   "info" : {

+ 0 - 18
Trio/Resources/Assets.xcassets/Colors/LoopGreen.colorset/Contents.json

@@ -11,24 +11,6 @@
         }
       },
       "idiom" : "universal"
-    },
-    {
-      "appearances" : [
-        {
-          "appearance" : "luminosity",
-          "value" : "dark"
-        }
-      ],
-      "color" : {
-        "color-space" : "srgb",
-        "components" : {
-          "alpha" : "1.000",
-          "blue" : "0.592",
-          "green" : "0.812",
-          "red" : "0.435"
-        }
-      },
-      "idiom" : "universal"
     }
   ],
   "info" : {

+ 0 - 38
Trio/Resources/Assets.xcassets/Colors/LoopPink.colorset/Contents.json

@@ -1,38 +0,0 @@
-{
-  "colors" : [
-    {
-      "color" : {
-        "color-space" : "srgb",
-        "components" : {
-          "alpha" : "1.000",
-          "blue" : "0.796",
-          "green" : "0.750",
-          "red" : "1.000"
-        }
-      },
-      "idiom" : "universal"
-    },
-    {
-      "appearances" : [
-        {
-          "appearance" : "luminosity",
-          "value" : "dark"
-        }
-      ],
-      "color" : {
-        "color-space" : "srgb",
-        "components" : {
-          "alpha" : "1.000",
-          "blue" : "0.796",
-          "green" : "0.750",
-          "red" : "1.000"
-        }
-      },
-      "idiom" : "universal"
-    }
-  ],
-  "info" : {
-    "author" : "xcode",
-    "version" : 1
-  }
-}

+ 0 - 18
Trio/Resources/Assets.xcassets/Colors/LoopRed.colorset/Contents.json

@@ -11,24 +11,6 @@
         }
       },
       "idiom" : "universal"
-    },
-    {
-      "appearances" : [
-        {
-          "appearance" : "luminosity",
-          "value" : "dark"
-        }
-      ],
-      "color" : {
-        "color-space" : "srgb",
-        "components" : {
-          "alpha" : "1.000",
-          "blue" : "0.341",
-          "green" : "0.341",
-          "red" : "0.922"
-        }
-      },
-      "idiom" : "universal"
     }
   ],
   "info" : {

+ 0 - 18
Trio/Resources/Assets.xcassets/Colors/ManualTempBasal.colorset/Contents.json

@@ -11,24 +11,6 @@
         }
       },
       "idiom" : "universal"
-    },
-    {
-      "appearances" : [
-        {
-          "appearance" : "luminosity",
-          "value" : "dark"
-        }
-      ],
-      "color" : {
-        "color-space" : "srgb",
-        "components" : {
-          "alpha" : "1.000",
-          "blue" : "0.988",
-          "green" : "0.588",
-          "red" : "0.118"
-        }
-      },
-      "idiom" : "universal"
     }
   ],
   "info" : {

+ 0 - 18
Trio/Resources/Assets.xcassets/Colors/TabBar.colorset/Contents.json

@@ -11,24 +11,6 @@
         }
       },
       "idiom" : "universal"
-    },
-    {
-      "appearances" : [
-        {
-          "appearance" : "luminosity",
-          "value" : "dark"
-        }
-      ],
-      "color" : {
-        "color-space" : "srgb",
-        "components" : {
-          "alpha" : "1.000",
-          "blue" : "0.950",
-          "green" : "0.550",
-          "red" : "0.490"
-        }
-      },
-      "idiom" : "universal"
     }
   ],
   "info" : {

+ 3 - 21
Trio/Resources/Assets.xcassets/Colors/UAM.colorset/Contents.json

@@ -5,27 +5,9 @@
         "color-space" : "srgb",
         "components" : {
           "alpha" : "1.000",
-          "blue" : "0.271",
-          "green" : "0.518",
-          "red" : "1.000"
-        }
-      },
-      "idiom" : "universal"
-    },
-    {
-      "appearances" : [
-        {
-          "appearance" : "luminosity",
-          "value" : "dark"
-        }
-      ],
-      "color" : {
-        "color-space" : "srgb",
-        "components" : {
-          "alpha" : "1.000",
-          "blue" : "0.271",
-          "green" : "0.518",
-          "red" : "1.000"
+          "blue" : "0.969",
+          "green" : "0.169",
+          "red" : "0.820"
         }
       },
       "idiom" : "universal"

+ 0 - 18
Trio/Resources/Assets.xcassets/Colors/ZT.colorset/Contents.json

@@ -11,24 +11,6 @@
         }
       },
       "idiom" : "universal"
-    },
-    {
-      "appearances" : [
-        {
-          "appearance" : "luminosity",
-          "value" : "dark"
-        }
-      ],
-      "color" : {
-        "color-space" : "srgb",
-        "components" : {
-          "alpha" : "1.000",
-          "blue" : "0.937",
-          "green" : "0.380",
-          "red" : "0.443"
-        }
-      },
-      "idiom" : "universal"
     }
   ],
   "info" : {

+ 0 - 18
Trio/Resources/Assets.xcassets/Colors/minus.colorset/Contents.json

@@ -11,24 +11,6 @@
         }
       },
       "idiom" : "universal"
-    },
-    {
-      "appearances" : [
-        {
-          "appearance" : "luminosity",
-          "value" : "dark"
-        }
-      ],
-      "color" : {
-        "color-space" : "srgb",
-        "components" : {
-          "alpha" : "1.000",
-          "blue" : "0.976",
-          "green" : "0.839",
-          "red" : "0.635"
-        }
-      },
-      "idiom" : "universal"
     }
   ],
   "info" : {

+ 0 - 184
Trio/Sources/APS/CGM/DexcomSourceG5.swift

@@ -1,184 +0,0 @@
-import CGMBLEKit
-import Combine
-import Foundation
-import LoopKit
-import LoopKitUI
-import ShareClient
-
-final class DexcomSourceG5: GlucoseSource {
-    private let processQueue = DispatchQueue(label: "DexcomSource.processQueue")
-    private let glucoseStorage: GlucoseStorage!
-    var glucoseManager: FetchGlucoseManager?
-
-    var cgmManager: CGMManagerUI?
-    var cgmType: CGMType = .dexcomG5
-
-    var cgmHasValidSensorSession: Bool = false
-
-    private var promise: Future<[BloodGlucose], Error>.Promise?
-
-    init(glucoseStorage: GlucoseStorage, glucoseManager: FetchGlucoseManager) {
-        self.glucoseStorage = glucoseStorage
-        self.glucoseManager = glucoseManager
-        cgmManager = G5CGMManager
-            .init(state: TransmitterManagerState(
-                transmitterID: UserDefaults.standard
-                    .dexcomTransmitterID ?? "000000",
-                shouldSyncToRemoteService: glucoseManager.settingsManager.settings.uploadGlucose
-            ))
-        cgmManager?.cgmManagerDelegate = self
-    }
-
-    var transmitterID: String {
-        guard let cgmG5Manager = cgmManager as? G5CGMManager else { return "000000" }
-        return cgmG5Manager.transmitter.ID
-    }
-
-    func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
-        Future<[BloodGlucose], Error> { [weak self] promise in
-            self?.promise = promise
-        }
-        .timeout(60 * 5, scheduler: processQueue, options: nil, customError: nil)
-        .replaceError(with: [])
-        .replaceEmpty(with: [])
-        .eraseToAnyPublisher()
-    }
-
-    func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never> {
-        Future<[BloodGlucose], Error> { _ in
-            self.processQueue.async {
-                guard let cgmManager = self.cgmManager else { return }
-                cgmManager.fetchNewDataIfNeeded { result in
-                    self.processCGMReadingResult(cgmManager, readingResult: result) {
-                        // nothing to do
-                    }
-                }
-            }
-        }
-        .timeout(60, scheduler: processQueue, options: nil, customError: nil)
-        .replaceError(with: [])
-        .replaceEmpty(with: [])
-        .eraseToAnyPublisher()
-    }
-
-    deinit {
-        // dexcomManager.transmitter.stopScanning()
-    }
-}
-
-extension DexcomSourceG5: CGMManagerDelegate {
-    func deviceManager(
-        _: LoopKit.DeviceManager,
-        logEventForDeviceIdentifier deviceIdentifier: String?,
-        type _: LoopKit.DeviceLogEntryType,
-        message: String,
-        completion _: ((Error?) -> Void)?
-    ) {
-        debug(.deviceManager, "device Manager for \(String(describing: deviceIdentifier)) : \(message)")
-    }
-
-    func issueAlert(_: LoopKit.Alert) {}
-
-    func retractAlert(identifier _: LoopKit.Alert.Identifier) {}
-
-    func doesIssuedAlertExist(identifier _: LoopKit.Alert.Identifier, completion _: @escaping (Result<Bool, Error>) -> Void) {}
-
-    func lookupAllUnretracted(
-        managerIdentifier _: String,
-        completion _: @escaping (Result<[LoopKit.PersistedAlert], Error>) -> Void
-    ) {}
-
-    func lookupAllUnacknowledgedUnretracted(
-        managerIdentifier _: String,
-        completion _: @escaping (Result<[LoopKit.PersistedAlert], Error>) -> Void
-    ) {}
-
-    func recordRetractedAlert(_: LoopKit.Alert, at _: Date) {}
-
-    func cgmManagerWantsDeletion(_ manager: CGMManager) {
-        dispatchPrecondition(condition: .onQueue(.main))
-        debug(.deviceManager, " CGM Manager with identifier \(manager.managerIdentifier) wants deletion")
-        glucoseManager?.cgmGlucoseSourceType = nil
-    }
-
-    func cgmManager(_ manager: CGMManager, hasNew readingResult: CGMReadingResult) {
-        dispatchPrecondition(condition: .onQueue(.main))
-        processCGMReadingResult(manager, readingResult: readingResult) {
-            debug(.deviceManager, "DEXCOM - Direct return done")
-        }
-    }
-
-    func startDateToFilterNewData(for _: CGMManager) -> Date? {
-        dispatchPrecondition(condition: .onQueue(.main))
-        return glucoseStorage.lastGlucoseDate()
-        //  return glucoseStore.latestGlucose?.startDate
-    }
-
-    func cgmManagerDidUpdateState(_ manager: CGMManager) {
-        dispatchPrecondition(condition: .onQueue(processQueue))
-        guard let g5Manager = manager as? TransmitterManager else {
-            return
-        }
-        glucoseManager?.settingsManager.settings.uploadGlucose = g5Manager.shouldSyncToRemoteService
-        UserDefaults.standard.dexcomTransmitterID = g5Manager.rawState["transmitterID"] as? String
-    }
-
-    func credentialStoragePrefix(for _: CGMManager) -> String {
-        // return string unique to this instance of the CGMManager
-        UUID().uuidString
-    }
-
-    func cgmManager(_: CGMManager, didUpdate status: CGMManagerStatus) {
-        DispatchQueue.main.async {
-            if self.cgmHasValidSensorSession != status.hasValidSensorSession {
-                self.cgmHasValidSensorSession = status.hasValidSensorSession
-            }
-        }
-    }
-
-    private func processCGMReadingResult(
-        _: CGMManager,
-        readingResult: CGMReadingResult,
-        completion: @escaping () -> Void
-    ) {
-        debug(.deviceManager, "DEXCOM - Process CGM Reading Result launched")
-        switch readingResult {
-        case let .newData(values):
-            let bloodGlucose = values.compactMap { newGlucoseSample -> BloodGlucose? in
-                let quantity = newGlucoseSample.quantity
-                let value = Int(quantity.doubleValue(for: .milligramsPerDeciliter))
-                return BloodGlucose(
-                    _id: UUID().uuidString,
-                    sgv: value,
-                    direction: .init(trendType: newGlucoseSample.trend),
-                    date: Decimal(Int(newGlucoseSample.date.timeIntervalSince1970 * 1000)),
-                    dateString: newGlucoseSample.date,
-                    unfiltered: Decimal(value),
-                    filtered: nil,
-                    noise: nil,
-                    glucose: value,
-                    type: "sgv",
-                    transmitterID: self.transmitterID
-                )
-            }
-            promise?(.success(bloodGlucose))
-            completion()
-        case .unreliableData:
-            // loopManager.receivedUnreliableCGMReading()
-            promise?(.failure(GlucoseDataError.unreliableData))
-            completion()
-        case .noData:
-            promise?(.failure(GlucoseDataError.noData))
-            completion()
-        case let .error(error):
-            promise?(.failure(error))
-            completion()
-        }
-    }
-}
-
-extension DexcomSourceG5 {
-    func sourceInfo() -> [String: Any]? {
-        [GlucoseSourceKey.description.rawValue: "Dexcom tramsmitter ID: \(transmitterID)"]
-    }
-}

+ 0 - 195
Trio/Sources/APS/CGM/DexcomSourceG6.swift

@@ -1,195 +0,0 @@
-import CGMBLEKit
-import Combine
-import Foundation
-import LoopKit
-import LoopKitUI
-import ShareClient
-
-final class DexcomSourceG6: GlucoseSource {
-    private let processQueue = DispatchQueue(label: "DexcomSource.processQueue")
-    private let glucoseStorage: GlucoseStorage!
-    var glucoseManager: FetchGlucoseManager?
-
-    var cgmManager: CGMManagerUI?
-    var cgmType: CGMType = .dexcomG6
-
-    var cgmHasValidSensorSession: Bool = false
-
-    private var promise: Future<[BloodGlucose], Error>.Promise?
-
-    init(glucoseStorage: GlucoseStorage, glucoseManager: FetchGlucoseManager) {
-        self.glucoseStorage = glucoseStorage
-        self.glucoseManager = glucoseManager
-        cgmManager = G6CGMManager
-            .init(state: TransmitterManagerState(
-                transmitterID: UserDefaults.standard
-                    .dexcomTransmitterID ?? "000000",
-                shouldSyncToRemoteService: glucoseManager.settingsManager.settings.uploadGlucose
-            ))
-        cgmManager?.delegateQueue = processQueue
-        cgmManager?.cgmManagerDelegate = self
-    }
-
-    var transmitterID: String {
-        guard let cgmG6Manager = cgmManager as? G6CGMManager else { return "000000" }
-        return cgmG6Manager.transmitter.ID
-    }
-
-    func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
-        Future<[BloodGlucose], Error> { [weak self] promise in
-            self?.promise = promise
-        }
-        .timeout(60 * 5, scheduler: processQueue, options: nil, customError: nil)
-        .replaceError(with: [])
-        .replaceEmpty(with: [])
-        .eraseToAnyPublisher()
-    }
-
-    func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never> {
-        Future<[BloodGlucose], Error> { _ in
-            self.processQueue.async {
-                guard let cgmManager = self.cgmManager else { return }
-                cgmManager.fetchNewDataIfNeeded { result in
-                    self.processCGMReadingResult(cgmManager, readingResult: result) {
-                        // nothing to do
-                    }
-                }
-            }
-        }
-        .timeout(60, scheduler: processQueue, options: nil, customError: nil)
-        .replaceError(with: [])
-        .replaceEmpty(with: [])
-        .eraseToAnyPublisher()
-    }
-
-    deinit {
-        // dexcomManager.transmitter.stopScanning()
-    }
-}
-
-extension DexcomSourceG6: CGMManagerDelegate {
-    func deviceManager(
-        _: LoopKit.DeviceManager,
-        logEventForDeviceIdentifier deviceIdentifier: String?,
-        type _: LoopKit.DeviceLogEntryType,
-        message: String,
-        completion _: ((Error?) -> Void)?
-    ) {
-        debug(.deviceManager, "device Manager for \(String(describing: deviceIdentifier)) : \(message)")
-    }
-
-    func issueAlert(_: LoopKit.Alert) {}
-
-    func retractAlert(identifier _: LoopKit.Alert.Identifier) {}
-
-    func doesIssuedAlertExist(identifier _: LoopKit.Alert.Identifier, completion _: @escaping (Result<Bool, Error>) -> Void) {}
-
-    func lookupAllUnretracted(
-        managerIdentifier _: String,
-        completion _: @escaping (Result<[LoopKit.PersistedAlert], Error>) -> Void
-    ) {}
-
-    func lookupAllUnacknowledgedUnretracted(
-        managerIdentifier _: String,
-        completion _: @escaping (Result<[LoopKit.PersistedAlert], Error>) -> Void
-    ) {}
-
-    func recordRetractedAlert(_: LoopKit.Alert, at _: Date) {}
-
-    func cgmManagerWantsDeletion(_ manager: CGMManager) {
-        dispatchPrecondition(condition: .onQueue(processQueue))
-        debug(.deviceManager, " CGM Manager with identifier \(manager.managerIdentifier) wants deletion")
-        glucoseManager?.cgmGlucoseSourceType = nil
-    }
-
-    func cgmManager(_ manager: CGMManager, hasNew readingResult: CGMReadingResult) {
-        dispatchPrecondition(condition: .onQueue(processQueue))
-        processCGMReadingResult(manager, readingResult: readingResult) {
-            debug(.deviceManager, "DEXCOM - Direct return done")
-        }
-    }
-
-    func startDateToFilterNewData(for _: CGMManager) -> Date? {
-        dispatchPrecondition(condition: .onQueue(processQueue))
-        return glucoseStorage.lastGlucoseDate()
-        //  return glucoseStore.latestGlucose?.startDate
-    }
-
-    func cgmManagerDidUpdateState(_ manager: CGMManager) {
-        dispatchPrecondition(condition: .onQueue(processQueue))
-        guard let g6Manager = manager as? TransmitterManager else {
-            return
-        }
-        glucoseManager?.settingsManager.settings.uploadGlucose = g6Manager.shouldSyncToRemoteService
-        UserDefaults.standard.dexcomTransmitterID = g6Manager.rawState["transmitterID"] as? String
-    }
-
-    func credentialStoragePrefix(for _: CGMManager) -> String {
-        // return string unique to this instance of the CGMManager
-        UUID().uuidString
-    }
-
-    func cgmManager(_: CGMManager, didUpdate status: CGMManagerStatus) {
-        processQueue.async {
-            if self.cgmHasValidSensorSession != status.hasValidSensorSession {
-                self.cgmHasValidSensorSession = status.hasValidSensorSession
-            }
-        }
-    }
-
-    private func processCGMReadingResult(
-        _: CGMManager,
-        readingResult: CGMReadingResult,
-        completion: @escaping () -> Void
-    ) {
-        debug(.deviceManager, "DEXCOM - Process CGM Reading Result launched with \(readingResult)")
-        switch readingResult {
-        case let .newData(values):
-            if let cgmG6Manager = cgmManager as? G6CGMManager,
-               let activationDate = cgmG6Manager.latestReading?.activationDate,
-               let sessionStartDate = cgmG6Manager.latestReading?.sessionStartDate
-            {
-                let bloodGlucose = values.compactMap { newGlucoseSample -> BloodGlucose? in
-                    let quantity = newGlucoseSample.quantity
-                    let value = Int(quantity.doubleValue(for: .milligramsPerDeciliter))
-                    return BloodGlucose(
-                        _id: UUID().uuidString,
-                        sgv: value,
-                        direction: .init(trendType: newGlucoseSample.trend),
-                        date: Decimal(Int(newGlucoseSample.date.timeIntervalSince1970 * 1000)),
-                        dateString: newGlucoseSample.date,
-                        unfiltered: Decimal(value),
-                        filtered: nil,
-                        noise: nil,
-                        glucose: value,
-                        type: "sgv",
-                        activationDate: activationDate,
-                        sessionStartDate: sessionStartDate,
-                        transmitterID: self.transmitterID
-                    )
-                }
-                promise?(.success(bloodGlucose))
-                completion()
-            } else {
-                // Handle the case where activationDate or sessionStartDate is nil
-                completion()
-            }
-        case .unreliableData:
-            // loopManager.receivedUnreliableCGMReading()
-            promise?(.failure(GlucoseDataError.unreliableData))
-            completion()
-        case .noData:
-            promise?(.failure(GlucoseDataError.noData))
-            completion()
-        case let .error(error):
-            promise?(.failure(error))
-            completion()
-        }
-    }
-}
-
-extension DexcomSourceG6 {
-    func sourceInfo() -> [String: Any]? {
-        [GlucoseSourceKey.description.rawValue: "Dexcom tramsmitter ID: \(transmitterID)"]
-    }
-}

+ 0 - 103
Trio/Sources/APS/CGM/LibreTransmitterSource.swift

@@ -1,103 +0,0 @@
-import Combine
-import Foundation
-import LibreTransmitter
-import LoopKitUI
-import Swinject
-
-protocol LibreTransmitterSource: GlucoseSource {
-    var manager: LibreTransmitterManager? { get set }
-}
-
-final class BaseLibreTransmitterSource: LibreTransmitterSource, Injectable {
-    var cgmManager: CGMManagerUI?
-    var cgmType: CGMType = .libreTransmitter
-
-    private let processQueue = DispatchQueue(label: "BaseLibreTransmitterSource.processQueue")
-
-    @Injected() var glucoseStorage: GlucoseStorage!
-    @Injected() var calibrationService: CalibrationService!
-
-    private var promise: Future<[BloodGlucose], Error>.Promise?
-
-    var glucoseManager: FetchGlucoseManager?
-
-    var manager: LibreTransmitterManager? {
-        didSet {
-            configured = manager != nil
-            manager?.cgmManagerDelegate = self
-        }
-    }
-
-    @Persisted(key: "LibreTransmitterManager.configured") private(set) var configured = false
-
-    init(resolver: Resolver) {
-        if configured {
-            manager = LibreTransmitterManager()
-            manager?.cgmManagerDelegate = self
-        }
-        injectServices(resolver)
-    }
-
-    func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
-        Future<[BloodGlucose], Error> { [weak self] promise in
-            self?.promise = promise
-        }
-        .timeout(60, scheduler: processQueue, options: nil, customError: nil)
-        .replaceError(with: [])
-        .replaceEmpty(with: [])
-        .eraseToAnyPublisher()
-    }
-
-    func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never> {
-        fetch(nil)
-    }
-
-    func sourceInfo() -> [String: Any]? {
-        if let battery = manager?.battery {
-            return ["transmitterBattery": battery]
-        }
-        return nil
-    }
-}
-
-extension BaseLibreTransmitterSource: LibreTransmitterManagerDelegate {
-    var queue: DispatchQueue { processQueue }
-
-    func startDateToFilterNewData(for _: LibreTransmitterManager) -> Date? {
-        glucoseStorage.syncDate()
-    }
-
-    func cgmManager(_ manager: LibreTransmitterManager, hasNew result: Result<[LibreGlucose], Error>) {
-        switch result {
-        case let .success(newGlucose):
-            let glucose = newGlucose.map { value -> BloodGlucose in
-                BloodGlucose(
-                    _id: UUID().uuidString,
-                    sgv: Int(value.glucose),
-                    direction: manager.glucoseDisplay?.trendType
-                        .map { .init(trendType: $0) },
-                    date: Decimal(Int(value.startDate.timeIntervalSince1970 * 1000)),
-                    dateString: value.startDate,
-                    unfiltered: Decimal(value.unsmoothedGlucose),
-                    filtered: nil,
-                    noise: nil,
-                    glucose: Int(value.glucose),
-                    type: "sgv",
-                    activationDate: value.sensorStartDate ?? manager.sensorStartDate,
-                    sessionStartDate: value.sensorStartDate ?? manager.sensorStartDate,
-                    transmitterID: manager.sensorSerialNumber
-                )
-            }
-            NSLog("Debug Libre \(glucose)")
-            promise?(.success(glucose))
-
-        case let .failure(error):
-            warning(.service, "LibreTransmitter error:", error: error)
-            promise?(.failure(error))
-        }
-    }
-
-    func overcalibration(for _: LibreTransmitterManager) -> ((Double) -> (Double))? {
-        calibrationService.calibrate
-    }
-}

+ 2 - 10
Trio/Sources/APS/CGM/PluginSource.swift

@@ -144,17 +144,9 @@ extension PluginSource: CGMManagerDelegate {
     }
 
     func startDateToFilterNewData(for _: CGMManager) -> Date? {
-        var date: Date?
+        dispatchPrecondition(condition: .onQueue(processQueue))
 
-        processQueue.async { [weak self] in
-            guard let self = self else { return }
-
-            dispatchPrecondition(condition: .onQueue(self.processQueue))
-
-            date = glucoseStorage.lastGlucoseDate()
-        }
-
-        return date
+        return glucoseStorage.lastGlucoseDate()
     }
 
     func cgmManagerDidUpdateState(_ cgmManager: CGMManager) {

+ 4 - 19
Trio/Sources/APS/DeviceDataManager.swift

@@ -596,7 +596,7 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
 
 extension BaseDeviceDataManager: DeviceManagerDelegate {
     func issueAlert(_ alert: Alert) {
-        alertHistoryStorage.storeAlert(
+        alertHistoryStorage.addAlert(
             AlertEntry(
                 alertIdentifier: alert.identifier.alertIdentifier,
                 primitiveInterruptionLevel: alert.interruptionLevel.storedValue as? Decimal,
@@ -611,7 +611,7 @@ extension BaseDeviceDataManager: DeviceManagerDelegate {
     }
 
     func retractAlert(identifier: Alert.Identifier) {
-        alertHistoryStorage.deleteAlert(identifier: identifier.alertIdentifier)
+        alertHistoryStorage.removeAlert(identifier: identifier.alertIdentifier)
     }
 
     func doesIssuedAlertExist(identifier _: Alert.Identifier, completion _: @escaping (Result<Bool, Error>) -> Void) {
@@ -681,27 +681,12 @@ extension BaseDeviceDataManager: AlertObserver {
         let alertIssueDate = alert.issuedDate
 
         processQueue.async {
-            // if not alert in OmniPod/BLE, the acknowledgeAlert didn't do callbacks- Hack to manage this case
-            if let omnipodBLE = self.pumpManager as? OmniBLEPumpManager {
-                if omnipodBLE.state.activeAlerts.isEmpty {
-                    // force to ack alert in the alertStorage
-                    self.alertHistoryStorage.ackAlert(alertIssueDate, nil)
-                }
-            }
-
-            if let omniPod = self.pumpManager as? OmnipodPumpManager {
-                if omniPod.state.activeAlerts.isEmpty {
-                    // force to ack alert in the alertStorage
-                    self.alertHistoryStorage.ackAlert(alertIssueDate, nil)
-                }
-            }
-
             self.pumpManager?.acknowledgeAlert(alertIdentifier: alert.alertIdentifier) { error in
                 if let error = error {
-                    self.alertHistoryStorage.ackAlert(alertIssueDate, error.localizedDescription)
+                    self.alertHistoryStorage.acknowledgeAlert(alertIssueDate, error.localizedDescription)
                     debug(.deviceManager, "acknowledge not succeeded with error \(error)")
                 } else {
-                    self.alertHistoryStorage.ackAlert(alertIssueDate, nil)
+                    self.alertHistoryStorage.acknowledgeAlert(alertIssueDate, nil)
                 }
             }
 

+ 22 - 6
Trio/Sources/APS/OpenAPS/OpenAPS.swift

@@ -537,15 +537,31 @@ final class OpenAPS {
 
         // Check for active Temp Targets and adjust HBT if necessary
         try await context.perform {
-            // Check if a Temp Target is active and if its HBT differs from user preferences
+            // Check if a Temp Target is active and check HBT differs from setting and adjust
             if let activeTempTarget = try self.fetchActiveTempTargets().first,
                activeTempTarget.enabled,
-               let activeHBT = activeTempTarget.halfBasalTarget?.decimalValue,
-               activeHBT != defaultHalfBasalTarget
+               let targetValue = activeTempTarget.target?.decimalValue
             {
-                // Overwrite the HBT in preferences
-                adjustedPreferences.halfBasalExerciseTarget = activeHBT
-                debug(.openAPS, "Updated halfBasalExerciseTarget to active Temp Target value: \(activeHBT)")
+                // Compute effective HBT - handles both custom HBT and standard TT (where HBT might need adjustment)
+                let effectiveHBT = TempTargetCalculations.computeEffectiveHBT(
+                    tempTargetHalfBasalTarget: activeTempTarget.halfBasalTarget?.decimalValue,
+                    settingHalfBasalTarget: defaultHalfBasalTarget,
+                    target: targetValue,
+                    autosensMax: preferences.autosensMax
+                )
+
+                if let effectiveHBT, effectiveHBT != defaultHalfBasalTarget {
+                    adjustedPreferences.halfBasalExerciseTarget = effectiveHBT
+                    let percentage = Int(TempTargetCalculations.computeAdjustedPercentage(
+                        halfBasalTarget: effectiveHBT,
+                        target: targetValue,
+                        autosensMax: preferences.autosensMax
+                    ))
+                    debug(
+                        .openAPS,
+                        "TempTarget: target=\(targetValue), HBT=\(defaultHalfBasalTarget), effectiveHBT=\(effectiveHBT), percentage=\(percentage)%, adjustmentType=Custom"
+                    )
+                }
             }
             // Overwrite the lowTTlowersSens if autosensMax does not support it
             if preferences.lowTemptargetLowersSensitivity, preferences.autosensMax <= 1 {

+ 203 - 47
Trio/Sources/APS/Storage/AlertStorage.swift

@@ -8,96 +8,252 @@ protocol AlertObserver {
 }
 
 protocol AlertHistoryStorage {
-    func storeAlert(_ alerts: AlertEntry)
+    func addAlert(_ alert: AlertEntry)
+    func acknowledgeAlert(_ issuedAt: Date, _ error: String?)
+    func removeAlert(identifier: String)
+    func unacknowledgedAlertsWithinLast24Hours() -> [AlertEntry]
+    func broadcastAlertUpdates()
     func syncDate() -> Date
-    func recentNotAck() -> [AlertEntry]
-    func deleteAlert(identifier: String)
-    func ackAlert(_ alert: Date, _ error: String?)
-    func forceNotification()
-    var alertNotAck: PassthroughSubject<Bool, Never> { get }
+    var unacknowledgedAlertsPublisher: PassthroughSubject<Bool, Never> { get }
 }
 
 final class BaseAlertHistoryStorage: AlertHistoryStorage, Injectable {
     private let processQueue = DispatchQueue.markedQueue(label: "BaseAlertsStorage.processQueue")
-    @Injected() private var storage: FileStorage!
+
+    private let defaults: UserDefaults
+
+    /// Legacy JSON file storage used only for one-time migration from the historical on-disk JSON file.
+    // FIXME: this can be removed in later releases
+    @Injected() private var fileStorage: FileStorage!
+
     @Injected() private var broadcaster: Broadcaster!
 
-    let alertNotAck = PassthroughSubject<Bool, Never>()
+    /// Emits `true` whenever there is at least one unacknowledged alert in the last 24 hours.
+    let unacknowledgedAlertsPublisher = PassthroughSubject<Bool, Never>()
 
-    init(resolver: Resolver) {
+    private enum Keys {
+        /// UserDefaults key holding the encoded `[AlertEntry]` payload.
+        static let alertsData = "openaps.monitor.alertHistory.data"
+        /// UserDefaults key used as a one-time migration flag.
+        static let alertsMigrationDone = "openaps.monitor.alertHistory.migrated"
+    }
+
+    /// Creates a new alert history storage.
+    ///
+    /// On initialization this performs a one-time migration from the legacy JSON file
+    /// (`OpenAPS.Monitor.alertHistory`, i.e.,`"monitor/alerthistory.json"`) into UserDefaults.
+    /// After initialization, all reads/writes happen via UserDefaults only.
+    ///
+    /// - Parameters:
+    ///   - resolver: Swinject resolver used for dependency injection.
+    ///   - userDefaults: The UserDefaults instance used for persistence. Defaults to `.standard`.
+    init(resolver: Resolver, userDefaults: UserDefaults = .standard) {
+        defaults = userDefaults
         injectServices(resolver)
-        alertNotAck.send(recentNotAck().isNotEmpty)
+
+        // FIXME: this can be removed in later releases
+        migrateFromLegacyJSONIfNeeded()
+
+        unacknowledgedAlertsPublisher.send(unacknowledgedAlertsWithinLast24Hours().isNotEmpty)
     }
 
-    func storeAlert(_ alert: AlertEntry) {
+    /// Stores a new alert entry and notifies observers.
+    ///
+    /// The history is:
+    /// - de-duplicated by `issuedDate`
+    /// - pruned to the last 24 hours
+    /// - sorted with newest first
+    ///
+    /// After persisting, this updates `unacknowledgedAlertsPublisher` and broadcasts the latest list to `AlertObserver`s.
+    /// - Parameter alert: The alert to store.
+    func addAlert(_ alert: AlertEntry) {
         processQueue.sync {
-            let file = OpenAPS.Monitor.alertHistory
-            var uniqEvents: [AlertEntry] = []
-            self.storage.transaction { storage in
-                storage.append(alert, to: file, uniqBy: \.issuedDate)
-                uniqEvents = storage.retrieve(file, as: [AlertEntry].self)?
-                    .filter { $0.issuedDate.addingTimeInterval(1.days.timeInterval) > Date() }
-                    .sorted { $0.issuedDate > $1.issuedDate } ?? []
-                storage.save(Array(uniqEvents), as: file)
-            }
-            alertNotAck.send(self.recentNotAck().isNotEmpty)
+            var all = loadAll()
+            all.append(alert)
+
+            let uniqEvents = pruneAndSort(dedupeByIssuedDate(all))
+            saveAll(uniqEvents)
+
+            unacknowledgedAlertsPublisher.send(self.unacknowledgedAlertsWithinLast24HoursOnQueue().isNotEmpty)
             broadcaster.notify(AlertObserver.self, on: processQueue) {
                 $0.AlertDidUpdate(uniqEvents)
             }
         }
     }
 
+    /// Returns the baseline sync date used by the alert subsystem.
+    ///
+    /// This matches the previous behavior: one day ago from "now".
     func syncDate() -> Date {
         Date().addingTimeInterval(-1.days.timeInterval)
     }
 
-    func recentNotAck() -> [AlertEntry] {
-        storage.retrieve(OpenAPS.Monitor.alertHistory, as: [AlertEntry].self)?
+    /// Returns all unacknowledged alerts from the last 24 hours, sorted newest first.
+    func unacknowledgedAlertsWithinLast24Hours() -> [AlertEntry] {
+        processQueue.sync {
+            self.unacknowledgedAlertsWithinLast24HoursOnQueue()
+        }
+    }
+
+    /// Returns all unacknowledged alerts from the last 24 hours, sorted newest first.
+    /// - Important: Must only be called while already executing on `processQueue`.
+    private func unacknowledgedAlertsWithinLast24HoursOnQueue() -> [AlertEntry] {
+        loadAll()
             .filter { $0.issuedDate.addingTimeInterval(1.days.timeInterval) > Date() && $0.acknowledgedDate == nil }
-            .sorted { $0.issuedDate > $1.issuedDate } ?? []
+            .sorted { $0.issuedDate > $1.issuedDate }
     }
 
-    func ackAlert(_ alert: Date, _ error: String?) {
+    /// Acknowledges an alert (by issued date), or stores an error for it.
+    ///
+    /// If `error` is non-nil, the alert is updated with `errorMessage`.
+    /// Otherwise, the alert is marked as acknowledged by setting `acknowledgedDate = Date()`.
+    ///
+    /// After persisting, this updates `unacknowledgedAlertsPublisher`.
+    /// - Parameters:
+    ///   - issuedAt: The issued date of the alert entry to update.
+    ///   - error: Optional error message to store instead of acknowledging.
+    func acknowledgeAlert(_ issuedAt: Date, _ error: String?) {
         processQueue.sync {
-            var allValues = storage.retrieve(OpenAPS.Monitor.alertHistory, as: [AlertEntry].self) ?? []
-            guard let entryIndex = allValues.firstIndex(where: { $0.issuedDate == alert }) else {
-                return
-            }
+            var all = loadAll()
+            guard let idx = all.firstIndex(where: { $0.issuedDate == issuedAt }) else { return }
 
             if let error {
-                allValues[entryIndex].errorMessage = error
+                all[idx].errorMessage = error
             } else {
-                allValues[entryIndex].acknowledgedDate = Date()
+                all[idx].acknowledgedDate = Date()
             }
-            storage.save(allValues, as: OpenAPS.Monitor.alertHistory)
-            alertNotAck.send(self.recentNotAck().isNotEmpty)
+
+            let cleaned = pruneAndSort(dedupeByIssuedDate(all))
+            saveAll(cleaned)
+            unacknowledgedAlertsPublisher.send(self.unacknowledgedAlertsWithinLast24HoursOnQueue().isNotEmpty)
         }
     }
 
-    func deleteAlert(identifier: String) {
+    /// Deletes an alert entry by its identifier and notifies observers.
+    ///
+    /// After persisting, this updates `unacknowledgedAlertsPublisher` and broadcasts the updated list.
+    /// - Parameter identifier: The `alertIdentifier` of the entry to delete.
+    func removeAlert(identifier: String) {
         processQueue.sync {
-            var allValues = storage.retrieve(OpenAPS.Monitor.alertHistory, as: [AlertEntry].self) ?? []
-            guard let entryIndex = allValues.firstIndex(where: { $0.alertIdentifier == identifier }) else {
-                return
-            }
-            allValues.remove(at: entryIndex)
-            storage.save(allValues, as: OpenAPS.Monitor.alertHistory)
-            alertNotAck.send(self.recentNotAck().isNotEmpty)
+            var all = loadAll()
+            guard let idx = all.firstIndex(where: { $0.alertIdentifier == identifier }) else { return }
+
+            all.remove(at: idx)
+
+            let cleaned = pruneAndSort(dedupeByIssuedDate(all))
+            saveAll(cleaned)
+
+            unacknowledgedAlertsPublisher.send(self.unacknowledgedAlertsWithinLast24HoursOnQueue().isNotEmpty)
             broadcaster.notify(AlertObserver.self, on: processQueue) {
-                $0.AlertDidUpdate(allValues)
+                $0.AlertDidUpdate(cleaned)
             }
         }
     }
 
-    func forceNotification() {
+    /// Forces a broadcast of the current alert list (last 24 hours) to observers.
+    ///
+    /// This does not modify the data; it only re-emits state via `unacknowledgedAlertsPublisher` and `AlertObserver`.
+    func broadcastAlertUpdates() {
         processQueue.sync {
-            let uniqEvents = storage.retrieve(OpenAPS.Monitor.alertHistory, as: [AlertEntry].self)?
-                .filter { $0.issuedDate.addingTimeInterval(1.days.timeInterval) > Date() }
-                .sorted { $0.issuedDate > $1.issuedDate } ?? []
-            alertNotAck.send(self.recentNotAck().isNotEmpty)
+            let uniqEvents = pruneAndSort(loadAll())
+            unacknowledgedAlertsPublisher.send(self.unacknowledgedAlertsWithinLast24HoursOnQueue().isNotEmpty)
             broadcaster.notify(AlertObserver.self, on: processQueue) {
                 $0.AlertDidUpdate(uniqEvents)
             }
         }
     }
+
+    // MARK: - Migration
+
+    /// Migrates alert history from the legacy on-disk JSON file into UserDefaults.
+    ///
+    /// Migration behavior:
+    /// - Runs at most once per install (guarded by `Keys.alertsMigrationDone`).
+    /// - If the new UserDefaults value already exists, migration is considered complete.
+    /// - If legacy alerts exist, they are normalized (dedupe/prune/sort) and stored in UserDefaults.
+    /// - After a successful migration, the legacy file is removed to avoid future drift.
+    private func migrateFromLegacyJSONIfNeeded() { // FIXME: this can be removed in later releases
+        processQueue.sync {
+            // Avoid repeated disk reads forever
+            if defaults.bool(forKey: Keys.alertsMigrationDone) { return }
+
+            // If new store already has data, consider migration done
+            if defaults.data(forKey: Keys.alertsData) != nil {
+                defaults.set(true, forKey: Keys.alertsMigrationDone)
+                return
+            }
+
+            // Read legacy file ("monitor/alerthistory.json") via existing FileStorage
+            let legacyJsonAlerts = fileStorage.retrieve(OpenAPS.Monitor.alertHistory, as: [AlertEntry].self) ?? []
+            guard legacyJsonAlerts.isNotEmpty else {
+                defaults.set(true, forKey: Keys.alertsMigrationDone)
+                return
+            }
+
+            // Normalize before persisting
+            let migrated = pruneAndSort(dedupeByIssuedDate(legacyJsonAlerts))
+            saveAll(migrated)
+
+            // Mark complete FIRST, then cleanup
+            defaults.set(true, forKey: Keys.alertsMigrationDone)
+
+            // Cleanup: remove legacy json so it cannot drift / get re-used accidentally
+            fileStorage.remove(OpenAPS.Monitor.alertHistory)
+        }
+    }
+
+    // MARK: - UserDefaults persistence
+
+    // Uses the same encoder/decoder as file storage to keep Date encoding consistent.
+
+    /// Loads all persisted alerts from UserDefaults.
+    ///
+    /// Decoding uses `JSONCoding.decoder` to match the previous on-disk JSON encoding/decoding behavior.
+    /// If decoding fails, the stored payload is removed so the app can recover cleanly.
+    private func loadAll() -> [AlertEntry] {
+        guard let data = defaults.data(forKey: Keys.alertsData) else { return [] }
+        do {
+            return try JSONCoding.decoder.decode([AlertEntry].self, from: data)
+        } catch {
+            debug(.storage, "Failed to decode alerts from UserDefaults: \(error)")
+            // Clear corrupt payload so app can recover
+            defaults.removeObject(forKey: Keys.alertsData)
+            return []
+        }
+    }
+
+    /// Persists all alerts to UserDefaults.
+    ///
+    /// Encoding uses `JSONCoding.encoder` to match the previous on-disk JSON encoding behavior.
+    private func saveAll(_ alerts: [AlertEntry]) {
+        do {
+            let data = try JSONCoding.encoder.encode(alerts)
+            defaults.set(data, forKey: Keys.alertsData)
+        } catch {
+            debug(.storage, "Failed to encode alerts to UserDefaults: \(error)")
+        }
+    }
+
+    // MARK: - Helpers
+
+    /// Filters the provided alerts to the last 24 hours and sorts them with newest first.
+    private func pruneAndSort(_ alerts: [AlertEntry]) -> [AlertEntry] {
+        alerts
+            .filter { $0.issuedDate.addingTimeInterval(1.days.timeInterval) > Date() }
+            .sorted { $0.issuedDate > $1.issuedDate }
+    }
+
+    /// De-duplicates alert entries by `issuedDate` (keeping the newest occurrence when duplicates exist).
+    ///
+    /// This matches `AlertEntry`'s `Equatable`/`Hashable` semantics (both based on `issuedDate`).
+    private func dedupeByIssuedDate(_ alerts: [AlertEntry]) -> [AlertEntry] {
+        var seen = Set<Date>()
+        var result: [AlertEntry] = []
+        for item in alerts.sorted(by: { $0.issuedDate > $1.issuedDate }) {
+            if seen.insert(item.issuedDate).inserted {
+                result.append(item)
+            }
+        }
+        return result
+    }
 }

+ 5 - 2
Trio/Sources/APS/Storage/ContactImageStorage.swift

@@ -52,6 +52,7 @@ final class BaseContactImageStorage: ContactImageStorage, Injectable {
                         hasHighContrast: entry.hasHighContrast,
                         ringWidth: ContactImageEntry.RingWidth(rawValue: Int(entry.ringWidth)) ?? .regular,
                         ringGap: ContactImageEntry.RingGap(rawValue: Int(entry.ringGap)) ?? .small,
+                        colorMode: ContactImageEntry.ColorMode(rawValue: entry.colorMode ?? "Color") ?? .color,
                         fontSize: ContactImageEntry.FontSize(rawValue: Int(entry.fontSize)) ?? .regular,
                         secondaryFontSize: ContactImageEntry.FontSize(rawValue: Int(entry.fontSizeSecondary)) ?? .small,
                         fontWeight: Font.Weight.fromString(entry.fontWeight ?? "regular"),
@@ -88,10 +89,11 @@ final class BaseContactImageStorage: ContactImageStorage, Injectable {
             newContactImageEntry.hasHighContrast = contactImageEntry.hasHighContrast
             newContactImageEntry.ringWidth = Int16(contactImageEntry.ringWidth.rawValue)
             newContactImageEntry.ringGap = Int16(contactImageEntry.ringGap.rawValue)
+            newContactImageEntry.colorMode = contactImageEntry.colorMode.rawValue
             newContactImageEntry.fontSize = Int16(contactImageEntry.fontSize.rawValue)
             newContactImageEntry.fontSizeSecondary = Int16(contactImageEntry.secondaryFontSize.rawValue)
-            newContactImageEntry.fontWidth = contactImageEntry.fontWeight.asString
-            newContactImageEntry.fontWeight = contactImageEntry.fontWidth.asString
+            newContactImageEntry.fontWidth = contactImageEntry.fontWidth.asString
+            newContactImageEntry.fontWeight = contactImageEntry.fontWeight.asString
 
             do {
                 guard self.backgroundContext.hasChanges else { return }
@@ -128,6 +130,7 @@ final class BaseContactImageStorage: ContactImageStorage, Injectable {
                     existingEntry.hasHighContrast = contactImageEntry.hasHighContrast
                     existingEntry.ringWidth = Int16(contactImageEntry.ringWidth.rawValue)
                     existingEntry.ringGap = Int16(contactImageEntry.ringGap.rawValue)
+                    existingEntry.colorMode = contactImageEntry.colorMode.rawValue
                     existingEntry.fontSize = Int16(contactImageEntry.fontSize.rawValue)
                     existingEntry.fontSizeSecondary = Int16(contactImageEntry.secondaryFontSize.rawValue)
                     existingEntry.fontWeight = contactImageEntry.fontWeight.asString

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

@@ -15,7 +15,7 @@ protocol GlucoseStorage {
     func isGlucoseDataFresh(_ glucoseDate: Date?) -> Bool
     func syncDate() -> Date
     func filterTooFrequentGlucose(_ glucose: [BloodGlucose], at: Date) -> [BloodGlucose]
-    func lastGlucoseDate() -> Date
+    func lastGlucoseDate() -> Date?
     func isGlucoseFresh() -> Bool
     func getGlucoseNotYetUploadedToNightscout() async throws -> [BloodGlucose]
     func getCGMStateNotYetUploadedToNightscout() async throws -> [NightscoutTreatment]
@@ -344,27 +344,27 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
         return fetchedDate
     }
 
-    func lastGlucoseDate() -> Date {
-        let fr = GlucoseStored.fetchRequest()
-        fr.predicate = NSPredicate.predicateForOneDayAgo
-        fr.sortDescriptors = [NSSortDescriptor(keyPath: \GlucoseStored.date, ascending: false)]
-        fr.fetchLimit = 1
+    func lastGlucoseDate() -> Date? {
+        let fetchRequest = GlucoseStored.fetchRequest()
+        fetchRequest.predicate = NSPredicate.predicateForOneDayAgo
+        fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \GlucoseStored.date, ascending: false)]
+        fetchRequest.fetchLimit = 1
 
         var date: Date?
         context.performAndWait {
             do {
-                let results = try self.context.fetch(fr)
+                let results = try self.context.fetch(fetchRequest)
                 date = results.first?.date
             } catch let error as NSError {
                 debug(.storage, "Fetch error: \(DebuggingIdentifiers.failed) \(error), \(error.userInfo)")
             }
         }
 
-        return date ?? .distantPast
+        return date
     }
 
     func isGlucoseFresh() -> Bool {
-        Date().timeIntervalSince(lastGlucoseDate()) <= Config.filterTime
+        Date().timeIntervalSince(lastGlucoseDate() ?? .distantPast) <= Config.filterTime
     }
 
     func filterTooFrequentGlucose(_ glucose: [BloodGlucose], at date: Date) -> [BloodGlucose] {

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

@@ -68,8 +68,6 @@ extension Color {
     static let tempBasal = Color("TempBasal")
     static let basal = Color("Basal")
     static let darkerBlue = Color("DarkerBlue")
-    static let loopPink = Color("LoopPink")
-    static let lemon = Color("Lemon")
     static let minus = Color("minus")
     static let darkGray = Color("darkGray")
     static let darkGreen = Color("darkGreen")

+ 104 - 0
Trio/Sources/Helpers/TempTargetCalculations.swift

@@ -0,0 +1,104 @@
+import Foundation
+
+/// Helper functions for TempTarget sensitivity calculations.
+/// These are used across the app (UI, OpenAPS) to ensure consistent behavior.
+enum TempTargetCalculations {
+    /// The minimum allowed sensitivity ratio for TempTargets (15%)
+    static let minSensitivityRatioTT: Double = 15
+
+    /// The normal target glucose value used as reference (100 mg/dL)
+    static let normalTarget: Decimal = 100
+
+    /// Computes the adjusted percentage (clamped to minSensitivityRatioTT).
+    /// - Parameters:
+    ///   - halfBasalTarget: The half basal target value
+    ///   - target: The target glucose value
+    ///   - autosensMax: The maximum autosens multiplier from settings
+    /// - Returns: The clamped percentage (minimum is minSensitivityRatioTT)
+    static func computeAdjustedPercentage(
+        halfBasalTarget: Decimal,
+        target: Decimal,
+        autosensMax: Decimal
+    ) -> Double {
+        let rawPercentage = computeRawPercentage(
+            halfBasalTarget: halfBasalTarget,
+            target: target,
+            autosensMax: autosensMax
+        )
+        return max(rawPercentage, minSensitivityRatioTT)
+    }
+
+    /// Computes the raw (unclamped) percentage - private helper.
+    private static func computeRawPercentage(
+        halfBasalTarget: Decimal,
+        target: Decimal,
+        autosensMax: Decimal
+    ) -> Double {
+        let deviationFromNormal = halfBasalTarget - normalTarget
+        let adjustmentFactor = deviationFromNormal + (target - normalTarget)
+        let adjustmentRatio: Decimal = (deviationFromNormal * adjustmentFactor <= 0)
+            ? autosensMax
+            : deviationFromNormal / adjustmentFactor
+        return Double(min(adjustmentRatio, autosensMax) * 100)
+    }
+
+    /// Computes the half-basal target needed to achieve a given percentage.
+    /// - Parameters:
+    ///   - target: The target glucose value
+    ///   - percentage: The desired sensitivity percentage
+    /// - Returns: The half basal target value that yields the given percentage
+    static func computeHalfBasalTarget(
+        target: Decimal,
+        percentage: Double
+    ) -> Double {
+        var adjustmentPercentage = percentage
+        if adjustmentPercentage < minSensitivityRatioTT {
+            adjustmentPercentage = minSensitivityRatioTT
+        }
+        let adjustmentRatio = Decimal(adjustmentPercentage / 100)
+        var halfBasalTargetValue: Decimal = 160 // default
+        if adjustmentRatio != 1 {
+            halfBasalTargetValue = ((2 * adjustmentRatio * normalTarget) - normalTarget - (adjustmentRatio * target)) /
+                (adjustmentRatio - 1)
+        }
+        return round(Double(halfBasalTargetValue))
+    }
+
+    /// Determines the effective HBT to use for a TempTarget.
+    /// If the stored HBT is nil (standard TT) and using settings HBT would result in <= 15%,
+    /// calculates an adjusted HBT. Otherwise returns the stored HBT or nil.
+    /// - Parameters:
+    ///   - tempTargetHalfBasalTarget: The HBT stored with the TempTarget (nil for standard TT)
+    ///   - settingHalfBasalTarget: The HBT from user settings
+    ///   - target: The target glucose value
+    ///   - autosensMax: The maximum autosens multiplier from settings
+    /// - Returns: The effective HBT to use, or nil if settings HBT should be used as-is
+    static func computeEffectiveHBT(
+        tempTargetHalfBasalTarget: Decimal?,
+        settingHalfBasalTarget: Decimal,
+        target: Decimal,
+        autosensMax: Decimal
+    ) -> Decimal? {
+        // If TempTarget has a stored HBT, use it directly
+        if let tempTargetHalfBasalTarget {
+            return tempTargetHalfBasalTarget
+        }
+
+        // For standard TT (no stored HBT), check if settings HBT would result in <= minimum
+        let rawPercentage = computeRawPercentage(
+            halfBasalTarget: settingHalfBasalTarget,
+            target: target,
+            autosensMax: autosensMax
+        )
+
+        // If raw percentage is at or below minimum, calculate an adjusted HBT
+        if rawPercentage <= minSensitivityRatioTT {
+            return Decimal(computeHalfBasalTarget(
+                target: target,
+                percentage: minSensitivityRatioTT
+            ))
+        }
+
+        return nil
+    }
+}

+ 34 - 1
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -67793,6 +67793,12 @@
         }
       }
     },
+    "Color" : {
+
+    },
+    "Color Mode" : {
+
+    },
     "Color Scheme Preference" : {
       "localizations" : {
         "bg" : {
@@ -115659,7 +115665,12 @@
         }
       }
     },
+    "Furthermore, you could adjust that sensitivity change independently from the Half Basal Exercise Target specified in Algorithm > Target Behavior settings by deliberately setting a customized Insulin Percentage for a Temp Target." : {
+      "comment" : "A paragraph explaining how to set a custom insulin percentage for a temporary target.",
+      "isCommentAutoGenerated" : true
+    },
     "Furthermore, you could adjust that sensitivity change independently from the Half Basal Exercise Target specified in Algorithm > Target Behavior settings by deliberatly setting a customized Insulin Percentage for a Temp Target." : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -132089,6 +132100,9 @@
         }
       }
     },
+    "Important: Autosens Min and Autosens Max do not affect Temp Targets in the same way. Autosens Max limits how much insulin can be increased, but Autosens Min does not remove the 15% minimum when insulin is reduced." : {
+
+    },
     "Importing Settings..." : {
       "comment" : "Progress text when importing settings via Nightscout",
       "localizations" : {
@@ -161960,6 +161974,9 @@
         }
       }
     },
+    "Monochrome" : {
+
+    },
     "Month" : {
       "extractionState" : "manual",
       "localizations" : {
@@ -202164,6 +202181,13 @@
         }
       }
     },
+    "Sensitivity changes from Temp Targets have a built-in minimum of 15%. Even very high Temp Targets cannot reduce insulin delivery below 15% of normal." : {
+
+    },
+    "Sensitivity Limits" : {
+      "comment" : "A label displayed above a section explaining the sensitivity limits of temp targets.",
+      "isCommentAutoGenerated" : true
+    },
     "Sensitivity Raises Target" : {
       "comment" : "Sensitivity Raises Target",
       "localizations" : {
@@ -212999,6 +213023,9 @@
         }
       }
     },
+    "SMBs Off%@" : {
+
+    },
     "Smooth CGM readings using Savitzky-Golay filtering." : {
       "localizations" : {
         "bg" : {
@@ -232338,6 +232365,9 @@
         }
       }
     },
+    "This 15% minimum is a safety limit taken from oref (OpenAPS reference design) and AndroidAPS. It helps prevent insulin delivery from dropping to unsafe levels." : {
+
+    },
     "This adjusted ISF is temporary, will change with the next loop cycle, and should not be directly used as your profile ISF value." : {
       "localizations" : {
         "bg" : {
@@ -233171,6 +233201,9 @@
         }
       }
     },
+    "This difference exists because situations like exercise often need a much larger insulin reduction than Autosens would detect during a normal daily routine." : {
+
+    },
     "This dotted line represents the hourly insulin rate of your scheduled basal insulin." : {
       "localizations" : {
         "bg" : {
@@ -270811,5 +270844,5 @@
       }
     }
   },
-  "version" : "1.0"
+  "version" : "1.1"
 }

+ 17 - 0
Trio/Sources/Models/ContactTrickEntry.swift

@@ -13,6 +13,7 @@ struct ContactImageEntry: Hashable, Equatable, Sendable {
     var hasHighContrast: Bool = true
     var ringWidth: RingWidth = .regular
     var ringGap: RingGap = .small
+    var colorMode: ColorMode = .color
     var fontSize: FontSize = .regular
     var secondaryFontSize: FontSize = .small
     var fontWeight: Font.Weight = .medium
@@ -31,6 +32,7 @@ struct ContactImageEntry: Hashable, Equatable, Sendable {
             lhs.hasHighContrast == rhs.hasHighContrast &&
             lhs.ringWidth == rhs.ringWidth &&
             lhs.ringGap == rhs.ringGap &&
+            lhs.colorMode == rhs.colorMode &&
             lhs.fontSize == rhs.fontSize &&
             lhs.secondaryFontSize == rhs.secondaryFontSize &&
             lhs.fontWeight == rhs.fontWeight &&
@@ -57,6 +59,21 @@ struct ContactImageEntry: Hashable, Equatable, Sendable {
         Font.Width.fromString(string)
     }
 
+    enum ColorMode: String, JSON, CaseIterable, Identifiable, Codable {
+        var id: String { rawValue }
+        case color
+        case monochrome
+
+        var displayName: String {
+            switch self {
+            case .color:
+                return String(localized: "Color", comment: "")
+            case .monochrome:
+                return String(localized: "Monochrome", comment: "")
+            }
+        }
+    }
+
     enum FontSize: Int, Codable, Sendable, CaseIterable {
         case tiny = 200
         case small = 250

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

@@ -72,7 +72,7 @@ struct DecimalPickerSettings {
     var halfBasalExerciseTarget = PickerSetting(
         value: 160,
         step: 5,
-        min: 100,
+        min: 105, // must be >100 to work mathematically, 5mg/dl steps are fine enough as this is just a calculation helper value
         max: 300,
         type: PickerSetting.PickerSettingType.glucose
     )

+ 20 - 37
Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+TempTargets.swift

@@ -138,6 +138,11 @@ extension Adjustments.StateModel {
         let date = self.date
         guard date > Date() else { return }
 
+        let adjustmentType = halfBasalTarget == settingHalfBasalTarget ? "Standard" : "Custom"
+        debug(
+            .default,
+            "TempTarget: target=\(tempTargetTarget), HBT=\(settingHalfBasalTarget), effectiveHBT=\(halfBasalTarget), percentage=\(Int(percentage))%, adjustmentType=\(adjustmentType)"
+        )
         let tempTarget = TempTarget(
             name: tempTargetName,
             createdAt: date,
@@ -203,6 +208,11 @@ extension Adjustments.StateModel {
     /// Saves a custom Temp Target and disables existing ones.
     func saveCustomTempTarget() async throws {
         await disableAllActiveTempTargets(createTempTargetRunEntry: true)
+        let adjustmentType = halfBasalTarget == settingHalfBasalTarget ? "Standard" : "Custom"
+        debug(
+            .default,
+            "TempTarget: target=\(tempTargetTarget), HBT=\(settingHalfBasalTarget), effectiveHBT=\(halfBasalTarget), percentage=\(Int(percentage))%, adjustmentType=\(adjustmentType)"
+        )
         let tempTarget = TempTarget(
             name: tempTargetName,
             /// We don't need to use the state var date here as we are using a different function for scheduled Temp Targets 'saveScheduledTempTarget()'
@@ -225,6 +235,11 @@ extension Adjustments.StateModel {
 
     /// Creates a new Temp Target preset.
     func saveTempTargetPreset() async throws {
+        let adjustmentType = halfBasalTarget == settingHalfBasalTarget ? "Standard" : "Custom"
+        debug(
+            .default,
+            "TempTarget: target=\(tempTargetTarget), HBT=\(settingHalfBasalTarget), effectiveHBT=\(halfBasalTarget), percentage=\(Int(percentage))%, adjustmentType=\(adjustmentType)"
+        )
         let tempTarget = TempTarget(
             name: tempTargetName,
             createdAt: Date(),
@@ -384,35 +399,19 @@ extension Adjustments.StateModel {
 
     // MARK: - Calculations
 
-    /// Computes the half-basal target based on the current settings.
-    func computeHalfBasalTarget(
-        usingTarget initialTarget: Decimal? = nil,
-        usingPercentage initialPercentage: Double? = nil
-    ) -> Double {
-        let adjustmentPercentage = initialPercentage ?? percentage
-        let adjustmentRatio = Decimal(adjustmentPercentage / 100)
-        let tempTargetValue: Decimal = initialTarget ?? tempTargetTarget
-        var halfBasalTargetValue = halfBasalTarget
-        if adjustmentRatio != 1 {
-            halfBasalTargetValue = ((2 * adjustmentRatio * normalTarget) - normalTarget - (adjustmentRatio * tempTargetValue)) /
-                (adjustmentRatio - 1)
-        }
-        return round(Double(halfBasalTargetValue))
-    }
-
     /// Determines if sensitivity adjustment is enabled based on target.
     func isAdjustSensEnabled(usingTarget initialTarget: Decimal? = nil) -> Bool {
         let target = initialTarget ?? tempTargetTarget
-        if target < normalTarget, lowTTlowersSens && autosensMax > 1 { return true }
-        if target > normalTarget, highTTraisesSens || isExerciseModeActive { return true }
+        if target < TempTargetCalculations.normalTarget, lowTTlowersSens && autosensMax > 1 { return true }
+        if target > TempTargetCalculations.normalTarget, highTTraisesSens || isExerciseModeActive { return true }
         return false
     }
 
     /// Computes the low value for the slider based on the target.
     func computeSliderLow(usingTarget initialTarget: Decimal? = nil) -> Double {
         let calcTarget = initialTarget ?? tempTargetTarget
-        guard calcTarget != 0 else { return 15 } // oref defined maximum sensitivity
-        let minSens = calcTarget < normalTarget ? 105 : 15
+        guard calcTarget != 0 else { return TempTargetCalculations.minSensitivityRatioTT } // oref defined maximum sensitivity
+        let minSens = calcTarget < TempTargetCalculations.normalTarget ? 105 : TempTargetCalculations.minSensitivityRatioTT
         return Double(max(0, minSens))
     }
 
@@ -421,25 +420,9 @@ extension Adjustments.StateModel {
         let calcTarget = initialTarget ?? tempTargetTarget
         guard calcTarget != 0
         else { return Double(autosensMax * 100) } // oref defined limit for increased insulin delivery
-        let maxSens = calcTarget > normalTarget ? 95 : Double(autosensMax * 100)
+        let maxSens = calcTarget > TempTargetCalculations.normalTarget ? 95 : Double(autosensMax * 100)
         return maxSens
     }
-
-    /// Computes the adjusted percentage for the slider.
-    func computeAdjustedPercentage(
-        usingHBT initialHalfBasalTarget: Decimal? = nil,
-        usingTarget initialTarget: Decimal? = nil
-    ) -> Double {
-        let halfBasalTargetValue = initialHalfBasalTarget ?? halfBasalTarget
-        let calcTarget = initialTarget ?? tempTargetTarget
-        let deviationFromNormal = halfBasalTargetValue - normalTarget
-
-        let adjustmentFactor = deviationFromNormal + (calcTarget - normalTarget)
-        let adjustmentRatio: Decimal = (deviationFromNormal * adjustmentFactor <= 0) ? autosensMax : deviationFromNormal /
-            adjustmentFactor
-
-        return Double(min(adjustmentRatio, autosensMax) * 100).rounded()
-    }
 }
 
 enum TempTargetSensitivityAdjustmentType: String, CaseIterable {

+ 10 - 3
Trio/Sources/Modules/Adjustments/AdjustmentsStateModel.swift

@@ -49,7 +49,6 @@ extension Adjustments {
         var units: GlucoseUnits = .mgdL
 
         // Temp Target Properties
-        let normalTarget: Decimal = 100
         var tempTargetDuration: Decimal = 0
         var tempTargetName: String = ""
         var tempTargetTarget: Decimal = 100
@@ -158,7 +157,11 @@ extension Adjustments {
             highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
             isExerciseModeActive = settingsManager.preferences.exerciseMode
             lowTTlowersSens = settingsManager.preferences.lowTemptargetLowersSensitivity
-            percentage = computeAdjustedPercentage()
+            percentage = TempTargetCalculations.computeAdjustedPercentage(
+                halfBasalTarget: halfBasalTarget,
+                target: tempTargetTarget,
+                autosensMax: autosensMax
+            )
             Task {
                 await getCurrentGlucoseTarget()
             }
@@ -268,7 +271,11 @@ extension Adjustments.StateModel: SettingsObserver, PreferencesObserver {
         highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
         isExerciseModeActive = settingsManager.preferences.exerciseMode
         lowTTlowersSens = settingsManager.preferences.lowTemptargetLowersSensitivity
-        percentage = computeAdjustedPercentage()
+        percentage = TempTargetCalculations.computeAdjustedPercentage(
+            halfBasalTarget: halfBasalTarget,
+            target: tempTargetTarget,
+            autosensMax: autosensMax
+        )
         Task {
             await getCurrentGlucoseTarget()
         }

+ 25 - 5
Trio/Sources/Modules/Adjustments/View/TempTargets/AddTempTargetForm.swift

@@ -61,7 +61,7 @@ struct AddTempTargetForm: View {
             }
             .onAppear {
                 targetStep = state.units == .mgdL ? 5 : 9
-                state.tempTargetTarget = state.normalTarget
+                state.tempTargetTarget = TempTargetCalculations.normalTarget
             }
             .sheet(isPresented: $state.isHelpSheetPresented) {
                 TempTargetHelpView(state: state, helpSheetDetent: $state.helpSheetDetent)
@@ -101,12 +101,25 @@ struct AddTempTargetForm: View {
                     toggleScrollWheel: toggleScrollWheel
                 )
                 .onChange(of: state.tempTargetTarget) {
-                    state.percentage = state.computeAdjustedPercentage()
+                    // when first setting a custom sensitivity the settings HBT is used and therefore we calculate the sensitivity
+                    if state.halfBasalTarget == state.settingHalfBasalTarget {
+                        state.percentage = TempTargetCalculations.computeAdjustedPercentage(
+                            halfBasalTarget: state.halfBasalTarget,
+                            target: state.tempTargetTarget,
+                            autosensMax: state.autosensMax
+                        )
+                    } else {
+                        // else when changing target value and the already adjusted HBT is used, keep the sensitivity and adjust the HBT instead
+                        state.halfBasalTarget = Decimal(TempTargetCalculations.computeHalfBasalTarget(
+                            target: state.tempTargetTarget,
+                            percentage: state.percentage
+                        ))
+                    }
                 }
             }
             .listRowBackground(Color.chart)
 
-            if state.tempTargetTarget != state.normalTarget {
+            if state.tempTargetTarget != TempTargetCalculations.normalTarget {
                 if state.isAdjustSensEnabled() {
                     Section(
                         footer: state.percentageDescription(state.percentage),
@@ -119,7 +132,11 @@ struct AddTempTargetForm: View {
                                 .onChange(of: tempTargetSensitivityAdjustmentType) { _, newValue in
                                     if newValue == .standard {
                                         state.halfBasalTarget = state.settingHalfBasalTarget
-                                        state.percentage = state.computeAdjustedPercentage()
+                                        state.percentage = TempTargetCalculations.computeAdjustedPercentage(
+                                            halfBasalTarget: state.halfBasalTarget,
+                                            target: state.tempTargetTarget,
+                                            autosensMax: state.autosensMax
+                                        )
                                     }
                                 }
                             }
@@ -141,7 +158,10 @@ struct AddTempTargetForm: View {
                                     Text("\(state.computeSliderHigh(), specifier: "%.0f")%")
                                 } onEditingChanged: { editing in
                                     isUsingSlider = editing
-                                    state.halfBasalTarget = Decimal(state.computeHalfBasalTarget())
+                                    state.halfBasalTarget = Decimal(TempTargetCalculations.computeHalfBasalTarget(
+                                        target: state.tempTargetTarget,
+                                        percentage: state.percentage
+                                    ))
                                 }
                                 .listRowSeparator(.hidden, edges: .top)
                             }

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

@@ -172,7 +172,11 @@ extension Adjustments.RootView {
                 .RawValue ?? Double(state.settingHalfBasalTarget)
         )
         let percentage = Int(
-            state.computeAdjustedPercentage(usingHBT: tempTargetHalfBasal, usingTarget: tempTargetValue)
+            TempTargetCalculations.computeAdjustedPercentage(
+                halfBasalTarget: tempTargetHalfBasal,
+                target: tempTargetValue,
+                autosensMax: state.autosensMax
+            )
         )
         let remainingTime = tempTarget.date?.timeIntervalSinceNow ?? 0
 

+ 41 - 12
Trio/Sources/Modules/Adjustments/View/TempTargets/EditTempTargetForm.swift

@@ -41,7 +41,11 @@ struct EditTempTargetForm: View {
 
         let H = tempTargetHalfBasal
         let T = tempTargetToEdit.target?.decimalValue ?? 100
-        let calcPercentage = state.computeAdjustedPercentage(usingHBT: H, usingTarget: T)
+        let calcPercentage = TempTargetCalculations.computeAdjustedPercentage(
+            halfBasalTarget: H,
+            target: T,
+            autosensMax: state.autosensMax
+        )
         _percentage = State(initialValue: calcPercentage)
     }
 
@@ -85,7 +89,23 @@ struct EditTempTargetForm: View {
                 }
             }
             .onAppear {
-                if halfBasalTarget != state.settingHalfBasalTarget { tempTargetSensitivityAdjustmentType = .slider }
+                // Determine if this is a custom slider adjustment or a standard with auto-adjustment
+                if halfBasalTarget != nil,
+                   halfBasalTarget != state.settingHalfBasalTarget
+                {
+                    // Check if this was an auto-adjusted standard TT:
+                    // If computeEffectiveHBT with nil stored HBT returns non-nil, settings HBT would produce <= 15%
+                    let isAutoAdjustedStandard = TempTargetCalculations.computeEffectiveHBT(
+                        tempTargetHalfBasalTarget: nil,
+                        settingHalfBasalTarget: state.settingHalfBasalTarget,
+                        target: target,
+                        autosensMax: state.autosensMax
+                    ) != nil
+
+                    if !isAutoAdjustedStandard {
+                        tempTargetSensitivityAdjustmentType = .slider
+                    }
+                }
             }
             .sheet(isPresented: $state.isHelpSheetPresented) {
                 TempTargetHelpView(state: state, helpSheetDetent: $state.helpSheetDetent)
@@ -151,15 +171,19 @@ struct EditTempTargetForm: View {
                     toggleScrollWheel: toggleScrollWheel
                 )
                 .onChange(of: target) {
-                    percentage = state.computeAdjustedPercentage(usingHBT: halfBasalTarget, usingTarget: target)
+                    // percentage = state.computeAdjustedPercentage(usingHBT: halfBasalTarget, usingTarget: target)
+                    // target value changes shall not alter the sensitivity, instead calculate new hbt with sensitivity from slider
+                    halfBasalTarget = Decimal(
+                        TempTargetCalculations
+                            .computeHalfBasalTarget(target: target, percentage: percentage)
+                    )
                 }
             }
             .listRowBackground(Color.chart)
 
-            if target != state.normalTarget {
+            if target != TempTargetCalculations.normalTarget {
                 let computedHalfBasalTarget = Decimal(
-                    state
-                        .computeHalfBasalTarget(usingTarget: target, usingPercentage: percentage)
+                    TempTargetCalculations.computeHalfBasalTarget(target: target, percentage: percentage)
                 )
 
                 if state.isAdjustSensEnabled(usingTarget: target) {
@@ -175,9 +199,10 @@ struct EditTempTargetForm: View {
                                     if newValue == .standard {
                                         halfBasalTarget = nil
                                         hasChanges = true
-                                        percentage = state.computeAdjustedPercentage(
-                                            usingHBT: halfBasalTarget,
-                                            usingTarget: target
+                                        percentage = TempTargetCalculations.computeAdjustedPercentage(
+                                            halfBasalTarget: state.settingHalfBasalTarget,
+                                            target: target,
+                                            autosensMax: state.autosensMax
                                         )
                                     }
                                 }
@@ -198,9 +223,9 @@ struct EditTempTargetForm: View {
                                         set: { newValue in
                                             percentage = newValue
                                             hasChanges = true
-                                            halfBasalTarget = Decimal(state.computeHalfBasalTarget(
-                                                usingTarget: target,
-                                                usingPercentage: percentage
+                                            halfBasalTarget = Decimal(TempTargetCalculations.computeHalfBasalTarget(
+                                                target: target,
+                                                percentage: percentage
                                             ))
                                         }
                                     ),
@@ -303,6 +328,10 @@ struct EditTempTargetForm: View {
             Spacer()
             Button(action: {
                 saveChanges()
+                debug(
+                    .default,
+                    "TempTarget: target=\(target), HBT=\(state.settingHalfBasalTarget), effectiveHBT=\(String(describing: halfBasalTarget)), percentage=\(Int(percentage))%, adjustmentType=\(tempTargetSensitivityAdjustmentType.rawValue)"
+                )
                 do {
                     guard let moc = tempTarget.managedObjectContext else { return }
                     guard moc.hasChanges else { return }

+ 39 - 14
Trio/Sources/Modules/Adjustments/View/TempTargets/TempTargetHelpView.swift

@@ -7,20 +7,45 @@ struct TempTargetHelpView: View {
     var body: some View {
         NavigationStack {
             List {
-                VStack(alignment: .leading, spacing: 10) {
-                    Text(
-                        "A Temporary Target replaces the current Target Glucose specified in Therapy settings."
-                    )
-                    Text(
-                        "Depending on your Target Behavior settings (see Settings > the Algorithm > Target Behavior), these temporary glucose targets can also raise Insulin Sensitivity for high targets or lower sensitivity for low targets."
-                    )
-                    Text(
-                        "Furthermore, you could adjust that sensitivity change independently from the Half Basal Exercise Target specified in Algorithm > Target Behavior settings by deliberatly setting a customized Insulin Percentage for a Temp Target."
-                    )
-                    Text(
-                        "A pre-condition to have Temp Targets adjust Sensitivity is that the respective Target Behavior settings High Temp Target Raises Sensitivity or Low Temp Target Lowers Sensitivity are set to enabled."
-                    )
-                }.listRowBackground(Color.gray.opacity(0.1))
+                Section {
+                    VStack(alignment: .leading, spacing: 10) {
+                        Text(
+                            "A Temporary Target replaces the current Target Glucose specified in Therapy settings."
+                        )
+                        Text(
+                            "Depending on your Target Behavior settings (see Settings > the Algorithm > Target Behavior), these temporary glucose targets can also raise Insulin Sensitivity for high targets or lower sensitivity for low targets."
+                        )
+                        Text(
+                            "Furthermore, you could adjust that sensitivity change independently from the Half Basal Exercise Target specified in Algorithm > Target Behavior settings by deliberately setting a customized Insulin Percentage for a Temp Target."
+                        )
+                        Text(
+                            "A pre-condition to have Temp Targets adjust Sensitivity is that the respective Target Behavior settings High Temp Target Raises Sensitivity or Low Temp Target Lowers Sensitivity are set to enabled."
+                        )
+                    }
+                } header: {
+                    Text("Overview")
+                }
+                .listRowBackground(Color.gray.opacity(0.1))
+
+                Section {
+                    VStack(alignment: .leading, spacing: 10) {
+                        Text(
+                            "Sensitivity changes from Temp Targets have a built-in minimum of 15%. Even very high Temp Targets cannot reduce insulin delivery below 15% of normal."
+                        )
+                        Text(
+                            "This 15% minimum is a safety limit taken from oref (OpenAPS reference design) and AndroidAPS. It helps prevent insulin delivery from dropping to unsafe levels."
+                        )
+                        Text(
+                            "Important: Autosens Min and Autosens Max do not affect Temp Targets in the same way. Autosens Max limits how much insulin can be increased, but Autosens Min does not remove the 15% minimum when insulin is reduced."
+                        )
+                        Text(
+                            "This difference exists because situations like exercise often need a much larger insulin reduction than Autosens would detect during a normal daily routine."
+                        )
+                    }
+                } header: {
+                    Text("Sensitivity Limits")
+                }
+                .listRowBackground(Color.gray.opacity(0.1))
             }
             .navigationBarTitle("Help", displayMode: .inline)
 

+ 11 - 0
Trio/Sources/Modules/ContactImage/View/AddContactImageSheet.swift

@@ -16,6 +16,7 @@ struct AddContactImageSheet: View {
     @State private var top: ContactImageValue = .none
     @State private var bottom: ContactImageValue = .trend
     @State private var ring: ContactImageLargeRing = .none
+    @State private var colorMode: ContactImageEntry.ColorMode = .color
     @State private var fontSize: ContactImageEntry.FontSize = .regular
     @State private var secondaryFontSize: ContactImageEntry.FontSize = .small
     @State private var fontWeight: Font.Weight = .medium
@@ -34,6 +35,7 @@ struct AddContactImageSheet: View {
             hasHighContrast: hasHighContrast,
             ringWidth: ringWidth,
             ringGap: ringGap,
+            colorMode: colorMode,
             fontSize: fontSize,
             secondaryFontSize: secondaryFontSize,
             fontWeight: fontWeight,
@@ -136,6 +138,7 @@ struct AddContactImageSheet: View {
 
                     // Font Settings Section
                     Section(header: Text("Font Settings")) {
+                        colorModePicker
                         fontSizePicker
                         if layout == .split {
                             secondaryFontSizePicker
@@ -201,6 +204,14 @@ struct AddContactImageSheet: View {
         }
     }
 
+    private var colorModePicker: some View {
+        Picker("Color Mode", selection: $colorMode) {
+            ForEach(ContactImageEntry.ColorMode.allCases, id: \.self) { mode in
+                Text(mode.displayName).tag(mode)
+            }
+        }
+    }
+
     private var fontSizePicker: some View {
         Picker("Font Size", selection: $fontSize) {
             ForEach(ContactImageEntry.FontSize.allCases, id: \.self) { size in

+ 9 - 0
Trio/Sources/Modules/ContactImage/View/ContactImageDetailView.swift

@@ -109,6 +109,7 @@ struct ContactImageDetailView: View {
 
                 // Font Settings Section
                 Section(header: Text("Font Settings")) {
+                    colorModePicker
                     fontSizePicker
                     if contactImageEntry.layout == .split {
                         secondaryFontSizePicker
@@ -177,6 +178,14 @@ struct ContactImageDetailView: View {
         }
     }
 
+    private var colorModePicker: some View {
+        Picker("Color Mode", selection: $contactImageEntry.colorMode) {
+            ForEach(ContactImageEntry.ColorMode.allCases, id: \.self) { mode in
+                Text(mode.displayName).tag(mode)
+            }
+        }
+    }
+
     private var fontSizePicker: some View {
         Picker("Font Size", selection: $contactImageEntry.fontSize) {
             ForEach(ContactImageEntry.FontSize.allCases, id: \.self) { size in

+ 3 - 3
Trio/Sources/Modules/DataTable/DataTableDataFlow.swift

@@ -3,7 +3,7 @@ import Foundation
 import HealthKit
 import SwiftUI
 
-enum DataTable {
+enum History {
     enum Config {}
 
     enum TreatmentType: String, CaseIterable {
@@ -227,7 +227,7 @@ enum DataTable {
     }
 
     class Glucose: Identifiable, Hashable, Equatable {
-        static func == (lhs: DataTable.Glucose, rhs: DataTable.Glucose) -> Bool {
+        static func == (lhs: History.Glucose, rhs: History.Glucose) -> Bool {
             lhs.glucose == rhs.glucose
         }
 
@@ -241,7 +241,7 @@ enum DataTable {
     }
 }
 
-protocol DataTableProvider: Provider {
+protocol HistoryProvider: Provider {
     func deleteCarbsFromNightscout(withID id: String)
     func deleteInsulinFromNightscout(withID id: String)
     func deleteManualGlucoseFromNightscout(withID id: String)

+ 2 - 2
Trio/Sources/Modules/DataTable/DataTableProvider.swift

@@ -2,8 +2,8 @@ import CoreData
 import Foundation
 import HealthKit
 
-extension DataTable {
-    final class Provider: BaseProvider, DataTableProvider {
+extension History {
+    final class Provider: BaseProvider, HistoryProvider {
         @Injected() var nightscoutManager: NightscoutManager!
         @Injected() var healthkitManager: HealthKitManager!
         @Injected() var tidepoolManager: TidepoolManager!

+ 2 - 2
Trio/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -3,7 +3,7 @@ import HealthKit
 import Observation
 import SwiftUI
 
-extension DataTable {
+extension History {
     @Observable final class StateModel: BaseStateModel<Provider> {
         @ObservationIgnored @Injected() var broadcaster: Broadcaster!
         @ObservationIgnored @Injected() var apsManager: APSManager!
@@ -585,7 +585,7 @@ extension DataTable {
     }
 }
 
-extension DataTable.StateModel: DeterminationObserver, SettingsObserver {
+extension History.StateModel: DeterminationObserver, SettingsObserver {
     func determinationDidUpdate(_: Determination) {
         DispatchQueue.main.async {
             self.waitForSuggestion = false

+ 2 - 2
Trio/Sources/Modules/DataTable/View/CarbEntryEditorView.swift

@@ -12,7 +12,7 @@ struct CarbEntryEditorView: View {
     @Environment(\.colorScheme) var colorScheme
     @Environment(AppState.self) var appState
 
-    var state: DataTable.StateModel
+    var state: History.StateModel
     let carbEntry: CarbEntryStored
 
     /*
@@ -28,7 +28,7 @@ struct CarbEntryEditorView: View {
     @State private var isFPU: Bool
     @State private var editedDate: Date
 
-    init(state: DataTable.StateModel, carbEntry: CarbEntryStored) {
+    init(state: History.StateModel, carbEntry: CarbEntryStored) {
         self.state = state
         self.carbEntry = carbEntry
         _editedCarbs = State(initialValue: 0) // gets updated in the task block

+ 14 - 11
Trio/Sources/Modules/DataTable/View/DataTableRootView.swift

@@ -2,7 +2,7 @@ import CoreData
 import SwiftUI
 import Swinject
 
-extension DataTable {
+extension History {
     struct RootView: BaseView {
         let resolver: Resolver
 
@@ -201,12 +201,15 @@ extension DataTable {
                         HStack(spacing: 20) {
                             Image(
                                 systemName: selectedTreatmentTypes.count == TreatmentType.allCases.count
-                                    ? "checkmark.circle.fill" : "circle"
+                                    ? "checkmark.square.fill" : "square"
                             )
                             .frame(width: 20)
                             .foregroundColor(Color.accentColor)
-                            Text(selectedTreatmentTypes.count == TreatmentType.allCases.count ? "Deselect All" : "Select All")
-                                .foregroundColor(Color.primary)
+                            Text(
+                                selectedTreatmentTypes.count == TreatmentType.allCases
+                                    .count ? String(localized: "Deselect All") : String(localized: "Select All")
+                            )
+                            .foregroundColor(Color.primary)
                         }.padding(4)
                     }
                     .buttonStyle(.borderless)
@@ -220,7 +223,7 @@ extension DataTable {
                             HStack(spacing: 20) {
                                 Image(
                                     systemName: selectedTreatmentTypes
-                                        .contains(treatmentType) ? "checkmark.circle.fill" : "circle"
+                                        .contains(treatmentType) ? "checkmark.square.fill" : "square"
                                 )
                                 .frame(width: 20)
                                 .foregroundColor(Color.accentColor)
@@ -252,7 +255,7 @@ extension DataTable {
                 },
                 label: {
                     HStack {
-                        Text(showFutureEntries ? "Hide Future" : "Show Future")
+                        Text(showFutureEntries ? String(localized: "Hide Future") : String(localized: "Show Future"))
                             .foregroundColor(Color.accentColor)
                         Image(systemName: showFutureEntries ? "eye.slash" : "eye")
                             .foregroundColor(Color.accentColor)
@@ -308,7 +311,7 @@ extension DataTable {
                     }
                 } else {
                     ContentUnavailableView(
-                        "No data.",
+                        String(localized: "No data."),
                         systemImage: "syringe"
                     )
                 }
@@ -328,7 +331,7 @@ extension DataTable {
                     }
                 } else {
                     ContentUnavailableView(
-                        "No data.",
+                        String(localized: "No data."),
                         systemImage: "fork.knife"
                     )
                 }
@@ -347,7 +350,7 @@ extension DataTable {
                     }
                 } else {
                     ContentUnavailableView(
-                        "No data.",
+                        String(localized: "No data."),
                         systemImage: "clock.arrow.2.circlepath"
                     )
                 }
@@ -523,7 +526,7 @@ extension DataTable {
                     }
                 } else {
                     ContentUnavailableView(
-                        "No data.",
+                        String(localized: "No data."),
                         systemImage: "drop.fill"
                     )
                 }
@@ -608,7 +611,7 @@ extension DataTable {
         private var filterEntriesButton: some View {
             Button(action: { showFutureEntries.toggle() }, label: {
                 HStack {
-                    Text(showFutureEntries ? "Hide Future" : "Show Future")
+                    Text(showFutureEntries ? String(localized: "Hide Future") : String(localized: "Show Future"))
                         .foregroundColor(Color.secondary)
                     Image(systemName: showFutureEntries ? "calendar.badge.minus" : "calendar.badge.plus")
                 }.frame(maxWidth: .infinity, alignment: .trailing)

+ 0 - 11
Trio/Sources/Modules/Home/HomeStateModel+Setup/TempTargetSetup.swift

@@ -118,15 +118,4 @@ extension Home.StateModel {
             debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save Temp Target with error: \(error)")
         }
     }
-
-    func computeAdjustedPercentage(halfBasalTargetValue: Decimal, tempTargetValue: Decimal) -> Int {
-        let normalTarget: Decimal = 100
-        let deviationFromNormal = halfBasalTargetValue - normalTarget
-
-        let adjustmentFactor = deviationFromNormal + (tempTargetValue - normalTarget)
-        let adjustmentRatio: Decimal = (deviationFromNormal * adjustmentFactor <= 0) ? autosensMax : deviationFromNormal /
-            adjustmentFactor
-
-        return Int(Double(min(adjustmentRatio, autosensMax) * 100).rounded())
-    }
 }

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

@@ -220,11 +220,15 @@ extension Home {
                 return nil
             }
 
+            guard let settingsManager = state.settingsManager else {
+                return nil
+            }
+
             let percent = latestOverride.percentage
             let percentString = percent == 100 ? "" : "\(percent.formatted(.number)) %"
 
             let unit = state.units
-            var target = (latestOverride.target ?? 100) as Decimal
+            var target = (latestOverride.target ?? 0) as Decimal
             target = unit == .mmolL ? target.asMmolL : target
 
             var targetString = target == 0 ? "" : (fetchedTargetFormatter.string(from: target as NSNumber) ?? "") + " " + unit
@@ -264,9 +268,29 @@ extension Home {
                 : ""
 
             let smbToggleString = latestOverride.smbIsOff || latestOverride
-                .smbIsScheduledOff ? "SMBs Off\(smbScheduleString)" : ""
+                .smbIsScheduledOff ? String(localized: "SMBs Off\(smbScheduleString)") : ""
+
+            var smbMinuteString: String = ""
+            var uamMinuteString: String = ""
+
+            if !latestOverride.smbIsOff, latestOverride.advancedSettings {
+                if let smbMinutes = latestOverride.smbMinutes,
+                   smbMinutes.decimalValue != settingsManager.preferences.maxSMBBasalMinutes
+                {
+                    smbMinuteString = "SMB\u{00A0}\(smbMinutes)\u{00A0}" +
+                        String(localized: "m", comment: "Abbreviation for Minutes")
+                }
 
-            let components = [durationString, percentString, targetString, smbToggleString].filter { !$0.isEmpty }
+                if let uamMinutes = latestOverride.uamMinutes,
+                   uamMinutes.decimalValue != settingsManager.preferences.maxUAMSMBBasalMinutes
+                {
+                    uamMinuteString = "UAM\u{00A0}\(uamMinutes)\u{00A0}" +
+                        String(localized: "m", comment: "Abbreviation for Minutes")
+                }
+            }
+
+            let components = [durationString, percentString, targetString, smbToggleString, smbMinuteString, uamMinuteString]
+                .filter { !$0.isEmpty }
             return components.isEmpty ? nil : components.joined(separator: ", ")
         }
 
@@ -284,16 +308,20 @@ extension Home {
             var durationString = ""
             var percentageString = ""
             var target = (latestTempTarget.target ?? 100) as Decimal
-            var halfBasalTarget: Decimal = 160
-            if latestTempTarget.halfBasalTarget != nil {
-                halfBasalTarget = latestTempTarget.halfBasalTarget! as Decimal
-            } else { halfBasalTarget = state.settingHalfBasalTarget }
+            // Use TempTargetCalculations to get effective HBT (handles both custom and auto-adjusted standard TT)
+            let effectiveHBT = TempTargetCalculations.computeEffectiveHBT(
+                tempTargetHalfBasalTarget: latestTempTarget.halfBasalTarget?.decimalValue,
+                settingHalfBasalTarget: state.settingHalfBasalTarget,
+                target: target,
+                autosensMax: state.autosensMax
+            ) ?? state.settingHalfBasalTarget
             var showPercentage = false
             if target > 100, state.isExerciseModeActive || state.highTTraisesSens { showPercentage = true }
             if target < 100, state.lowTTlowersSens, state.autosensMax > 1 { showPercentage = true }
             if showPercentage {
                 percentageString =
-                    " \(state.computeAdjustedPercentage(halfBasalTargetValue: halfBasalTarget, tempTargetValue: target))%" }
+                    " \(Int(TempTargetCalculations.computeAdjustedPercentage(halfBasalTarget: effectiveHBT, target: target, autosensMax: state.autosensMax)))%"
+            }
             target = state.units == .mmolL ? target.asMmolL : target
             let targetString = target == 0 ? "" : (fetchedTargetFormatter.string(from: target as NSNumber) ?? "") + " " +
                 state.units.rawValue + percentageString
@@ -1075,7 +1103,7 @@ extension Home {
                         .tabItem { Label("Main", systemImage: "chart.xyaxis.line") }
                         .badge(carbsRequiredBadge).tag(0)
 
-                    NavigationStack { DataTable.RootView(resolver: resolver) }
+                    NavigationStack { History.RootView(resolver: resolver) }
                         .tabItem { Label("History", systemImage: historySFSymbol) }.tag(1)
 
                     Spacer()

+ 8 - 3
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/TherapySettings/InsulinSensitivityStepView.swift

@@ -145,7 +145,8 @@ struct InsulinSensitivityStepView: View {
     private var isfChart: some View {
         Chart {
             ForEach(Array(state.isfItems.enumerated()), id: \.element.id) { index, item in
-                let displayValue = state.isfRateValues[item.rateIndex]
+                let displayValue = state.units == .mgdL ? state.isfRateValues[item.rateIndex] : state
+                    .isfRateValues[item.rateIndex].asMmolL
 
                 let startDate = Calendar.current
                     .startOfDay(for: now)
@@ -196,8 +197,12 @@ struct InsulinSensitivityStepView: View {
                 .addingTimeInterval(60 * 60 * 24)
         )
         .chartYAxis {
-            AxisMarks(values: .automatic(desiredCount: 4)) { _ in
-                AxisValueLabel()
+            AxisMarks(values: .automatic(desiredCount: 4)) { value in
+                if let val = value.as(Double.self) {
+                    AxisValueLabel {
+                        Text(numberFormatter.string(from: NSNumber(value: val)) ?? "")
+                    }
+                }
                 AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
             }
         }

+ 4 - 4
Trio/Sources/Modules/PumpConfig/PumpConfigProvider.swift

@@ -26,12 +26,12 @@ extension PumpConfig {
                 ?? PumpSettings(insulinActionCurve: 10, maxBolus: 10, maxBasal: 2)
         }
 
-        var alertNotAck: AnyPublisher<Bool, Never> {
-            deviceManager.alertHistoryStorage.alertNotAck.eraseToAnyPublisher()
+        var unacknowledgedAlertsPublisher: AnyPublisher<Bool, Never> {
+            deviceManager.alertHistoryStorage.unacknowledgedAlertsPublisher.eraseToAnyPublisher()
         }
 
-        func initialAlertNotAck() -> Bool {
-            deviceManager.alertHistoryStorage.recentNotAck().isNotEmpty
+        func hasInitialUnacknowledgedAlerts() -> Bool {
+            deviceManager.alertHistoryStorage.unacknowledgedAlertsWithinLast24Hours().isNotEmpty
         }
     }
 }

+ 5 - 5
Trio/Sources/Modules/PumpConfig/PumpConfigStateModel.swift

@@ -9,7 +9,7 @@ extension PumpConfig {
         private(set) var setupPumpType: PumpType = .minimed
         @Published var pumpState: PumpDisplayState?
         private(set) var initialSettings: PumpInitialSettings = .default
-        @Published var alertNotAck: Bool = false
+        @Published var hasUnacknowledgedAlert: Bool = false
         @Injected() var bluetoothManager: BluetoothStateManager!
 
         override func subscribe() {
@@ -18,10 +18,10 @@ extension PumpConfig {
                 .assign(to: \.pumpState, on: self)
                 .store(in: &lifetime)
 
-            alertNotAck = provider.initialAlertNotAck()
-            provider.alertNotAck
+            hasUnacknowledgedAlert = provider.hasInitialUnacknowledgedAlerts()
+            provider.unacknowledgedAlertsPublisher
                 .receive(on: DispatchQueue.main)
-                .assign(to: \.alertNotAck, on: self)
+                .assign(to: \.hasUnacknowledgedAlert, on: self)
                 .store(in: &lifetime)
 
             Task {
@@ -49,7 +49,7 @@ extension PumpConfig {
         }
 
         func ack() {
-            provider.deviceManager.alertHistoryStorage.forceNotification()
+            provider.deviceManager.alertHistoryStorage.broadcastAlertUpdates()
         }
     }
 }

+ 1 - 1
Trio/Sources/Modules/PumpConfig/View/PumpConfigRootView.swift

@@ -41,7 +41,7 @@ extension PumpConfig {
                                     .frame(maxWidth: .infinity, minHeight: 50, alignment: .center)
                                     .font(.title2)
                                 }.padding()
-                                if state.alertNotAck {
+                                if state.hasUnacknowledgedAlert {
                                     Spacer()
                                     Button("Acknowledge all alerts") { state.ack() }
                                 }

+ 8 - 9
Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseDailyPercentileChart.swift

@@ -396,26 +396,25 @@ struct GlucoseDailyPercentileChart: View {
 
     // Calculate an appropriate Y axis domain for the chart
     private func glucoseYScaleDomain() -> ClosedRange<Double> {
-        // Find actual min/max from data
+        let padding = units == .mgdL ? 20.0 : 1.0
+        let bottomLimit = 40.0.asUnit(units)
+        let topLimit = 400.0.asUnit(units)
+
         if visibleDailyStats.isEmpty {
-            return 0 ... (units == .mgdL ? 250 : 14.0)
+            return bottomLimit ... topLimit
         }
 
         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)
+            return bottomLimit ... topLimit
         }
 
-        let minValue = allValues.min() ?? 0
-        let maxValue = allValues.max() ?? (units == .mgdL ? 250 : 14.0)
+        let maxValue = allValues.max() ?? topLimit
 
-        // Add some padding
-        let padding = units == .mgdL ? 20.0 : 1.0
-        return max(0, minValue - padding) ... maxValue + padding
+        return bottomLimit ... max(Double(highLimit.asUnit(units)), maxValue + padding)
     }
 }

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

@@ -47,7 +47,7 @@ struct GlucosePercentileChart: View {
         let validStats = hourlyStats.filter { $0.median > 0 }
         guard !validStats.isEmpty else { return topLimit }
         let maxPercentile90 = validStats.map(\.percentile90).max() ?? topLimit
-        return maxPercentile90.asUnit(units)
+        return max(maxPercentile90.asUnit(units), Double(highLimit.asUnit(units)))
     }
 
     var body: some View {

+ 6 - 6
Trio/Sources/Modules/Treatments/View/MealPreset/MealPresetView.swift

@@ -66,7 +66,6 @@ struct MealPresetView: View {
                 ToolbarItem(placement: .topBarLeading) {
                     Button {
                         dismiss()
-                        resetValues()
                     } label: {
                         Text("Close")
                     }
@@ -74,7 +73,6 @@ struct MealPresetView: View {
                 ToolbarItem(placement: .topBarTrailing) {
                     Button(action: {
                         showAddNewPresetSheet.toggle()
-                        resetValues()
                     }, label: {
                         HStack {
                             Text("New Preset")
@@ -93,7 +91,7 @@ struct MealPresetView: View {
                     onSave: savePreset,
                     onCancel: {
                         showAddNewPresetSheet.toggle()
-                        resetValues()
+                        resetNewPresetForm()
                     }
                 )
             }
@@ -267,12 +265,15 @@ struct MealPresetView: View {
     }
 
     private func resetValues() {
+        state.selection = nil
+        state.summation.removeAll()
+    }
+
+    private func resetNewPresetForm() {
         dish = ""
         presetCarbs = 0
         presetFat = 0
         presetProtein = 0
-        state.selection = nil
-        state.summation.removeAll()
     }
 
     private var minusButton: some View {
@@ -345,7 +346,6 @@ struct MealPresetView: View {
                 guard moc.hasChanges else { return }
                 try moc.save()
                 showAddNewPresetSheet.toggle()
-                resetValues()
             } catch let error as NSError {
                 debugPrint("\(DebuggingIdentifiers.failed) Failed to save Meal Preset with error: \(error.userInfo)")
             }

+ 3 - 3
Trio/Sources/Router/Screen.swift

@@ -17,7 +17,7 @@ enum Screen: Identifiable, Hashable {
     case targetsEditor
     case treatmentView
     case manualTempBasal
-    case dataTable
+    case history
     case cgm
     case healthkit
     case glucoseNotificationSettings
@@ -94,8 +94,8 @@ extension Screen {
             Treatments.RootView(resolver: resolver)
         case .manualTempBasal:
             ManualTempBasal.RootView(resolver: resolver)
-        case .dataTable:
-            DataTable.RootView(resolver: resolver)
+        case .history:
+            History.RootView(resolver: resolver)
         case .cgm:
             CGMSettings.RootView(
                 resolver: resolver,

+ 9 - 1
Trio/Sources/Services/ContactImage/ContactPicture.swift

@@ -289,7 +289,7 @@ struct ContactPicture: View {
                 fontSize: fontSize,
                 fontWeight: fontWeight,
                 fontWidth: fontWidth,
-                color: textColor
+                color: contact.colorMode == .color ? textColor : .white
             )
         }
     }
@@ -634,6 +634,7 @@ struct ContactPicture_Previews: PreviewProvider {
     struct Preview: View {
         @State var rangeIndicator: Bool = true
         @State var hasHighContrast: Bool = true
+        @State var colorMode: ContactImageEntry.ColorMode = .color
         @State var fontSize: ContactImageEntry.FontSize = .small
         @State var fontWeight: UIFont.Weight = .bold
         @State var fontName: String? = "AmericanTypewriter"
@@ -645,6 +646,7 @@ struct ContactPicture_Previews: PreviewProvider {
                         primary: .glucose,
                         top: .delta,
                         bottom: .trend,
+                        colorMode: .color,
                         fontSize: fontSize,
                         fontWeight: .medium
                     )
@@ -683,6 +685,7 @@ struct ContactPicture_Previews: PreviewProvider {
                         primary: .glucose,
                         top: .ring,
                         bottom: .trend,
+                        colorMode: .color,
                         fontSize: fontSize,
                         fontWeight: .medium
                     )
@@ -702,6 +705,7 @@ struct ContactPicture_Previews: PreviewProvider {
                         primary: .glucose,
                         top: .none,
                         bottom: .trend,
+                        colorMode: .color,
                         fontSize: fontSize,
                         fontWeight: .medium
                     )
@@ -720,6 +724,7 @@ struct ContactPicture_Previews: PreviewProvider {
                         primary: .glucose,
                         top: .none,
                         bottom: .eventualBG,
+                        colorMode: .color,
                         fontSize: fontSize,
                         fontWeight: .medium
                     )
@@ -738,6 +743,7 @@ struct ContactPicture_Previews: PreviewProvider {
                         primary: .lastLoopDate,
                         top: .none,
                         bottom: .none,
+                        colorMode: .color,
                         fontSize: fontSize,
                         fontWeight: .medium
                     )
@@ -756,6 +762,7 @@ struct ContactPicture_Previews: PreviewProvider {
                         primary: .glucose,
                         top: .none,
                         bottom: .none,
+                        colorMode: .color,
                         fontSize: fontSize,
                         fontWeight: .medium
                     )
@@ -775,6 +782,7 @@ struct ContactPicture_Previews: PreviewProvider {
                         layout: .split,
                         top: .iob,
                         bottom: .cob,
+                        colorMode: .color,
                         fontSize: fontSize,
                         fontWeight: .medium
                     )

+ 0 - 12
fastlane/Fastfile

@@ -91,18 +91,6 @@ platform :ios do
       ]
     )
 
-    previous_build_number = latest_testflight_build_number(
-      app_identifier: "#{BUNDLE_ID}",
-      api_key: api_key,
-    )
-
-    current_build_number = previous_build_number + 1
-
-    increment_build_number(
-      xcodeproj: "#{GITHUB_WORKSPACE}/Trio.xcodeproj",
-      build_number: current_build_number
-    )
-    
     mapping = Actions.lane_context[
       SharedValues::MATCH_PROVISIONING_PROFILE_MAPPING
     ]

+ 15 - 25
fastlane/testflight.md

@@ -11,11 +11,9 @@ These instructions allow you to build Trio without having access to a Mac.
 >
 > The browser build defaults to automatically updating and building a new version of Trio according to this schedule:
 >
-> * automatically checks for updates weekly on Wednesdays and if updates are found, it will build a new version of the app
-> * automatically builds once a month regardless of whether there are updates on the first of the month
-> * with each scheduled run (weekly or monthly), a successful Build Trio log appears - if the time is very short, it did not need to build - only the long actions (>10 minutes) built a new Trio app
->
-> It also creates an alive branch, if you don't already have one. See [Why do I have an alive branch?](#why-do-i-have-an-alive-branch).
+> * automatically checks for updates weekly and if updates are found, it will build a new version of the app
+>   - even when there are no updates, it builds on the second Sunday of the month
+> * with each scheduled weekly run, a successful build log appears - if the time is very short, it did not need to build - only the longer actions (>10 minutes) built a new app
 >
 > The [**Optional**](#optional) section provides instructions to modify the default behavior if desired.
 
@@ -233,14 +231,6 @@ For more details, please refer to [LoopDocs: Set Up Users](https://loopkit.githu
 
 ## Automatic Build FAQs
 
-### Why do I have an `alive` branch?
-
-If a GitHub repository has no activity (no commits are made) in 60 days, then GitHub disables the ability to use automated actions for that repository. We need to take action more frequently than that or the automated build process won't work.
-
-The `build_trio.yml` file uses a special branch called `alive` and adds a dummy commit to the `alive` branch at regular intervals. This "trick" keeps the Actions enabled so the automated build works.
-
-The branch `alive` is created automatically for you. Do not delete or rename it! Do not modify `alive` yourself; it is not used for building the app.
-
 ## OPTIONAL
 
 What if you don't want to allow automated updates of the repository or automatic builds?
@@ -269,18 +259,18 @@ You can modify the automation by creating and using some variables.
 
 To configure the automated build more granularly involves creating up to two environment variables: `SCHEDULED_BUILD` and/or `SCHEDULED_SYNC`. See [How to configure a variable](#how-to-configure-a-variable).
 
-Note that the weekly and monthly Build Trio actions will continue, but the actions are modified if one or more of these variables is set to false. **A successful Action Log will still appear, even if no automatic activity happens**.
+Note that the weekly build actions will continue, but the actions are modified if one or more of these variables is set to false. **A successful Action Log will still appear, even if no automatic activity happens**.
 
-* If you want to manually decide when to update your repository to the latest commit, but you want the monthly builds and keep-alive to continue: set `SCHEDULED_SYNC` to false and either do not create `SCHEDULED_BUILD` or set it to true
+* If you want to manually decide when to update your repository to the latest commit, but you want the monthly builds to continue: set `SCHEDULED_SYNC` to false and either do not create `SCHEDULED_BUILD` or set it to true
 * If you want to only build when an update has been found: set `SCHEDULED_BUILD` to false and either do not create `SCHEDULED_SYNC` or set it to true
     * **Warning**: if no updates to your default branch are detected within 90 days, your previous TestFlight build may expire requiring a manual build
 
 |`SCHEDULED_SYNC`|`SCHEDULED_BUILD`|Automatic Actions|
 |---|---|---|
-| `true` (or NA) | `true` (or NA) | keep-alive, weekly update check (auto update/build), monthly build with auto update |
-| `true` (or NA) | `false` | keep-alive, weekly update check with auto update, only builds if update detected |
-| `false` | `true` (or NA) | keep-alive, monthly build, no auto update |
-| `false` | `false` | no automatic activity, no keep-alive |
+| `true` (or NA) | `true` (or NA) | weekly update check (auto update/build), monthly build with auto update |
+| `true` (or NA) | `false` | weekly update check with auto update, only builds if update detected |
+| `false` | `true` (or NA) | monthly build, no auto update |
+| `false` | `false` | no automatic activity |
 
 ### How to configure a variable
 
@@ -302,12 +292,12 @@ Note that the weekly and monthly Build Trio actions will continue, but the actio
 Your build will run on the following conditions:
 
 * Default behaviour:
-  * Run weekly, every Wednesday at 08:00 UTC to check for changes; if there are changes, it will update your repository and build
-  * Run monthly, every first of the month at 06:00 UTC, if there are changes, it will update your repository; regardless of changes, it will build
-  * Each time the action runs, it makes a keep-alive commit to the `alive` branch if necessary
-* If you disable any automation (both variables set to `false`), no updates, keep-alive or building happens when Build Trio runs
-* If you disabled just scheduled synchronization (`SCHEDULED_SYNC` set to`false`), it will only run once a month, on the first of the month, no update will happen; keep-alive will run
-* If you disabled just scheduled build (`SCHEDULED_BUILD` set to`false`), it will run once weekly, every Wednesday, to check for changes; if there are changes, it will update and build; keep-alive will run
+  * Run weekly every Sunday
+      - If updates are detected, it will update your repository and build
+      - If it is the second Sunday of the month, it will build even when no changes are detected
+* If you disable any automation (both variables set to `false`), no updates or building happens when Build Trio runs
+* If you disabled just scheduled synchronization (`SCHEDULED_SYNC` set to`false`), it will still build once a month, but no update will happen
+* If you disabled just scheduled build (`SCHEDULED_BUILD` set to`false`), it will run once weekly, to check for changes; if there are changes, it will update and build
 
 ## What if I build using more than one GitHub username
 

+ 1 - 1
scripts/swiftformat.sh

@@ -110,5 +110,5 @@ trailingClosures \
   RileyLinkKit, \
   OmniBLE, \
   MinimedKit, \
-  TidepoolService \
+  TidepoolService, \
   DanaKit