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

Merge branch 'dev' into fix-negative-iob-after-onboarding

Deniz Cengiz 3 месяцев назад
Родитель
Сommit
ca24c5d831
78 измененных файлов с 2188 добавлено и 1294 удалено
  1. 6 3
      .github/workflows/unit_tests.yml
  2. 1 1
      CGMBLEKit
  3. 1 1
      Config.xcconfig
  4. 1 1
      DanaKit
  5. 1 1
      G7SensorKit
  6. 1 1
      Gemfile
  7. 27 39
      Gemfile.lock
  8. 1 1
      LibreTransmitter
  9. 1 1
      LoopKit
  10. 1 1
      MinimedKit
  11. 1 0
      Model/Classes+Properties/ContactTrickEntryStored+CoreDataProperties.swift
  12. 2 1
      Model/TrioCoreDataPersistentContainer.xcdatamodeld/TrioCoreDataPersistentContainer.xcdatamodel/contents
  13. 1 1
      OmniBLE
  14. 1 1
      OmniKit
  15. 1 1
      RileyLinkKit
  16. 1 1
      TidepoolService
  17. 0 18
      Trio Watch App Extension/Assets.xcassets/Colors/Background_DarkBlue.colorset/Contents.json
  18. 0 18
      Trio Watch App Extension/Assets.xcassets/Colors/Background_DarkerDarkBlue.colorset/Contents.json
  19. 0 18
      Trio Watch App Extension/Assets.xcassets/Colors/Basal.colorset/Contents.json
  20. 0 18
      Trio Watch App Extension/Assets.xcassets/Colors/Insulin.colorset/Contents.json
  21. 0 18
      Trio Watch App Extension/Assets.xcassets/Colors/LoopGray.colorset/Contents.json
  22. 0 18
      Trio Watch App Extension/Assets.xcassets/Colors/LoopGreen.colorset/Contents.json
  23. 0 38
      Trio Watch App Extension/Assets.xcassets/Colors/LoopPink.colorset/Contents.json
  24. 0 18
      Trio Watch App Extension/Assets.xcassets/Colors/LoopRed.colorset/Contents.json
  25. 0 18
      Trio Watch App Extension/Assets.xcassets/Colors/TabBar.colorset/Contents.json
  26. 3 21
      Trio Watch App Extension/Assets.xcassets/Colors/UAM.colorset/Contents.json
  27. 0 18
      Trio Watch App Extension/Assets.xcassets/Colors/ZT.colorset/Contents.json
  28. 62 26
      Trio.xcodeproj/project.pbxproj
  29. 0 18
      Trio/Resources/Assets.xcassets/Colors/Background_DarkBlue.colorset/Contents.json
  30. 0 18
      Trio/Resources/Assets.xcassets/Colors/Background_DarkerDarkBlue.colorset/Contents.json
  31. 0 18
      Trio/Resources/Assets.xcassets/Colors/Basal.colorset/Contents.json
  32. 0 18
      Trio/Resources/Assets.xcassets/Colors/DarkerBlue.colorset/Contents.json
  33. 0 18
      Trio/Resources/Assets.xcassets/Colors/Insulin.colorset/Contents.json
  34. 0 38
      Trio/Resources/Assets.xcassets/Colors/Lemon.colorset/Contents.json
  35. 0 18
      Trio/Resources/Assets.xcassets/Colors/LoopGray.colorset/Contents.json
  36. 0 18
      Trio/Resources/Assets.xcassets/Colors/LoopGreen.colorset/Contents.json
  37. 0 38
      Trio/Resources/Assets.xcassets/Colors/LoopPink.colorset/Contents.json
  38. 0 18
      Trio/Resources/Assets.xcassets/Colors/LoopRed.colorset/Contents.json
  39. 0 18
      Trio/Resources/Assets.xcassets/Colors/ManualTempBasal.colorset/Contents.json
  40. 0 18
      Trio/Resources/Assets.xcassets/Colors/TabBar.colorset/Contents.json
  41. 3 21
      Trio/Resources/Assets.xcassets/Colors/UAM.colorset/Contents.json
  42. 0 18
      Trio/Resources/Assets.xcassets/Colors/ZT.colorset/Contents.json
  43. 0 18
      Trio/Resources/Assets.xcassets/Colors/minus.colorset/Contents.json
  44. 0 184
      Trio/Sources/APS/CGM/DexcomSourceG5.swift
  45. 0 195
      Trio/Sources/APS/CGM/DexcomSourceG6.swift
  46. 0 103
      Trio/Sources/APS/CGM/LibreTransmitterSource.swift
  47. 2 10
      Trio/Sources/APS/CGM/PluginSource.swift
  48. 4 19
      Trio/Sources/APS/DeviceDataManager.swift
  49. 203 47
      Trio/Sources/APS/Storage/AlertStorage.swift
  50. 5 2
      Trio/Sources/APS/Storage/ContactImageStorage.swift
  51. 9 9
      Trio/Sources/APS/Storage/GlucoseStorage.swift
  52. 0 2
      Trio/Sources/Helpers/Color+Extensions.swift
  53. 15 0
      Trio/Sources/Helpers/String+Extensions.swift
  54. 90 0
      Trio/Sources/Localizations/Main/Localizable.xcstrings
  55. 17 0
      Trio/Sources/Models/ContactTrickEntry.swift
  56. 21 0
      Trio/Sources/Models/ExportSetting.swift
  57. 11 0
      Trio/Sources/Modules/ContactImage/View/AddContactImageSheet.swift
  58. 9 0
      Trio/Sources/Modules/ContactImage/View/ContactImageDetailView.swift
  59. 3 3
      Trio/Sources/Modules/DataTable/DataTableDataFlow.swift
  60. 2 2
      Trio/Sources/Modules/DataTable/DataTableProvider.swift
  61. 2 2
      Trio/Sources/Modules/DataTable/DataTableStateModel.swift
  62. 2 2
      Trio/Sources/Modules/DataTable/View/CarbEntryEditorView.swift
  63. 14 11
      Trio/Sources/Modules/DataTable/View/DataTableRootView.swift
  64. 28 4
      Trio/Sources/Modules/Home/View/HomeRootView.swift
  65. 4 4
      Trio/Sources/Modules/PumpConfig/PumpConfigProvider.swift
  66. 5 5
      Trio/Sources/Modules/PumpConfig/PumpConfigStateModel.swift
  67. 1 1
      Trio/Sources/Modules/PumpConfig/View/PumpConfigRootView.swift
  68. 4 0
      Trio/Sources/Modules/Settings/SettingsStateModel.swift
  69. 11 63
      Trio/Sources/Modules/Settings/View/SettingsRootView.swift
  70. 5 0
      Trio/Sources/Modules/SettingsExport/SettingsExportDataFlow.swift
  71. 3 0
      Trio/Sources/Modules/SettingsExport/SettingsExportProvider.swift
  72. 1329 0
      Trio/Sources/Modules/SettingsExport/SettingsExportStateModel.swift
  73. 192 0
      Trio/Sources/Modules/SettingsExport/View/SettingsExportRootView.swift
  74. 6 6
      Trio/Sources/Modules/Treatments/View/MealPreset/MealPresetView.swift
  75. 6 3
      Trio/Sources/Router/Screen.swift
  76. 9 1
      Trio/Sources/Services/ContactImage/ContactPicture.swift
  77. 59 0
      TrioTests/SettingsExportTests.swift
  78. 1 1
      scripts/swiftformat.sh

+ 6 - 3
.github/workflows/unit_tests.yml

@@ -28,7 +28,7 @@ jobs:
 
     steps:
       - name: Select Xcode version
-        run: sudo xcode-select -s /Applications/Xcode_16.3.app/Contents/Developer
+        run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer
 
       - name: Checkout code
         uses: actions/checkout@v4
@@ -55,13 +55,16 @@ jobs:
           echo "📂 Contents of .build:"
           ls -lah .build || echo ".build directory not found"
 
