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

Merge pull request #942 from nightscout/dev

Sync dev to feat/garmin as of 2026-01-20
Deniz Cengiz 3 месяцев назад
Родитель
Сommit
67731d2c69
67 измененных файлов с 411 добавлено и 1203 удалено
  1. 1 1
      CGMBLEKit
  2. 1 1
      Config.xcconfig
  3. 1 1
      DanaKit
  4. 1 1
      G7SensorKit
  5. 1 1
      LibreTransmitter
  6. 1 1
      LoopKit
  7. 1 1
      MinimedKit
  8. 1 0
      Model/Classes+Properties/ContactTrickEntryStored+CoreDataProperties.swift
  9. 2 1
      Model/TrioCoreDataPersistentContainer.xcdatamodeld/TrioCoreDataPersistentContainer.xcdatamodel/contents
  10. 1 1
      OmniBLE
  11. 1 1
      OmniKit
  12. 1 1
      RileyLinkKit
  13. 1 1
      TidepoolService
  14. 0 18
      Trio Watch App Extension/Assets.xcassets/Colors/Background_DarkBlue.colorset/Contents.json
  15. 0 18
      Trio Watch App Extension/Assets.xcassets/Colors/Background_DarkerDarkBlue.colorset/Contents.json
  16. 0 18
      Trio Watch App Extension/Assets.xcassets/Colors/Basal.colorset/Contents.json
  17. 0 18
      Trio Watch App Extension/Assets.xcassets/Colors/Insulin.colorset/Contents.json
  18. 0 18
      Trio Watch App Extension/Assets.xcassets/Colors/LoopGray.colorset/Contents.json
  19. 0 18
      Trio Watch App Extension/Assets.xcassets/Colors/LoopGreen.colorset/Contents.json
  20. 0 38
      Trio Watch App Extension/Assets.xcassets/Colors/LoopPink.colorset/Contents.json
  21. 0 18
      Trio Watch App Extension/Assets.xcassets/Colors/LoopRed.colorset/Contents.json
  22. 0 18
      Trio Watch App Extension/Assets.xcassets/Colors/TabBar.colorset/Contents.json
  23. 3 21
      Trio Watch App Extension/Assets.xcassets/Colors/UAM.colorset/Contents.json
  24. 0 18
      Trio Watch App Extension/Assets.xcassets/Colors/ZT.colorset/Contents.json
  25. 26 26
      Trio.xcodeproj/project.pbxproj
  26. 0 18
      Trio/Resources/Assets.xcassets/Colors/Background_DarkBlue.colorset/Contents.json
  27. 0 18
      Trio/Resources/Assets.xcassets/Colors/Background_DarkerDarkBlue.colorset/Contents.json
  28. 0 18
      Trio/Resources/Assets.xcassets/Colors/Basal.colorset/Contents.json
  29. 0 18
      Trio/Resources/Assets.xcassets/Colors/DarkerBlue.colorset/Contents.json
  30. 0 18
      Trio/Resources/Assets.xcassets/Colors/Insulin.colorset/Contents.json
  31. 0 38
      Trio/Resources/Assets.xcassets/Colors/Lemon.colorset/Contents.json
  32. 0 18
      Trio/Resources/Assets.xcassets/Colors/LoopGray.colorset/Contents.json
  33. 0 18
      Trio/Resources/Assets.xcassets/Colors/LoopGreen.colorset/Contents.json
  34. 0 38
      Trio/Resources/Assets.xcassets/Colors/LoopPink.colorset/Contents.json
  35. 0 18
      Trio/Resources/Assets.xcassets/Colors/LoopRed.colorset/Contents.json
  36. 0 18
      Trio/Resources/Assets.xcassets/Colors/ManualTempBasal.colorset/Contents.json
  37. 0 18
      Trio/Resources/Assets.xcassets/Colors/TabBar.colorset/Contents.json
  38. 3 21
      Trio/Resources/Assets.xcassets/Colors/UAM.colorset/Contents.json
  39. 0 18
      Trio/Resources/Assets.xcassets/Colors/ZT.colorset/Contents.json
  40. 0 18
      Trio/Resources/Assets.xcassets/Colors/minus.colorset/Contents.json
  41. 0 184
      Trio/Sources/APS/CGM/DexcomSourceG5.swift
  42. 0 195
      Trio/Sources/APS/CGM/DexcomSourceG6.swift
  43. 0 103
      Trio/Sources/APS/CGM/LibreTransmitterSource.swift
  44. 2 10
      Trio/Sources/APS/CGM/PluginSource.swift
  45. 4 19
      Trio/Sources/APS/DeviceDataManager.swift
  46. 203 47
      Trio/Sources/APS/Storage/AlertStorage.swift
  47. 5 2
      Trio/Sources/APS/Storage/ContactImageStorage.swift
  48. 9 9
      Trio/Sources/APS/Storage/GlucoseStorage.swift
  49. 0 2
      Trio/Sources/Helpers/Color+Extensions.swift
  50. 23 14
      Trio/Sources/Localizations/Main/Localizable.xcstrings
  51. 17 0
      Trio/Sources/Models/ContactTrickEntry.swift
  52. 11 0
      Trio/Sources/Modules/ContactImage/View/AddContactImageSheet.swift
  53. 9 0
      Trio/Sources/Modules/ContactImage/View/ContactImageDetailView.swift
  54. 3 3
      Trio/Sources/Modules/DataTable/DataTableDataFlow.swift
  55. 2 2
      Trio/Sources/Modules/DataTable/DataTableProvider.swift
  56. 2 2
      Trio/Sources/Modules/DataTable/DataTableStateModel.swift
  57. 2 2
      Trio/Sources/Modules/DataTable/View/CarbEntryEditorView.swift
  58. 14 11
      Trio/Sources/Modules/DataTable/View/DataTableRootView.swift
  59. 28 4
      Trio/Sources/Modules/Home/View/HomeRootView.swift
  60. 2 1
      Trio/Sources/Modules/Onboarding/View/OnboardingSteps/TherapySettings/InsulinSensitivityStepView.swift
  61. 4 4
      Trio/Sources/Modules/PumpConfig/PumpConfigProvider.swift
  62. 5 5
      Trio/Sources/Modules/PumpConfig/PumpConfigStateModel.swift
  63. 1 1
      Trio/Sources/Modules/PumpConfig/View/PumpConfigRootView.swift
  64. 6 6
      Trio/Sources/Modules/Treatments/View/MealPreset/MealPresetView.swift
  65. 3 3
      Trio/Sources/Router/Screen.swift
  66. 9 1
      Trio/Sources/Services/ContactImage/ContactPicture.swift
  67. 1 1
      scripts/swiftformat.sh

