Browse Source

Merge branch 'dev' of github.com:nightscout/Trio into aps-manager-thread-safety-fixes

Marvin Polscheit 11 giờ trước cách đây
mục cha
commit
0bd9d090e4
55 tập tin đã thay đổi với 1029 bổ sung167 xóa
  1. 1 1
      .github/CODEOWNERS
  2. 3 0
      .gitmodules
  3. 3 3
      Config.xcconfig
  4. 1 1
      Gemfile
  5. 60 51
      Gemfile.lock
  6. 1 0
      OmnipodKit
  7. 7 2
      PRIVACY_POLICY.md
  8. 38 0
      Trio.xcodeproj/project.pbxproj
  9. 3 0
      Trio.xcworkspace/contents.xcworkspacedata
  10. 1 1
      Trio.xcworkspace/xcshareddata/swiftpm/Package.resolved
  11. 10 3
      Trio/Sources/APS/APSManager.swift
  12. 59 1
      Trio/Sources/APS/DeviceDataManager.swift
  13. 22 4
      Trio/Sources/Application/AppDelegate.swift
  14. 1 0
      Trio/Sources/Assemblies/ServiceAssembly.swift
  15. 163 0
      Trio/Sources/Localizations/Main/Localizable.xcstrings
  16. 3 3
      Trio/Sources/Models/TrioSettings.swift
  17. 1 0
      Trio/Sources/Modules/AlgorithmAdvancedSettings/View/AlgorithmAdvancedSettingsRootView.swift
  18. 1 0
      Trio/Sources/Modules/AutosensSettings/View/AutosensSettingsRootView.swift
  19. 1 0
      Trio/Sources/Modules/BolusCalculatorConfig/View/BolusCalculatorConfigRootView.swift
  20. 1 0
      Trio/Sources/Modules/CGMSettings/View/CGMRootView.swift
  21. 1 0
      Trio/Sources/Modules/CalendarEventSettings/View/CalendarEventSettingsRootView.swift
  22. 2 1
      Trio/Sources/Modules/DynamicSettings/View/DynamicSettingsRootView.swift
  23. 1 0
      Trio/Sources/Modules/GeneralSettings/View/UnitsLimitsSettingsRootView.swift
  24. 1 0
      Trio/Sources/Modules/GlucoseNotificationSettings/View/GlucoseNotificationSettingsRootView.swift
  25. 3 0
      Trio/Sources/Modules/Home/View/HomeRootView.swift
  26. 1 0
      Trio/Sources/Modules/LiveActivitySettings/View/LiveActivitySettingsRootView.swift
  27. 15 6
      Trio/Sources/Modules/LiveActivitySettings/View/LiveActivityWidgetConfiguration.swift
  28. 1 0
      Trio/Sources/Modules/MealSettings/View/MealSettingsRootView.swift
  29. 1 0
      Trio/Sources/Modules/NightscoutConfig/View/NightscoutFetchView.swift
  30. 1 0
      Trio/Sources/Modules/NightscoutConfig/View/NightscoutUploadView.swift
  31. 10 2
      Trio/Sources/Modules/Onboarding/View/TherapySettingEditorView.swift
  32. 1 0
      Trio/Sources/Modules/PumpConfig/PumpConfigDataFlow.swift
  33. 2 0
      Trio/Sources/Modules/PumpConfig/View/PumpConfigRootView.swift
  34. 10 0
      Trio/Sources/Modules/PumpConfig/View/PumpSetupView.swift
  35. 1 0
      Trio/Sources/Modules/RemoteControlConfig/View/RemoteControlConfig.swift
  36. 1 0
      Trio/Sources/Modules/SMBSettings/View/SMBSettingsRootView.swift
  37. 66 2
      Trio/Sources/Modules/Settings/SettingItems.swift
  38. 19 8
      Trio/Sources/Modules/Settings/View/SettingsRootView.swift
  39. 2 2
      Trio/Sources/Modules/ShortcutsConfig/ShortcutsConfigStateModel.swift
  40. 1 0
      Trio/Sources/Modules/ShortcutsConfig/View/ShortcutsConfigView.swift
  41. 1 0
      Trio/Sources/Modules/TargetBehavoir/View/TargetBehavoirRootView.swift
  42. 9 8
      Trio/Sources/Modules/UserInterfaceSettings/View/UserInterfaceSettingsRootView.swift
  43. 1 0
      Trio/Sources/Modules/WatchConfig/View/WatchConfigAppleWatchView.swift
  44. 106 0
      Trio/Sources/Services/BolusSafety/BolusSafetyValidator.swift
  45. 22 45
      Trio/Sources/Services/RemoteControl/TrioRemoteControl+Bolus.swift
  46. 1 3
      Trio/Sources/Services/RemoteControl/TrioRemoteControl.swift
  47. 71 7
      Trio/Sources/Services/Telemetry/TelemetryClient.swift
  48. 1 0
      Trio/Sources/Shortcuts/BaseIntentsRequest.swift
  49. 37 10
      Trio/Sources/Shortcuts/Bolus/BolusIntentRequest.swift
  50. 2 1
      Trio/Sources/Views/SettingInputSection.swift
  51. 73 0
      Trio/Sources/Views/SettingsSearchHighlight.swift
  52. 113 0
      TrioTests/BolusSafetyTests/BolusSafetyValidatorTests.swift
  53. 69 0
      TrioTests/SettingsSearchTests.swift
  54. 1 1
      scripts/capture-build-details.sh
  55. 2 1
      scripts/swiftformat.sh

+ 1 - 1
.github/CODEOWNERS

@@ -1 +1 @@
-*    @dnzxy @bjornoleh @MikePlante1 @AndreasStokholm @Sjoerd-Bo3 @t1dude @marv-out
+*    @dnzxy @bjornoleh @MikePlante1 @AndreasStokholm @Sjoerd-Bo3 @t1dude @marv-out @kingst @marionbarker

+ 3 - 0
.gitmodules

@@ -34,3 +34,6 @@
 [submodule "MedtrumKit"]
 	path = MedtrumKit
 	url = https://github.com/loopandlearn/MedtrumKit
+[submodule "OmnipodKit"]
+	path = OmnipodKit
+	url = https://github.com/loopandlearn/OmnipodKit

+ 3 - 3
Config.xcconfig

@@ -18,12 +18,12 @@ BUNDLE_IDENTIFIER = org.nightscout.$(DEVELOPMENT_TEAM).trio
 TRIO_APP_GROUP_ID = group.org.nightscout.$(DEVELOPMENT_TEAM).trio.trio-app-group
 
 // The developers set the version numbers, please leave them alone
-APP_VERSION = 0.7.0
-APP_DEV_VERSION = 0.7.0.19
+APP_VERSION = 0.8.1
+APP_DEV_VERSION = 0.8.1.3
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 
 // Optional overrides - these can be used to insert your TEAMID into the DEVELOPER_TEAM field
 #include? "../../ConfigOverride.xcconfig"
 #include? "../ConfigOverride.xcconfig"
-#include? "ConfigOverride.xcconfig"
+#include? "ConfigOverride.xcconfig"

+ 1 - 1
Gemfile

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

+ 60 - 51
Gemfile.lock

@@ -8,8 +8,8 @@ GEM
     artifactory (3.0.17)
     atomos (0.1.3)
     aws-eventstream (1.4.0)
-    aws-partitions (1.1206.0)
-    aws-sdk-core (3.241.4)
+    aws-partitions (1.1254.0)
+    aws-sdk-core (3.250.0)
       aws-eventstream (~> 1, >= 1.3.0)
       aws-partitions (~> 1, >= 1.992.0)
       aws-sigv4 (~> 1.9)
@@ -17,19 +17,19 @@ GEM
       bigdecimal
       jmespath (~> 1, >= 1.6.1)
       logger
-    aws-sdk-kms (1.121.0)
-      aws-sdk-core (~> 3, >= 3.241.4)
+    aws-sdk-kms (1.128.0)
+      aws-sdk-core (~> 3, >= 3.248.0)
       aws-sigv4 (~> 1.5)
-    aws-sdk-s3 (1.211.0)
-      aws-sdk-core (~> 3, >= 3.241.3)
+    aws-sdk-s3 (1.224.0)
+      aws-sdk-core (~> 3, >= 3.248.0)
       aws-sdk-kms (~> 1)
       aws-sigv4 (~> 1.5)
     aws-sigv4 (1.12.1)
       aws-eventstream (~> 1, >= 1.0.2)
     babosa (1.0.4)
-    base64 (0.2.0)
+    base64 (0.3.0)
     benchmark (0.5.0)
-    bigdecimal (4.0.1)
+    bigdecimal (4.1.2)
     claide (1.1.0)
     colored (1.2)
     colored2 (3.1.2)
@@ -43,16 +43,17 @@ GEM
     dotenv (2.8.1)
     emoji_regex (3.2.3)
     excon (0.112.0)
-    faraday (1.8.0)
+    faraday (1.10.5)
       faraday-em_http (~> 1.0)
       faraday-em_synchrony (~> 1.0)
       faraday-excon (~> 1.1)
-      faraday-httpclient (~> 1.0.1)
+      faraday-httpclient (~> 1.0)
+      faraday-multipart (~> 1.0)
       faraday-net_http (~> 1.0)
-      faraday-net_http_persistent (~> 1.1)
+      faraday-net_http_persistent (~> 1.0)
       faraday-patron (~> 1.0)
       faraday-rack (~> 1.0)
-      multipart-post (>= 1.2, < 3)
+      faraday-retry (~> 1.0)
       ruby2_keywords (>= 0.0.4)
     faraday-cookie_jar (0.0.8)
       faraday (>= 0.8.0)
@@ -61,23 +62,26 @@ GEM
     faraday-em_synchrony (1.0.1)
     faraday-excon (1.1.0)
     faraday-httpclient (1.0.1)
+    faraday-multipart (1.2.0)
+      multipart-post (~> 2.0)
     faraday-net_http (1.0.2)
     faraday-net_http_persistent (1.2.0)
     faraday-patron (1.0.0)
     faraday-rack (1.0.0)
+    faraday-retry (1.0.4)
     faraday_middleware (1.2.1)
       faraday (~> 1.0)
-    fastimage (2.4.0)
-    fastlane (2.233.1)
-      CFPropertyList (>= 2.3, < 4.0.0)
-      abbrev (~> 0.1.2)
+    fastimage (2.4.1)
+    fastlane (2.235.0)
+      CFPropertyList (>= 2.3, < 5.0.0)
+      abbrev (~> 0.1)
       addressable (>= 2.8, < 3.0.0)
       artifactory (~> 3.0)
       aws-sdk-s3 (~> 1.197)
       babosa (>= 1.0.3, < 2.0.0)
-      base64 (~> 0.2.0)
+      base64 (~> 0.2)
       benchmark (>= 0.1.0)
-      bundler (>= 1.17.3, < 5.0.0)
+      bundler (>= 2.4.0, < 5.0.0)
       colored (~> 1.2)
       commander (~> 4.6)
       csv (~> 3.3)
@@ -92,18 +96,18 @@ GEM
       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.1.1)
+      google-cloud-env (>= 1.6.0, < 2.3.0)
       google-cloud-storage (~> 1.31)
       highline (~> 2.0)
       http-cookie (~> 1.0.5)
       json (< 3.0.0)
-      jwt (>= 2.1.0, < 3)
+      jwt (>= 2.1.0, < 4)
       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)
+      mutex_m (~> 0.3)
       naturally (~> 2.2)
-      nkf (~> 0.2.0)
+      nkf (~> 0.2)
       optparse (>= 0.1.1, < 1.0.0)
       ostruct (>= 0.1.0)
       plist (>= 3.1.0, < 4.0.0)
@@ -120,39 +124,44 @@ GEM
       xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
     fastlane-sirp (1.1.0)
     gh_inspector (1.1.3)
-    google-apis-androidpublisher_v3 (0.54.0)
-      google-apis-core (>= 0.11.0, < 2.a)
-    google-apis-core (0.11.3)
+    google-apis-androidpublisher_v3 (0.101.0)
+      google-apis-core (>= 0.15.0, < 2.a)
+    google-apis-core (0.18.0)
       addressable (~> 2.5, >= 2.5.1)
-      googleauth (>= 0.16.2, < 2.a)
-      httpclient (>= 2.8.1, < 3.a)
+      googleauth (~> 1.9)
+      httpclient (>= 2.8.3, < 3.a)
       mini_mime (~> 1.0)
+      mutex_m
       representable (~> 3.0)
       retriable (>= 2.0, < 4.a)