+      - name: List available simulators
+        run: xcrun simctl list devices available
+
       - name: Build for testing
         run: |
           set -o pipefail && \
           time xcodebuild build-for-testing \
             -workspace Trio.xcworkspace \
             -scheme "Trio Tests" \
-            -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.4' \
+            -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.5' \
 
       - name: Check for uncommitted changes
         run: |
@@ -104,7 +107,7 @@ jobs:
           time xcodebuild test-without-building \
             -workspace Trio.xcworkspace \
             -scheme "Trio Tests" \
-            -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.4' \
+            -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.5' \
             $([ "$ENABLE_PARALLEL_TESTING" = "true" ] && echo "-parallel-testing-enabled YES") \
             2>&1 | tee xcodebuild.log
 

+ 1 - 1
CGMBLEKit

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

+ 1 - 1
Config.xcconfig

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

+ 1 - 1
DanaKit

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

+ 1 - 1
G7SensorKit

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

+ 1 - 1
Gemfile

@@ -1,3 +1,3 @@
 source "https://rubygems.org"
 
-gem "fastlane", "2.230.0"
+gem "fastlane", "2.231.0"

+ 27 - 39
Gemfile.lock

@@ -1,18 +1,15 @@
 GEM
   remote: https://rubygems.org/
   specs:
-    CFPropertyList (3.0.7)
-      base64
-      nkf
-      rexml
+    CFPropertyList (3.0.8)
     abbrev (0.1.2)
-    addressable (2.8.7)
-      public_suffix (>= 2.0.2, < 7.0)
+    addressable (2.8.8)
+      public_suffix (>= 2.0.2, < 8.0)
     artifactory (3.0.17)
     atomos (0.1.3)
     aws-eventstream (1.4.0)
-    aws-partitions (1.1163.0)
-    aws-sdk-core (3.232.0)
+    aws-partitions (1.1206.0)
+    aws-sdk-core (3.241.4)
       aws-eventstream (~> 1, >= 1.3.0)
       aws-partitions (~> 1, >= 1.992.0)
       aws-sigv4 (~> 1.9)
@@ -20,18 +17,18 @@ GEM
       bigdecimal
       jmespath (~> 1, >= 1.6.1)
       logger
-    aws-sdk-kms (1.112.0)
-      aws-sdk-core (~> 3, >= 3.231.0)
+    aws-sdk-kms (1.121.0)
+      aws-sdk-core (~> 3, >= 3.241.4)
       aws-sigv4 (~> 1.5)
-    aws-sdk-s3 (1.199.0)
-      aws-sdk-core (~> 3, >= 3.231.0)
+    aws-sdk-s3 (1.211.0)
+      aws-sdk-core (~> 3, >= 3.241.3)
       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)
-    bigdecimal (3.2.3)
+    bigdecimal (4.0.1)
     claide (1.1.0)
     colored (1.2)
     colored2 (3.1.2)
@@ -45,36 +42,32 @@ GEM
     dotenv (2.8.1)
     emoji_regex (3.2.3)
     excon (0.112.0)
-    faraday (1.10.4)
+    faraday (1.8.0)
       faraday-em_http (~> 1.0)
       faraday-em_synchrony (~> 1.0)
       faraday-excon (~> 1.1)
-      faraday-httpclient (~> 1.0)
-      faraday-multipart (~> 1.0)
+      faraday-httpclient (~> 1.0.1)
       faraday-net_http (~> 1.0)
-      faraday-net_http_persistent (~> 1.0)
+      faraday-net_http_persistent (~> 1.1)
       faraday-patron (~> 1.0)
       faraday-rack (~> 1.0)
-      faraday-retry (~> 1.0)
+      multipart-post (>= 1.2, < 3)
       ruby2_keywords (>= 0.0.4)
-    faraday-cookie_jar (0.0.7)
+    faraday-cookie_jar (0.0.8)
       faraday (>= 0.8.0)
-      http-cookie (~> 1.0.0)
+      http-cookie (>= 1.0.0)
     faraday-em_http (1.0.0)
     faraday-em_synchrony (1.0.1)
     faraday-excon (1.1.0)
     faraday-httpclient (1.0.1)
-    faraday-multipart (1.1.1)
-      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.3)
     faraday_middleware (1.2.1)
       faraday (~> 1.0)
     fastimage (2.4.0)
-    fastlane (2.230.0)
+    fastlane (2.231.0)
       CFPropertyList (>= 2.3, < 4.0.0)
       abbrev (~> 0.1.2)
       addressable (>= 2.8, < 3.0.0)
@@ -82,7 +75,7 @@ GEM
       aws-sdk-s3 (~> 1.0)
       babosa (>= 1.0.3, < 2.0.0)
       base64 (~> 0.2.0)
-      bundler (>= 1.12.0, < 3.0.0)
+      bundler (>= 1.17.3, < 5.0.0)
       colored (~> 1.2)
       commander (~> 4.6)
       csv (~> 3.3)
@@ -167,23 +160,23 @@ GEM
     httpclient (2.9.0)
       mutex_m
     jmespath (1.6.2)
-    json (2.15.0)
+    json (2.18.0)
     jwt (2.10.2)
       base64
     logger (1.7.0)
     mini_magick (4.13.2)
     mini_mime (1.1.5)
-    multi_json (1.17.0)
+    multi_json (1.19.1)
     multipart-post (2.4.1)
     mutex_m (0.3.0)
     nanaimo (0.4.0)
     naturally (2.3.0)
     nkf (0.2.0)
-    optparse (0.6.0)
+    optparse (0.8.1)
     os (1.1.4)
     plist (3.7.2)
-    public_suffix (6.0.2)
-    rake (13.3.0)
+    public_suffix (7.0.2)
+    rake (13.3.1)
     representable (3.2.0)
       declarative (< 0.1.0)
       trailblazer-option (>= 0.1.1, < 0.2.0)
@@ -227,16 +220,11 @@ GEM
       xcpretty (~> 0.2, >= 0.0.7)
 
 PLATFORMS
-  arm64-darwin-21
-  arm64-darwin-22
-  arm64-darwin-23
-  arm64-darwin-24
-  x86_64-darwin-19
-  x86_64-darwin-24
-  x86_64-linux
+  arm64-darwin-25
+  ruby
 
 DEPENDENCIES
-  fastlane (= 2.230.0)
+  fastlane (= 2.231.0)
 
 BUNDLED WITH
-   2.6.2
+  4.0.4

+ 1 - 1
LibreTransmitter

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

+ 1 - 1
LoopKit

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

+ 1 - 1
MinimedKit

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

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

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

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

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

+ 1 - 1
OmniBLE

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

+ 1 - 1
OmniKit

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

+ 1 - 1
RileyLinkKit

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

+ 1 - 1
TidepoolService

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 62 - 26
Trio.xcodeproj/project.pbxproj

@@ -9,7 +9,7 @@
 /* Begin PBXBuildFile section */
 		041D1E995A6AE92E9289DC49 /* TreatmentsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8D1A7CA8C10C4403D4BBFA7 /* TreatmentsDataFlow.swift */; };
 		0437CE46C12535A56504EC19 /* SnoozeRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5822B15939E719628E9FF7C /* SnoozeRootView.swift */; };
-		0D9A5E34A899219C5C4CDFAF /* DataTableStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9455FA2D92E77A6C4AFED8A3 /* DataTableStateModel.swift */; };
+		0D9A5E34A899219C5C4CDFAF /* HistoryStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9455FA2D92E77A6C4AFED8A3 /* HistoryStateModel.swift */; };
 		0F7A65FBD2CD8D6477ED4539 /* GlucoseNotificationSettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E625985B47742D498CB1681A /* GlucoseNotificationSettingsProvider.swift */; };
 		110AEDE32C5193D200615CC9 /* BolusIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 110AEDE02C5193D100615CC9 /* BolusIntent.swift */; };
 		110AEDE42C5193D200615CC9 /* BolusIntentRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 110AEDE12C5193D100615CC9 /* BolusIntentRequest.swift */; };