+ 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.43
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 

+ 1 - 1
DanaKit

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

+ 1 - 1
G7SensorKit

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

+ 1 - 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" : {

+ 26 - 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; };
@@ -332,7 +332,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 */; };
@@ -515,7 +515,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 */; };
@@ -1139,7 +1139,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>"; };
@@ -1159,16 +1159,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>"; };
@@ -1689,7 +1689,7 @@
 			isa = PBXGroup;
 			children = (
 				BDA7593D2D37CFC000E649A4 /* CarbEntryEditorView.swift */,
-				881E04BA5E0A003DE8E0A9C6 /* DataTableRootView.swift */,
+				881E04BA5E0A003DE8E0A9C6 /* HistoryRootView.swift */,
 			);
 			path = View;
 			sourceTree = "<group>";
@@ -1900,7 +1900,7 @@
 				F75CB57ED6971B46F8756083 /* CGMSettings */,
 				0610F7D6D2EC00E3BA1569F0 /* ConfigEditor */,
 				E592A3762CEEC038009A472C /* ContactImage */,
-				9E56E3626FAD933385101B76 /* DataTable */,
+				9E56E3626FAD933385101B76 /* History */,
 				195D80B22AF696EE00D25097 /* DynamicSettings */,
 				DD17454C2C55CA0200211FAC /* GeneralSettings */,
 				F66B236E00924A05D6A9F9DF /* GlucoseNotificationSettings */,
@@ -2310,18 +2310,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>";
 		};
@@ -2797,15 +2797,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 */ = {
@@ -4627,18 +4627,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")

+ 23 - 14
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -10037,6 +10037,7 @@
       }
     },
     "%lld h" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -67812,6 +67813,12 @@
         }
       }
     },
+    "Color" : {
+
+    },
+    "Color Mode" : {
+
+    },
     "Color Scheme Preference" : {
       "localizations" : {
         "bg" : {
@@ -132134,6 +132141,9 @@
         }
       }
     },