-      rexml
-    google-apis-iamcredentials_v1 (0.17.0)
-      google-apis-core (>= 0.11.0, < 2.a)
-    google-apis-playcustomapp_v1 (0.13.0)
-      google-apis-core (>= 0.11.0, < 2.a)
-    google-apis-storage_v1 (0.31.0)
-      google-apis-core (>= 0.11.0, < 2.a)
+    google-apis-iamcredentials_v1 (0.27.0)
+      google-apis-core (>= 0.15.0, < 2.a)
+    google-apis-playcustomapp_v1 (0.17.0)
+      google-apis-core (>= 0.15.0, < 2.a)
+    google-apis-storage_v1 (0.62.0)
+      google-apis-core (>= 0.15.0, < 2.a)
     google-cloud-core (1.8.0)
       google-cloud-env (>= 1.0, < 3.a)
       google-cloud-errors (~> 1.0)
-    google-cloud-env (1.6.0)
-      faraday (>= 0.17.3, < 3.0)
-    google-cloud-errors (1.5.0)
-    google-cloud-storage (1.47.0)
+    google-cloud-env (2.2.2)
+      base64 (~> 0.2)
+      faraday (>= 1.0, < 3.a)
+    google-cloud-errors (1.6.0)
+    google-cloud-storage (1.60.0)
       addressable (~> 2.8)
       digest-crc (~> 0.4)
-      google-apis-iamcredentials_v1 (~> 0.1)
-      google-apis-storage_v1 (~> 0.31.0)
+      google-apis-core (>= 0.18, < 2)
+      google-apis-iamcredentials_v1 (~> 0.18)
+      google-apis-storage_v1 (>= 0.42)
       google-cloud-core (~> 1.6)
-      googleauth (>= 0.16.2, < 2.a)
+      googleauth (~> 1.9)
       mini_mime (~> 1.0)
-    googleauth (1.8.1)
-      faraday (>= 0.17.3, < 3.a)
-      jwt (>= 1.4, < 3.0)
+    google-logging-utils (0.2.0)
+    googleauth (1.16.2)
+      faraday (>= 1.0, < 3.a)
+      google-cloud-env (~> 2.2)
+      google-logging-utils (~> 0.1)
+      jwt (>= 1.4, < 4.0)
       multi_json (~> 1.11)
       os (>= 0.9, < 2.0)
       signet (>= 0.16, < 2.a)
@@ -162,13 +171,13 @@ GEM
     httpclient (2.9.0)
       mutex_m
     jmespath (1.6.2)
-    json (2.19.4)
-    jwt (2.10.2)
+    json (2.19.7)
+    jwt (3.2.0)
       base64
     logger (1.7.0)
     mini_magick (4.13.2)
     mini_mime (1.1.5)
-    multi_json (1.19.1)
+    multi_json (1.21.1)
     multipart-post (2.4.1)
     mutex_m (0.3.0)
     nanaimo (0.4.0)
@@ -178,13 +187,13 @@ GEM
     os (1.1.4)
     ostruct (0.6.3)
     plist (3.7.2)
-    public_suffix (7.0.2)
-    rake (13.3.1)
+    public_suffix (7.0.5)
+    rake (13.4.2)
     representable (3.2.0)
       declarative (< 0.1.0)
       trailblazer-option (>= 0.1.1, < 0.2.0)
       uber (< 0.2.0)
-    retriable (3.1.2)
+    retriable (3.8.0)
     rexml (3.4.4)
     rouge (3.28.0)
     ruby2_keywords (0.0.5)
@@ -227,7 +236,7 @@ PLATFORMS
 
 DEPENDENCIES
   addressable (>= 2.9.0)
-  fastlane (= 2.233.1)
+  fastlane (= 2.235.0)
   json (>= 2.19.2)
 
 BUNDLED WITH

+ 1 - 0
OmnipodKit

@@ -0,0 +1 @@
+Subproject commit adf210d20bcfc05f8119e8665d8f5904db828e03

+ 7 - 2
PRIVACY_POLICY.md

@@ -73,6 +73,11 @@ The following information is included in the telemetry payload:
   on/off, Live Activity enabled, calendar integration enabled
 - A rolling 7-day count of how often the app was cold-launched
 - The commit SHAs of pinned submodules (e.g. LoopKit, OmniBLE)
+- The device's system locale (e.g. "en_US") — used to help Trio
+  developers understand which languages to prioritize for translation
+- The device's time zone identifier (e.g. "America/New_York") — used
+  to help Trio developers understand which regions of the world Trio
+  is being used in
 
 The payload sends once every 24 hours while the app is running, plus
 once after a new build is installed. Sending failures simply retry on
@@ -85,7 +90,7 @@ the next launch or scheduler tick — there is no continued retry.
 - Your Nightscout URL or API token
 - Your Tidepool email, password, or session token
 - Remote-command secrets or APNS keys
-- Time zone or location
+- GPS coordinates or any precise location data
 - App logs — log sharing remains a separate, user-initiated flow under Settings
 
 ### Debug Symbols (dSYMs)
@@ -177,4 +182,4 @@ trio.diy.diabetes@gmail.com.
 
 ## Last Updated
 
-May 14, 2025
+May 28, 2026

+ 38 - 0
Trio.xcodeproj/project.pbxproj

@@ -260,6 +260,7 @@
 		3BD9687C2D8DDD4600899469 /* SlideButton in Frameworks */ = {isa = PBXBuildFile; productRef = 3BD9687B2D8DDD4600899469 /* SlideButton */; };
 		3BD9687F2D8DDD8800899469 /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 3BD9687E2D8DDD8800899469 /* CryptoSwift */; };
 		3BF85FE32E427312000D7351 /* IOBService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF85FE12E427312000D7351 /* IOBService.swift */; };
+		B015AFE32E500000000D7351 /* BolusSafetyValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B015AFE12E500000000D7351 /* BolusSafetyValidator.swift */; };
 		3E28F2AB2EB5337F00FB9EEB /* ConnectIQ in Frameworks */ = {isa = PBXBuildFile; productRef = 3E28F2AA2EB5337F00FB9EEB /* ConnectIQ */; };
 		3E54EF2C2E476DA40006F54D /* MedtrumKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3E54EF2B2E476DA40006F54D /* MedtrumKit.framework */; };
 		3E54EF2D2E476DA40006F54D /* MedtrumKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3E54EF2B2E476DA40006F54D /* MedtrumKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
@@ -346,8 +347,11 @@
 		9702FF92A09C53942F20D7EA /* TargetsEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DD795BA46B193644D48138C /* TargetsEditorRootView.swift */; };
 		9825E5E923F0B8FA80C8C7C7 /* NightscoutConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0A48AE3AC813A49A517846A /* NightscoutConfigStateModel.swift */; };
 		98641AF4F92123DA668AB931 /* CarbRatioEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BDC6993C1087310EDFC428 /* CarbRatioEditorRootView.swift */; };
+		41740E936552456AAC0EDAC3 /* SettingsSearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3919BBB515547118D684CA2 /* SettingsSearchTests.swift */; };
 		A33352ED40476125EBAC6EE0 /* CarbRatioEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E22146D3DF4853786C78132 /* CarbRatioEditorDataFlow.swift */; };
 		AD3D2CD42CD01B9EB8F26522 /* PumpConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF65DA88F972B56090AD6AC3 /* PumpConfigDataFlow.swift */; };
+		B6E925132EB3932A0076D719 /* OmnipodKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B6E925122EB3932A0076D719 /* OmnipodKit.framework */; };
+		B6E925142EB3932A0076D719 /* OmnipodKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B6E925122EB3932A0076D719 /* OmnipodKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		B7C465E9472624D8A2BE2A6A /* (null) in Sources */ = {isa = PBXBuildFile; };
 		B958F1B72BA0711600484851 /* MKRingProgressView in Frameworks */ = {isa = PBXBuildFile; productRef = B958F1B62BA0711600484851 /* MKRingProgressView */; };
 		BA00D96F7B2FF169A06FB530 /* CGMSettingsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C018D1680307A31C9ED7120 /* CGMSettingsStateModel.swift */; };
@@ -414,6 +418,7 @@
 		BD8FC0592D66189700B95AED /* TestAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC0582D66189700B95AED /* TestAssembly.swift */; };
 		BD8FC05B2D6618AF00B95AED /* DeterminationStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC05A2D6618AF00B95AED /* DeterminationStorageTests.swift */; };
 		BD8FC05E2D6618CE00B95AED /* BolusCalculatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC05D2D6618CE00B95AED /* BolusCalculatorTests.swift */; };
+		B015AFE52E500000000D7351 /* BolusSafetyValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B015AFE42E500000000D7351 /* BolusSafetyValidatorTests.swift */; };
 		BD8FC0602D6619DB00B95AED /* CarbsStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC05F2D6619DB00B95AED /* CarbsStorageTests.swift */; };
 		BD8FC0622D6619E600B95AED /* OverrideStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC0612D6619E600B95AED /* OverrideStorageTests.swift */; };
 		BD8FC0642D6619EF00B95AED /* TempTargetStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC0632D6619EF00B95AED /* TempTargetStorageTests.swift */; };
@@ -547,6 +552,7 @@
 		DD1745242C55526000211FAC /* SMBSettingsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1745232C55526000211FAC /* SMBSettingsStateModel.swift */; };
 		DD1745262C55526F00211FAC /* SMBSettingsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1745252C55526F00211FAC /* SMBSettingsRootView.swift */; };
 		DD1745292C55642100211FAC /* SettingInputSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1745282C55642100211FAC /* SettingInputSection.swift */; };
+		AC19EF2C94084B5BA0175D1D /* SettingsSearchHighlight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48B83503461B4F8D97B30115 /* SettingsSearchHighlight.swift */; };
 		DD17452B2C556E8100211FAC /* SettingInputHintView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD17452A2C556E8100211FAC /* SettingInputHintView.swift */; };
 		DD17452E2C55AE4800211FAC /* TargetBehavoirDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD17452D2C55AE4800211FAC /* TargetBehavoirDataFlow.swift */; };
 		DD1745302C55AE5300211FAC /* TargetBehaviorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD17452F2C55AE5300211FAC /* TargetBehaviorProvider.swift */; };
@@ -838,6 +844,7 @@
 				3B4BA78B2D8DC0EC0069D5B8 /* ShareClient.framework in Embed Frameworks */,
 				3B4BA7912D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework in Embed Frameworks */,
 				CE95BF602BA7715800DC3DE3 /* MockKit.framework in Embed Frameworks */,
+				B6E925142EB3932A0076D719 /* OmnipodKit.framework in Embed Frameworks */,
 				CE95BF5E2BA770C300DC3DE3 /* LoopKitUI.framework in Embed Frameworks */,
 			);
 			name = "Embed Frameworks";