@@ -54,7 +54,7 @@
 		19F95FF729F10FEE00314DDC /* StatStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19F95FF629F10FEE00314DDC /* StatStateModel.swift */; };
 		19F95FFA29F1102A00314DDC /* StatRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19F95FF929F1102A00314DDC /* StatRootView.swift */; };
 		1BBB001DAD60F3B8CEA4B1C7 /* ISFEditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505E09DC17A0C3D0AF4B66FE /* ISFEditorStateModel.swift */; };
-		1D845DF2E3324130E1D95E67 /* DataTableProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60744C3E9BB3652895C908CC /* DataTableProvider.swift */; };
+		1D845DF2E3324130E1D95E67 /* HistoryProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60744C3E9BB3652895C908CC /* HistoryProvider.swift */; };
 		23888883D4EA091C88480FF2 /* TreatmentsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C19984D62EFC0035A9E9644D /* TreatmentsProvider.swift */; };
 		3171D2818C7C72CD1584BB5E /* GlucoseNotificationSettingsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC2C6489D29ECCCAD78E0721 /* GlucoseNotificationSettingsStateModel.swift */; };
 		320D030F724170A637F06D50 /* (null) in Sources */ = {isa = PBXBuildFile; };
@@ -330,7 +330,7 @@
 		71D44AAB2CA5F5EA0036EE9E /* AlertPermissionsChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D44AAA2CA5F5EA0036EE9E /* AlertPermissionsChecker.swift */; };
 		72F1BD388F42FCA6C52E4500 /* ConfigEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44080E4709E3AE4B73054563 /* ConfigEditorProvider.swift */; };
 		7BCFACB97C821041BA43A114 /* ManualTempBasalRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */; };
-		7F7B756BE8543965D9FDF1A2 /* DataTableDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A401509D21F7F35D4E109EDA /* DataTableDataFlow.swift */; };
+		7F7B756BE8543965D9FDF1A2 /* HistoryDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A401509D21F7F35D4E109EDA /* HistoryDataFlow.swift */; };
 		8194B80890CDD6A3C13B0FEE /* SnoozeStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E26904AACA8D9C15D229D675 /* SnoozeStateModel.swift */; };
 		88AB39B23C9552BD6E0C9461 /* ISFEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBB3BAE7494CB771ABAC7B8B /* ISFEditorRootView.swift */; };
 		8A91342A2D63D9A1007F8874 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
@@ -456,6 +456,10 @@
 		C29E268A2DADFD2A00F87E75 /* GlucoseDailyPercentileChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = C29E26892DADFD2A00F87E75 /* GlucoseDailyPercentileChart.swift */; };
 		C2A0A42F2CE03131003B98E8 /* ConstantValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2A0A42E2CE0312C003B98E8 /* ConstantValues.swift */; };
 		C2A6D1E42DB1581D0036DB66 /* GlucoseStatsSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2A6D1E32DB1581D0036DB66 /* GlucoseStatsSetup.swift */; };
+		C2AA6CF62E1A734A00BF6C16 /* SettingsExportRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2AA6CF02E1A734A00BF6C16 /* SettingsExportRootView.swift */; };
+		C2AA6CF72E1A734A00BF6C16 /* SettingsExportProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2AA6CF32E1A734A00BF6C16 /* SettingsExportProvider.swift */; };
+		C2AA6CF82E1A734A00BF6C16 /* SettingsExportDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2AA6CF22E1A734A00BF6C16 /* SettingsExportDataFlow.swift */; };
+		C2AA6CF92E1A734A00BF6C16 /* SettingsExportStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2AA6CF42E1A734A00BF6C16 /* SettingsExportStateModel.swift */; };
 		C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.swift */; };
 		CA370FC152BC98B3D1832968 /* BasalProfileEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */; };
 		CC6C406E2ACDD69E009B8058 /* RawFetchedProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */; };
@@ -513,7 +517,7 @@
 		CEE9A65C2BBB41C800EB5194 /* CalibrationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9A65B2BBB41C800EB5194 /* CalibrationService.swift */; };
 		CEE9A65E2BBC9F6500EB5194 /* CalibrationsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9A65D2BBC9F6500EB5194 /* CalibrationsTests.swift */; };
 		CEF1ED6B2D58FB5800FAF41E /* CGMOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF1ED6A2D58FB4600FAF41E /* CGMOptions.swift */; };
-		D6D02515BBFBE64FEBE89856 /* DataTableRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 881E04BA5E0A003DE8E0A9C6 /* DataTableRootView.swift */; };
+		D6D02515BBFBE64FEBE89856 /* HistoryRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 881E04BA5E0A003DE8E0A9C6 /* HistoryRootView.swift */; };
 		D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F2A13DF0EDEEEDC4106AA2A /* NightscoutConfigDataFlow.swift */; };
 		DBA5254DBB2586C98F61220C /* ISFEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9F137F126D9F8DEB799F26 /* ISFEditorProvider.swift */; };
 		DD09D47B2C5986D1003FEA5D /* CalendarEventSettingsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD09D47A2C5986D1003FEA5D /* CalendarEventSettingsDataFlow.swift */; };
@@ -571,6 +575,7 @@
 		DD3A3CE92D29C97800AE478E /* Helper+ButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3A3CE82D29C97800AE478E /* Helper+ButtonStyles.swift */; };
 		DD3C47B32DC5608A003DD20D /* newerSuggested.json in Resources */ = {isa = PBXBuildFile; fileRef = DD3C47B22DC5608A003DD20D /* newerSuggested.json */; };
 		DD3C47B52DC57E06003DD20D /* MainMigrationErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3C47B42DC57E06003DD20D /* MainMigrationErrorView.swift */; };
+		DD3D60312F0377350021A33B /* ExportSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3D60302F0377350021A33B /* ExportSetting.swift */; };
 		DD3F1F832D9DC78800DCE7B3 /* UnitSelectionStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F822D9DC78300DCE7B3 /* UnitSelectionStepView.swift */; };
 		DD3F1F852D9DD84000DCE7B3 /* DeliveryLimitsStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F842D9DD83B00DCE7B3 /* DeliveryLimitsStepView.swift */; };
 		DD3F1F892D9E078D00DCE7B3 /* TherapySettingEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3F1F882D9E078300DCE7B3 /* TherapySettingEditorView.swift */; };