+    "Important: Autosens Min and Autosens Max do not affect Temp Targets in the same way. Autosens Max limits how much insulin can be increased, but Autosens Min does not remove the 15% minimum when insulin is reduced." : {
+
+    },
     "Importing Settings..." : {
       "comment" : "Progress text when importing settings via Nightscout",
       "localizations" : {
@@ -162006,6 +162016,9 @@
         }
       }
     },
+    "Monochrome" : {
+
+    },
     "Month" : {
       "extractionState" : "manual",
       "localizations" : {
@@ -169946,10 +169959,6 @@
         }
       }
     },
-    "Note: Autosens Min and Autosens Max settings do not apply symmetrically to Temp Target sensitivity adjustments. Autosens Max limits how much sensitivity can be decreased (more insulin), but Autosens Min does not override the 15% floor for increased sensitivity (less insulin)." : {
-      "comment" : "A note explaining the asymmetry in how Autosens Min and Max affect Temp Target sensitivity adjustments.",
-      "isCommentAutoGenerated" : true
-    },
     "Note: Basal may be resumed if there is negative IOB and glucose is rising faster than the forecast." : {
       "localizations" : {
         "bg" : {
@@ -202206,9 +202215,8 @@
         }
       }
     },
-    "Sensitivity adjustments from Temp Targets have a hard-coded minimum of 15%. This means even very high Temp Targets cannot reduce insulin delivery below 15% of normal." : {
-      "comment" : "A description of the 15% floor for Temp Target sensitivity adjustments.",
-      "isCommentAutoGenerated" : true
+    "Sensitivity changes from Temp Targets have a built-in minimum of 15%. Even very high Temp Targets cannot reduce insulin delivery below 15% of normal." : {
+
     },
     "Sensitivity Limits" : {
       "comment" : "A label displayed above a section explaining the sensitivity limits of temp targets.",
@@ -213057,6 +213065,9 @@
         }
       }
     },
+    "SMBs Off%@" : {
+
+    },
     "Smooth CGM readings using Savitzky-Golay filtering." : {
       "localizations" : {
         "bg" : {
@@ -232400,9 +232411,8 @@
         }
       }
     },
-    "This 15% floor is a safety limit inherited from oref (OpenAPS reference design) and AndroidAPS. It prevents Temp Targets from reducing insulin to dangerously low levels." : {
-      "comment" : "A text describing the 15% floor that prevents Temp Targets from reducing insulin to dangerously low levels.",
-      "isCommentAutoGenerated" : true
+    "This 15% minimum is a safety limit taken from oref (OpenAPS reference design) and AndroidAPS. It helps prevent insulin delivery from dropping to unsafe levels." : {
+
     },
     "This adjusted ISF is temporary, will change with the next loop cycle, and should not be directly used as your profile ISF value." : {
       "localizations" : {
@@ -232765,10 +232775,6 @@
         }
       }
     },
-    "This asymmetry exists because reducing insulin delivery during exercise, normally realized by using high Temp Targets, typically requires a higher insulin reduction than what autosens would identify in a regular dayly routine." : {
-      "comment" : "A note explaining the asymmetry in the Temp Target sensitivity adjustment rules.",
-      "isCommentAutoGenerated" : true
-    },
     "This cap prevents the system from overestimating how much insulin is needed when carb absorption isn't visible, offering a safeguard for accurate dosing." : {
       "localizations" : {
         "bg" : {
@@ -233241,6 +233247,9 @@
         }
       }
     },
+    "This difference exists because situations like exercise often need a much larger insulin reduction than Autosens would detect during a normal daily routine." : {
+
+    },
     "This dotted line represents the hourly insulin rate of your scheduled basal insulin." : {
       "localizations" : {
         "bg" : {

+ 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

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

+ 2 - 1
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/TherapySettings/InsulinSensitivityStepView.swift

@@ -145,7 +145,8 @@ struct InsulinSensitivityStepView: View {
     private var isfChart: some View {
         Chart {
             ForEach(Array(state.isfItems.enumerated()), id: \.element.id) { index, item in
-                let displayValue = state.units == .mgdL ? state.isfRateValues[item.rateIndex] : state.isfRateValues[item.rateIndex].asMmolL
+                let displayValue = state.units == .mgdL ? state.isfRateValues[item.rateIndex] : state
+                    .isfRateValues[item.rateIndex].asMmolL
 
                 let startDate = Calendar.current
                     .startOfDay(for: now)

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

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

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

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

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

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

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

+ 1 - 1
scripts/swiftformat.sh

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