@@ -1124,6 +1131,7 @@
 		3BDEA2DC60EDE0A3CA54DC73 /* TargetsEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorProvider.swift; sourceTree = "<group>"; };
 		3BF768BD6264FF7D71D66767 /* NightscoutConfigProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NightscoutConfigProvider.swift; sourceTree = "<group>"; };
 		3BF85FE12E427312000D7351 /* IOBService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOBService.swift; sourceTree = "<group>"; };
+		B015AFE12E500000000D7351 /* BolusSafetyValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusSafetyValidator.swift; sourceTree = "<group>"; };
 		3E54EF2B2E476DA40006F54D /* MedtrumKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MedtrumKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		3E62C7812F54CC1600433237 /* BolusDisplayThreshold.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusDisplayThreshold.swift; sourceTree = "<group>"; };
 		3F60E97100041040446F44E7 /* PumpConfigStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpConfigStateModel.swift; sourceTree = "<group>"; };
@@ -1213,6 +1221,7 @@
 		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>"; };
 		B5822B15939E719628E9FF7C /* SnoozeRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SnoozeRootView.swift; sourceTree = "<group>"; };
+		B6E925122EB3932A0076D719 /* OmnipodKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OmnipodKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		B9B5C0607505A38F256BF99A /* CGMSettingsDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CGMSettingsDataFlow.swift; sourceTree = "<group>"; };
 		BA49538D56989D8DA6FCF538 /* TargetsEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorDataFlow.swift; sourceTree = "<group>"; };
 		BD04ECCD2D299522008C5FEB /* BolusProgressOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusProgressOverlay.swift; sourceTree = "<group>"; };
@@ -1273,6 +1282,7 @@
 		BD8FC0582D66189700B95AED /* TestAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAssembly.swift; sourceTree = "<group>"; };
 		BD8FC05A2D6618AF00B95AED /* DeterminationStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeterminationStorageTests.swift; sourceTree = "<group>"; };
 		BD8FC05D2D6618CE00B95AED /* BolusCalculatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorTests.swift; sourceTree = "<group>"; };
+		B015AFE42E500000000D7351 /* BolusSafetyValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusSafetyValidatorTests.swift; sourceTree = "<group>"; };
 		BD8FC05F2D6619DB00B95AED /* CarbsStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbsStorageTests.swift; sourceTree = "<group>"; };
 		BD8FC0612D6619E600B95AED /* OverrideStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideStorageTests.swift; sourceTree = "<group>"; };
 		BD8FC0632D6619EF00B95AED /* TempTargetStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetStorageTests.swift; sourceTree = "<group>"; };
@@ -1416,6 +1426,7 @@
 		DD1745232C55526000211FAC /* SMBSettingsStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMBSettingsStateModel.swift; sourceTree = "<group>"; };
 		DD1745252C55526F00211FAC /* SMBSettingsRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMBSettingsRootView.swift; sourceTree = "<group>"; };
 		DD1745282C55642100211FAC /* SettingInputSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingInputSection.swift; sourceTree = "<group>"; };
+		48B83503461B4F8D97B30115 /* SettingsSearchHighlight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSearchHighlight.swift; sourceTree = "<group>"; };
 		DD17452A2C556E8100211FAC /* SettingInputHintView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingInputHintView.swift; sourceTree = "<group>"; };
 		DD17452D2C55AE4800211FAC /* TargetBehavoirDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TargetBehavoirDataFlow.swift; sourceTree = "<group>"; };
 		DD17452F2C55AE5300211FAC /* TargetBehaviorProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TargetBehaviorProvider.swift; sourceTree = "<group>"; };
@@ -1606,6 +1617,7 @@
 		E592A3742CEEC038009A472C /* ContactImageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactImageProvider.swift; sourceTree = "<group>"; };
 		E592A3752CEEC038009A472C /* ContactImageStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactImageStateModel.swift; sourceTree = "<group>"; };
 		E625985B47742D498CB1681A /* GlucoseNotificationSettingsProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GlucoseNotificationSettingsProvider.swift; sourceTree = "<group>"; };
+		B3919BBB515547118D684CA2 /* SettingsSearchTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsSearchTests.swift; sourceTree = "<group>"; };
 		F816825D28DB441200054060 /* HeartBeatManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeartBeatManager.swift; sourceTree = "<group>"; };
 		F816825F28DB441800054060 /* BluetoothTransmitter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BluetoothTransmitter.swift; sourceTree = "<group>"; };
 		F90692A9274B7AAE0037068D /* HealthKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitManager.swift; sourceTree = "<group>"; };
@@ -1658,6 +1670,7 @@
 				38E87401274F77E400975559 /* CoreNFC.framework in Frameworks */,
 				3B4BA78A2D8DC0EC0069D5B8 /* ShareClient.framework in Frameworks */,
 				3B4BA77E2D8DBD690069D5B8 /* OmniKit.framework in Frameworks */,
+				B6E925132EB3932A0076D719 /* OmnipodKit.framework in Frameworks */,
 				3811DE1025C9D37700A708ED /* Swinject in Frameworks */,
 				3B4BA78E2D8DC0EC0069D5B8 /* TidepoolServiceKit.framework in Frameworks */,
 				B958F1B72BA0711600484851 /* MKRingProgressView in Frameworks */,
@@ -2123,6 +2136,7 @@
 				DDA9AC072D67291600E6F1A9 /* AppVersionChecker */,
 				CEB434E128B8F9BC00B70274 /* Bluetooth */,
 				BD7DB88C2D2C49FF003D3155 /* BolusCalculator */,
+				B015AFE22E500000000D7351 /* BolusSafety */,
 				3862CC2C2743F9DC00BF832C /* Calendar */,
 				E592A37E2CEEC046009A472C /* ContactImage */,
 				F90692A8274B7A980037068D /* HealthKit */,
@@ -2294,6 +2308,7 @@
 		3818AA48274C267000843DB3 /* Frameworks */ = {
 			isa = PBXGroup;
 			children = (
+				B6E925122EB3932A0076D719 /* OmnipodKit.framework */,
 				3E54EF2B2E476DA40006F54D /* MedtrumKit.framework */,
 				3B4BA7882D8DC0EC0069D5B8 /* TidepoolServiceKit.framework */,
 				3B4BA7892D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework */,
@@ -2410,6 +2425,7 @@
 				38DF1785276A73D400B3528F /* TagCloudView.swift */,
 				DD88C8E12C50420800F2D558 /* DefinitionRow.swift */,
 				DD1745282C55642100211FAC /* SettingInputSection.swift */,
+				48B83503461B4F8D97B30115 /* SettingsSearchHighlight.swift */,
 				DD17452A2C556E8100211FAC /* SettingInputHintView.swift */,
 			);
 			path = Views;
@@ -2738,6 +2754,7 @@
 				DDC6CA6C2DD90A2A0060EE25 /* LocalizationTests.swift */,
 				3B997DD22DC02AEF006B6BB2 /* JSONImporterData */,
 				BD8FC05C2D6618BE00B95AED /* BolusCalculatorTests */,
+				B015AFE62E500000000D7351 /* BolusSafetyTests */,
 				BD8FC0552D66187700B95AED /* CoreDataTests */,
 				38FCF3F125E9028E0078B0D1 /* Info.plist */,
 				CEE9A65D2BBC9F6500EB5194 /* CalibrationsTests.swift */,
@@ -2745,6 +2762,7 @@
 				38FCF3F825E902C20078B0D1 /* FileStorageTests.swift */,
 				3B997DCE2DC00A3A006B6BB2 /* JSONImporterTests.swift */,
 				CE1F6DD82BADF4620064EB8D /* PluginManagerTests.swift */,
+				B3919BBB515547118D684CA2 /* SettingsSearchTests.swift */,
 				BD8FC0532D66186000B95AED /* TestError.swift */,
 				BD8FC0702D661B0000B95AED /* TidepoolTherapySettingsTests.swift */,
 			);
@@ -2773,6 +2791,14 @@
 			path = IOB;
 			sourceTree = "<group>";
 		};