@@ -1135,7 +1140,7 @@
 		5A2325572BFCC168003518CA /* NightscoutConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutConnectView.swift; sourceTree = "<group>"; };
 		5C018D1680307A31C9ED7120 /* CGMSettingsStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CGMSettingsStateModel.swift; sourceTree = "<group>"; };
 		5D5B4F8B4194BB7E260EF251 /* ConfigEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConfigEditorStateModel.swift; sourceTree = "<group>"; };
-		60744C3E9BB3652895C908CC /* DataTableProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DataTableProvider.swift; sourceTree = "<group>"; };
+		60744C3E9BB3652895C908CC /* HistoryProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HistoryProvider.swift; sourceTree = "<group>"; };
 		64AA5E04A2761F6EEA6568E1 /* CarbRatioEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CarbRatioEditorStateModel.swift; sourceTree = "<group>"; };
 		65070A322BFDCB83006F213F /* TidepoolStartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TidepoolStartView.swift; sourceTree = "<group>"; };
 		67F94DD2853CF42BA4E30616 /* BasalProfileEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorDataFlow.swift; sourceTree = "<group>"; };
@@ -1155,16 +1160,16 @@
 		79BDA519C9B890FD9A5DFCF3 /* ISFEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorDataFlow.swift; sourceTree = "<group>"; };
 		7E22146D3DF4853786C78132 /* CarbRatioEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CarbRatioEditorDataFlow.swift; sourceTree = "<group>"; };
 		8782B44544F38F2B2D82C38E /* NightscoutConfigRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NightscoutConfigRootView.swift; sourceTree = "<group>"; };
-		881E04BA5E0A003DE8E0A9C6 /* DataTableRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DataTableRootView.swift; sourceTree = "<group>"; };
+		881E04BA5E0A003DE8E0A9C6 /* HistoryRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HistoryRootView.swift; sourceTree = "<group>"; };
 		8A9134292D63D9A1007F8874 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
 		8A91342B2D63D9A2007F8874 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = "<group>"; };
 		920DDB21E5D0EB813197500D /* ConfigEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConfigEditorRootView.swift; sourceTree = "<group>"; };
-		9455FA2D92E77A6C4AFED8A3 /* DataTableStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DataTableStateModel.swift; sourceTree = "<group>"; };
+		9455FA2D92E77A6C4AFED8A3 /* HistoryStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HistoryStateModel.swift; sourceTree = "<group>"; };
 		96653287EDB276A111288305 /* ManualTempBasalDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalDataFlow.swift; sourceTree = "<group>"; };
 		9C8D5F457B5AFF763F8CF3DF /* CarbRatioEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CarbRatioEditorProvider.swift; sourceTree = "<group>"; };
 		9F9F137F126D9F8DEB799F26 /* ISFEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorProvider.swift; sourceTree = "<group>"; };
 		A0A48AE3AC813A49A517846A /* NightscoutConfigStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NightscoutConfigStateModel.swift; sourceTree = "<group>"; };
-		A401509D21F7F35D4E109EDA /* DataTableDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DataTableDataFlow.swift; sourceTree = "<group>"; };
+		A401509D21F7F35D4E109EDA /* HistoryDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HistoryDataFlow.swift; sourceTree = "<group>"; };
 		A8630D58BDAD6D9C650B9B39 /* PumpConfigProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpConfigProvider.swift; sourceTree = "<group>"; };
 		AAFF91130F2FCCC7EBBA11AD /* BasalProfileEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorStateModel.swift; sourceTree = "<group>"; };
 		AF65DA88F972B56090AD6AC3 /* PumpConfigDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpConfigDataFlow.swift; sourceTree = "<group>"; };
@@ -1283,6 +1288,10 @@
 		C29E26892DADFD2A00F87E75 /* GlucoseDailyPercentileChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseDailyPercentileChart.swift; sourceTree = "<group>"; };
 		C2A0A42E2CE0312C003B98E8 /* ConstantValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantValues.swift; sourceTree = "<group>"; };
 		C2A6D1E32DB1581D0036DB66 /* GlucoseStatsSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseStatsSetup.swift; sourceTree = "<group>"; };
+		C2AA6CF02E1A734A00BF6C16 /* SettingsExportRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsExportRootView.swift; sourceTree = "<group>"; };
+		C2AA6CF22E1A734A00BF6C16 /* SettingsExportDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsExportDataFlow.swift; sourceTree = "<group>"; };
+		C2AA6CF32E1A734A00BF6C16 /* SettingsExportProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsExportProvider.swift; sourceTree = "<group>"; };
+		C2AA6CF42E1A734A00BF6C16 /* SettingsExportStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsExportStateModel.swift; sourceTree = "<group>"; };
 		C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalRootView.swift; sourceTree = "<group>"; };
 		C8D1A7CA8C10C4403D4BBFA7 /* TreatmentsDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TreatmentsDataFlow.swift; sourceTree = "<group>"; };
 		CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawFetchedProfile.swift; sourceTree = "<group>"; };
@@ -1403,6 +1412,7 @@
 		DD3A3CE82D29C97800AE478E /* Helper+ButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Helper+ButtonStyles.swift"; sourceTree = "<group>"; };
 		DD3C47B22DC5608A003DD20D /* newerSuggested.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = newerSuggested.json; sourceTree = "<group>"; };
 		DD3C47B42DC57E06003DD20D /* MainMigrationErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMigrationErrorView.swift; sourceTree = "<group>"; };
+		DD3D60302F0377350021A33B /* ExportSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportSetting.swift; sourceTree = "<group>"; };
 		DD3F1F822D9DC78300DCE7B3 /* UnitSelectionStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitSelectionStepView.swift; sourceTree = "<group>"; };
 		DD3F1F842D9DD83B00DCE7B3 /* DeliveryLimitsStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeliveryLimitsStepView.swift; sourceTree = "<group>"; };
 		DD3F1F882D9E078300DCE7B3 /* TherapySettingEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TherapySettingEditorView.swift; sourceTree = "<group>"; };
@@ -1685,7 +1695,7 @@
 			isa = PBXGroup;
 			children = (
 				BDA7593D2D37CFC000E649A4 /* CarbEntryEditorView.swift */,
-				881E04BA5E0A003DE8E0A9C6 /* DataTableRootView.swift */,
+				881E04BA5E0A003DE8E0A9C6 /* HistoryRootView.swift */,
 			);
 			path = View;
 			sourceTree = "<group>";
@@ -1896,8 +1906,9 @@
 				F75CB57ED6971B46F8756083 /* CGMSettings */,
 				0610F7D6D2EC00E3BA1569F0 /* ConfigEditor */,
 				E592A3762CEEC038009A472C /* ContactImage */,
-				9E56E3626FAD933385101B76 /* DataTable */,
+				9E56E3626FAD933385101B76 /* History */,
 				195D80B22AF696EE00D25097 /* DynamicSettings */,
+				C2AA6CF52E1A734A00BF6C16 /* SettingsExport */,
 				DD17454C2C55CA0200211FAC /* GeneralSettings */,
 				F66B236E00924A05D6A9F9DF /* GlucoseNotificationSettings */,
 				F90692CD274B99850037068D /* HealthKit */,
@@ -2306,18 +2317,18 @@
 				CE1F6DE62BAF1A180064EB8D /* BuildDetails.plist */,
 				38F3783A2613555C009DB701 /* Config.xcconfig */,
 				BD1CF8B72C1A4A8400CB930A /* ConfigOverride.xcconfig */,
-				388E595A25AD948C0019842D /* Trio */,
-				587A54C82BCDCE0F009D38E2 /* Model */,
-				38FCF3EE25E9028E0078B0D1 /* TrioTests */,
+				3818AA48274C267000843DB3 /* Frameworks */,
 				6B1A8D1C2B14D91600E76752 /* LiveActivity */,
+				587A54C82BCDCE0F009D38E2 /* Model */,
+				3818AA44274C229000843DB3 /* Packages */,
+				388E595925AD948C0019842D /* Products */,
+				192F0FF5276AC36D0085BE4D /* Recovered References */,
+				388E595A25AD948C0019842D /* Trio */,
 				BDFF7AA12D25FAC70016C40C /* Trio Watch App */,
 				BDFF7A9C2D25FA730016C40C /* Trio Watch App Extension */,
 				BDFF7AA02D25FAA80016C40C /* Trio Watch App Tests */,
 				DD09D6492D2B6253000D82C9 /* Trio Watch Complication */,
-				3818AA48274C267000843DB3 /* Frameworks */,
-				3818AA44274C229000843DB3 /* Packages */,
-				388E595925AD948C0019842D /* Products */,
-				192F0FF5276AC36D0085BE4D /* Recovered References */,
+				38FCF3EE25E9028E0078B0D1 /* TrioTests */,
 			);
 			sourceTree = "<group>";
 		};