+		B015AFE22E500000000D7351 /* BolusSafety */ = {
+			isa = PBXGroup;
+			children = (
+				B015AFE12E500000000D7351 /* BolusSafetyValidator.swift */,
+			);
+			path = BolusSafety;
+			sourceTree = "<group>";
+		};
 		4E8C7B59F8065047ECE20965 /* View */ = {
 			isa = PBXGroup;
 			children = (
@@ -3127,6 +3153,14 @@
 			path = BolusCalculatorTests;
 			sourceTree = "<group>";
 		};
+		B015AFE62E500000000D7351 /* BolusSafetyTests */ = {
+			isa = PBXGroup;
+			children = (
+				B015AFE42E500000000D7351 /* BolusSafetyValidatorTests.swift */,
+			);
+			path = BolusSafetyTests;
+			sourceTree = "<group>";
+		};
 		BDA25F1A2D26BCE800035F34 /* Views */ = {
 			isa = PBXGroup;
 			children = (
@@ -4435,6 +4469,7 @@
 				BDF34F832C10C5B600D51995 /* DataManager.swift in Sources */,
 				38B4F3C625E5017E00E76A18 /* NotificationCenter.swift in Sources */,
 				19D466A729AA2C22004D5F33 /* MealSettingsStateModel.swift in Sources */,
+				AC19EF2C94084B5BA0175D1D /* SettingsSearchHighlight.swift in Sources */,
 				DD17452B2C556E8100211FAC /* SettingInputHintView.swift in Sources */,
 				38E44528274E401C00EC9A94 /* Protected.swift in Sources */,
 				DD3F1F8B2D9E08B600DCE7B3 /* NightscoutLoginStepView.swift in Sources */,
@@ -4561,6 +4596,7 @@
 				3811DEEA25CA063400A708ED /* SyncAccess.swift in Sources */,
 				190EBCC829FF13AA00BA767D /* UserInterfaceSettingsStateModel.swift in Sources */,
 				3BF85FE32E427312000D7351 /* IOBService.swift in Sources */,
+				B015AFE32E500000000D7351 /* BolusSafetyValidator.swift in Sources */,
 				DDF847EA2C5DABAC0049BB3B /* WatchConfigGarminView.swift in Sources */,
 				38BF021F25E7F0DE00579895 /* DeviceDataManager.swift in Sources */,
 				BD4E1A7A2D3681B700D21626 /* GlucoseTargetSetup.swift in Sources */,
@@ -4939,6 +4975,8 @@
 				38FCF3F925E902C20078B0D1 /* FileStorageTests.swift in Sources */,
 				BD8FC0602D6619DB00B95AED /* CarbsStorageTests.swift in Sources */,
 				BD8FC05E2D6618CE00B95AED /* BolusCalculatorTests.swift in Sources */,
+				41740E936552456AAC0EDAC3 /* SettingsSearchTests.swift in Sources */,
+				B015AFE52E500000000D7351 /* BolusSafetyValidatorTests.swift in Sources */,
 				BD8FC0712D661B0000B95AED /* TidepoolTherapySettingsTests.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;

+ 3 - 0
Trio.xcworkspace/contents.xcworkspacedata

@@ -8,6 +8,9 @@
       location = "group:MinimedKit/MinimedKit.xcodeproj">
    </FileRef>
    <FileRef
+      location = "group:OmnipodKit/OmnipodKit.xcodeproj">
+   </FileRef>
+   <FileRef
       location = "group:LibreTransmitter/LibreTransmitter.xcodeproj">
    </FileRef>
    <FileRef

+ 1 - 1
Trio.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -1,5 +1,5 @@
 {
-  "originHash" : "598841ae6fe892058ca678f5672f34299df2d62843330367c207648003263ccd",
+  "originHash" : "1e72c1cdf8ea5ec9fe527ebfab01ea55fca9e8651fe3252338fd3d4ea2cb327a",
   "pins" : [
     {
       "identity" : "abseil-cpp-binary",

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

@@ -604,11 +604,18 @@ final class BaseAPSManager: APSManager, Injectable {
         do {
             try await pump.enactBolus(units: roundedAmount, automatic: isSMB)
             debug(.apsManager, "Bolus succeeded")
-            if !isSMB {
-                try await determineBasalSync()
-            }
             bolusProgress.send(0)
             callback?(true, String(localized: "Bolus enacted successfully.", comment: "Success message for enacting a bolus"))
+            if !isSMB {
+                do {
+                    try await determineBasalSync()
+                } catch {
+                    warning(
+                        .apsManager,
+                        "determineBasalSync after manual bolus failed: \(error.localizedDescription)"
+                    )
+                }
+            }
         } catch {
             warning(.apsManager, "Bolus failed with error: \(error)")
             processError(APSError.pumpError(error))

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

@@ -10,6 +10,7 @@ import MinimedKit
 import MockKit
 import OmniBLE
 import OmniKit
+import OmnipodKit
 import ShareClient
 import SwiftDate
 import Swinject
@@ -39,6 +40,7 @@ private let staticPumpManagers: [PumpManagerUI.Type] = [
     MinimedPumpManager.self,
     OmnipodPumpManager.self,
     OmniBLEPumpManager.self,
+    OmniPumpManager.self,
     DanaKitPumpManager.self,
     MedtrumPumpManager.self,
     MockPumpManager.self
@@ -48,6 +50,7 @@ private let staticPumpManagersByIdentifier: [String: PumpManagerUI.Type] = [
     MinimedPumpManager.pluginIdentifier: MinimedPumpManager.self,
     OmnipodPumpManager.pluginIdentifier: OmnipodPumpManager.self,
     OmniBLEPumpManager.pluginIdentifier: OmniBLEPumpManager.self,
+    OmniPumpManager.pluginIdentifier: OmniPumpManager.self,
     DanaKitPumpManager.pluginIdentifier: DanaKitPumpManager.self,
     MedtrumPumpManager.pluginIdentifier: MedtrumPumpManager.self,
     MockPumpManager.pluginIdentifier: MockPumpManager.self
@@ -142,6 +145,13 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
                         pumpActivatedAtDate.send(medtrumPump.state.patchActivatedAt)
                     }
                 }
+                if let omni = pumpManager as? OmniPumpManager {
+                    guard let endTime = omni.state.podState?.expiresAt else {
+                        pumpExpiresAtDate.send(nil)
+                        return
+                    }
+                    pumpExpiresAtDate.send(endTime)
+                }
                 if let simulatorPump = pumpManager as? MockPumpManager {
                     pumpDisplayState.value = PumpDisplayState(name: simulatorPump.localizedTitle, image: simulatorPump.smallImage)
                     pumpName.send(simulatorPump.localizedTitle)
@@ -292,6 +302,10 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
         self.recommendsLoop.send()
     }
 
+    public func pumpManagerTypeByIdentifier(_ identifier: String) -> PumpManagerUI.Type? {
+        staticPumpManagersByIdentifier[identifier]
+    }
+
     private func pumpManagerFromRawValue(_ rawValue: [String: Any]) -> PumpManagerUI? {
         guard let rawState = rawValue["state"] as? PumpManager.RawStateValue,
               let Manager = pumpManagerTypeFromRawValue(rawValue)
@@ -307,7 +321,18 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
             return nil
         }
 
-        return staticPumpManagersByIdentifier[managerIdentifier]
+        if let pumpManager = pumpManagerTypeByIdentifier(managerIdentifier) {
+            return pumpManager
+        }
+
+        /// The pumpManager was not found for managerIdentifier. If this was for an "Omnipod" (OmniKit) or
+        /// "Omnipod-DASH" (OmniBLE), have the universal "Omni" pumpManager (OmnipodKit) handle instead.
+        let OmniStr = "Omni"
+        if managerIdentifier.hasPrefix(OmniStr) {
+            return pumpManagerTypeByIdentifier(OmniStr)
+        }
+
+        return nil
     }
 
     // MARK: - GlucoseSource
@@ -554,6 +579,39 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
             }
         }
 
+        if let omni = pumpManager as? OmniPumpManager {
+            let reservoirVal = omni.state.podState?.lastInsulinMeasurements?.reservoirLevel ?? 0xDEAD_BEEF
+            // TODO: find the value Pod.maximumReservoirReading
+            let reservoir = Decimal(reservoirVal) > 50.0 ? 0xDEAD_BEEF : reservoirVal
+
+            storage.save(Decimal(reservoir), as: OpenAPS.Monitor.reservoir)
+            broadcaster.notify(PumpReservoirObserver.self, on: processQueue) {
+                $0.pumpReservoirDidChange(Decimal(reservoir))
+            }
+
+            // manual temp basal on
+            if let tempBasal = omni.state.podState?.unfinalizedTempBasal, !tempBasal.isFinished(),
+               !tempBasal.automatic
+            {
+                // the manual basal temp is launch - block every thing
+                debug(.deviceManager, "manual temp basal")
+                manualTempBasal.send(true)
+            } else {
+                // no more manual Temp Basal !
+                manualTempBasal.send(false)
+            }
+
+            guard let endTime = omni.state.podState?.expiresAt else {
+                pumpExpiresAtDate.send(nil)
+                return
+            }
+            pumpExpiresAtDate.send(endTime)
+
+            if let startTime = omni.state.podState?.activatedAt {
+                storage.save(startTime, as: OpenAPS.Monitor.podAge)
+            }
+        }
+
         if let simulatorPump = pumpManager as? MockPumpManager {
             broadcaster.notify(PumpReservoirObserver.self, on: processQueue) {
                 $0.pumpReservoirDidChange(Decimal(simulatorPump.state.reservoirUnitsRemaining))

+ 22 - 4
Trio/Sources/Application/AppDelegate.swift

@@ -20,21 +20,39 @@ class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject, UNUserNoti
         Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(crashReportingEnabled)
         Crashlytics.crashlytics().setCustomValue(Bundle.main.appDevVersion ?? "unknown", forKey: "app_dev_version")
 
-        // Telemetry: record this cold launch into the sliding 7-day window. If
-        // consent is set and the build SHA changed since the last successful
-        // send, fire an immediate ping — the 24h scheduler can't notice a
-        // build update on its own. Then arm the recurring 24h timer.
+        // Telemetry: record this cold launch into the sliding 7-day window,
+        // then drive cadence via three layered triggers — listed below in
+        // priority of reliability:
+        //
+        //   1. SHA-change ping: build updated since last send. Awaited so
+        //      the lastSentAt stamp is fresh before the overdue check.
+        //   2. checkAndSendIfOverdue: covers the regular cold launch on the
+        //      same build when >24h has passed since the last successful
+        //      send. Together with the foreground-transition hook below
+        //      (`applicationWillEnterForeground`), this keeps daily pings
+        //      flowing on iOS.
+        //   3. scheduleRecurring: best-effort fallback for the rare case
+        //      where the app stays foregrounded for a full 24h.
         TelemetryClient.shared.recordColdLaunch()
         Task.detached {
             if TelemetryClient.shared.buildShaChangedSinceLastSend() {
                 await TelemetryClient.shared.maybeSend()
             }
             TelemetryClient.shared.scheduleRecurring()
+            TelemetryClient.shared.checkAndSendIfOverdue()
         }
 
         return true
     }
 
+    /// Foreground-transition entry point for telemetry cadence. Re-evaluates
+    /// the overdue window every time the user brings Trio to the foreground,
+    /// since `scheduleRecurring`'s GCD timer doesn't fire while suspended.
+    /// No-op if a send already landed within the last 24h.
+    func applicationWillEnterForeground(_: UIApplication) {
+        TelemetryClient.shared.checkAndSendIfOverdue()
+    }
+
     func application(
         _: UIApplication,
         didReceiveRemoteNotification userInfo: [AnyHashable: Any],

+ 1 - 0
Trio/Sources/Assemblies/ServiceAssembly.swift

@@ -29,5 +29,6 @@ final class ServiceAssembly: Assembly {
             }
         }
         container.register(IOBService.self) { r in BaseIOBService(resolver: r) }
+        container.register(BolusSafetyValidator.self) { r in BaseBolusSafetyValidator(resolver: r) }
     }
 }

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 163 - 0
Trio/Sources/Localizations/Main/Localizable.xcstrings


+ 3 - 3
Trio/Sources/Models/TrioSettings.swift

@@ -3,14 +3,14 @@ import Foundation
 enum BolusShortcutLimit: String, JSON, CaseIterable, Identifiable {
     var id: String { rawValue }
     case notAllowed
-    case limitBolusMax
+    case limitWithSafetyChecks
 
     var displayName: String {
         switch self {
         case .notAllowed:
             return String(localized: "Not allowed")
-        case .limitBolusMax:
-            return String(localized: "Max bolus")
+        case .limitWithSafetyChecks:
+            return String(localized: "Limit with Safety Checks")
         }
     }
 }

+ 1 - 0
Trio/Sources/Modules/AlgorithmAdvancedSettings/View/AlgorithmAdvancedSettingsRootView.swift

@@ -406,6 +406,7 @@ extension AlgorithmAdvancedSettings {
             .onAppear(perform: configureView)
             .navigationTitle("Additionals")
             .navigationBarTitleDisplayMode(.automatic)
+            .settingsHighlightScroll()
             .onDisappear {
                 state.saveIfChanged()
             }

+ 1 - 0
Trio/Sources/Modules/AutosensSettings/View/AutosensSettingsRootView.swift

@@ -230,6 +230,7 @@ extension AutosensSettings {
             .onAppear(perform: configureView)
             .navigationTitle("Autosens")
             .navigationBarTitleDisplayMode(.automatic)
+            .settingsHighlightScroll()
         }
     }
 }

+ 1 - 0
Trio/Sources/Modules/BolusCalculatorConfig/View/BolusCalculatorConfigRootView.swift

@@ -200,6 +200,7 @@ extension BolusCalculatorConfig {
             .onAppear(perform: configureView)
             .navigationBarTitle("Bolus Calculator")
             .navigationBarTitleDisplayMode(.automatic)
+            .settingsHighlightScroll()
         }
     }
 }

+ 1 - 0
Trio/Sources/Modules/CGMSettings/View/CGMRootView.swift

@@ -150,6 +150,7 @@ extension CGMSettings {
                 .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
                 .onAppear(perform: configureView)
                 .navigationTitle("CGM")
+                .settingsHighlightScroll()
                 .navigationBarTitleDisplayMode(.automatic)
                 .navigationBarItems(leading: displayClose ? Button("Close", action: state.hideModal) : nil)
                 .sheet(isPresented: $state.shouldDisplayCGMSetupSheet) {

+ 1 - 0
Trio/Sources/Modules/CalendarEventSettings/View/CalendarEventSettingsRootView.swift

@@ -147,6 +147,7 @@ extension CalendarEventSettings {
             .onAppear(perform: configureView)
             .navigationTitle("Calendar Events")
             .navigationBarTitleDisplayMode(.automatic)
+            .settingsHighlightScroll()
         }
     }
 }

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

@@ -165,7 +165,7 @@ extension DynamicSettings {
                             }.padding(.top)
                         }.padding(.bottom)
                     }
-                ).listRowBackground(Color.chart)
+                ).settingsSearchTarget(label: String(localized: "Dynamic ISF"))
 
                 if state.dynamicSensitivityType != .disabled {
                     if state.dynamicSensitivityType == .logarithmic {
@@ -283,6 +283,7 @@ extension DynamicSettings {
             .onAppear(perform: configureView)
             .navigationBarTitle("Dynamic Settings")
             .navigationBarTitleDisplayMode(.automatic)
+            .settingsHighlightScroll()
         }
     }
 }

+ 1 - 0
Trio/Sources/Modules/GeneralSettings/View/UnitsLimitsSettingsRootView.swift

@@ -227,6 +227,7 @@ extension UnitsLimitsSettings {
             .onDisappear {
                 state.saveIfChanged()
             }
+            .settingsHighlightScroll()
         }
     }
 }

+ 1 - 0
Trio/Sources/Modules/GlucoseNotificationSettings/View/GlucoseNotificationSettingsRootView.swift

@@ -264,6 +264,7 @@ extension GlucoseNotificationSettings {
             .onAppear(perform: configureView)
             .navigationBarTitle("Trio Notifications")
             .navigationBarTitleDisplayMode(.automatic)
+            .settingsHighlightScroll()
         }
 
         var lowAndHighGlucoseAlertSection: some View {

+ 3 - 0
Trio/Sources/Modules/Home/View/HomeRootView.swift

@@ -22,6 +22,7 @@ extension Home {
         @State var state = StateModel()
 
         @State var settingsPath = NavigationPath()
+        @State var settingsSearchHighlight = SettingsSearchHighlight()
         @State var isStatusPopupPresented = false
         @State var showCancelAlert = false
         @State var showCancelConfirmDialog = false
@@ -993,6 +994,7 @@ extension Home {
             // PUMP RELATED
             .confirmationDialog("Pump Model", isPresented: $showPumpSelection) {
                 Button("Medtronic") { state.addPump(.minimed) }
+                Button("All Omnipod Types") { state.addPump(.omni) }
                 Button("Omnipod Eros") { state.addPump(.omnipod) }
                 Button("Omnipod DASH") { state.addPump(.omnipodBLE) }
                 Button("Dana(RS/-i)") { state.addPump(.dana) }
@@ -1097,6 +1099,7 @@ extension Home {
 
                     NavigationStack(path: self.$settingsPath) {
                         Settings.RootView(resolver: resolver) }
+                        .environment(settingsSearchHighlight)
                         .tabItem { Label(
                             "Settings",
                             systemImage: "gear"

+ 1 - 0
Trio/Sources/Modules/LiveActivitySettings/View/LiveActivitySettingsRootView.swift

@@ -233,6 +233,7 @@ extension LiveActivitySettings {
             .onAppear(perform: configureView)
             .navigationTitle("Live Activity")
             .navigationBarTitleDisplayMode(.automatic)
+            .settingsHighlightScroll()
         }
     }
 }

+ 15 - 6
Trio/Sources/Modules/LiveActivitySettings/View/LiveActivityWidgetConfiguration.swift

@@ -383,17 +383,26 @@ enum LiveActivityItem: String, CaseIterable, Identifiable {
     var displayName: String {
         switch self {
         case .currentGlucoseLarge:
-            return "Glucose and Trend, no Delta"
+            return String(
+                localized: "Glucose and Trend, no Delta",
+                comment: "Live Activity widget icon label for Glucose and Trend, no Delta"
+            )
         case .currentGlucose:
-            return "Glucose, Trend, Delta"
+            return String(
+                localized: "Glucose, Trend, Delta",
+                comment: "Live Activity widget icon label for Glucose, Trend, Delta"
+            )
         case .iob:
-            return "Insulin on Board (IOB)"
+            return String(
+                localized: "Insulin on Board (IOB)",
+                comment: "Live Activity widget icon label for Insulin on Board (IOB)"
+            )
         case .cob:
-            return "Carbs on Board (IOB)"
+            return String(localized: "Carbs on Board (COB)", comment: "Live Activity widget icon label for Carbs on Board (COB)")
         case .updatedLabel:
-            return "Last Updated"
+            return String(localized: "Last Updated", comment: "Live Activity widget icon label for Last Updated")
         case .totalDailyDose:
-            return "Total Daily Dose"
+            return String(localized: "Total Daily Dose", comment: "Live Activity widget icon label for Total Daily Dose")
         }
     }
 }

+ 1 - 0
Trio/Sources/Modules/MealSettings/View/MealSettingsRootView.swift

@@ -371,6 +371,7 @@ extension MealSettings {
             .onAppear(perform: configureView)
             .navigationBarTitle("Meal Settings")
             .navigationBarTitleDisplayMode(.automatic)
+            .settingsHighlightScroll()
         }
     }
 }

+ 1 - 0
Trio/Sources/Modules/NightscoutConfig/View/NightscoutFetchView.swift

@@ -52,6 +52,7 @@ struct NightscoutFetchView: View {
         }
         .navigationTitle("Fetch")
         .navigationBarTitleDisplayMode(.automatic)
+        .settingsHighlightScroll()
         .scrollContentBackground(.hidden)
         .background(appState.trioBackgroundColor(for: colorScheme))
     }

+ 1 - 0
Trio/Sources/Modules/NightscoutConfig/View/NightscoutUploadView.swift

@@ -81,6 +81,7 @@ struct NightscoutUploadView: View {
         }
         .navigationTitle("Upload")
         .navigationBarTitleDisplayMode(.automatic)
+        .settingsHighlightScroll()
         .scrollContentBackground(.hidden)
         .background(appState.trioBackgroundColor(for: colorScheme))
     }

+ 10 - 2
Trio/Sources/Modules/Onboarding/View/TherapySettingEditorView.swift

@@ -8,6 +8,13 @@ struct TherapySettingEditorView: View {
     var validateOnDelete: (() -> Void)?
     var onItemAdded: (() -> Void)?
 
+    private let basalFormatter: NumberFormatter = {
+        let numberFormatter = NumberFormatter()
+        numberFormatter.maximumFractionDigits = 3
+        numberFormatter.minimumFractionDigits = 2
+        return numberFormatter
+    }()
+
     @State private var selectedItemID: UUID?
     @Namespace var bottomID
 
@@ -280,10 +287,11 @@ struct TherapySettingEditorView: View {
         case .mmolL,
              .mmolLPerUnit:
             return decimalValue.formattedAsMmolL
+        case .unitPerHour:
+            return basalFormatter.string(from: decimalValue as NSNumber) ?? ""
         case .gramPerUnit,
              .mgdL,
-             .mgdLPerUnit,
-             .unitPerHour:
+             .mgdLPerUnit:
             return decimalValue.description
         }
     }

+ 1 - 0
Trio/Sources/Modules/PumpConfig/PumpConfigDataFlow.swift

@@ -9,6 +9,7 @@ enum PumpConfig {
         case minimed
         case omnipod
         case omnipodBLE
+        case omni
         case dana
         case medtrum
         case simulator

+ 2 - 0
Trio/Sources/Modules/PumpConfig/View/PumpConfigRootView.swift

@@ -118,6 +118,7 @@ extension PumpConfig {
                                 )
                                 VStack(alignment: .leading) {
                                     Text("• Medtronic")
+                                    Text("• All Omnipod Types")
                                     Text("• Omnipod Eros")
                                     Text("• Omnipod DASH")
                                     Text("• Dana (RS/-i)")
@@ -134,6 +135,7 @@ extension PumpConfig {
                 }
                 .confirmationDialog("Pump Model", isPresented: $showPumpSelection) {
                     Button("Medtronic") { state.addPump(.minimed) }
+                    Button("All Omnipod Types") { state.addPump(.omni) }
                     Button("Omnipod Eros") { state.addPump(.omnipod) }
                     Button("Omnipod DASH") { state.addPump(.omnipodBLE) }
                     Button("Dana(RS/-i)") { state.addPump(.dana) }

+ 10 - 0
Trio/Sources/Modules/PumpConfig/View/PumpSetupView.swift

@@ -9,6 +9,7 @@ import MockKitUI
 import OmniBLE
 import OmniKit
 import OmniKitUI
+import OmnipodKit
 import SwiftUI
 import UIKit
 
@@ -60,6 +61,15 @@ extension PumpConfig {
                     allowDebugFeatures: true,
                     allowedInsulinTypes: [.apidra, .humalog, .novolog, .fiasp, .lyumjev]
                 )
+            case .omni:
+                setupViewController = OmniPumpManager.setupViewController(
+                    initialSettings: initialSettings,
+                    bluetoothProvider: bluetoothManager,
+                    colorPalette: .default,
+                    allowDebugFeatures: true,
+                    prefersToSkipUserInteraction: false,
+                    allowedInsulinTypes: [.apidra, .humalog, .novolog, .fiasp, .lyumjev]
+                )
             case .dana:
                 setupViewController = DanaKitPumpManager.setupViewController(
                     initialSettings: initialSettings,

+ 1 - 0
Trio/Sources/Modules/RemoteControlConfig/View/RemoteControlConfig.swift

@@ -101,6 +101,7 @@ extension RemoteControlConfig {
             .onAppear(perform: configureView)
             .navigationTitle("Remote Control")
             .navigationBarTitleDisplayMode(.automatic)
+            .settingsHighlightScroll()
         }
     }
 }

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

@@ -370,6 +370,7 @@ extension SMBSettings {
             .onAppear(perform: configureView)
             .navigationTitle("SMB Settings")
             .navigationBarTitleDisplayMode(.automatic)
+            .settingsHighlightScroll()
         }
     }
 }

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

@@ -8,16 +8,22 @@ struct SettingItem: Identifiable {
     let view: Screen
     let searchContents: [String]?
     let path: [String]?
+    /// Maps a `searchContents` string to the exact label used in `SettingInputSection`
+    /// when the two differ (e.g. `"Max IOB"` → `"Maximum Insulin on Board (IOB)"`).
+    /// Entries whose searchContents string already matches the label don't need an entry here.
+    let scrollTargetLabels: [String: String]?
 
     init(
         title: String,
         view: Screen,
         searchContents: [String]? = nil,
+        scrollTargetLabels: [String: String]? = nil,
         path: [String]? = nil
     ) {
         self.title = title
         self.view = view
         self.searchContents = searchContents
+        self.scrollTargetLabels = scrollTargetLabels
         self.path = path
     }
 }
@@ -26,6 +32,11 @@ struct FilteredSettingItem: Identifiable {
     let id = UUID()
     let settingItem: SettingItem
     let matchedContent: String
+    /// The label string used as the scroll/highlight target in the destination view.
+    /// Falls back to `matchedContent` when no explicit mapping exists.
+    var scrollLabel: String {
+        settingItem.scrollTargetLabels?[matchedContent] ?? matchedContent
+    }
 }
 
 enum SettingItems {
@@ -89,6 +100,12 @@ enum SettingItems {
                 "Minimum Safety Threshold",
                 "Delivery Limits"
             ],
+            scrollTargetLabels: [
+                "Max IOB": "Maximum Insulin on Board (IOB)",
+                "Max Bolus": "Maximum Bolus",
+                "Max Basal": "Maximum Basal Rate",
+                "Max COB": "Maximum Carbs on Board (COB)"
+            ],
             path: ["Therapy Settings", "Units and Limits"]
         ),
         SettingItem(title: "Basal Rates", view: .basalProfileEditor, path: ["Therapy Settings"]),
@@ -122,6 +139,12 @@ enum SettingItems {
                 "Max UAM SMB Basal Minutes",
                 "Max Allowed Glucose Rise for SMB"
             ],
+            scrollTargetLabels: [
+                "Enable SMB With Temporary Target": "Enable SMB With Temptarget",
+                "Allow SMB With High Temporary Target": "Allow SMB With High Temptarget",
+                "Max UAM SMB Basal Minutes": "Max UAM Basal Minutes",
+                "High Glucose Target": "Enable SMB With High Glucose"
+            ],
             path: ["Algorithm", "Super Micro Bolus (SMB)"]
         ),
         SettingItem(
@@ -148,6 +171,10 @@ enum SettingItems {
                 "Resistance Lowers Target",
                 "Half Basal Exercise Target"
             ],
+            scrollTargetLabels: [
+                "High Temptarget Raises Sensitivity": "High Temp Target Raises Sensitivity",
+                "Low Temptarget Lowers Sensitivity": "Low Temp Target Lowers Sensitivity"
+            ],
             path: ["Algorithm", "Target Behavior"]
         ),
         SettingItem(
@@ -168,6 +195,11 @@ enum SettingItems {
                 "Remaining Carbs Cap",
                 "Noisy CGM Target Multiplier"
             ],
+            scrollTargetLabels: [
+                "Min 5m Carbimpact": "Min 5m Carb Impact",
+                "Remaining Carbs Fraction": "Remaining Carbs Percentage",
+                "Noisy CGM Target Multiplier": "Noisy CGM Target Increase"
+            ],
             path: ["Algorithm", "Additionals"]
         )
     ]
@@ -185,6 +217,12 @@ enum SettingItems {
                 "Super Bolus Factor",
                 "Very Low Glucose Warning"
             ],
+            scrollTargetLabels: [
+                "Enable Reduced Bolus Factor": "Enable Reduced Bolus Option",
+                "Reduced Bolus Factor": "Enable Reduced Bolus Option",
+                "Enable Super Bolus": "Enable Super Bolus Option",
+                "Super Bolus Factor": "Enable Super Bolus Option"
+            ],
             path: ["Features", "Bolus Calculator"]
         ),
         SettingItem(
@@ -201,6 +239,14 @@ enum SettingItems {
                 "Fat and Protein Percentage",
                 "FPU"
             ],
+            scrollTargetLabels: [
+                "Max Fat": "Enable Fat and Protein Entries",
+                "Max Protein": "Enable Fat and Protein Entries",
+                "Fat and Protein Delay": "Enable Fat and Protein Entries",
+                "Spread Interval": "Enable Fat and Protein Entries",
+                "Fat and Protein Percentage": "Enable Fat and Protein Entries",
+                "FPU": "Enable Fat and Protein Entries"
+            ],
             path: ["Features", "Meal Settings"]
         ),
         SettingItem(
@@ -224,7 +270,6 @@ enum SettingItems {
                 "Show Low and High Thresholds",
                 "Low Threshold",
                 "High Threshold",
-                "X-Axis Interval Step",
                 "eA1c/GMI Display Unit",
                 "Show Carbs Required Badge",
                 "Carbs Required Threshold",
@@ -232,17 +277,32 @@ enum SettingItems {
                 "Bolus Display Threshold",
                 "Cone",
                 "Lines",
+                "Appearance",
                 "Dark Mode",
                 "Light Mode",
-                "Appearance",
                 "Dark Scheme",
                 "Light Scheme",
                 "Glucose Color Scheme",
                 "Time in Range Type",
                 "Time in Tight Range (TITR)",
                 "Time in Normoglycemia (TING)",
+                "X-Axis Interval Step",
                 "Require Adjustments Confirmation"
             ],
+            scrollTargetLabels: [
+                "Show Y-Axis Grid Lines": "Show X-Axis Grid Lines",
+                "High Threshold": "Low Threshold",
+                "Cone": "Forecast Display Type",
+                "Lines": "Forecast Display Type",
+                "Dark Mode": "Appearance",
+                "Light Mode": "Appearance",
+                "Time in Tight Range (TITR)": "Time in Range Type",
+                "Time in Normoglycemia (TING)": "Time in Range Type",
+                "Dark Scheme": "Appearance",
+                "Light Scheme": "Appearance",
+                "X-Axis Interval Step": "Show X-Axis Grid Lines",
+                "Carbs Required Threshold": "Show Carbs Required Badge"
+            ],
             path: ["Features", "User Interface"]
         ),
         SettingItem(
@@ -275,6 +335,10 @@ enum SettingItems {
                 "Low Glucose Alarm Limit",
                 "High Glucose Alarm Limit"
             ],
+            scrollTargetLabels: [
+                "Low Glucose Alarm Limit": "Glucose Notifications",
+                "High Glucose Alarm Limit": "Glucose Notifications"
+            ],
             path: ["Notifications", "Trio Notifications"] // Glucose
         ),
         SettingItem(

+ 19 - 8
Trio/Sources/Modules/Settings/View/SettingsRootView.swift

@@ -38,6 +38,7 @@ extension Settings {
         @Environment(\.colorScheme) var colorScheme
         @EnvironmentObject var appIcons: Icons
         @Environment(AppState.self) var appState
+        @Environment(SettingsSearchHighlight.self) var searchHighlight
 
         private var filteredItems: [FilteredSettingItem] {
             SettingItems.filteredItems(searchText: searchText)
@@ -284,15 +285,19 @@ extension Settings {
                         content: {
                             if filteredItems.isNotEmpty {
                                 ForEach(filteredItems) { filteredItem in
-                                    VStack(alignment: .leading) {
-                                        Text(filteredItem.matchedContent.localized).bold()
-                                        if let path = filteredItem.settingItem.path {
-                                            Text(path.map(\.localized).joined(separator: " > "))
-                                                .font(.caption)
-                                                .foregroundColor(.secondary)
+                                    NavigationLink(value: SearchResultTarget(
+                                        screen: filteredItem.settingItem.view,
+                                        scrollLabel: filteredItem.scrollLabel.localized
+                                    )) {
+                                        VStack(alignment: .leading) {
+                                            Text(filteredItem.matchedContent.localized).bold()
+                                            if let path = filteredItem.settingItem.path {
+                                                Text(path.map(\.localized).joined(separator: " > "))
+                                                    .font(.caption)
+                                                    .foregroundColor(.secondary)
+                                            }
                                         }
-
-                                    }.navigationLink(to: filteredItem.settingItem.view, from: self)
+                                    }
                                 }
                             } else {
                                 Text("No settings matching your search query")
@@ -339,6 +344,12 @@ extension Settings {
                 }
             }
             .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
+            .navigationDestination(for: SearchResultTarget.self) { target in
+                state.view(for: target.screen)
+                    .onAppear {
+                        searchHighlight.highlightedSetting = target.scrollLabel
+                    }
+            }
             .screenNavigation(self)
             .onAppear {
                 Task { @MainActor in

+ 2 - 2
Trio/Sources/Modules/ShortcutsConfig/ShortcutsConfigStateModel.swift

@@ -16,7 +16,7 @@ extension ShortcutsConfig {
             units = settingsManager.settings.units
 
             subscribeSetting(\.bolusShortcut, on: $maxBolusByShortcuts) {
-                maxBolusByShortcuts = ($0 == .notAllowed) ? .limitBolusMax : $0
+                maxBolusByShortcuts = ($0 == .notAllowed) ? .limitWithSafetyChecks : $0
                 allowBolusByShortcuts = ($0 != .notAllowed)
             }
 
@@ -29,7 +29,7 @@ extension ShortcutsConfig {
                         if let bs = self?.maxBolusByShortcuts {
                             self?.settingsManager.settings.bolusShortcut = bs
                         } else {
-                            self?.settingsManager.settings.bolusShortcut = .limitBolusMax
+                            self?.settingsManager.settings.bolusShortcut = .limitWithSafetyChecks
                         }
                     }
                 }

+ 1 - 0
Trio/Sources/Modules/ShortcutsConfig/View/ShortcutsConfigView.swift

@@ -80,6 +80,7 @@ extension ShortcutsConfig {
             .onAppear(perform: configureView)
             .navigationTitle("Shortcuts")
             .navigationBarTitleDisplayMode(.automatic)
+            .settingsHighlightScroll()
         }
     }
 }

+ 1 - 0
Trio/Sources/Modules/TargetBehavoir/View/TargetBehavoirRootView.swift

@@ -200,6 +200,7 @@ extension TargetBehavoir {
             }
             .navigationTitle("Target Behavior")
             .navigationBarTitleDisplayMode(.automatic)
+            .settingsHighlightScroll()
         }
 
         private var effectiveLowTTLowersSensBinding: Binding<Bool> {

+ 9 - 8
Trio/Sources/Modules/UserInterfaceSettings/View/UserInterfaceSettingsRootView.swift

@@ -96,7 +96,7 @@ extension UserInterfaceSettings {
                             }.padding(.top)
                         }.padding(.bottom)
                     }
-                ).listRowBackground(Color.chart)
+                ).settingsSearchTarget(label: String(localized: "Appearance"))
 
                 Section {
                     VStack {
@@ -154,7 +154,7 @@ extension UserInterfaceSettings {
                             ).buttonStyle(BorderlessButtonStyle())
                         }.padding(.top)
                     }.padding(.bottom)
-                }.listRowBackground(Color.chart)
+                }.settingsSearchTarget(label: String(localized: "Glucose Color Scheme"))
 
                 Section(
                     header: Text("Home View Settings"),
@@ -189,7 +189,7 @@ extension UserInterfaceSettings {
                             }.padding(.top)
                         }.padding(.vertical)
                     }
-                ).listRowBackground(Color.chart)
+                ).settingsSearchTarget(label: String(localized: "Show X-Axis Grid Lines"))
 
                 SettingInputSection(
                     decimalValue: $decimalPlaceholder,
@@ -319,7 +319,7 @@ extension UserInterfaceSettings {
                                 ).buttonStyle(BorderlessButtonStyle())
                             }.padding(.top)
                         }.padding(.bottom)
-                    }.listRowBackground(Color.chart)
+                    }.settingsSearchTarget(label: String(localized: "Low Threshold"))
                 }
 
                 Section {
@@ -374,7 +374,7 @@ extension UserInterfaceSettings {
                             ).buttonStyle(BorderlessButtonStyle())
                         }.padding(.top)
                     }.padding(.bottom)
-                }.listRowBackground(Color.chart)
+                }.settingsSearchTarget(label: String(localized: "Forecast Display Type"))
 
                 Section {
                     VStack {
@@ -416,7 +416,7 @@ extension UserInterfaceSettings {
                             ).buttonStyle(BorderlessButtonStyle())
                         }.padding(.top)
                     }.padding(.bottom)
-                }.listRowBackground(Color.chart)
+                }.settingsSearchTarget(label: String(localized: "Bolus Display Threshold"))
 
                 Section(
                     header: Text("Trio Statistics"),
@@ -459,7 +459,7 @@ extension UserInterfaceSettings {
                             }.padding(.top)
                         }.padding(.bottom)
                     }
-                ).listRowBackground(Color.chart)
+                ).settingsSearchTarget(label: String(localized: "eA1c/GMI Display Unit"))
 
                 Section {
                     VStack(alignment: .leading) {
@@ -538,7 +538,7 @@ extension UserInterfaceSettings {
                             ).buttonStyle(BorderlessButtonStyle())
                         }.padding(.top)
                     }.padding(.bottom)
-                }.listRowBackground(Color.chart)
+                }.settingsSearchTarget(label: String(localized: "Time in Range Type"))
 
                 SettingInputSection(
                     decimalValue: $state.carbsRequiredThreshold,
@@ -600,6 +600,7 @@ extension UserInterfaceSettings {
             .onAppear(perform: configureView)
             .navigationBarTitle("User Interface")
             .navigationBarTitleDisplayMode(.automatic)
+            .settingsHighlightScroll()
         }
     }
 }

+ 1 - 0
Trio/Sources/Modules/WatchConfig/View/WatchConfigAppleWatchView.swift

@@ -68,6 +68,7 @@ struct WatchConfigAppleWatchView: BaseView {
         }
         .navigationTitle("Apple Watch")
         .navigationBarTitleDisplayMode(.automatic)
+        .settingsHighlightScroll()
         .scrollContentBackground(.hidden)
         .background(appState.trioBackgroundColor(for: colorScheme))
     }

+ 106 - 0
Trio/Sources/Services/BolusSafety/BolusSafetyValidator.swift

@@ -0,0 +1,106 @@
+import CoreData
+import Foundation
+import Swinject
+
+/// Shared safety checks applied to any bolus command originating outside the main bolus UI
+/// (remote notifications, Shortcuts, etc.). Keeps validation logic consistent across call sites.
+protocol BolusSafetyValidator {
+    /// - Parameter lookbackStart: start of the window used for the recent-bolus 20% check.
+    ///   Defaults to `now - BolusSafetyEvaluator.recentBolusWindowMinutes`. Callers that know when the
+    ///   command was originally issued (e.g. APNS payload timestamp) should pass that instead so the
+    ///   check covers any bolus since the command was sent.
+    func validate(bolusAmount: Decimal, lookbackStart: Date?) async throws -> BolusSafetyResult
+    func fetchTotalRecentBolusAmount(since date: Date) async throws -> Decimal
+}
+
+extension BolusSafetyValidator {
+    func validate(bolusAmount: Decimal) async throws -> BolusSafetyResult {
+        try await validate(bolusAmount: bolusAmount, lookbackStart: nil)
+    }
+}
+
+enum BolusSafetyResult: Equatable {
+    case allowed
+    case rejected(BolusSafetyRejection)
+}
+
+enum BolusSafetyRejection: Equatable {
+    case exceedsMaxBolus(maxBolus: Decimal)
+    case iobUnavailable
+    case exceedsMaxIOB(currentIOB: Decimal, maxIOB: Decimal)
+    case recentBolusWithinWindow(totalRecent: Decimal)
+}
+
+struct BolusSafetyInputs: Equatable {
+    let maxBolus: Decimal
+    let maxIOB: Decimal
+    let currentIOB: Decimal?
+    /// Sum of bolus amounts delivered within the recent-bolus window (see `BolusSafetyEvaluator.recentBolusWindowMinutes`).
+    let totalRecentBolus: Decimal
+}
+
+enum BolusSafetyEvaluator {
+    static let recentBolusThreshold: Decimal = 0.2
+    static let recentBolusWindowMinutes: Int = 6
+
+    static func evaluate(bolusAmount: Decimal, inputs: BolusSafetyInputs) -> BolusSafetyResult {
+        if bolusAmount > inputs.maxBolus {
+            return .rejected(.exceedsMaxBolus(maxBolus: inputs.maxBolus))
+        }
+        guard let currentIOB = inputs.currentIOB else {
+            return .rejected(.iobUnavailable)
+        }
+        if (currentIOB + bolusAmount) > inputs.maxIOB {
+            return .rejected(.exceedsMaxIOB(currentIOB: currentIOB, maxIOB: inputs.maxIOB))
+        }
+        if inputs.totalRecentBolus >= bolusAmount * recentBolusThreshold {
+            return .rejected(.recentBolusWithinWindow(totalRecent: inputs.totalRecentBolus))
+        }
+        return .allowed
+    }
+}
+
+final class BaseBolusSafetyValidator: BolusSafetyValidator, Injectable {
+    @Injected() private var settingsManager: SettingsManager!
+    @Injected() private var iobService: IOBService!
+
+    private let fetchContext: NSManagedObjectContext
+
+    init(resolver: Resolver) {
+        fetchContext = CoreDataStack.shared.newTaskContext()
+        injectServices(resolver)
+    }
+
+    func validate(bolusAmount: Decimal, lookbackStart: Date?) async throws -> BolusSafetyResult {
+        let windowStart = lookbackStart
+            ?? Date().addingTimeInterval(-Double(BolusSafetyEvaluator.recentBolusWindowMinutes * 60))
+        let inputs = BolusSafetyInputs(
+            maxBolus: settingsManager.pumpSettings.maxBolus,
+            maxIOB: settingsManager.preferences.maxIOB,
+            currentIOB: iobService.currentIOB,
+            totalRecentBolus: try await fetchTotalRecentBolusAmount(since: windowStart)
+        )
+        return BolusSafetyEvaluator.evaluate(bolusAmount: bolusAmount, inputs: inputs)
+    }
+
+    func fetchTotalRecentBolusAmount(since date: Date) async throws -> Decimal {
+        let predicate = NSPredicate(
+            format: "type == %@ AND timestamp > %@",
+            PumpEventStored.EventType.bolus.rawValue,
+            date as NSDate
+        )
+        let results: Any = try await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: PumpEventStored.self,
+            onContext: fetchContext,
+            predicate: predicate,
+            key: "timestamp",
+            ascending: true,
+            fetchLimit: nil,
+            propertiesToFetch: ["bolus.amount"]
+        )
+        guard let bolusDictionaries = results as? [[String: Any]] else {
+            throw CoreDataError.fetchError(function: #function, file: #file)
+        }
+        return bolusDictionaries.compactMap { ($0["bolus.amount"] as? NSNumber)?.decimalValue }.reduce(0, +)
+    }
+}

+ 22 - 45
Trio/Sources/Services/RemoteControl/TrioRemoteControl+Bolus.swift

@@ -2,42 +2,22 @@ import Foundation
 import HealthKit
 
 extension TrioRemoteControl {
-    internal func handleBolusCommand(_ payload: CommandPayload) async throws {
+    func handleBolusCommand(_ payload: CommandPayload) async throws {
         guard let bolusAmount = payload.bolusAmount else {
             await logError("Command rejected: bolus amount is missing or invalid.", payload: payload)
             return
         }
 
-        let maxBolus = await TrioApp.resolver.resolve(SettingsManager.self)?.pumpSettings.maxBolus ?? Decimal(0)
-
-        if bolusAmount > maxBolus {
-            await logError(
-                "Command rejected: bolus amount (\(bolusAmount) units) exceeds the maximum allowed (\(maxBolus) units).",
-                payload: payload
-            )
-            return
-        }
-
-        let maxIOB = settings.preferences.maxIOB
-        guard let currentIOB = iobService.currentIOB else {
-            throw CoreDataError.fetchError(function: #function, file: #file)
-        }
-        if (currentIOB + bolusAmount) > maxIOB {
-            await logError(
-                "Command rejected: bolus amount (\(bolusAmount) units) would exceed max IOB (\(maxIOB) units). Current IOB: \(currentIOB) units.",
-                payload: payload
-            )
-            return
-        }
-
-        let totalRecentBolusAmount =
-            try await fetchTotalRecentBolusAmount(since: Date(timeIntervalSince1970: payload.timestamp))
+        let validation = try await bolusSafetyValidator.validate(
+            bolusAmount: bolusAmount,
+            lookbackStart: Date(timeIntervalSince1970: payload.timestamp)
+        )
 
-        if totalRecentBolusAmount >= bolusAmount * 0.2 {
-            await logError(
-                "Command rejected: boluses totaling more than 20% of the requested amount have been delivered since the command was sent.",
-                payload: payload
-            )
+        switch validation {
+        case .allowed:
+            break
+        case let .rejected(reason):
+            await logError(reason.remoteCommandMessage(bolusAmount: bolusAmount), payload: payload)
             return
         }
 
@@ -79,22 +59,19 @@ extension TrioRemoteControl {
                 }
             }
     }
+}
 
-    private func fetchTotalRecentBolusAmount(since date: Date) async throws -> Decimal {
-        let predicate = NSPredicate(
-            format: "type == %@ AND timestamp > %@",
-            PumpEventStored.EventType.bolus.rawValue,
-            date as NSDate
-        )
-        let results: Any = try await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: PumpEventStored.self, onContext: pumpHistoryFetchContext, predicate: predicate, key: "timestamp",
-            ascending: true, fetchLimit: nil, propertiesToFetch: ["bolus.amount"]
-        )
-        guard let bolusDictionaries = results as? [[String: Any]] else {
-            await logError("Failed to cast fetched bolus events. Fetched entities type: \(type(of: results))")
-            throw CoreDataError.fetchError(function: #function, file: #file)
+private extension BolusSafetyRejection {
+    func remoteCommandMessage(bolusAmount: Decimal) -> String {
+        switch self {
+        case let .exceedsMaxBolus(maxBolus):
+            return "Command rejected: bolus amount (\(bolusAmount) units) exceeds the maximum allowed (\(maxBolus) units)."
+        case .iobUnavailable:
+            return "Command rejected: current IOB is not available."
+        case let .exceedsMaxIOB(currentIOB, maxIOB):
+            return "Command rejected: bolus amount (\(bolusAmount) units) would exceed max IOB (\(maxIOB) units). Current IOB: \(currentIOB) units."
+        case .recentBolusWithinWindow:
+            return "Command rejected: boluses totaling more than 20% of the requested amount have been delivered since the command was sent."
         }
-        let totalAmount = bolusDictionaries.compactMap { ($0["bolus.amount"] as? NSNumber)?.decimalValue }.reduce(0, +)
-        return totalAmount
     }
 }

+ 1 - 3
Trio/Sources/Services/RemoteControl/TrioRemoteControl.swift

@@ -10,15 +10,13 @@ class TrioRemoteControl: Injectable {
     @Injected() internal var nightscoutManager: NightscoutManager!
     @Injected() internal var overrideStorage: OverrideStorage!
     @Injected() internal var settings: SettingsManager!
-    @Injected() internal var iobService: IOBService!
+    @Injected() internal var bolusSafetyValidator: BolusSafetyValidator!
 
     private let timeWindow: TimeInterval = 600
 
-    internal let pumpHistoryFetchContext: NSManagedObjectContext
     internal let viewContext: NSManagedObjectContext
 
     private init() {
-        pumpHistoryFetchContext = CoreDataStack.shared.newTaskContext()
         viewContext = CoreDataStack.shared.persistentContainer.viewContext
         injectServices(TrioApp.resolver)
     }

+ 71 - 7
Trio/Sources/Services/Telemetry/TelemetryClient.swift

@@ -1,4 +1,5 @@
 import Foundation
+import HealthKit
 import LoopKit
 import Swinject
 import UIKit
@@ -20,6 +21,11 @@ final class TelemetryClient: Injectable {
 
     private static let productionBaseURL: URL? = URL(string: "https://telemetry.triodocs.org")
 
+    // MARK: if you fork Trio and keep telemetry enabled, please change the name here
+
+    // so that we can distinguish forks from mainline Trio builds in our telemetry.
+    private static let telemetryAppName: String = "Trio"
+
     /// Effective base URL: respects the debug override in
     /// `PropertyPersistentFlags.telemetryDebugServerURL`, then falls back to
     /// `productionBaseURL`. Used by both the registration and `/checkin` paths.
@@ -38,6 +44,14 @@ final class TelemetryClient: Injectable {
     private static let dailyInterval: TimeInterval = 24 * 60 * 60
     private static let maxPayloadBytes = 4096
 
+    private static let buildDateFormatter: DateFormatter = {
+        let f = DateFormatter()
+        f.dateFormat = "yyyy-MM-dd"
+        f.locale = Locale(identifier: "en_US_POSIX")
+        f.timeZone = TimeZone(identifier: "UTC")
+        return f
+    }()
+
     // MARK: Injected services
 
     @Injected() private var apsManager: APSManager!
@@ -103,6 +117,12 @@ final class TelemetryClient: Injectable {
     /// Arms (or re-arms) the 24h send timer. Idempotent. Bails out without
     /// scheduling if the user hasn't decided on consent yet or has opted out
     /// — there's nothing for the timer to do.
+    ///
+    /// Best-effort fallback only. GCD timers don't advance while the app is
+    /// suspended, so on iOS this effectively means "fires only if the app
+    /// stays foregrounded for 24h." The reliable cadence driver is
+    /// `checkAndSendIfOverdue()` called on every foreground transition and
+    /// cold launch.
     func scheduleRecurring() {
         guard PropertyPersistentFlags.shared.telemetryConsentDecisionMade == true,
               PropertyPersistentFlags.shared.telemetryEnabled == true
@@ -123,6 +143,31 @@ final class TelemetryClient: Injectable {
         }
     }
 
+    /// If consent is set and we haven't successfully sent within the last 24h
+    /// (or have never sent), fire a send. Called on foreground transitions
+    /// and from the cold-launch path so daily cadence is kept.
+    ///
+    /// Mirrors the pattern used by LoopFollow's `TaskScheduler.checkTasksNow()`:
+    /// wall-clock comparison against `telemetryLastSentAt`, fire-and-forget
+    /// if overdue. Safe to call repeatedly — if a send already fired within
+    /// the window, this is a no-op.
+    func checkAndSendIfOverdue() {
+        guard PropertyPersistentFlags.shared.telemetryConsentDecisionMade == true,
+              PropertyPersistentFlags.shared.telemetryEnabled == true
+        else {
+            return
+        }
+
+        let lastSent = PropertyPersistentFlags.shared.telemetryLastSentAt
+        let overdue: Bool = {
+            guard let lastSent else { return true }
+            return Date().timeIntervalSince(lastSent) >= Self.dailyInterval
+        }()
+        guard overdue else { return }
+
+        Task.detached { await self.maybeSend() }
+    }
+
     /// Single entry point for all sends (scheduler tick, consent-yes, startup
     /// SHA-change). Gated on consent + opt-in. *When* to send is the caller's
     /// decision — startup handles the SHA-change shortcut, the timer handles
@@ -149,6 +194,7 @@ final class TelemetryClient: Injectable {
         var payload: [String: Any] = [:]
 
         if let v = info["CFBundleShortVersionString"] as? String { payload["appVersion"] = v }
+        payload["appName"] = TelemetryClient.telemetryAppName
         // appDevVersion is Trio's 4-component dev counter (e.g. "0.7.0.14") —
         // the most precise build identifier we have. Always emit, even when
         // the Info.plist key is missing, so dashboards can rely on the field.
@@ -156,10 +202,10 @@ final class TelemetryClient: Injectable {
         payload["commitSha"] = bd.trioCommitSHA
         payload["branch"] = bd.trioBranch
 
-        // Date-only prefix of the build-date string. Keeps the field a
-        // low-resolution build identifier, not a precise timestamp.
-        if let raw = bd.buildDateString, raw.count >= 10 {
-            payload["buildDate"] = String(raw.prefix(10))
+        // Date-only (yyyy-MM-dd, UTC) build identifier, parsed from the
+        // "Tue May 26 12:34:56 UTC 2025" form added in BuildDetails.plist.
+        if let date = bd.buildDate() {
+            payload["buildDate"] = Self.buildDateFormatter.string(from: date)
         }
 
         payload["isTestFlight"] = bd.isTestFlightBuild()
@@ -172,6 +218,8 @@ final class TelemetryClient: Injectable {
         payload["device"] = Self.hardwareIdentifier()
         payload["platform"] = Self.detectPlatform()
         payload["osVersion"] = UIDevice.current.systemVersion
+        payload["locale"] = Locale.current.identifier
+        payload["timeZone"] = TimeZone.current.identifier
 
         // Pump model — omitted entirely when no pump is paired.
         if let pump = apsManager?.pumpManager {
@@ -197,9 +245,25 @@ final class TelemetryClient: Injectable {
 
         payload["tidepoolPaired"] = tidepoolManager?.getTidepoolServiceUI() != nil
 
-        let useHealth = settings?.useAppleHealth ?? false
-        let healthAuthorized = healthKitManager?.hasGrantedFullWritePermissions ?? false
-        payload["appleHealthEnabled"] = useHealth && healthAuthorized
+        // Apple Health: report `enabled = true` as soon as *any* per-type write
+        // permission is granted, with the full per-type breakdown in
+        // `appleHealthWrites`.
+        let appleHealthSampleTypes: [(name: String, type: HKObjectType?)] = [
+            ("glucose", AppleHealthConfig.healthBGObject),
+            ("insulin", AppleHealthConfig.healthInsulinObject),
+            ("carbs", AppleHealthConfig.healthCarbObject),
+            ("fat", AppleHealthConfig.healthFatObject),
+            ("protein", AppleHealthConfig.healthProteinObject)
+        ]
+        var writePermissions: [String: Bool] = [:]
+        for (name, type) in appleHealthSampleTypes {
+            let granted = type.flatMap { healthKitManager?.checkWriteToHealthPermissions(objectTypeToHealthStore: $0) } ?? false
+            writePermissions[name] = granted
+        }
+        payload["appleHealthEnabled"] = writePermissions.values.contains(true)
+        if !writePermissions.isEmpty {
+            payload["appleHealthWrites"] = writePermissions
+        }
 
         if let settings = settings {
             payload["closedLoop"] = settings.closedLoop

+ 1 - 0
Trio/Sources/Shortcuts/BaseIntentsRequest.swift

@@ -17,6 +17,7 @@ import Swinject
     @Injected() var liveActivityManager: LiveActivityManager!
     @Injected() var pumpHistoryStorage: PumpHistoryStorage!
     @Injected() var iobService: IOBService!
+    @Injected() var bolusSafetyValidator: BolusSafetyValidator!
 
     let resolver: Resolver
 

+ 37 - 10
Trio/Sources/Shortcuts/Bolus/BolusIntentRequest.swift

@@ -4,25 +4,25 @@ import Foundation
 
 final class BolusIntentRequest: BaseIntentsRequest {
     func bolus(_ bolusAmount: Double) async throws -> String {
-        var bolusQuantity: Decimal = 0
         switch settingsManager.settings.bolusShortcut {
-        // Block boluses if they are disabled
         case .notAllowed:
             return String(
                 localized:
                 "Bolusing via Shortcuts is disabled in Trio settings."
             )
 
-        // Block any bolus attempted if it is larger than the max bolus in settings
-        case .limitBolusMax:
-            if Decimal(bolusAmount) > settingsManager.pumpSettings.maxBolus {
-                return String(
-                    localized:
-                    "The bolus cannot be larger than the pump setting max bolus (\(settingsManager.pumpSettings.maxBolus.description))."
+        case .limitWithSafetyChecks:
+            let requestedAmount = Decimal(bolusAmount)
+            let validation = try await bolusSafetyValidator.validate(bolusAmount: requestedAmount)
+
+            if case let .rejected(reason) = validation {
+                return reason.shortcutMessage(
+                    requestedAmount: requestedAmount,
+                    pumpMaxBolus: settingsManager.pumpSettings.maxBolus
                 )
-            } else {
-                bolusQuantity = apsManager.roundBolus(amount: Decimal(bolusAmount))
             }
+
+            let bolusQuantity = apsManager.roundBolus(amount: requestedAmount)
             await apsManager.enactBolus(amount: Double(bolusQuantity), isSMB: false, callback: nil)
             return String(
                 localized:
@@ -52,3 +52,30 @@ final class BolusIntentRequest: BaseIntentsRequest {
         }
     }
 }
+
+private extension BolusSafetyRejection {
+    func shortcutMessage(requestedAmount: Decimal, pumpMaxBolus: Decimal) -> String {
+        switch self {
+        case .exceedsMaxBolus:
+            return String(
+                localized:
+                "The bolus cannot be larger than the pump setting max bolus (\(pumpMaxBolus.description))."
+            )
+        case .iobUnavailable:
+            return String(
+                localized:
+                "Bolus blocked: current IOB is not available."
+            )
+        case let .exceedsMaxIOB(currentIOB, maxIOB):
+            return String(
+                localized:
+                "Bolus blocked: a \(requestedAmount.formatted()) U bolus would exceed max IOB (\(maxIOB.formatted()) U). Current IOB: \(currentIOB.formatted()) U."
+            )
+        case .recentBolusWithinWindow:
+            return String(
+                localized:
+                "Bolus blocked: a significant bolus was delivered within the last \(BolusSafetyEvaluator.recentBolusWindowMinutes) minutes."
+            )
+        }
+    }
+}

+ 2 - 1
Trio/Sources/Views/SettingInputSection.swift

@@ -83,7 +83,8 @@ struct SettingInputSection<VerboseHint: View>: View {
             },
             header: { headerText.map(Text.init) },
             footer: { footerText.map(Text.init) }
-        ).listRowBackground(Color.chart)
+        )
+        .settingsSearchTarget(label: label)
     }
 
     // Helper function to retrieve PickerSetting based on key

+ 73 - 0
Trio/Sources/Views/SettingsSearchHighlight.swift

@@ -0,0 +1,73 @@
+import SwiftUI
+
+@MainActor @Observable final class SettingsSearchHighlight {
+    var highlightedSetting: String?
+}
+
+/// Wraps a Screen value with the scroll-target label for search-result navigation.
+struct SearchResultTarget: Hashable {
+    let screen: Screen
+    let scrollLabel: String
+}
+
+private struct SettingsHighlightScrollModifier: ViewModifier {
+    @Environment(SettingsSearchHighlight.self) private var searchHighlight
+
+    func body(content: Content) -> some View {
+        ScrollViewReader { proxy in
+            content
+                .task(id: searchHighlight.highlightedSetting) {
+                    guard let target = searchHighlight.highlightedSetting else { return }
+                    try? await Task.sleep(for: .milliseconds(500))
+                    guard !Task.isCancelled else { return }
+                    withAnimation { proxy.scrollTo(target, anchor: .center) }
+                }
+        }
+    }
+}
+
+private struct SettingsSearchHighlightAnimationModifier: ViewModifier {
+    let label: String
+    @Environment(SettingsSearchHighlight.self) private var searchHighlight
+    @State private var highlightOpacity: Double = 0.0
+
+    func body(content: Content) -> some View {
+        content
+            .listRowBackground(
+                Color.chart.overlay(Color.accentColor.opacity(highlightOpacity))
+                    .animation(.easeOut(duration: 1.2), value: highlightOpacity)
+            )
+            .onAppear {
+                guard searchHighlight.highlightedSetting == label else { return }
+                startHighlightAnimation()
+            }
+            .onChange(of: searchHighlight.highlightedSetting) { _, newValue in
+                guard newValue == label else { return }
+                startHighlightAnimation()
+            }
+    }
+
+    private func startHighlightAnimation() {
+        Task { @MainActor in
+            try? await Task.sleep(for: .milliseconds(500))
+            highlightOpacity = 0.6
+            try? await Task.sleep(for: .milliseconds(800))
+            searchHighlight.highlightedSetting = nil
+            highlightOpacity = 0.0
+        }
+    }
+}
+
+extension View {
+    /// Enables scroll-to-highlight on a settings screen. Add once per destination view.
+    func settingsHighlightScroll() -> some View {
+        modifier(SettingsHighlightScrollModifier())
+    }
+
+    /// Marks a section as a scroll-to and highlight target for settings search.
+    /// Combines `.id(label)` with a highlight flash animation in a single call.
+    func settingsSearchTarget(label: String) -> some View {
+        id(label)
+            .modifier(SettingsSearchHighlightAnimationModifier(label: label))
+    }
+}

+ 113 - 0
TrioTests/BolusSafetyTests/BolusSafetyValidatorTests.swift

@@ -0,0 +1,113 @@
+import Foundation
+import Testing
+
+@testable import Trio
+
+@Suite("Bolus Safety Validator Tests") struct BolusSafetyValidatorTests: Injectable {
+    @Injected() var validator: BolusSafetyValidator!
+    let resolver = TrioApp().resolver
+
+    init() {
+        injectServices(resolver)
+    }
+
+    @Test("Validator resolves from the service container") func testValidatorResolves() {
+        #expect(validator != nil, "BolusSafetyValidator should be registered in ServiceAssembly")
+        #expect(validator is BaseBolusSafetyValidator, "Validator should be of type BaseBolusSafetyValidator")
+    }
+
+    @Test("Allows bolus when all inputs are within limits") func testAllowed() {
+        let inputs = BolusSafetyInputs(
+            maxBolus: 10,
+            maxIOB: 10,
+            currentIOB: 1,
+            totalRecentBolus: 0
+        )
+        #expect(BolusSafetyEvaluator.evaluate(bolusAmount: 5, inputs: inputs) == .allowed)
+    }
+
+    @Test("Rejects when amount exceeds max bolus") func testExceedsMaxBolus() {
+        let inputs = BolusSafetyInputs(
+            maxBolus: 5,
+            maxIOB: 10,
+            currentIOB: 0,
+            totalRecentBolus: 0
+        )
+        let result = BolusSafetyEvaluator.evaluate(bolusAmount: 6, inputs: inputs)
+        #expect(result == .rejected(.exceedsMaxBolus(maxBolus: 5)))
+    }
+
+    @Test("Rejects when current IOB is unavailable") func testIOBUnavailable() {
+        let inputs = BolusSafetyInputs(
+            maxBolus: 10,
+            maxIOB: 10,
+            currentIOB: nil,
+            totalRecentBolus: 0
+        )
+        let result = BolusSafetyEvaluator.evaluate(bolusAmount: 1, inputs: inputs)
+        #expect(result == .rejected(.iobUnavailable))
+    }
+
+    @Test("Rejects when amount would exceed max IOB") func testExceedsMaxIOB() {
+        let inputs = BolusSafetyInputs(
+            maxBolus: 10,
+            maxIOB: 5,
+            currentIOB: 3,
+            totalRecentBolus: 0
+        )
+        let result = BolusSafetyEvaluator.evaluate(bolusAmount: 2.5, inputs: inputs)
+        #expect(result == .rejected(.exceedsMaxIOB(currentIOB: 3, maxIOB: 5)))
+    }
+
+    @Test("Rejects when recent bolus totals >= 20% of requested amount") func testRecentBolusWithinWindow() {
+        let inputs = BolusSafetyInputs(
+            maxBolus: 10,
+            maxIOB: 10,
+            currentIOB: 0,
+            totalRecentBolus: 1.0
+        )
+        let result = BolusSafetyEvaluator.evaluate(bolusAmount: 5, inputs: inputs)
+        #expect(result == .rejected(.recentBolusWithinWindow(totalRecent: 1.0)))
+    }
+
+    @Test("Allows when recent bolus total is below 20% threshold") func testRecentBolusBelowThreshold() {
+        let inputs = BolusSafetyInputs(
+            maxBolus: 10,
+            maxIOB: 10,
+            currentIOB: 0,
+            totalRecentBolus: 0.99
+        )
+        #expect(BolusSafetyEvaluator.evaluate(bolusAmount: 5, inputs: inputs) == .allowed)
+    }
+
+    @Test("Max bolus check runs before IOB check") func testCheckOrdering() {
+        let inputs = BolusSafetyInputs(
+            maxBolus: 5,
+            maxIOB: 10,
+            currentIOB: nil,
+            totalRecentBolus: 0
+        )
+        let result = BolusSafetyEvaluator.evaluate(bolusAmount: 6, inputs: inputs)
+        #expect(result == .rejected(.exceedsMaxBolus(maxBolus: 5)))
+    }
+
+    @Test("Equal-to-max-bolus is allowed") func testEqualsMaxBolus() {
+        let inputs = BolusSafetyInputs(
+            maxBolus: 5,
+            maxIOB: 10,
+            currentIOB: 0,
+            totalRecentBolus: 0
+        )
+        #expect(BolusSafetyEvaluator.evaluate(bolusAmount: 5, inputs: inputs) == .allowed)
+    }
+
+    @Test("Current IOB plus amount equal to max IOB is allowed") func testEqualToMaxIOB() {
+        let inputs = BolusSafetyInputs(
+            maxBolus: 10,
+            maxIOB: 5,
+            currentIOB: 3,
+            totalRecentBolus: 0
+        )
+        #expect(BolusSafetyEvaluator.evaluate(bolusAmount: 2, inputs: inputs) == .allowed)
+    }
+}

+ 69 - 0
TrioTests/SettingsSearchTests.swift

@@ -0,0 +1,69 @@
+import Foundation
+import Testing
+@testable import Trio
+
+@Suite("Settings Search Navigation") struct SettingsSearchTests {
+    @Test("Searching 'Dynamic ISF' finds the Dynamic Settings screen") func searchDynamicISF() {
+        let results = SettingItems.filteredItems(searchText: "Dynamic ISF")
+        #expect(!results.isEmpty)
+        let match = results.first { $0.matchedContent == "Dynamic ISF" }
+        #expect(match != nil)
+        #expect(match?.settingItem.view == .dynamicISF)
+        #expect(match?.scrollLabel == "Dynamic ISF")
+    }
+
+    @Test("All scrollTargetLabels have valid non-empty targets") func scrollTargetLabelsNonEmpty() {
+        for item in SettingItems.allItems {
+            guard let labels = item.scrollTargetLabels else { continue }
+            for (key, value) in labels {
+                #expect(!value.isEmpty)
+                #expect(item.searchContents?.contains(key) == true)
+            }
+        }
+    }
+
+    @Test("Every searchContents entry produces at least one result") func allSearchContentsAreSearchable() {
+        for item in SettingItems.allItems {
+            guard let contents = item.searchContents else { continue }
+            for content in contents {
+                let results = SettingItems.filteredItems(searchText: content)
+                #expect(!results.isEmpty)
+            }
+        }
+    }
+
+    @Test("SearchResultTarget is Hashable and equatable by value") func searchResultTargetHashable() {
+        let a = SearchResultTarget(screen: .dynamicISF, scrollLabel: "Dynamic ISF")
+        let b = SearchResultTarget(screen: .dynamicISF, scrollLabel: "Dynamic ISF")
+        let c = SearchResultTarget(screen: .dynamicISF, scrollLabel: "Adjust Basal")
+        #expect(a == b)
+        #expect(a != c)
+        #expect(a.hashValue == b.hashValue)
+    }
+
+    @Test("SettingsSearchHighlight starts nil and accepts assignments")
+    @MainActor func highlightStateTransitions() {
+        let highlight = SettingsSearchHighlight()
+        #expect(highlight.highlightedSetting == nil)
+
+        highlight.highlightedSetting = "Dynamic ISF"
+        #expect(highlight.highlightedSetting == "Dynamic ISF")
+
+        highlight.highlightedSetting = nil
+        #expect(highlight.highlightedSetting == nil)
+    }
+
+    @Test("SettingsSearchHighlight can be set and cleared in sequence")
+    @MainActor func highlightSequentialUpdates() async {
+        let highlight = SettingsSearchHighlight()
+
+        highlight.highlightedSetting = "First Setting"
+        #expect(highlight.highlightedSetting == "First Setting")
+
+        highlight.highlightedSetting = "Second Setting"
+        #expect(highlight.highlightedSetting == "Second Setting")
+
+        highlight.highlightedSetting = nil
+        #expect(highlight.highlightedSetting == nil)
+    }
+}

+ 1 - 1
scripts/capture-build-details.sh

@@ -16,7 +16,7 @@ fi
 echo "Gathering build details..."
 
 # Capture the current date
-plutil -replace com-trio-build-date -string "$(date -u '+%a %b %e %H:%M:%S UTC %Y')" "${info_plist_path}"
+plutil -replace com-trio-build-date -string "$(LC_ALL=C date -u '+%a %b %e %H:%M:%S UTC %Y')" "${info_plist_path}"
 
 # --- Root repo details ---
 # Retrieve current branch (or tag) and commit SHA.

+ 2 - 1
scripts/swiftformat.sh

@@ -112,4 +112,5 @@ trailingClosures \
   MinimedKit, \
   TidepoolService, \
   DanaKit, \
-  MedtrumKit
+  MedtrumKit, \
+  OmnipodKit