@@ -2358,6 +2369,7 @@
 		388E5A5925B6F0250019842D /* Models */ = {
 			isa = PBXGroup;
 			children = (
+				DD3D60302F0377350021A33B /* ExportSetting.swift */,
 				DDFF204F2DB2C11900AB8A96 /* WatchStateSnapshot.swift */,
 				DDEBB05B2D89E9050032305D /* TimeInRangeType.swift */,
 				3B2F77852D7E52ED005ED9FA /* TDD.swift */,
@@ -2792,15 +2804,15 @@
 			path = PumpConfig;
 			sourceTree = "<group>";
 		};
-		9E56E3626FAD933385101B76 /* DataTable */ = {
+		9E56E3626FAD933385101B76 /* History */ = {
 			isa = PBXGroup;
 			children = (
-				A401509D21F7F35D4E109EDA /* DataTableDataFlow.swift */,
-				60744C3E9BB3652895C908CC /* DataTableProvider.swift */,
-				9455FA2D92E77A6C4AFED8A3 /* DataTableStateModel.swift */,
+				A401509D21F7F35D4E109EDA /* HistoryDataFlow.swift */,
+				60744C3E9BB3652895C908CC /* HistoryProvider.swift */,
+				9455FA2D92E77A6C4AFED8A3 /* HistoryStateModel.swift */,
 				0EE66DD474AFFD4FD787D5B9 /* View */,
 			);
-			path = DataTable;
+			path = History;
 			sourceTree = "<group>";
 		};
 		A42F1FEDFFD0DDE00AAD54D3 /* BasalProfileEditor */ = {
@@ -3056,6 +3068,25 @@
 			path = "Trio Watch App";
 			sourceTree = "<group>";
 		};
+		C2AA6CF12E1A734A00BF6C16 /* View */ = {
+			isa = PBXGroup;
+			children = (
+				C2AA6CF02E1A734A00BF6C16 /* SettingsExportRootView.swift */,
+			);
+			path = View;
+			sourceTree = "<group>";
+		};
+		C2AA6CF52E1A734A00BF6C16 /* SettingsExport */ = {
+			isa = PBXGroup;
+			children = (
+				C2AA6CF12E1A734A00BF6C16 /* View */,
+				C2AA6CF22E1A734A00BF6C16 /* SettingsExportDataFlow.swift */,
+				C2AA6CF32E1A734A00BF6C16 /* SettingsExportProvider.swift */,
+				C2AA6CF42E1A734A00BF6C16 /* SettingsExportStateModel.swift */,
+			);
+			path = SettingsExport;
+			sourceTree = "<group>";
+		};
 		C2C98283C436DB934D7E7994 /* Treatments */ = {
 			isa = PBXGroup;
 			children = (
@@ -4404,6 +4435,10 @@
 				DD6A4E842DBEDD39008C4B26 /* AlgorithmSettingsImportantNotesStepView.swift in Sources */,
 				3811DEB025C9D88300A708ED /* BaseKeychain.swift in Sources */,
 				110AEDE42C5193D200615CC9 /* BolusIntentRequest.swift in Sources */,
+				C2AA6CF62E1A734A00BF6C16 /* SettingsExportRootView.swift in Sources */,
+				C2AA6CF72E1A734A00BF6C16 /* SettingsExportProvider.swift in Sources */,
+				C2AA6CF82E1A734A00BF6C16 /* SettingsExportDataFlow.swift in Sources */,
+				C2AA6CF92E1A734A00BF6C16 /* SettingsExportStateModel.swift in Sources */,
 				3811DE4325C9D4A100A708ED /* SettingsProvider.swift in Sources */,
 				45252C95D220E796FDB3B022 /* ConfigEditorDataFlow.swift in Sources */,
 				CE7CA34E2A064973004BE681 /* AppShortcuts.swift in Sources */,
@@ -4480,6 +4515,7 @@
 				CE7CA3512A064973004BE681 /* ApplyTempPresetIntent.swift in Sources */,
 				FA630397F76B582C8D8681A7 /* BasalProfileEditorProvider.swift in Sources */,
 				DD1745172C54389F00211FAC /* FeatureSettingsView.swift in Sources */,
+				DD3D60312F0377350021A33B /* ExportSetting.swift in Sources */,
 				DD9ECB712CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift in Sources */,
 				63E890B4D951EAA91C071D5C /* BasalProfileEditorStateModel.swift in Sources */,
 				38FEF3FA2737E42000574A46 /* BaseStateModel.swift in Sources */,
@@ -4619,18 +4655,18 @@
 				3BA8D1B32DDB87150006191F /* DecimalExtensions.swift in Sources */,
 				DD3F1F892D9E078D00DCE7B3 /* TherapySettingEditorView.swift in Sources */,
 				DD1745132C54169400211FAC /* DevicesView.swift in Sources */,
-				7F7B756BE8543965D9FDF1A2 /* DataTableDataFlow.swift in Sources */,
-				1D845DF2E3324130E1D95E67 /* DataTableProvider.swift in Sources */,
+				7F7B756BE8543965D9FDF1A2 /* HistoryDataFlow.swift in Sources */,
+				1D845DF2E3324130E1D95E67 /* HistoryProvider.swift in Sources */,
 				DDCE790F2D6F97FC000A4D7A /* SubmodulesView.swift in Sources */,
 				19F95FFA29F1102A00314DDC /* StatRootView.swift in Sources */,
-				0D9A5E34A899219C5C4CDFAF /* DataTableStateModel.swift in Sources */,
+				0D9A5E34A899219C5C4CDFAF /* HistoryStateModel.swift in Sources */,
 				6BCF84DD2B16843A003AD46E /* LiveActitiyAttributes.swift in Sources */,
 				195D80B92AF697F700D25097 /* DynamicSettingsProvider.swift in Sources */,
 				DD3F1F832D9DC78800DCE7B3 /* UnitSelectionStepView.swift in Sources */,
 				DD09D47D2C5986DA003FEA5D /* CalendarEventSettingsProvider.swift in Sources */,
 				DD09D47B2C5986D1003FEA5D /* CalendarEventSettingsDataFlow.swift in Sources */,
 				DD1745202C55523E00211FAC /* SMBSettingsDataFlow.swift in Sources */,
-				D6D02515BBFBE64FEBE89856 /* DataTableRootView.swift in Sources */,
+				D6D02515BBFBE64FEBE89856 /* HistoryRootView.swift in Sources */,
 				DD1745292C55642100211FAC /* SettingInputSection.swift in Sources */,
 				38569349270B5DFB0002C50D /* AppGroupSource.swift in Sources */,
 				F5CA3DB1F9DC8B05792BBFAA /* CGMSettingsDataFlow.swift in Sources */,

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 15 - 0
Trio/Sources/Helpers/String+Extensions.swift

@@ -8,6 +8,21 @@ extension String {
     mutating func capitalizeFirstLetter() {
         self = capitalizingFirstLetter()
     }
+
+    func formattedHourMinuteFromTimeString() -> String {
+        let input = DateFormatter()
+        input.dateFormat = "HH:mm:ss"
+
+        let output = DateFormatter()
+        output.timeStyle = .short
+        output.dateStyle = .none
+
+        guard let date = input.date(from: self) else {
+            return self
+        }
+
+        return output.string(from: date)
+    }
 }
 
 extension LosslessStringConvertible {

+ 90 - 0
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -36158,6 +36158,9 @@
         }
       }
     },
+    "Allow Fetching From Nightscout" : {
+
+    },
     "Allow Notifications" : {
       "localizations" : {
         "bg" : {
@@ -43696,6 +43699,9 @@
         }
       }
     },
+    "App Version" : {
+
+    },
     "Appearance" : {
       "localizations" : {
         "bg" : {
@@ -51835,6 +51841,9 @@
         }
       }
     },
+    "Basal Rate (%@)" : {
+
+    },
     "Basal Rate Adjustment" : {
       "localizations" : {
         "bg" : {
@@ -57473,6 +57482,9 @@
         }
       }
     },
+    "Branch" : {
+
+    },
     "BRANCH: %@" : {
       "localizations" : {
         "bg" : {
@@ -57591,6 +57603,9 @@
         }
       }
     },
+    "Build Number" : {
+
+    },
     "By default, Trio collects crash reports and other anonymized data related to errors, exceptions, and overall app performance." : {
       "localizations" : {
         "bg" : {
@@ -60731,6 +60746,9 @@
         }
       }
     },
+    "Carb Ratio (%@)" : {
+
+    },
     "Carb Ratios" : {
       "comment" : "Carb Ratios header",
       "localizations" : {
@@ -67790,6 +67808,12 @@
         }
       }
     },
+    "Color" : {
+
+    },
+    "Color Mode" : {
+
+    },
     "Color Scheme Preference" : {
       "localizations" : {
         "bg" : {
@@ -72831,6 +72855,9 @@
         }
       }
     },
+    "Could not access documents directory" : {
+
+    },
     "Count" : {
       "localizations" : {
         "bg" : {
@@ -107440,6 +107467,18 @@
         }
       }
     },
+    "Export Categories" : {
+
+    },
+    "Export Date" : {
+
+    },
+    "Export Error" : {
+
+    },
+    "Export failed: %@" : {
+
+    },
     "Export logs" : {
       "extractionState" : "manual",
       "localizations" : {
@@ -107690,6 +107729,12 @@
         }
       }
     },
+    "Export Settings" : {
+      "comment" : "Export Settings menu item in Trio Settings Root View"
+    },
+    "Exporting..." : {
+
+    },
     "Extended" : {
       "comment" : "Title string for BeepPreference.extended",
       "extractionState" : "manual",
@@ -110130,6 +110175,9 @@
         }
       }
     },
+    "Failed to write export file: %@" : {
+
+    },
     "Fat" : {
       "comment" : "Add Fat",
       "localizations" : {
@@ -125126,6 +125174,9 @@
         }
       }
     },
+    "High Temptarget Raises Sensitivity" : {
+
+    },
     "High Threshold" : {
       "localizations" : {
         "bg" : {
@@ -133271,6 +133322,9 @@
         }
       }
     },
+    "Indefinite" : {
+
+    },
     "Indicates glucose smoothing is enabled." : {
       "localizations" : {
         "bg" : {
@@ -135686,6 +135740,9 @@
         }
       }
     },
+    "Insulin Type" : {
+
+    },
     "Intercept" : {
       "localizations" : {
         "bg" : {
@@ -137859,6 +137916,12 @@
         }
       }
     },
+    "ISF (%@)" : {
+
+    },
+    "ISF and CR" : {
+
+    },
     "ISF/CR" : {
       "comment" : "Option for both ISF and CR",
       "localizations" : {
@@ -148875,6 +148938,9 @@
         }
       }
     },
+    "Low Temptarget Lowers Sensitivity" : {
+
+    },
     "Low Threshold" : {
       "localizations" : {
         "bg" : {
@@ -158203,6 +158269,9 @@
         }
       }
     },
+    "Metadata" : {
+
+    },
     "Meter glucose" : {
       "comment" : "When adding capillary glucose meater reading",
       "localizations" : {
@@ -161943,6 +162012,9 @@
         }
       }
     },
+    "Monochrome" : {
+
+    },
     "Month" : {
       "extractionState" : "manual",
       "localizations" : {
@@ -168687,6 +168759,9 @@
         }
       }
     },
+    "Not Connected" : {
+
+    },
     "Not enough glucose data. You need at least three glucose readings in the last six hours to run the algorithm." : {
       "localizations" : {
         "bg" : {
@@ -188979,6 +189054,9 @@
         }
       }
     },
+    "Pump Type" : {
+
+    },
     "Quantity Carbs" : {
       "localizations" : {
         "bg" : {
@@ -212977,6 +213055,9 @@
         }
       }
     },
+    "SMBs Off%@" : {
+
+    },
     "Smooth CGM readings using Savitzky-Golay filtering." : {
       "localizations" : {
         "bg" : {
@@ -220618,6 +220699,9 @@
         }
       }
     },
+    "Suspend Zeros IOB" : {
+
+    },
     "suspend-end" : {
       "localizations" : {
         "bg" : {
@@ -223235,6 +223319,9 @@
         }
       }
     },
+    "Target (%@)" : {
+
+    },
     "Target Behavior" : {
       "localizations" : {
         "bg" : {
@@ -246213,6 +246300,9 @@
         }
       }
     },
+    "Trio Backup" : {
+
+    },
     "Trio calculates your current Insulin On Board (IOB) from:" : {
       "localizations" : {
         "bg" : {

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

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

+ 21 - 0
Trio/Sources/Models/ExportSetting.swift

@@ -0,0 +1,21 @@
+struct ExportSetting: Codable {
+    let category: String
+    let subcategory: String
+    let name: String
+    let value: String
+    let unit: String
+
+    init(category: String, subcategory: String = "", name: String, value: String, unit: String = "") {
+        self.category = category
+        self.subcategory = subcategory
+        self.name = name
+        self.value = value
+        self.unit = unit
+    }
+}
+
+struct ExportSettingPayload: Codable {
+    let exportFormat: String
+    let exportDate: String
+    let settings: [ExportSetting]
+}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 28 - 4
Trio/Sources/Modules/Home/View/HomeRootView.swift

@@ -220,11 +220,15 @@ extension Home {
                 return nil
             }
 
+            guard let settingsManager = state.settingsManager else {
+                return nil
+            }
+
             let percent = latestOverride.percentage
             let percentString = percent == 100 ? "" : "\(percent.formatted(.number)) %"
 
             let unit = state.units
-            var target = (latestOverride.target ?? 100) as Decimal
+            var target = (latestOverride.target ?? 0) as Decimal
             target = unit == .mmolL ? target.asMmolL : target
 
             var targetString = target == 0 ? "" : (fetchedTargetFormatter.string(from: target as NSNumber) ?? "") + " " + unit
@@ -264,9 +268,29 @@ extension Home {
                 : ""
 
             let smbToggleString = latestOverride.smbIsOff || latestOverride
-                .smbIsScheduledOff ? "SMBs Off\(smbScheduleString)" : ""
+                .smbIsScheduledOff ? String(localized: "SMBs Off\(smbScheduleString)") : ""
+
+            var smbMinuteString: String = ""
+            var uamMinuteString: String = ""
+
+            if !latestOverride.smbIsOff, latestOverride.advancedSettings {
+                if let smbMinutes = latestOverride.smbMinutes,
+                   smbMinutes.decimalValue != settingsManager.preferences.maxSMBBasalMinutes
+                {
+                    smbMinuteString = "SMB\u{00A0}\(smbMinutes)\u{00A0}" +
+                        String(localized: "m", comment: "Abbreviation for Minutes")
+                }
+
+                if let uamMinutes = latestOverride.uamMinutes,
+                   uamMinutes.decimalValue != settingsManager.preferences.maxUAMSMBBasalMinutes
+                {
+                    uamMinuteString = "UAM\u{00A0}\(uamMinutes)\u{00A0}" +
+                        String(localized: "m", comment: "Abbreviation for Minutes")
+                }
+            }
 
-            let components = [durationString, percentString, targetString, smbToggleString].filter { !$0.isEmpty }
+            let components = [durationString, percentString, targetString, smbToggleString, smbMinuteString, uamMinuteString]
+                .filter { !$0.isEmpty }
             return components.isEmpty ? nil : components.joined(separator: ", ")
         }
 
@@ -1079,7 +1103,7 @@ extension Home {
                         .tabItem { Label("Main", systemImage: "chart.xyaxis.line") }
                         .badge(carbsRequiredBadge).tag(0)
 
-                    NavigationStack { DataTable.RootView(resolver: resolver) }
+                    NavigationStack { History.RootView(resolver: resolver) }
                         .tabItem { Label("History", systemImage: historySFSymbol) }.tag(1)
 
                     Spacer()

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

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

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

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

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

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

+ 4 - 0
Trio/Sources/Modules/Settings/SettingsStateModel.swift

@@ -1,3 +1,5 @@
+import CoreData
+import Foundation
 import LoopKit
 import LoopKitUI
 import SwiftUI
@@ -10,6 +12,8 @@ extension Settings {
         @Injected() private var nightscoutManager: NightscoutManager!
         @Injected() var pluginManager: PluginManager!
         @Injected() var fetchCgmManager: FetchGlucoseManager!
+        @Injected() private var storage: FileStorage!
+        @Injected() var overrideStorage: OverrideStorage!
 
         @Published var units: GlucoseUnits = .mgdL
         @Published var closedLoop = false

+ 11 - 63
Trio/Sources/Modules/Settings/View/SettingsRootView.swift

@@ -267,6 +267,17 @@ extension Settings {
                         }
                     ).listRowBackground(Color.chart)
 
+                    Section(
+                        header: Text("Trio Backup"),
+                        content: {
+                            Text(String(
+                                localized: "Export Settings",
+                                comment: "Export Settings menu item in Trio Settings Root View"
+                            ))
+                                .navigationLink(to: .settingsExport, from: self)
+                        }
+                    ).listRowBackground(Color.chart)
+
                 } else {
                     Section(
                         header: Text("Search Results"),
@@ -293,69 +304,6 @@ extension Settings {
                         }
                     ).listRowBackground(Color.chart)
                 }
-
-                // TODO: remove this more or less entirely; add build-time flag to enable Middleware; add settings export feature
-//                Section {
-//                    Toggle("Developer Options", isOn: $state.debugOptions)
-//                    if state.debugOptions {
-//                        Group {
-//                            HStack {
-//                                Text("NS Upload Profile and Settings")
-//                                Button("Upload") { state.uploadProfileAndSettings(true) }
-//                                    .frame(maxWidth: .infinity, alignment: .trailing)
-//                                    .buttonStyle(.borderedProminent)
-//                            }
-//                            // Commenting this out for now, as not needed and possibly dangerous for users to be able to nuke their pump pairing informations via the debug menu
-//                            // Leaving it in here, as it may be a handy functionality for further testing or developers.
-//                            // See https://github.com/nightscout/Trio/pull/277 for more information
-//                            //
-//                            //                            HStack {
-//                            //                                Text("Delete Stored Pump State Binary Files")
-//                            //                                Button("Delete") { state.resetLoopDocuments() }
-//                            //                                    .frame(maxWidth: .infinity, alignment: .trailing)
-//                            //                                    .buttonStyle(.borderedProminent)
-//                            //                            }
-//                        }
-//                        Group {
-//                            Text("Preferences")
-//                                .navigationLink(to: .configEditor(file: OpenAPS.Settings.preferences), from: self)
-//                            Text("Pump Settings")
-//                                .navigationLink(to: .configEditor(file: OpenAPS.Settings.settings), from: self)
-//                            Text("Autosense")
-//                                .navigationLink(to: .configEditor(file: OpenAPS.Settings.autosense), from: self)
-//                            //                            Text("Pump History")
-//                            //                                .navigationLink(to: .configEditor(file: OpenAPS.Monitor.pumpHistory), from: self)
-//                            Text("Basal profile")
-//                                .navigationLink(to: .configEditor(file: OpenAPS.Settings.basalProfile), from: self)
-//                    Text("Targets ranges")
-//                        .navigationLink(to: .configEditor(file: OpenAPS.Settings.bgTargets), from: self)
-//                            Text("Temp targets")
-//                                .navigationLink(to: .configEditor(file: OpenAPS.Settings.tempTargets), from: self)
-//                        }
-//
-//                        Group {
-//                            Text("Pump profile")
-//                                .navigationLink(to: .configEditor(file: OpenAPS.Settings.pumpProfile), from: self)
-//                            Text("Profile")
-//                                .navigationLink(to: .configEditor(file: OpenAPS.Settings.profile), from: self)
-//                            //                            Text("Carbs")
-//                            //                                .navigationLink(to: .configEditor(file: OpenAPS.Monitor.carbHistory), from: self)
-//                        }
-//
-//                        Group {
-//                            Text("Target presets")
-//                                .navigationLink(to: .configEditor(file: OpenAPS.Trio.tempTargetsPresets), from: self)
-//                            Text("Calibrations")
-//                                .navigationLink(to: .configEditor(file: OpenAPS.Trio.calibrations), from: self)
-//                            Text("Middleware")
-//                                .navigationLink(to: .configEditor(file: OpenAPS.Middleware.determineBasal), from: self)
-//                            //                            Text("Statistics")
-//                            //                                .navigationLink(to: .configEditor(file: OpenAPS.Monitor.statistics), from: self)
-//                            Text("Edit settings json")
-//                                .navigationLink(to: .configEditor(file: OpenAPS.Trio.settings), from: self)
-//                        }
-//                    }
-//                }.listRowBackground(Color.chart)
             }
             .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
             .sheet(isPresented: $shouldDisplayHint) {

+ 5 - 0
Trio/Sources/Modules/SettingsExport/SettingsExportDataFlow.swift

@@ -0,0 +1,5 @@
+enum SettingsExport {
+    enum Config {}
+}
+
+protocol SettingsExportProvider: Provider {}

+ 3 - 0
Trio/Sources/Modules/SettingsExport/SettingsExportProvider.swift

@@ -0,0 +1,3 @@
+extension SettingsExport {
+    final class Provider: BaseProvider, SettingsExportProvider {}
+}

Разница между файлами не показана из-за своего большого размера
+ 1329 - 0
Trio/Sources/Modules/SettingsExport/SettingsExportStateModel.swift


+ 192 - 0
Trio/Sources/Modules/SettingsExport/View/SettingsExportRootView.swift

@@ -0,0 +1,192 @@
+import SwiftUI
+import Swinject
+
+extension SettingsExport {
+    struct RootView: BaseView {
+        let resolver: Resolver
+        @StateObject var state = StateModel()
+
+        @State private var showSettingsExport = false
+        @State private var showExportError = false
+        @State private var exportErrorMessage = ""
+        @State private var exportedFileURL: URL?
+
+        @Environment(\.colorScheme) var colorScheme
+        @Environment(AppState.self) var appState
+
+        var body: some View {
+            List {
+                Section(
+                    header: Text("Export Categories"),
+                    content: {
+                        // Select All toggle
+                        HStack {
+                            Button(action: {
+                                state.toggleAllCategories(!state.allCategoriesSelected)
+                            }) {
+                                HStack {
+                                    Image(systemName: state.allCategoriesSelected ? "checkmark.square.fill" : "square")
+                                        .foregroundColor(state.allCategoriesSelected ? .blue : .secondary)
+                                    Text(
+                                        state
+                                            .allCategoriesSelected ? String(localized: "Deselect All") :
+                                            String(localized: "Select All")
+                                    )
+                                    .fontWeight(.bold)
+                                    .foregroundColor(.primary)
+                                    Spacer()
+                                }
+                            }
+                            .buttonStyle(PlainButtonStyle())
+                        }
+
+                        // Individual category toggles
+                        ForEach(SettingsExport.StateModel.ExportCategory.allCases) { category in
+                            HStack {
+                                Button(action: {
+                                    if state.selectedCategories.contains(category) {
+                                        state.selectedCategories.remove(category)
+                                    } else {
+                                        state.selectedCategories.insert(category)
+                                    }
+                                }) {
+                                    HStack {
+                                        Image(
+                                            systemName: state.selectedCategories
+                                                .contains(category) ? "checkmark.square.fill" : "square"
+                                        )
+                                        .foregroundColor(state.selectedCategories.contains(category) ? .blue : .secondary)
+
+                                        Text(category.rawValue)
+
+                                        Spacer()
+                                    }
+                                }
+                                .buttonStyle(PlainButtonStyle())
+                            }
+                            .padding(.vertical, 2)
+                        }
+                    }
+                ).listRowBackground(Color.chart)
+
+                Section {
+                    Button(action: {
+                        Task {
+                            let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
+                            impactHeavy.impactOccurred()
+                            state.isExporting = true
+
+                            switch await state.exportSelectedSettings() {
+                            case let .success(fileURL):
+                                if FileManager.default.fileExists(atPath: fileURL.path) {
+                                    do {
+                                        let attributes = try FileManager.default.attributesOfItem(atPath: fileURL.path)
+                                        let fileSize = attributes[.size] as? Int ?? 0
+
+                                        if fileSize > 0 {
+                                            exportedFileURL = fileURL
+                                            // Stop spinner on successful export
+                                            state.isExporting = false
+                                            showSettingsExport = true
+                                        } else {
+                                            exportErrorMessage = "Export file is empty (0 bytes)"
+                                            showExportError = true
+                                            state.isExporting = false
+                                        }
+                                    } catch {
+                                        exportErrorMessage = "Could not verify file attributes: \(error.localizedDescription)"
+                                        showExportError = true
+                                        // Stop spinner on error
+                                        state.isExporting = false
+                                    }
+                                } else {
+                                    exportErrorMessage = "Export file was created but could not be found at: \(fileURL.path)"
+                                    showExportError = true
+                                    // Stop spinner on error
+                                    state.isExporting = false
+                                }
+                            case let .failure(error):
+                                exportErrorMessage = error.localizedDescription
+                                showExportError = true
+                                // Stop spinner on error
+                                state.isExporting = false
+                            }
+                        }
+                    }, label: {
+                        if state.isExporting {
+                            HStack {
+                                ProgressView().padding(.trailing, 10)
+                                Text("Exporting...")
+                            }
+                        } else {
+                            Text("Export Settings")
+                        }
+
+                    })
+                        .disabled(state.selectedCategories.isEmpty)
+                        .frame(maxWidth: .infinity, alignment: .center)
+                        .tint(.white)
+                }.listRowBackground(
+                    state.selectedCategories.isEmpty ? Color(.systemGray4) : Color(.systemBlue)
+                )
+            }
+            .listSectionSpacing(sectionSpacing)
+            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
+            .onAppear(perform: configureView)
+            .navigationTitle("Export Settings")
+            .navigationBarTitleDisplayMode(.automatic)
+//            // TODO: implement help sheet
+//            .toolbar {
+//                ToolbarItem(placement: .topBarTrailing) {
+//                    Button(
+//                        action: {
+//                            state.isHelpSheetPresented.toggle()
+//                        },
+//                        label: {
+//                            Image(systemName: "questionmark.circle")
+//                        }
+//                    )
+//                }
+//            }
+//            .sheet(isPresented: $state.isHelpSheetPresented) {
+//                NavigationStack {
+//                    List {
+//                        Text("Hello World!")
+//                    }
+//                }
+//                .padding()
+//                .presentationDetents(
+//                    [.fraction(0.9), .large],
+//                    selection: $state.helpSheetDetent
+//                )
+//            }
+            .sheet(isPresented: $showSettingsExport) {
+                if let fileURL = exportedFileURL {
+                    ShareSheet(activityItems: [fileURL])
+                }
+            }
+            .alert("Export Error", isPresented: $showExportError) {
+                Button("OK", role: .cancel) {}
+            } message: {
+                Text(exportErrorMessage)
+            }
+        }
+    }
+}
+
+private struct ExportCategoryRow: View {
+    let title: String
+    let description: String
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 4) {
+            Text(title)
+                .font(.subheadline)
+                .fontWeight(.medium)
+            Text(description)
+                .font(.caption)
+                .foregroundColor(.secondary)
+        }
+        .padding(.vertical, 2)
+    }
+}

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

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

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

@@ -17,7 +17,7 @@ enum Screen: Identifiable, Hashable {
     case targetsEditor
     case treatmentView
     case manualTempBasal
-    case dataTable
+    case history
     case cgm
     case healthkit
     case glucoseNotificationSettings
@@ -49,6 +49,7 @@ enum Screen: Identifiable, Hashable {
     case algorithmAdvancedSettings
     case unitsAndLimits
     case appDiagnostics
+    case settingsExport
 
     var id: Int { String(reflecting: self).hashValue }
 }
@@ -94,8 +95,8 @@ extension Screen {
             Treatments.RootView(resolver: resolver)
         case .manualTempBasal:
             ManualTempBasal.RootView(resolver: resolver)
-        case .dataTable:
-            DataTable.RootView(resolver: resolver)
+        case .history:
+            History.RootView(resolver: resolver)
         case .cgm:
             CGMSettings.RootView(
                 resolver: resolver,
@@ -162,6 +163,8 @@ extension Screen {
             UnitsLimitsSettings.RootView(resolver: resolver)
         case .appDiagnostics:
             AppDiagnostics.RootView(resolver: resolver)
+        case .settingsExport:
+            SettingsExport.RootView(resolver: resolver)
         }
     }
 

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

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

+ 59 - 0
TrioTests/SettingsExportTests.swift

@@ -0,0 +1,59 @@
+@testable import Trio
+import XCTest
+
+final class SettingsExportTests: XCTestCase {
+    func testCSVEscaping() {
+        // Test CSV escaping functionality
+        let testValue = "Test,Value\"With\nSpecial Characters"
+        let escaped = csvEscape(testValue)
+        let expected = "\"Test,Value\"\"With\nSpecial Characters\""
+        XCTAssertEqual(escaped, expected, "CSV escaping should handle commas, quotes, and newlines")
+    }
+
+    func testCSVEscapingSimple() {
+        // Test simple values don't get escaped
+        let testValue = "SimpleValue"
+        let escaped = csvEscape(testValue)
+        XCTAssertEqual(escaped, testValue, "Simple values should not be escaped")
+    }
+
+    func testExportCSVStructure() {
+        // Test that the CSV has the expected header structure
+        let expectedHeader = "Setting Category,Subcategory,Setting Name,Value,Unit"
+        // This test would require mocking the settings manager and file storage
+        // For now, we verify the header format is correct
+        XCTAssertEqual(expectedHeader.components(separatedBy: ",").count, 5, "CSV header should have 5 columns")
+    }
+
+    func testExportErrorTypes() {
+        // Test that our export error types are properly defined
+        let documentError = Settings.StateModel.ExportError.documentsDirectoryNotFound
+        XCTAssertNotNil(documentError.errorDescription, "Document error should have description")
+
+        let writeError = Settings.StateModel.ExportError.fileWriteError(TestError.testError)
+        XCTAssertNotNil(writeError.errorDescription, "Write error should have description")
+
+        let unknownError = Settings.StateModel.ExportError.unknown("Test message")
+        XCTAssertNotNil(unknownError.errorDescription, "Unknown error should have description")
+    }
+
+    func testExportFileNaming() {
+        // Test that export files have the correct naming pattern
+        let formatter = DateFormatter()
+        formatter.dateFormat = "yyyyMMdd_HHmmss"
+        let timestamp = formatter.string(from: Date())
+        let fileName = "TrioSettings_\(timestamp).csv"
+
+        XCTAssertTrue(fileName.hasPrefix("TrioSettings_"), "File name should start with TrioSettings_")
+        XCTAssertTrue(fileName.hasSuffix(".csv"), "File name should end with .csv")
+        XCTAssertEqual(fileName.components(separatedBy: "_").count, 2, "File name should have one underscore")
+    }
+
+    // Helper function to test CSV escaping (extracted from Settings.StateModel)
+    private func csvEscape(_ value: String) -> String {
+        if value.contains(",") || value.contains("\"") || value.contains("\n") {
+            return "\"\(value.replacingOccurrences(of: "\"", with: "\"\""))\""
+        }
+        return value
+    }
+}

+ 1 - 1
scripts/swiftformat.sh

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